diff --git a/.eslintrc.js b/.eslintrc.js index 40dd6a55a2a3f..c64f03a8398e5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -893,6 +893,8 @@ module.exports = { files: [ 'x-pack/plugins/security_solution/public/**/*.{js,mjs,ts,tsx}', 'x-pack/plugins/security_solution/common/**/*.{js,mjs,ts,tsx}', + 'x-pack/plugins/timelines/public/**/*.{js,mjs,ts,tsx}', + 'x-pack/plugins/timelines/common/**/*.{js,mjs,ts,tsx}', ], rules: { 'import/no-nodejs-modules': 'error', @@ -907,7 +909,10 @@ module.exports = { }, { // typescript only for front and back end - files: ['x-pack/plugins/security_solution/**/*.{ts,tsx}'], + files: [ + 'x-pack/plugins/security_solution/**/*.{ts,tsx}', + 'x-pack/plugins/timelines/**/*.{ts,tsx}', + ], rules: { '@typescript-eslint/no-this-alias': 'error', '@typescript-eslint/no-explicit-any': 'error', @@ -917,7 +922,10 @@ module.exports = { }, { // typescript and javascript for front and back end - files: ['x-pack/plugins/security_solution/**/*.{js,mjs,ts,tsx}'], + files: [ + 'x-pack/plugins/security_solution/**/*.{js,mjs,ts,tsx}', + 'x-pack/plugins/timelines/**/*.{js,mjs,ts,tsx}', + ], plugins: ['eslint-plugin-node', 'react'], env: { jest: true, diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index e8b950a696f55..217645b903818 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -70,6 +70,7 @@ yarn kbn watch-bazel - @kbn/apm-utils - @kbn/babel-code-parser - @kbn/babel-preset +- @kbn/cli-dev-mode - @kbn/config - @kbn/config-schema - @kbn/crypto @@ -86,6 +87,8 @@ yarn kbn watch-bazel - @kbn/logging - @kbn/mapbox-gl - @kbn/monaco +- @kbn/optimizer +- @kbn/plugin-helpers - @kbn/rule-data-utils - @kbn/securitysolution-es-utils - @kbn/securitysolution-hook-utils 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 ae433e3db14c6..b10ad949c4944 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 @@ -106,6 +106,7 @@ readonly links: { }; readonly search: { readonly sessions: string; + readonly sessionLimits: string; }; readonly indexPatterns: { readonly introduction: string; 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 b0800c7dfc65e..c020f57faa882 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) | <code>string</code> | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | <code>string</code> | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | <code>{</code><br/><code> readonly canvas: {</code><br/><code> readonly guide: string;</code><br/><code> };</code><br/><code> readonly dashboard: {</code><br/><code> readonly guide: string;</code><br/><code> readonly drilldowns: string;</code><br/><code> readonly drilldownsTriggerPicker: string;</code><br/><code> readonly urlDrilldownTemplateSyntax: string;</code><br/><code> readonly urlDrilldownVariables: string;</code><br/><code> };</code><br/><code> readonly discover: Record<string, string>;</code><br/><code> readonly filebeat: {</code><br/><code> readonly base: string;</code><br/><code> readonly installation: string;</code><br/><code> readonly configuration: string;</code><br/><code> readonly elasticsearchOutput: string;</code><br/><code> readonly elasticsearchModule: string;</code><br/><code> readonly startup: string;</code><br/><code> readonly exportedFields: string;</code><br/><code> };</code><br/><code> readonly auditbeat: {</code><br/><code> readonly base: string;</code><br/><code> };</code><br/><code> readonly metricbeat: {</code><br/><code> readonly base: string;</code><br/><code> readonly configure: string;</code><br/><code> readonly httpEndpoint: string;</code><br/><code> readonly install: string;</code><br/><code> readonly start: string;</code><br/><code> };</code><br/><code> readonly enterpriseSearch: {</code><br/><code> readonly base: string;</code><br/><code> readonly appSearchBase: string;</code><br/><code> readonly workplaceSearchBase: string;</code><br/><code> };</code><br/><code> readonly heartbeat: {</code><br/><code> readonly base: string;</code><br/><code> };</code><br/><code> readonly logstash: {</code><br/><code> readonly base: string;</code><br/><code> };</code><br/><code> readonly functionbeat: {</code><br/><code> readonly base: string;</code><br/><code> };</code><br/><code> readonly winlogbeat: {</code><br/><code> readonly base: string;</code><br/><code> };</code><br/><code> readonly aggs: {</code><br/><code> readonly composite: string;</code><br/><code> readonly composite_missing_bucket: string;</code><br/><code> readonly date_histogram: string;</code><br/><code> readonly date_range: string;</code><br/><code> readonly date_format_pattern: string;</code><br/><code> readonly filter: string;</code><br/><code> readonly filters: string;</code><br/><code> readonly geohash_grid: string;</code><br/><code> readonly histogram: string;</code><br/><code> readonly ip_range: string;</code><br/><code> readonly range: string;</code><br/><code> readonly significant_terms: string;</code><br/><code> readonly terms: string;</code><br/><code> readonly avg: string;</code><br/><code> readonly avg_bucket: string;</code><br/><code> readonly max_bucket: string;</code><br/><code> readonly min_bucket: string;</code><br/><code> readonly sum_bucket: string;</code><br/><code> readonly cardinality: string;</code><br/><code> readonly count: string;</code><br/><code> readonly cumulative_sum: string;</code><br/><code> readonly derivative: string;</code><br/><code> readonly geo_bounds: string;</code><br/><code> readonly geo_centroid: string;</code><br/><code> readonly max: string;</code><br/><code> readonly median: string;</code><br/><code> readonly min: string;</code><br/><code> readonly moving_avg: string;</code><br/><code> readonly percentile_ranks: string;</code><br/><code> readonly serial_diff: string;</code><br/><code> readonly std_dev: string;</code><br/><code> readonly sum: string;</code><br/><code> readonly top_hits: string;</code><br/><code> };</code><br/><code> readonly runtimeFields: {</code><br/><code> readonly overview: string;</code><br/><code> readonly mapping: string;</code><br/><code> };</code><br/><code> readonly scriptedFields: {</code><br/><code> readonly scriptFields: string;</code><br/><code> readonly scriptAggs: string;</code><br/><code> readonly painless: string;</code><br/><code> readonly painlessApi: string;</code><br/><code> readonly painlessLangSpec: string;</code><br/><code> readonly painlessSyntax: string;</code><br/><code> readonly painlessWalkthrough: string;</code><br/><code> readonly luceneExpressions: string;</code><br/><code> };</code><br/><code> readonly search: {</code><br/><code> readonly sessions: string;</code><br/><code> };</code><br/><code> readonly indexPatterns: {</code><br/><code> readonly introduction: string;</code><br/><code> readonly fieldFormattersNumber: string;</code><br/><code> readonly fieldFormattersString: string;</code><br/><code> readonly runtimeFields: string;</code><br/><code> };</code><br/><code> readonly addData: string;</code><br/><code> readonly kibana: string;</code><br/><code> readonly upgradeAssistant: string;</code><br/><code> readonly rollupJobs: string;</code><br/><code> readonly elasticsearch: Record<string, string>;</code><br/><code> readonly siem: {</code><br/><code> readonly guide: string;</code><br/><code> readonly gettingStarted: string;</code><br/><code> };</code><br/><code> readonly query: {</code><br/><code> readonly eql: string;</code><br/><code> readonly kueryQuerySyntax: string;</code><br/><code> readonly luceneQuerySyntax: string;</code><br/><code> readonly percolate: string;</code><br/><code> readonly queryDsl: string;</code><br/><code> };</code><br/><code> readonly date: {</code><br/><code> readonly dateMath: string;</code><br/><code> readonly dateMathIndexNames: string;</code><br/><code> };</code><br/><code> readonly management: Record<string, string>;</code><br/><code> readonly ml: Record<string, string>;</code><br/><code> readonly transforms: Record<string, string>;</code><br/><code> readonly visualize: Record<string, string>;</code><br/><code> readonly apis: Readonly<{</code><br/><code> bulkIndexAlias: string;</code><br/><code> byteSizeUnits: string;</code><br/><code> createAutoFollowPattern: string;</code><br/><code> createFollower: string;</code><br/><code> createIndex: string;</code><br/><code> createSnapshotLifecyclePolicy: string;</code><br/><code> createRoleMapping: string;</code><br/><code> createRoleMappingTemplates: string;</code><br/><code> createRollupJobsRequest: string;</code><br/><code> createApiKey: string;</code><br/><code> createPipeline: string;</code><br/><code> createTransformRequest: string;</code><br/><code> cronExpressions: string;</code><br/><code> executeWatchActionModes: string;</code><br/><code> indexExists: string;</code><br/><code> openIndex: string;</code><br/><code> putComponentTemplate: string;</code><br/><code> painlessExecute: string;</code><br/><code> painlessExecuteAPIContexts: string;</code><br/><code> putComponentTemplateMetadata: string;</code><br/><code> putSnapshotLifecyclePolicy: string;</code><br/><code> putIndexTemplateV1: string;</code><br/><code> putWatch: string;</code><br/><code> simulatePipeline: string;</code><br/><code> timeUnits: string;</code><br/><code> updateTransform: string;</code><br/><code> }>;</code><br/><code> readonly observability: Record<string, string>;</code><br/><code> readonly alerting: Record<string, string>;</code><br/><code> readonly maps: Record<string, string>;</code><br/><code> readonly monitoring: Record<string, string>;</code><br/><code> readonly security: Readonly<{</code><br/><code> apiKeyServiceSettings: string;</code><br/><code> clusterPrivileges: string;</code><br/><code> elasticsearchSettings: string;</code><br/><code> elasticsearchEnableSecurity: string;</code><br/><code> indicesPrivileges: string;</code><br/><code> kibanaTLS: string;</code><br/><code> kibanaPrivileges: string;</code><br/><code> mappingRoles: string;</code><br/><code> mappingRolesFieldRules: string;</code><br/><code> runAsPrivilege: string;</code><br/><code> }>;</code><br/><code> readonly watcher: Record<string, string>;</code><br/><code> readonly ccs: Record<string, string>;</code><br/><code> readonly plugins: Record<string, string>;</code><br/><code> readonly snapshotRestore: Record<string, string>;</code><br/><code> readonly ingest: Record<string, string>;</code><br/><code> readonly fleet: Readonly<{</code><br/><code> guide: string;</code><br/><code> fleetServer: string;</code><br/><code> fleetServerAddFleetServer: string;</code><br/><code> settings: string;</code><br/><code> settingsFleetServerHostSettings: string;</code><br/><code> troubleshooting: string;</code><br/><code> elasticAgent: string;</code><br/><code> datastreams: string;</code><br/><code> datastreamsNamingScheme: string;</code><br/><code> upgradeElasticAgent: string;</code><br/><code> upgradeElasticAgent712lower: string;</code><br/><code> }>;</code><br/><code> }</code> | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | <code>{</code><br/><code> readonly canvas: {</code><br/><code> readonly guide: string;</code><br/><code> };</code><br/><code> readonly dashboard: {</code><br/><code> readonly guide: string;</code><br/><code> readonly drilldowns: string;</code><br/><code> readonly drilldownsTriggerPicker: string;</code><br/><code> readonly urlDrilldownTemplateSyntax: string;</code><br/><code> readonly urlDrilldownVariables: string;</code><br/><code> };</code><br/><code> readonly discover: Record<string, string>;</code><br/><code> readonly filebeat: {</code><br/><code> readonly base: string;</code><br/><code> readonly installation: string;</code><br/><code> readonly configuration: string;</code><br/><code> readonly elasticsearchOutput: string;</code><br/><code> readonly elasticsearchModule: string;</code><br/><code> readonly startup: string;</code><br/><code> readonly exportedFields: string;</code><br/><code> };</code><br/><code> readonly auditbeat: {</code><br/><code> readonly base: string;</code><br/><code> };</code><br/><code> readonly metricbeat: {</code><br/><code> readonly base: string;</code><br/><code> readonly configure: string;</code><br/><code> readonly httpEndpoint: string;</code><br/><code> readonly install: string;</code><br/><code> readonly start: string;</code><br/><code> };</code><br/><code> readonly enterpriseSearch: {</code><br/><code> readonly base: string;</code><br/><code> readonly appSearchBase: string;</code><br/><code> readonly workplaceSearchBase: string;</code><br/><code> };</code><br/><code> readonly heartbeat: {</code><br/><code> readonly base: string;</code><br/><code> };</code><br/><code> readonly logstash: {</code><br/><code> readonly base: string;</code><br/><code> };</code><br/><code> readonly functionbeat: {</code><br/><code> readonly base: string;</code><br/><code> };</code><br/><code> readonly winlogbeat: {</code><br/><code> readonly base: string;</code><br/><code> };</code><br/><code> readonly aggs: {</code><br/><code> readonly composite: string;</code><br/><code> readonly composite_missing_bucket: string;</code><br/><code> readonly date_histogram: string;</code><br/><code> readonly date_range: string;</code><br/><code> readonly date_format_pattern: string;</code><br/><code> readonly filter: string;</code><br/><code> readonly filters: string;</code><br/><code> readonly geohash_grid: string;</code><br/><code> readonly histogram: string;</code><br/><code> readonly ip_range: string;</code><br/><code> readonly range: string;</code><br/><code> readonly significant_terms: string;</code><br/><code> readonly terms: string;</code><br/><code> readonly avg: string;</code><br/><code> readonly avg_bucket: string;</code><br/><code> readonly max_bucket: string;</code><br/><code> readonly min_bucket: string;</code><br/><code> readonly sum_bucket: string;</code><br/><code> readonly cardinality: string;</code><br/><code> readonly count: string;</code><br/><code> readonly cumulative_sum: string;</code><br/><code> readonly derivative: string;</code><br/><code> readonly geo_bounds: string;</code><br/><code> readonly geo_centroid: string;</code><br/><code> readonly max: string;</code><br/><code> readonly median: string;</code><br/><code> readonly min: string;</code><br/><code> readonly moving_avg: string;</code><br/><code> readonly percentile_ranks: string;</code><br/><code> readonly serial_diff: string;</code><br/><code> readonly std_dev: string;</code><br/><code> readonly sum: string;</code><br/><code> readonly top_hits: string;</code><br/><code> };</code><br/><code> readonly runtimeFields: {</code><br/><code> readonly overview: string;</code><br/><code> readonly mapping: string;</code><br/><code> };</code><br/><code> readonly scriptedFields: {</code><br/><code> readonly scriptFields: string;</code><br/><code> readonly scriptAggs: string;</code><br/><code> readonly painless: string;</code><br/><code> readonly painlessApi: string;</code><br/><code> readonly painlessLangSpec: string;</code><br/><code> readonly painlessSyntax: string;</code><br/><code> readonly painlessWalkthrough: string;</code><br/><code> readonly luceneExpressions: string;</code><br/><code> };</code><br/><code> readonly search: {</code><br/><code> readonly sessions: string;</code><br/><code> readonly sessionLimits: string;</code><br/><code> };</code><br/><code> readonly indexPatterns: {</code><br/><code> readonly introduction: string;</code><br/><code> readonly fieldFormattersNumber: string;</code><br/><code> readonly fieldFormattersString: string;</code><br/><code> readonly runtimeFields: string;</code><br/><code> };</code><br/><code> readonly addData: string;</code><br/><code> readonly kibana: string;</code><br/><code> readonly upgradeAssistant: string;</code><br/><code> readonly elasticsearch: Record<string, string>;</code><br/><code> readonly siem: {</code><br/><code> readonly guide: string;</code><br/><code> readonly gettingStarted: string;</code><br/><code> };</code><br/><code> readonly query: {</code><br/><code> readonly eql: string;</code><br/><code> readonly kueryQuerySyntax: string;</code><br/><code> readonly luceneQuerySyntax: string;</code><br/><code> readonly percolate: string;</code><br/><code> readonly queryDsl: string;</code><br/><code> };</code><br/><code> readonly date: {</code><br/><code> readonly dateMath: string;</code><br/><code> readonly dateMathIndexNames: string;</code><br/><code> };</code><br/><code> readonly management: Record<string, string>;</code><br/><code> readonly ml: Record<string, string>;</code><br/><code> readonly transforms: Record<string, string>;</code><br/><code> readonly visualize: Record<string, string>;</code><br/><code> readonly apis: Readonly<{</code><br/><code> bulkIndexAlias: string;</code><br/><code> byteSizeUnits: string;</code><br/><code> createAutoFollowPattern: string;</code><br/><code> createFollower: string;</code><br/><code> createIndex: string;</code><br/><code> createSnapshotLifecyclePolicy: string;</code><br/><code> createRoleMapping: string;</code><br/><code> createRoleMappingTemplates: string;</code><br/><code> createRollupJobsRequest: string;</code><br/><code> createApiKey: string;</code><br/><code> createPipeline: string;</code><br/><code> createTransformRequest: string;</code><br/><code> cronExpressions: string;</code><br/><code> executeWatchActionModes: string;</code><br/><code> indexExists: string;</code><br/><code> openIndex: string;</code><br/><code> putComponentTemplate: string;</code><br/><code> painlessExecute: string;</code><br/><code> painlessExecuteAPIContexts: string;</code><br/><code> putComponentTemplateMetadata: string;</code><br/><code> putSnapshotLifecyclePolicy: string;</code><br/><code> putIndexTemplateV1: string;</code><br/><code> putWatch: string;</code><br/><code> simulatePipeline: string;</code><br/><code> timeUnits: string;</code><br/><code> updateTransform: string;</code><br/><code> }>;</code><br/><code> readonly observability: Record<string, string>;</code><br/><code> readonly alerting: Record<string, string>;</code><br/><code> readonly maps: Record<string, string>;</code><br/><code> readonly monitoring: Record<string, string>;</code><br/><code> readonly security: Readonly<{</code><br/><code> apiKeyServiceSettings: string;</code><br/><code> clusterPrivileges: string;</code><br/><code> elasticsearchSettings: string;</code><br/><code> elasticsearchEnableSecurity: string;</code><br/><code> indicesPrivileges: string;</code><br/><code> kibanaTLS: string;</code><br/><code> kibanaPrivileges: string;</code><br/><code> mappingRoles: string;</code><br/><code> mappingRolesFieldRules: string;</code><br/><code> runAsPrivilege: string;</code><br/><code> }>;</code><br/><code> readonly watcher: Record<string, string>;</code><br/><code> readonly ccs: Record<string, string>;</code><br/><code> readonly plugins: Record<string, string>;</code><br/><code> readonly snapshotRestore: Record<string, string>;</code><br/><code> readonly ingest: Record<string, string>;</code><br/><code> }</code> | | diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md index 143cd397c40ae..bf08ca1682f3b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md @@ -24,5 +24,7 @@ set(status$: Observable<ServiceStatus>): void; ## Remarks +The first emission from this Observable should occur within 30s, else this plugin's status will fallback to `unavailable` until the first emission. + See the [StatusServiceSetup.derivedStatus$](./kibana-plugin-core-server.statusservicesetup.derivedstatus_.md) API for leveraging the default status calculation that is provided by Core. diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md index 54b5a33ccf682..2ca4847d6dc39 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md @@ -13,11 +13,11 @@ esFilters: { FILTERS: typeof FILTERS; FilterStateStore: typeof FilterStateStore; buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("../common").Filter; - buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").IIndexPattern) => import("../common").PhrasesFilter; - buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").IIndexPattern) => import("../common").ExistsFilter; - buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").IIndexPattern) => import("../common").PhraseFilter; + buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").MinimalIndexPattern) => import("../common").PhrasesFilter; + buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").MinimalIndexPattern) => import("../common").ExistsFilter; + buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").MinimalIndexPattern) => import("../common").PhraseFilter; buildQueryFilter: (query: any, index: string, alias: string) => import("../common").QueryStringFilter; - buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").IIndexPattern, formattedValue?: string | undefined) => import("../common").RangeFilter; + buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").MinimalIndexPattern, formattedValue?: string | undefined) => import("../common").RangeFilter; isPhraseFilter: (filter: any) => filter is import("../common").PhraseFilter; isExistsFilter: (filter: any) => filter is import("../common").ExistsFilter; isPhrasesFilter: (filter: any) => filter is import("../common").PhrasesFilter; diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md index 2cde2b7455585..881a1fa803ca6 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.eskuery.md @@ -10,6 +10,6 @@ esKuery: { nodeTypes: import("../common/es_query/kuery/node_types").NodeTypes; fromKueryExpression: (expression: any, parseOptions?: Partial<import("../common").KueryParseOptions>) => import("../common").KueryNode; - toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IIndexPattern | undefined, config?: Record<string, any> | undefined, context?: Record<string, any> | undefined) => import("@kbn/common-utils").JsonObject; + toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").MinimalIndexPattern | undefined, config?: Record<string, any> | undefined, context?: Record<string, any> | undefined) => import("@kbn/common-utils").JsonObject; } ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esquery.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esquery.md index 2430e6a93bd2b..70805aaaaee8c 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esquery.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esquery.md @@ -10,7 +10,7 @@ esQuery: { buildEsQuery: typeof buildEsQuery; getEsQueryConfig: typeof getEsQueryConfig; - buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IIndexPattern | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => { + buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").MinimalIndexPattern | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => { must: never[]; filter: import("../common").Filter[]; should: never[]; diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.fields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.fields.md deleted file mode 100644 index 792bee44f96a8..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.fields.md +++ /dev/null @@ -1,11 +0,0 @@ -<!-- Do not edit this file. It is automatically generated by API Documenter. --> - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IIndexPattern](./kibana-plugin-plugins-data-public.iindexpattern.md) > [fields](./kibana-plugin-plugins-data-public.iindexpattern.fields.md) - -## IIndexPattern.fields property - -<b>Signature:</b> - -```typescript -fields: IFieldType[]; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.id.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.id.md deleted file mode 100644 index 917a80975df6c..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.id.md +++ /dev/null @@ -1,11 +0,0 @@ -<!-- Do not edit this file. It is automatically generated by API Documenter. --> - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IIndexPattern](./kibana-plugin-plugins-data-public.iindexpattern.md) > [id](./kibana-plugin-plugins-data-public.iindexpattern.id.md) - -## IIndexPattern.id property - -<b>Signature:</b> - -```typescript -id?: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md index bf7f88ab37039..88d8520a373c6 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md @@ -12,7 +12,7 @@ <b>Signature:</b> ```typescript -export interface IIndexPattern +export interface IIndexPattern extends MinimalIndexPattern ``` ## Properties @@ -20,9 +20,7 @@ export interface IIndexPattern | Property | Type | Description | | --- | --- | --- | | [fieldFormatMap](./kibana-plugin-plugins-data-public.iindexpattern.fieldformatmap.md) | <code>Record<string, SerializedFieldFormat<unknown> | undefined></code> | | -| [fields](./kibana-plugin-plugins-data-public.iindexpattern.fields.md) | <code>IFieldType[]</code> | | | [getFormatterForField](./kibana-plugin-plugins-data-public.iindexpattern.getformatterforfield.md) | <code>(field: IndexPatternField | IndexPatternField['spec'] | IFieldType) => FieldFormat</code> | Look up a formatter for a given field | -| [id](./kibana-plugin-plugins-data-public.iindexpattern.id.md) | <code>string</code> | | | [timeFieldName](./kibana-plugin-plugins-data-public.iindexpattern.timefieldname.md) | <code>string</code> | | | [title](./kibana-plugin-plugins-data-public.iindexpattern.title.md) | <code>string</code> | | | [type](./kibana-plugin-plugins-data-public.iindexpattern.type.md) | <code>string</code> | Type is used for identifying rollup indices, otherwise left undefined | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md new file mode 100644 index 0000000000000..d649212ae0547 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md @@ -0,0 +1,13 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IKibanaSearchResponse](./kibana-plugin-plugins-data-public.ikibanasearchresponse.md) > [isRestored](./kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md) + +## IKibanaSearchResponse.isRestored property + +Indicates whether the results returned are from the async-search index + +<b>Signature:</b> + +```typescript +isRestored?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.md index 1d3e0c08dfc18..c7046902dac72 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ikibanasearchresponse.md @@ -16,6 +16,7 @@ export interface IKibanaSearchResponse<RawResponse = any> | --- | --- | --- | | [id](./kibana-plugin-plugins-data-public.ikibanasearchresponse.id.md) | <code>string</code> | Some responses may contain a unique id to identify the request this response came from. | | [isPartial](./kibana-plugin-plugins-data-public.ikibanasearchresponse.ispartial.md) | <code>boolean</code> | Indicates whether the results returned are complete or partial | +| [isRestored](./kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md) | <code>boolean</code> | Indicates whether the results returned are from the async-search index | | [isRunning](./kibana-plugin-plugins-data-public.ikibanasearchresponse.isrunning.md) | <code>boolean</code> | Indicates whether search is still in flight | | [loaded](./kibana-plugin-plugins-data-public.ikibanasearchresponse.loaded.md) | <code>number</code> | If relevant to the search strategy, return a loaded number that represents how progress is indicated. | | [rawResponse](./kibana-plugin-plugins-data-public.ikibanasearchresponse.rawresponse.md) | <code>RawResponse</code> | The raw response returned by the internal search method (usually the raw ES response) | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esfilters.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esfilters.md index d7e80d94db4e6..d951cb2426943 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esfilters.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esfilters.md @@ -11,11 +11,11 @@ esFilters: { buildQueryFilter: (query: any, index: string, alias: string) => import("../common").QueryStringFilter; buildCustomFilter: typeof buildCustomFilter; buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("../common").Filter; - buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").IIndexPattern) => import("../common").ExistsFilter; + buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").MinimalIndexPattern) => import("../common").ExistsFilter; buildFilter: typeof buildFilter; - buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").IIndexPattern) => import("../common").PhraseFilter; - buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").IIndexPattern) => import("../common").PhrasesFilter; - buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").IIndexPattern, formattedValue?: string | undefined) => import("../common").RangeFilter; + buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").MinimalIndexPattern) => import("../common").PhraseFilter; + buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").MinimalIndexPattern) => import("../common").PhrasesFilter; + buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").MinimalIndexPattern, formattedValue?: string | undefined) => import("../common").RangeFilter; isFilterDisabled: (filter: import("../common").Filter) => boolean; } ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md index 4b96d8af756f3..6274eb5f4f4a5 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md @@ -10,6 +10,6 @@ esKuery: { nodeTypes: import("../common/es_query/kuery/node_types").NodeTypes; fromKueryExpression: (expression: any, parseOptions?: Partial<import("../common").KueryParseOptions>) => import("../common").KueryNode; - toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IIndexPattern | undefined, config?: Record<string, any> | undefined, context?: Record<string, any> | undefined) => import("@kbn/common-utils").JsonObject; + toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").MinimalIndexPattern | undefined, config?: Record<string, any> | undefined, context?: Record<string, any> | undefined) => import("@kbn/common-utils").JsonObject; } ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esquery.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esquery.md index ac9be23bc6b6f..0d1baecb014f5 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esquery.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.esquery.md @@ -8,7 +8,7 @@ ```typescript esQuery: { - buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IIndexPattern | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => { + buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").MinimalIndexPattern | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => { must: never[]; filter: import("../common").Filter[]; should: never[]; diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index b1745b298e27e..9816b884c4614 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -13,6 +13,7 @@ | [IndexPatternsFetcher](./kibana-plugin-plugins-data-server.indexpatternsfetcher.md) | | | [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) | | | [IndexPatternsServiceProvider](./kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md) | | +| [NoSearchIdInSessionError](./kibana-plugin-plugins-data-server.nosearchidinsessionerror.md) | | | [OptionedParamType](./kibana-plugin-plugins-data-server.optionedparamtype.md) | | | [Plugin](./kibana-plugin-plugins-data-server.plugin.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md new file mode 100644 index 0000000000000..e48a1c98f8578 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md @@ -0,0 +1,13 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [NoSearchIdInSessionError](./kibana-plugin-plugins-data-server.nosearchidinsessionerror.md) > [(constructor)](./kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md) + +## NoSearchIdInSessionError.(constructor) + +Constructs a new instance of the `NoSearchIdInSessionError` class + +<b>Signature:</b> + +```typescript +constructor(); +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror.md new file mode 100644 index 0000000000000..707739f845cd1 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.nosearchidinsessionerror.md @@ -0,0 +1,18 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [NoSearchIdInSessionError](./kibana-plugin-plugins-data-server.nosearchidinsessionerror.md) + +## NoSearchIdInSessionError class + +<b>Signature:</b> + +```typescript +export declare class NoSearchIdInSessionError extends KbnError +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)()](./kibana-plugin-plugins-data-server.nosearchidinsessionerror._constructor_.md) | | Constructs a new instance of the <code>NoSearchIdInSessionError</code> class | + diff --git a/docs/management/action-types.asciidoc b/docs/management/action-types.asciidoc index 65b600d4b7281..3d3d7aeb2d777 100644 --- a/docs/management/action-types.asciidoc +++ b/docs/management/action-types.asciidoc @@ -43,6 +43,10 @@ a| <<slack-action-type, Slack>> | Send a message to a Slack channel or user. +a| <<swimlane-action-type, Swimlane>> + +| Create an incident in Swimlane. + a| <<webhook-action-type, Webhook>> | Send a request to a web service. diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 853180ec816e9..66a23ee189ae1 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -482,6 +482,9 @@ of buckets to try to represent. [[visualization-visualize-chartslibrary]]`visualization:visualize:legacyChartsLibrary`:: Enables the legacy charts library for aggregation-based area, line, and bar charts in *Visualize*. +[[visualization-visualize-pieChartslibrary]]`visualization:visualize:legacyPieChartsLibrary`:: +Enables the legacy charts library for aggregation-based pie charts in *Visualize*. + [[visualization-colormapping]]`visualization:colorMapping`:: **This setting is deprecated and will not be supported as of 8.0.** Maps values to specific colors in charts using the *Compatibility* palette. diff --git a/docs/management/connectors/action-types/swimlane.asciidoc b/docs/management/connectors/action-types/swimlane.asciidoc new file mode 100644 index 0000000000000..88447bb496a86 --- /dev/null +++ b/docs/management/connectors/action-types/swimlane.asciidoc @@ -0,0 +1,105 @@ +[role="xpack"] +[[swimlane-action-type]] +=== Swimlane connector and action +++++ +<titleabbrev>Swimlane</titleabbrev> +++++ + +The Swimlane connector uses the https://swimlane.com/knowledge-center/docs/developer-guide/rest-api/[Swimlane REST API] to create Swimlane records. + +[float] +[[swimlane-connector-configuration]] +==== Connector configuration + +Swimlane connectors have the following configuration properties. + +Name:: The name of the connector. The name is used to identify a connector in the **Stack Management** UI connector listing, and in the connector list when configuring an action. +URL:: Swimlane instance URL. +Application ID:: Swimlane application ID. +API token:: Swimlane API authentication token for HTTP Basic authentication. + +[float] +[[Preconfigured-swimlane-configuration]] +==== Preconfigured connector type + +[source,text] +-- + my-swimlane: + name: preconfigured-swimlane-connector-type + actionTypeId: .swimlane + config: + apiUrl: https://elastic.swimlaneurl.us + appId: app-id + mappings: + alertIdConfig: + fieldType: text + id: agp4s + key: alert-id + name: Alert ID + caseIdConfig: + fieldType: text + id: ae1mi + key: case-id + name: Case ID + caseNameConfig: + fieldType: text + id: anxnr + key: case-name + name: Case Name + commentsConfig: + fieldType: comments + id: au18d + key: comments + name: Comments + descriptionConfig: + fieldType: text + id: ae1gd + key: description + name: Description + ruleNameConfig: + fieldType: text + id: avfsl + key: rule-name + name: Rule Name + severityConfig: + fieldType: text + id: a71ik + key: severity + name: severity + secrets: + apiToken: tokenkeystorevalue +-- + +Config defines information for the connector type. + +`apiUrl`:: An address that corresponds to *URL*. +`appId`:: A key that corresponds to *Application ID*. + +Secrets defines sensitive information for the connector type. + +`apiToken`:: A string that corresponds to *API Token*. Should be stored in the <<creating-keystore, {kib} keystore>>. + +[float] +[[define-swimlane-ui]] +==== Define connector in Stack Management + +Define Swimlane connector properties. + +[role="screenshot"] +image::management/connectors/images/swimlane-connector.png[Swimlane connector] + +Test Swimlane action parameters. + +[role="screenshot"] +image::management/connectors/images/swimlane-params-test.png[Swimlane params test] + +[float] +[[swimlane-action-configuration]] +==== Action configuration + +Swimlane actions have the following configuration properties. + +Comments:: Additional information for the client, such as how to troubleshoot the issue. +Severity:: The severity of the incident. + +NOTE: Alert ID and Rule Name are filled automatically. Specifically, Alert ID is set to `{{alert.id}}` and Rule Name to `{{rule.name}}`. \ No newline at end of file diff --git a/docs/management/connectors/images/swimlane-connector.png b/docs/management/connectors/images/swimlane-connector.png new file mode 100644 index 0000000000000..520c35d00381b Binary files /dev/null and b/docs/management/connectors/images/swimlane-connector.png differ diff --git a/docs/management/connectors/images/swimlane-params-test.png b/docs/management/connectors/images/swimlane-params-test.png new file mode 100644 index 0000000000000..c0e02c2c7b18f Binary files /dev/null and b/docs/management/connectors/images/swimlane-params-test.png differ diff --git a/docs/management/connectors/index.asciidoc b/docs/management/connectors/index.asciidoc index ea4fa46d3e808..033b1c3ac150e 100644 --- a/docs/management/connectors/index.asciidoc +++ b/docs/management/connectors/index.asciidoc @@ -6,6 +6,7 @@ include::action-types/teams.asciidoc[] include::action-types/pagerduty.asciidoc[] include::action-types/server-log.asciidoc[] include::action-types/servicenow.asciidoc[] +include::action-types/swimlane.asciidoc[] include::action-types/slack.asciidoc[] include::action-types/webhook.asciidoc[] include::pre-configured-connectors.asciidoc[] diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index 71f141d1ed5d6..d1d283ca60fbb 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -69,7 +69,7 @@ You can configure the following settings in the `kibana.yml` file. -- xpack.actions.customHostSettings: - url: smtp://mail.example.com:465 - tls: + ssl: verificationMode: 'full' certificateAuthoritiesFiles: [ 'one.crt' ] certificateAuthoritiesData: | @@ -79,7 +79,7 @@ xpack.actions.customHostSettings: smtp: requireTLS: true - url: https://webhook.example.com - tls: + ssl: // legacy rejectUnauthorized: false verificationMode: 'none' @@ -97,8 +97,8 @@ xpack.actions.customHostSettings: server, and the `https` URLs are used for actions which use `https` to connect to services. + + - Entries with `https` URLs can use the `tls` options, and entries with `smtp` - URLs can use both the `tls` and `smtp` options. + + Entries with `https` URLs can use the `ssl` options, and entries with `smtp` + URLs can use both the `ssl` and `smtp` options. + + No other URL values should be part of this URL, including paths, query strings, and authentication information. When an http or smtp request @@ -117,24 +117,24 @@ xpack.actions.customHostSettings: The options `smtp.ignoreTLS` and `smtp.requireTLS` can not both be set to true. | `xpack.actions.customHostSettings[n]` -`.tls.rejectUnauthorized` {ess-icon} - | Deprecated. Use <<action-config-custom-host-verification-mode,`xpack.actions.customHostSettings.tls.verificationMode`>> instead. A boolean value indicating whether to bypass server certificate validation. +`.ssl.rejectUnauthorized` {ess-icon} + | Deprecated. Use <<action-config-custom-host-verification-mode,`xpack.actions.customHostSettings.ssl.verificationMode`>> instead. A boolean value indicating whether to bypass server certificate validation. Overrides the general `xpack.actions.rejectUnauthorized` configuration for requests made for this hostname/port. |[[action-config-custom-host-verification-mode]] `xpack.actions.customHostSettings[n]` -`.tls.verificationMode` +`.ssl.verificationMode` | Controls the verification of the server certificate that {hosted-ems} receives when making an outbound SSL/TLS connection to the host server. Valid values are `full`, `certificate`, and `none`. - Use `full` to perform hostname verification, `certificate` to skip hostname verification, and `none` to skip verification. Default: `full`. <<elasticsearch-ssl-verificationMode,Equivalent {kib} setting>>. Overrides the general `xpack.actions.tls.verificationMode` configuration + Use `full` to perform hostname verification, `certificate` to skip hostname verification, and `none` to skip verification. Default: `full`. <<elasticsearch-ssl-verificationMode,Equivalent {kib} setting>>. Overrides the general `xpack.actions.ssl.verificationMode` configuration for requests made for this hostname/port. | `xpack.actions.customHostSettings[n]` -`.tls.certificateAuthoritiesFiles` +`.ssl.certificateAuthoritiesFiles` | A file name or list of file names of PEM-encoded certificate files to use to validate the server. | `xpack.actions.customHostSettings[n]` -`.tls.certificateAuthoritiesData` {ess-icon} +`.ssl.certificateAuthoritiesData` {ess-icon} | The contents of a PEM-encoded certificate file, or multiple files appended into a single string. This configuration can be used for environments where the files cannot be made available. @@ -165,28 +165,28 @@ xpack.actions.customHostSettings: a|`xpack.actions.` `proxyRejectUnauthorizedCertificates` {ess-icon} - | Deprecated. Use <<action-config-proxy-verification-mode,`xpack.actions.tls.proxyVerificationMode`>> instead. Set to `false` to bypass certificate validation for the proxy, if using a proxy for actions. Default: `true`. + | Deprecated. Use <<action-config-proxy-verification-mode,`xpack.actions.ssl.proxyVerificationMode`>> instead. Set to `false` to bypass certificate validation for the proxy, if using a proxy for actions. Default: `true`. |[[action-config-proxy-verification-mode]] `xpack.actions[n]` -`.tls.proxyVerificationMode` {ess-icon} +`.ssl.proxyVerificationMode` {ess-icon} | Controls the verification for the proxy server certificate that {hosted-ems} receives when making an outbound SSL/TLS connection to the proxy server. Valid values are `full`, `certificate`, and `none`. Use `full` to perform hostname verification, `certificate` to skip hostname verification, and `none` to skip verification. Default: `full`. <<elasticsearch-ssl-verificationMode,Equivalent {kib} setting>>. | `xpack.actions.rejectUnauthorized` {ess-icon} - | Deprecated. Use <<action-config-verification-mode,`xpack.actions.tls.verificationMode`>> instead. Set to `false` to bypass certificate validation for actions. Default: `true`. + + | Deprecated. Use <<action-config-verification-mode,`xpack.actions.ssl.verificationMode`>> instead. Set to `false` to bypass certificate validation for actions. Default: `true`. + + As an alternative to setting `xpack.actions.rejectUnauthorized`, you can use the setting - `xpack.actions.customHostSettings` to set TLS options for specific servers. + `xpack.actions.customHostSettings` to set SSL options for specific servers. |[[action-config-verification-mode]] `xpack.actions[n]` -`.tls.verificationMode` {ess-icon} +`.ssl.verificationMode` {ess-icon} | Controls the verification for the server certificate that {hosted-ems} receives when making an outbound SSL/TLS connection for actions. Valid values are `full`, `certificate`, and `none`. Use `full` to perform hostname verification, `certificate` to skip hostname verification, and `none` to skip verification. Default: `full`. <<elasticsearch-ssl-verificationMode,Equivalent {kib} setting>>. + + - As an alternative to setting `xpack.actions.tls.verificationMode`, you can use the setting - `xpack.actions.customHostSettings` to set TLS options for specific servers. + As an alternative to setting `xpack.actions.ssl.verificationMode`, you can use the setting + `xpack.actions.customHostSettings` to set SSL options for specific servers. diff --git a/docs/user/dashboard/aggregation-reference.asciidoc b/docs/user/dashboard/aggregation-reference.asciidoc index cb5c484def3b9..17bfc19c2e0c9 100644 --- a/docs/user/dashboard/aggregation-reference.asciidoc +++ b/docs/user/dashboard/aggregation-reference.asciidoc @@ -12,91 +12,168 @@ This reference can help simplify the comparison if you need a specific feature. [options="header"] |=== -| Type | Aggregation-based | Lens | TSVB | Timelion | Vega +| Type | Lens | TSVB | Agg-based | Vega | Timelion | Table -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ | | -| Table with summary row -^| X -^| X -| +| Bar, line, and area +| ✓ +| ✓ +| ✓ +| ✓ +| ✓ + +| Split chart/small multiples | +| ✓ +| ✓ +| ✓ | -| Bar, line, and area charts -^| X -^| X -^| X -^| X -^| X +| Pie and donut +| ✓ +| +| ✓ +| ✓ +| -| Percentage bar or area chart +| Sunburst +| ✓ | -^| X -^| X +| ✓ +| ✓ | -^| X -| Split bar, line, and area charts -^| X +| Treemap +| ✓ +| | +| ✓ | + +| Heat map +| ✓ +| ✓ +| ✓ +| ✓ | -^| X -| Pie and donut charts -^| X -^| X +| Gauge and Goal | +| ✓ +| ✓ +| ✓ | -^| X -| Sunburst chart -^| X -^| X +| Markdown +| +| ✓ | | | -| Heat map -^| X -^| X +| Metric +| ✓ +| ✓ +| ✓ +| ✓ +| + +| Tag cloud | | -^| X +| ✓ +| ✓ +| -| Gauge and Goal -^| X +|=== + +[float] +[[table-features]] +=== Table features + +[options="header"] +|=== + +| Type | Lens | TSVB | Agg-based + +| Summary row +| ✓ | -^| X +| ✓ + +| Pivot table +| ✓ | | -| Markdown +| Calculated column +| Formula +| ✓ +| Percent only + +| Color by value +| ✓ +| ✓ | + +|=== + +[float] +[[xy-features]] +=== Bar, line, area features + +[options="header"] +|=== + +| Type | Lens | TSVB | Agg-based | Vega | Timelion + +| Dense time series +| Customizable +| ✓ +| Customizable +| ✓ +| ✓ + +| Percentage mode +| ✓ +| ✓ +| ✓ +| ✓ | -^| X + +| Break downs +| 1 +| 1 +| 3 +| ∞ +| 1 + +| Custom color with break downs | +| Only for Filters +| ✓ +| ✓ | -| Metric -^| X -^| X -^| X +| Fit missing values +| ✓ | -^| X +| ✓ +| ✓ +| ✓ -| Tag cloud -^| X +| Synchronized tooltips +| +| ✓ | | | -^| X |=== @@ -111,67 +188,57 @@ For information about {es} bucket aggregations, refer to {ref}/search-aggregatio [options="header"] |=== -| Type | Agg-based | Markdown | Lens | TSVB +| Type | Lens | TSVB | Agg-based | Histogram -^| X -^| X -^| X +| ✓ | +| ✓ | Date histogram -^| X -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ | Date range -^| X -^| X -| +| Use filters | +| ✓ | Filter -^| X -^| X | -^| X +| ✓ +| | Filters -^| X -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ | GeoHash grid -^| X -^| X | | +| ✓ | IP range -^| X -^| X -| -| +| Use filters +| Use filters +| ✓ | Range -^| X -^| X -^| X -| +| ✓ +| Use filters +| ✓ | Terms -^| X -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ | Significant terms -^| X -^| X | -^| X +| +| ✓ |=== @@ -186,67 +253,57 @@ For information about {es} metrics aggregations, refer to {ref}/search-aggregati [options="header"] |=== -| Type | Agg-based | Markdown | Lens | TSVB +| Type | Lens | TSVB | Agg-based | Metrics with filters +| ✓ | | -^| X -| - -| Average -^| X -^| X -^| X -^| X -| Sum -^| X -^| X -^| X -^| X +| Average, Sum, Max, Min +| ✓ +| ✓ +| ✓ | Unique count (Cardinality) -^| X -^| X -^| X -^| X - -| Max -^| X -^| X -^| X -^| X - -| Min -^| X -^| X -^| X -^| X - -| Percentiles -^| X -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ + +| Percentiles and Median +| ✓ +| ✓ +| ✓ | Percentiles Rank -^| X -^| X -| -^| X +| +| ✓ +| ✓ + +| Standard deviation +| +| ✓ +| ✓ + +| Sum of squares +| +| ✓ +| | Top hit (Last value) -^| X -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ | Value count | | +| ✓ + +| Variance +| +| ✓ | -^| X |=== @@ -261,61 +318,94 @@ For information about {es} pipeline aggregations, refer to {ref}/search-aggregat [options="header"] |=== -| Type | Agg-based | Markdown | Lens | TSVB +| Type | Lens | TSVB | Agg-based | Avg bucket -^| X -^| X -| -^| X +| <<lens-formulas, `overall_average` formula>> +| ✓ +| ✓ | Derivative -^| X -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ | Max bucket -^| X -^| X -| -^| X +| <<lens-formulas, `overall_max` formula>> +| ✓ +| ✓ | Min bucket -^| X -^| X -| -^| X +| <<lens-formulas, `overall_min` formula>> +| ✓ +| ✓ | Sum bucket -^| X -^| X -| -^| X +| <<lens-formulas, `overall_sum` formula>> +| ✓ +| ✓ | Moving average -^| X -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ | Cumulative sum -^| X -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ | Bucket script | | +| ✓ + +| Bucket selector +| | -^| X +| | Serial differencing -^| X -^| X | -^| X +| ✓ +| ✓ + +|=== + +[float] +[[custom-functions]] +=== Additional functions + +[options="header"] +|=== + +| Type | Lens | TSVB | Agg-based + +| Counter rate +| ✓ +| ✓ +| + +| <<tsvb-function-reference, Filter ratio>> +| Use <<lens-formulas, formula>> +| ✓ +| + +| <<tsvb-function-reference, Positive only>> +| +| ✓ +| + +| <<tsvb-function-reference, Series agg>> +| +| ✓ +| + +| Static value +| +| ✓ +| + |=== @@ -329,41 +419,49 @@ build their advanced visualization. [options="header"] |=== -| Type | Agg-based | Lens | TSVB | Timelion | Vega +| Type | Lens | TSVB | Agg-based | Vega | Timelion -| Math on aggregated data +| Math +| ✓ +| ✓ | -^| X -^| X -^| X -^| X +| ✓ +| ✓ | Visualize two indices +| ✓ +| ✓ | -^| X -^| X -^| X -^| X +| ✓ +| ✓ | Math across indices | | | -^| X -^| X +| ✓ +| ✓ | Time shifts +| ✓ +| ✓ | -^| X -^| X -^| X -^| X +| ✓ +| ✓ | Fully custom {es} queries | | | +| ✓ | -^| X + +| Normalize by time +| ✓ +| ✓ +| +| +| + |=== diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc index 4ecfcc9250122..2071f17ecff3d 100644 --- a/docs/user/dashboard/lens.asciidoc +++ b/docs/user/dashboard/lens.asciidoc @@ -139,6 +139,42 @@ image::images/lens_drag_drop_3.gif[Using drag and drop to reorder] . Press Space bar to confirm, or to cancel, press Esc. +[float] +[[lens-formulas]] +==== Use formulas to perform math + +Formulas let you perform math on aggregated data in Lens by typing +math and quick functions. To access formulas, +click the *Formula* tab in the dimension editor. Access the complete +reference for formulas from the help menu. + +The most common formulas are dividing two values to produce a percent. +To display accurately, set *Value format* to *Percent*. + +Filter ratio:: + +Use `kql=''` to filter one set of documents and compare it to other documents within the same grouping. +For example, to see how the error rate changes over time: ++ +``` +count(kql='response.status_code > 400') / count() +``` + +Week over week:: Use `shift='1w'` to get the value of each grouping from +the previous week. Time shift should not be used with the *Top values* function. ++ +``` +percentile(system.network.in.bytes, percentile=99) / +percentile(system.network.in.bytes, percentile=99, shift='1w') +``` + +Percent of total:: Formulas can calculate `overall_sum` for all the groupings, +which lets you convert each grouping into a percent of total: ++ +``` +sum(products.base_price) / overall_sum(sum(products.base_price)) +``` + [float] [[lens-faq]] ==== Frequently asked questions diff --git a/package.json b/package.json index 36fa086657adf..ecedb64c343ec 100644 --- a/package.json +++ b/package.json @@ -103,16 +103,16 @@ "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.13", "@elastic/ems-client": "7.14.0", - "@elastic/eui": "33.0.0", + "@elastic/eui": "34.3.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "^9.0.1-kibana3", "@elastic/maki": "6.3.0", "@elastic/node-crypto": "1.2.1", "@elastic/numeral": "^2.5.1", - "@elastic/react-search-ui": "^1.5.1", + "@elastic/react-search-ui": "^1.6.0", "@elastic/request-crypto": "1.1.4", "@elastic/safer-lodash-set": "link:bazel-bin/packages/elastic-safer-lodash-set", - "@elastic/search-ui-app-search-connector": "^1.5.0", + "@elastic/search-ui-app-search-connector": "^1.6.0", "@elastic/ui-ace": "0.2.3", "@hapi/accept": "^5.0.2", "@hapi/boom": "^9.1.1", @@ -149,6 +149,7 @@ "@kbn/securitysolution-list-api": "link:bazel-bin/packages/kbn-securitysolution-list-api", "@kbn/securitysolution-list-hooks": "link:bazel-bin/packages/kbn-securitysolution-list-hooks", "@kbn/securitysolution-list-utils": "link:bazel-bin/packages/kbn-securitysolution-list-utils", + "@kbn/securitysolution-t-grid": "link:bazel-bin/packages/kbn-securitysolution-t-grid", "@kbn/securitysolution-utils": "link:bazel-bin/packages/kbn-securitysolution-utils", "@kbn/server-http-tools": "link:bazel-bin/packages/kbn-server-http-tools", "@kbn/server-route-repository": "link:bazel-bin/packages/kbn-server-route-repository", @@ -217,6 +218,8 @@ "cytoscape-dagre": "^2.2.2", "d3": "3.5.17", "d3-array": "1.2.4", + "d3-cloud": "1.2.5", + "d3-interpolate": "^3.0.1", "d3-scale": "1.0.7", "d3-shape": "^1.1.0", "d3-time": "^1.1.0", @@ -454,7 +457,7 @@ "@jest/reporters": "^26.6.2", "@kbn/babel-code-parser": "link:bazel-bin/packages/kbn-babel-code-parser", "@kbn/babel-preset": "link:bazel-bin/packages/kbn-babel-preset", - "@kbn/cli-dev-mode": "link:packages/kbn-cli-dev-mode", + "@kbn/cli-dev-mode": "link:bazel-bin/packages/kbn-cli-dev-mode", "@kbn/dev-utils": "link:bazel-bin/packages/kbn-dev-utils", "@kbn/docs-utils": "link:bazel-bin/packages/kbn-docs-utils", "@kbn/es": "link:bazel-bin/packages/kbn-es", @@ -462,9 +465,9 @@ "@kbn/eslint-import-resolver-kibana": "link:bazel-bin/packages/kbn-eslint-import-resolver-kibana", "@kbn/eslint-plugin-eslint": "link:bazel-bin/packages/kbn-eslint-plugin-eslint", "@kbn/expect": "link:bazel-bin/packages/kbn-expect", - "@kbn/optimizer": "link:packages/kbn-optimizer", + "@kbn/optimizer": "link:bazel-bin/packages/kbn-optimizer", "@kbn/plugin-generator": "link:bazel-bin/packages/kbn-plugin-generator", - "@kbn/plugin-helpers": "link:packages/kbn-plugin-helpers", + "@kbn/plugin-helpers": "link:bazel-bin/packages/kbn-plugin-helpers", "@kbn/pm": "link:packages/kbn-pm", "@kbn/storybook": "link:bazel-bin/packages/kbn-storybook", "@kbn/telemetry-tools": "link:bazel-bin/packages/kbn-telemetry-tools", @@ -511,6 +514,7 @@ "@types/cytoscape": "^3.14.0", "@types/d3": "^3.5.43", "@types/d3-array": "^1.2.7", + "@types/d3-interpolate": "^2.0.0", "@types/d3-scale": "^2.1.1", "@types/d3-shape": "^1.3.1", "@types/d3-time": "^1.0.10", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index b1c3f580c6baf..1094a2def3e70 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -3,7 +3,7 @@ filegroup( name = "build", srcs = [ - "//packages/elastic-datemath:build", + "//packages/elastic-datemath:build", "//packages/elastic-eslint-config-kibana:build", "//packages/elastic-safer-lodash-set:build", "//packages/kbn-ace:build", @@ -12,6 +12,7 @@ filegroup( "//packages/kbn-apm-utils:build", "//packages/kbn-babel-code-parser:build", "//packages/kbn-babel-preset:build", + "//packages/kbn-cli-dev-mode:build", "//packages/kbn-common-utils:build", "//packages/kbn-config:build", "//packages/kbn-config-schema:build", @@ -29,7 +30,9 @@ filegroup( "//packages/kbn-logging:build", "//packages/kbn-mapbox-gl:build", "//packages/kbn-monaco:build", + "//packages/kbn-optimizer:build", "//packages/kbn-plugin-generator:build", + "//packages/kbn-plugin-helpers:build", "//packages/kbn-rule-data-utils:build", "//packages/kbn-securitysolution-list-constants:build", "//packages/kbn-securitysolution-io-ts-types:build", @@ -41,6 +44,7 @@ filegroup( "//packages/kbn-securitysolution-list-utils:build", "//packages/kbn-securitysolution-utils:build", "//packages/kbn-securitysolution-es-utils:build", + "//packages/kbn-securitysolution-t-grid:build", "//packages/kbn-securitysolution-hook-utils:build", "//packages/kbn-server-http-tools:build", "//packages/kbn-server-route-repository:build", diff --git a/packages/elastic-eslint-config-kibana/.eslintrc.js b/packages/elastic-eslint-config-kibana/.eslintrc.js index a8c2e9546510e..3220a01184004 100644 --- a/packages/elastic-eslint-config-kibana/.eslintrc.js +++ b/packages/elastic-eslint-config-kibana/.eslintrc.js @@ -75,6 +75,11 @@ module.exports = { to: '@kbn/test', disallowedMessage: `import from the root of @kbn/test instead` }, + { + from: 'react-intl', + to: '@kbn/i18n/react', + disallowedMessage: `import from @kbn/i18n/react instead` + } ], ], }, diff --git a/packages/kbn-cli-dev-mode/BUILD.bazel b/packages/kbn-cli-dev-mode/BUILD.bazel new file mode 100644 index 0000000000000..ab1b6601f429b --- /dev/null +++ b/packages/kbn-cli-dev-mode/BUILD.bazel @@ -0,0 +1,103 @@ +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-cli-dev-mode" +PKG_REQUIRE_NAME = "@kbn/cli-dev-mode" + +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", + "//packages/kbn-config-schema", + "//packages/kbn-dev-utils", + "//packages/kbn-logging", + "//packages/kbn-optimizer", + "//packages/kbn-server-http-tools", + "//packages/kbn-std", + "//packages/kbn-utils", + "@npm//@hapi/h2o2", + "@npm//@hapi/hapi", + "@npm//argsplit", + "@npm//chokidar", + "@npm//elastic-apm-node", + "@npm//execa", + "@npm//getopts", + "@npm//lodash", + "@npm//moment", + "@npm//rxjs", + "@npm//supertest", +] + +TYPES_DEPS = [ + "@npm//@types/hapi__h2o2", + "@npm//@types/hapi__hapi", + "@npm//@types/getopts", + "@npm//@types/jest", + "@npm//@types/lodash", + "@npm//@types/node", + "@npm//@types/supertest", +] + +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 = DEPS + [":tsc"], + 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-cli-dev-mode/package.json b/packages/kbn-cli-dev-mode/package.json index dd491de55c075..ac86ee2ef369b 100644 --- a/packages/kbn-cli-dev-mode/package.json +++ b/packages/kbn-cli-dev-mode/package.json @@ -5,15 +5,7 @@ "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", "private": true, - "scripts": { - "build": "../../node_modules/.bin/tsc", - "kbn:bootstrap": "yarn build", - "kbn:watch": "yarn build --watch" - }, "kibana": { "devOnly": true - }, - "dependencies": { - "@kbn/optimizer": "link:../kbn-optimizer" } } \ No newline at end of file diff --git a/packages/kbn-cli-dev-mode/tsconfig.json b/packages/kbn-cli-dev-mode/tsconfig.json index 4436d27dbff88..0c71ad8e245d4 100644 --- a/packages/kbn-cli-dev-mode/tsconfig.json +++ b/packages/kbn-cli-dev-mode/tsconfig.json @@ -1,10 +1,11 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, + "incremental": true, "outDir": "./target", "declaration": true, "declarationMap": true, + "rootDir": "./src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-cli-dev-mode/src", "types": [ diff --git a/packages/kbn-i18n/src/react/index.tsx b/packages/kbn-i18n/src/react/index.tsx index 08fa7173978d9..bc0a164d412af 100644 --- a/packages/kbn-i18n/src/react/index.tsx +++ b/packages/kbn-i18n/src/react/index.tsx @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +// eslint-disable-next-line @kbn/eslint/module_migration import { InjectedIntl as _InjectedIntl, InjectedIntlProps as _InjectedIntlProps } from 'react-intl'; export type { InjectedIntl, InjectedIntlProps } from 'react-intl'; diff --git a/packages/kbn-i18n/src/react/provider.tsx b/packages/kbn-i18n/src/react/provider.tsx index 2d88125291aa0..fc0f6769c7160 100644 --- a/packages/kbn-i18n/src/react/provider.tsx +++ b/packages/kbn-i18n/src/react/provider.tsx @@ -8,6 +8,8 @@ import * as PropTypes from 'prop-types'; import * as React from 'react'; + +// eslint-disable-next-line @kbn/eslint/module_migration import { IntlProvider } from 'react-intl'; import * as i18n from '../core'; diff --git a/packages/kbn-interpreter/BUILD.bazel b/packages/kbn-interpreter/BUILD.bazel index 4492faabfdf81..c29faf65638ca 100644 --- a/packages/kbn-interpreter/BUILD.bazel +++ b/packages/kbn-interpreter/BUILD.bazel @@ -1,5 +1,5 @@ load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") -load("@npm//pegjs:index.bzl", "pegjs") +load("@npm//peggy:index.bzl", "peggy") load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") PKG_BASE_NAME = "kbn-interpreter" @@ -37,10 +37,10 @@ TYPES_DEPS = [ DEPS = SRC_DEPS + TYPES_DEPS -pegjs( +peggy( name = "grammar", data = [ - ":grammar/grammar.pegjs" + ":grammar/grammar.peggy" ], output_dir = True, args = [ @@ -48,7 +48,7 @@ pegjs( "expression,argument", "-o", "$(@D)/index.js", - "./%s/grammar/grammar.pegjs" % package_name() + "./%s/grammar/grammar.peggy" % package_name() ], ) diff --git a/packages/kbn-interpreter/grammar/grammar.pegjs b/packages/kbn-interpreter/grammar/grammar.peggy similarity index 100% rename from packages/kbn-interpreter/grammar/grammar.pegjs rename to packages/kbn-interpreter/grammar/grammar.peggy diff --git a/packages/kbn-optimizer/BUILD.bazel b/packages/kbn-optimizer/BUILD.bazel new file mode 100644 index 0000000000000..3809c2b33d500 --- /dev/null +++ b/packages/kbn-optimizer/BUILD.bazel @@ -0,0 +1,120 @@ +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-optimizer" +PKG_REQUIRE_NAME = "@kbn/optimizer" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/__fixtures__/**", + "**/__snapshots__/**", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "limits.yml", + "package.json", + "postcss.config.js", + "README.md" +] + +SRC_DEPS = [ + "//packages/kbn-config", + "//packages/kbn-dev-utils", + "//packages/kbn-std", + "//packages/kbn-ui-shared-deps", + "//packages/kbn-utils", + "@npm//chalk", + "@npm//clean-webpack-plugin", + "@npm//compression-webpack-plugin", + "@npm//cpy", + "@npm//del", + "@npm//execa", + "@npm//jest-diff", + "@npm//json-stable-stringify", + "@npm//lmdb-store", + "@npm//loader-utils", + "@npm//node-sass", + "@npm//normalize-path", + "@npm//pirates", + "@npm//resize-observer-polyfill", + "@npm//rxjs", + "@npm//source-map-support", + "@npm//watchpack", + "@npm//webpack", + "@npm//webpack-merge", + "@npm//webpack-sources", + "@npm//zlib" +] + +TYPES_DEPS = [ + "@npm//@types/compression-webpack-plugin", + "@npm//@types/jest", + "@npm//@types/json-stable-stringify", + "@npm//@types/loader-utils", + "@npm//@types/node", + "@npm//@types/normalize-path", + "@npm//@types/source-map-support", + "@npm//@types/watchpack", + "@npm//@types/webpack", + "@npm//@types/webpack-merge", + "@npm//@types/webpack-sources", +] + +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 = DEPS + [":tsc"], + 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-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index f9127e4629f43..c6960621359c7 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -67,7 +67,7 @@ pageLoadAssetSize: searchprofiler: 67080 security: 95864 securityOss: 30806 - securitySolution: 76000 + securitySolution: 217673 share: 99061 snapshotRestore: 79032 spaces: 57868 @@ -107,7 +107,7 @@ pageLoadAssetSize: dataVisualizer: 27530 banners: 17946 mapsEms: 26072 - timelines: 28613 + timelines: 230410 screenshotMode: 17856 visTypePie: 35583 cases: 144442 diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index a6c8284ad15f6..d23512f7c418d 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/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": "../../node_modules/.bin/tsc", - "kbn:bootstrap": "yarn build", - "kbn:watch": "yarn build --watch" - } + "types": "./target/index.d.ts" } \ No newline at end of file diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index c175979f0e820..1f1e33d3dda7c 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -123,7 +123,7 @@ exports[`prepares assets for distribution: metrics.json 1`] = ` \\"group\\": \\"page load bundle size\\", \\"id\\": \\"foo\\", \\"value\\": 4627, - \\"limitConfigPath\\": \\"packages/kbn-optimizer/limits.yml\\" + \\"limitConfigPath\\": \\"node_modules/@kbn/optimizer/limits.yml\\" }, { \\"group\\": \\"async chunks size\\", diff --git a/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts b/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts index 92875d3f69e46..d9e1bee22557b 100644 --- a/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts +++ b/packages/kbn-optimizer/src/worker/bundle_metrics_plugin.ts @@ -79,7 +79,7 @@ export class BundleMetricsPlugin { id: bundle.id, value: entry.size, limit: bundle.pageLoadAssetSizeLimit, - limitConfigPath: `packages/kbn-optimizer/limits.yml`, + limitConfigPath: `node_modules/@kbn/optimizer/limits.yml`, }, { group: `async chunks size`, diff --git a/packages/kbn-optimizer/tsconfig.json b/packages/kbn-optimizer/tsconfig.json index f2d508cf14a55..76beaf7689fd4 100644 --- a/packages/kbn-optimizer/tsconfig.json +++ b/packages/kbn-optimizer/tsconfig.json @@ -1,10 +1,11 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, + "incremental": true, "outDir": "./target", "declaration": true, "declarationMap": true, + "rootDir": "./src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-optimizer/src" }, diff --git a/packages/kbn-plugin-helpers/BUILD.bazel b/packages/kbn-plugin-helpers/BUILD.bazel new file mode 100644 index 0000000000000..1a1f3453f768a --- /dev/null +++ b/packages/kbn-plugin-helpers/BUILD.bazel @@ -0,0 +1,97 @@ + +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-plugin-helpers" +PKG_REQUIRE_NAME = "@kbn/plugin-helpers" + +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-dev-utils", + "//packages/kbn-optimizer", + "//packages/kbn-utils", + "@npm//del", + "@npm//execa", + "@npm//extract-zip", + "@npm//globby", + "@npm//gulp-zip", + "@npm//inquirer", + "@npm//load-json-file", + "@npm//vinyl-fs", +] + +TYPES_DEPS = [ + "@npm//@types/extract-zip", + "@npm//@types/gulp-zip", + "@npm//@types/inquirer", + "@npm//@types/jest", + "@npm//@types/node", + "@npm//@types/vinyl-fs", +] + +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 = DEPS + [":tsc"], + 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-plugin-helpers/package.json b/packages/kbn-plugin-helpers/package.json index 2d642d9ede13b..1f4df52a03304 100644 --- a/packages/kbn-plugin-helpers/package.json +++ b/packages/kbn-plugin-helpers/package.json @@ -11,12 +11,5 @@ "types": "target/index.d.ts", "bin": { "plugin-helpers": "bin/plugin-helpers.js" - }, - "scripts": { - "kbn:bootstrap": "rm -rf target && ../../node_modules/.bin/tsc", - "kbn:watch": "../../node_modules/.bin/tsc --watch" - }, - "dependencies": { - "@kbn/optimizer": "link:../kbn-optimizer" } } \ No newline at end of file diff --git a/packages/kbn-plugin-helpers/tsconfig.json b/packages/kbn-plugin-helpers/tsconfig.json index 87d11843f398a..4348f1e1a7516 100644 --- a/packages/kbn-plugin-helpers/tsconfig.json +++ b/packages/kbn-plugin-helpers/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, + "incremental": true, "outDir": "target", "target": "ES2018", "declaration": true, diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index e455f487d1384..5be9dff630ed5 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -63827,6 +63827,7 @@ function getProjectPaths({ projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'test/plugin_functional/plugins/*')); projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'test/interpreter_functional/plugins/*')); + projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'test/server_integration/__fixtures__/plugins/*')); projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'examples/*')); if (!ossOnly) { diff --git a/packages/kbn-pm/src/config.ts b/packages/kbn-pm/src/config.ts index a11b2ad9c72c3..666a2fed7a33c 100644 --- a/packages/kbn-pm/src/config.ts +++ b/packages/kbn-pm/src/config.ts @@ -31,6 +31,7 @@ export function getProjectPaths({ rootPath, ossOnly, skipKibanaPlugins }: Option // correct and the expect behavior. projectPaths.push(resolve(rootPath, 'test/plugin_functional/plugins/*')); projectPaths.push(resolve(rootPath, 'test/interpreter_functional/plugins/*')); + projectPaths.push(resolve(rootPath, 'test/server_integration/__fixtures__/plugins/*')); projectPaths.push(resolve(rootPath, 'examples/*')); if (!ossOnly) { diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts index f75f0dcebf4f6..1909bcb1bcc2e 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts @@ -42,6 +42,7 @@ export interface UseExceptionListsProps { notifications: NotificationsStart; pagination?: Pagination; showTrustedApps: boolean; + showEventFilters: boolean; } export interface UseExceptionListProps { diff --git a/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts b/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts index a9a93aa8df49a..0bd4c6c705668 100644 --- a/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts +++ b/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts @@ -28,6 +28,7 @@ export type ReturnExceptionLists = [boolean, ExceptionListSchema[], Pagination, * @param namespaceTypes spaces to be searched * @param notifications kibana service for displaying toasters * @param showTrustedApps boolean - include/exclude trusted app lists + * @param showEventFilters boolean - include/exclude event filters lists * @param pagination * */ @@ -43,6 +44,7 @@ export const useExceptionLists = ({ namespaceTypes, notifications, showTrustedApps = false, + showEventFilters = false, }: UseExceptionListsProps): ReturnExceptionLists => { const [exceptionLists, setExceptionLists] = useState<ExceptionListSchema[]>([]); const [paginationInfo, setPagination] = useState<Pagination>(pagination); @@ -51,8 +53,9 @@ export const useExceptionLists = ({ const namespaceTypesAsString = useMemo(() => namespaceTypes.join(','), [namespaceTypes]); const filters = useMemo( - (): string => getFilters(filterOptions, namespaceTypes, showTrustedApps), - [namespaceTypes, filterOptions, showTrustedApps] + (): string => + getFilters({ filters: filterOptions, namespaceTypes, showTrustedApps, showEventFilters }), + [namespaceTypes, filterOptions, showTrustedApps, showEventFilters] ); useEffect(() => { diff --git a/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.test.ts b/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.test.ts new file mode 100644 index 0000000000000..934a9cbff56a6 --- /dev/null +++ b/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.test.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getEventFiltersFilter } from '.'; + +describe('getEventFiltersFilter', () => { + test('it returns filter to search for "exception-list" namespace trusted apps', () => { + const filter = getEventFiltersFilter(true, ['exception-list']); + + expect(filter).toEqual('(exception-list.attributes.list_id: endpoint_event_filters*)'); + }); + + test('it returns filter to search for "exception-list" and "agnostic" namespace trusted apps', () => { + const filter = getEventFiltersFilter(true, ['exception-list', 'exception-list-agnostic']); + + expect(filter).toEqual( + '(exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it returns filter to exclude "exception-list" namespace trusted apps', () => { + const filter = getEventFiltersFilter(false, ['exception-list']); + + expect(filter).toEqual('(not exception-list.attributes.list_id: endpoint_event_filters*)'); + }); + + test('it returns filter to exclude "exception-list" and "agnostic" namespace trusted apps', () => { + const filter = getEventFiltersFilter(false, ['exception-list', 'exception-list-agnostic']); + + expect(filter).toEqual( + '(not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); +}); diff --git a/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.ts b/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.ts new file mode 100644 index 0000000000000..7e55073228fca --- /dev/null +++ b/packages/kbn-securitysolution-list-utils/src/get_event_filters_filter/index.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '@kbn/securitysolution-list-constants'; +import { SavedObjectType } from '../types'; + +export const getEventFiltersFilter = ( + showEventFilter: boolean, + namespaceTypes: SavedObjectType[] +): string => { + if (showEventFilter) { + const filters = namespaceTypes.map((namespace) => { + return `${namespace}.attributes.list_id: ${ENDPOINT_EVENT_FILTERS_LIST_ID}*`; + }); + return `(${filters.join(' OR ')})`; + } else { + const filters = namespaceTypes.map((namespace) => { + return `not ${namespace}.attributes.list_id: ${ENDPOINT_EVENT_FILTERS_LIST_ID}*`; + }); + return `(${filters.join(' AND ')})`; + } +}; diff --git a/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts b/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts index 327a29dc1b987..bfaad52ee8147 100644 --- a/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts +++ b/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts @@ -11,106 +11,318 @@ import { getFilters } from '.'; describe('getFilters', () => { describe('single', () => { test('it properly formats when no filters passed and "showTrustedApps" is false', () => { - const filter = getFilters({}, ['single'], false); + const filter = getFilters({ + filters: {}, + namespaceTypes: ['single'], + showTrustedApps: false, + showEventFilters: false, + }); - expect(filter).toEqual('(not exception-list.attributes.list_id: endpoint_trusted_apps*)'); + expect(filter).toEqual( + '(not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)' + ); }); test('it properly formats when no filters passed and "showTrustedApps" is true', () => { - const filter = getFilters({}, ['single'], true); + const filter = getFilters({ + filters: {}, + namespaceTypes: ['single'], + showTrustedApps: true, + showEventFilters: false, + }); - expect(filter).toEqual('(exception-list.attributes.list_id: endpoint_trusted_apps*)'); + expect(filter).toEqual( + '(exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)' + ); }); test('it properly formats when filters passed and "showTrustedApps" is false', () => { - const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['single'], false); + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['single'], + showTrustedApps: false, + showEventFilters: false, + }); expect(filter).toEqual( - '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)' ); }); test('it if filters passed and "showTrustedApps" is true', () => { - const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['single'], true); + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['single'], + showTrustedApps: true, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when no filters passed and "showEventFilters" is false', () => { + const filter = getFilters({ + filters: {}, + namespaceTypes: ['single'], + showTrustedApps: false, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when no filters passed and "showEventFilters" is true', () => { + const filter = getFilters({ + filters: {}, + namespaceTypes: ['single'], + showTrustedApps: false, + showEventFilters: true, + }); + + expect(filter).toEqual( + '(not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when filters passed and "showEventFilters" is false', () => { + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['single'], + showTrustedApps: false, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it if filters passed and "showEventFilters" is true', () => { + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['single'], + showTrustedApps: false, + showEventFilters: true, + }); expect(filter).toEqual( - '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters*)' ); }); }); describe('agnostic', () => { test('it properly formats when no filters passed and "showTrustedApps" is false', () => { - const filter = getFilters({}, ['agnostic'], false); + const filter = getFilters({ + filters: {}, + namespaceTypes: ['agnostic'], + showTrustedApps: false, + showEventFilters: false, + }); expect(filter).toEqual( - '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); }); test('it properly formats when no filters passed and "showTrustedApps" is true', () => { - const filter = getFilters({}, ['agnostic'], true); + const filter = getFilters({ + filters: {}, + namespaceTypes: ['agnostic'], + showTrustedApps: true, + showEventFilters: false, + }); expect(filter).toEqual( - '(exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); }); test('it properly formats when filters passed and "showTrustedApps" is false', () => { - const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['agnostic'], false); + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['agnostic'], + showTrustedApps: false, + showEventFilters: false, + }); expect(filter).toEqual( - '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); }); test('it if filters passed and "showTrustedApps" is true', () => { - const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['agnostic'], true); + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['agnostic'], + showTrustedApps: true, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when no filters passed and "showEventFilters" is false', () => { + const filter = getFilters({ + filters: {}, + namespaceTypes: ['agnostic'], + showTrustedApps: false, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when no filters passed and "showEventFilters" is true', () => { + const filter = getFilters({ + filters: {}, + namespaceTypes: ['agnostic'], + showTrustedApps: false, + showEventFilters: true, + }); + + expect(filter).toEqual( + '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when filters passed and "showEventFilters" is false', () => { + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['agnostic'], + showTrustedApps: false, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it if filters passed and "showEventFilters" is true', () => { + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['agnostic'], + showTrustedApps: false, + showEventFilters: true, + }); expect(filter).toEqual( - '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); }); }); describe('single, agnostic', () => { test('it properly formats when no filters passed and "showTrustedApps" is false', () => { - const filter = getFilters({}, ['single', 'agnostic'], false); + const filter = getFilters({ + filters: {}, + namespaceTypes: ['single', 'agnostic'], + showTrustedApps: false, + showEventFilters: false, + }); expect(filter).toEqual( - '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); }); test('it properly formats when no filters passed and "showTrustedApps" is true', () => { - const filter = getFilters({}, ['single', 'agnostic'], true); + const filter = getFilters({ + filters: {}, + namespaceTypes: ['single', 'agnostic'], + showTrustedApps: true, + showEventFilters: false, + }); expect(filter).toEqual( - '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); }); test('it properly formats when filters passed and "showTrustedApps" is false', () => { - const filter = getFilters( - { created_by: 'moi', name: 'Sample' }, - ['single', 'agnostic'], - false - ); + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['single', 'agnostic'], + showTrustedApps: false, + showEventFilters: false, + }); expect(filter).toEqual( - '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); }); test('it properly formats when filters passed and "showTrustedApps" is true', () => { - const filter = getFilters( - { created_by: 'moi', name: 'Sample' }, - ['single', 'agnostic'], - true + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['single', 'agnostic'], + showTrustedApps: true, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when no filters passed and "showEventFilters" is false', () => { + const filter = getFilters({ + filters: {}, + namespaceTypes: ['single', 'agnostic'], + showTrustedApps: false, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when no filters passed and "showEventFilters" is true', () => { + const filter = getFilters({ + filters: {}, + namespaceTypes: ['single', 'agnostic'], + showTrustedApps: false, + showEventFilters: true, + }); + + expect(filter).toEqual( + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + ); + }); + + test('it properly formats when filters passed and "showEventFilters" is false', () => { + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['single', 'agnostic'], + showTrustedApps: false, + showEventFilters: false, + }); + + expect(filter).toEqual( + '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); + }); + + test('it properly formats when filters passed and "showEventFilters" is true', () => { + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['single', 'agnostic'], + showTrustedApps: false, + showEventFilters: true, + }); expect(filter).toEqual( - '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' ); }); }); diff --git a/packages/kbn-securitysolution-list-utils/src/get_filters/index.ts b/packages/kbn-securitysolution-list-utils/src/get_filters/index.ts index c9dd6ccae484c..238ae5541343c 100644 --- a/packages/kbn-securitysolution-list-utils/src/get_filters/index.ts +++ b/packages/kbn-securitysolution-list-utils/src/get_filters/index.ts @@ -10,14 +10,26 @@ import { ExceptionListFilter, NamespaceType } from '@kbn/securitysolution-io-ts- import { getGeneralFilters } from '../get_general_filters'; import { getSavedObjectTypes } from '../get_saved_object_types'; import { getTrustedAppsFilter } from '../get_trusted_apps_filter'; +import { getEventFiltersFilter } from '../get_event_filters_filter'; -export const getFilters = ( - filters: ExceptionListFilter, - namespaceTypes: NamespaceType[], - showTrustedApps: boolean -): string => { +export interface GetFiltersParams { + filters: ExceptionListFilter; + namespaceTypes: NamespaceType[]; + showTrustedApps: boolean; + showEventFilters: boolean; +} + +export const getFilters = ({ + filters, + namespaceTypes, + showTrustedApps, + showEventFilters, +}: GetFiltersParams): string => { const namespaces = getSavedObjectTypes({ namespaceType: namespaceTypes }); const generalFilters = getGeneralFilters(filters, namespaces); const trustedAppsFilter = getTrustedAppsFilter(showTrustedApps, namespaces); - return [generalFilters, trustedAppsFilter].filter((filter) => filter.trim() !== '').join(' AND '); + const eventFiltersFilter = getEventFiltersFilter(showEventFilters, namespaces); + return [generalFilters, trustedAppsFilter, eventFiltersFilter] + .filter((filter) => filter.trim() !== '') + .join(' AND '); }; diff --git a/packages/kbn-securitysolution-t-grid/BUILD.bazel b/packages/kbn-securitysolution-t-grid/BUILD.bazel new file mode 100644 index 0000000000000..5cf1081bdd32e --- /dev/null +++ b/packages/kbn-securitysolution-t-grid/BUILD.bazel @@ -0,0 +1,125 @@ +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-securitysolution-t-grid" + +PKG_REQUIRE_NAME = "@kbn/securitysolution-t-grid" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + "src/**/*.tsx", + ], + exclude = [ + "**/*.test.*", + "**/*.mock.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "react/package.json", + "package.json", + "README.md", +] + +SRC_DEPS = [ + "//packages/kbn-babel-preset", + "//packages/kbn-dev-utils", + "//packages/kbn-i18n", + "@npm//@babel/core", + "@npm//babel-loader", + "@npm//enzyme", + "@npm//jest", + "@npm//lodash", + "@npm//react", + "@npm//react-beautiful-dnd", + "@npm//tslib", +] + +TYPES_DEPS = [ + "@npm//typescript", + "@npm//@types/enzyme", + "@npm//@types/jest", + "@npm//@types/lodash", + "@npm//@types/node", + "@npm//@types/react", + "@npm//@types/react-beautiful-dnd", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_config( + name = "tsconfig_browser", + src = "tsconfig.browser.json", + deps = [ + "//:tsconfig.base.json", + "//:tsconfig.browser.json", + ], +) + +ts_project( + name = "tsc", + args = ["--pretty"], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_dir = "target_types", + declaration_map = True, + incremental = True, + out_dir = "target_node", + root_dir = "src", + source_map = True, + tsconfig = ":tsconfig", +) + +ts_project( + name = "tsc_browser", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + allow_js = True, + declaration = False, + incremental = True, + out_dir = "target_web", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig_browser", +) + +js_library( + name = PKG_BASE_NAME, + package_name = PKG_REQUIRE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + visibility = ["//visibility:public"], + deps = [":tsc", ":tsc_browser"] + DEPS, +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ], +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-securitysolution-t-grid/README.md b/packages/kbn-securitysolution-t-grid/README.md new file mode 100644 index 0000000000000..a49669c81689a --- /dev/null +++ b/packages/kbn-securitysolution-t-grid/README.md @@ -0,0 +1,3 @@ +# kbn-securitysolution-t-grid + +We do not want to create circular dependencies between security_solution and timelines plugins. Therefore , we will use this packages to share components between these two plugins. diff --git a/packages/kbn-securitysolution-t-grid/babel.config.js b/packages/kbn-securitysolution-t-grid/babel.config.js new file mode 100644 index 0000000000000..b4a118df51af5 --- /dev/null +++ b/packages/kbn-securitysolution-t-grid/babel.config.js @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + env: { + web: { + presets: ['@kbn/babel-preset/webpack_preset'], + }, + node: { + presets: ['@kbn/babel-preset/node_preset'], + }, + }, + ignore: ['**/*.test.ts', '**/*.test.tsx'], +}; diff --git a/packages/kbn-securitysolution-t-grid/jest.config.js b/packages/kbn-securitysolution-t-grid/jest.config.js new file mode 100644 index 0000000000000..21e7d2d71b61a --- /dev/null +++ b/packages/kbn-securitysolution-t-grid/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['<rootDir>/packages/kbn-securitysolution-t-grid'], +}; diff --git a/packages/kbn-securitysolution-t-grid/package.json b/packages/kbn-securitysolution-t-grid/package.json new file mode 100644 index 0000000000000..68d3a8c71e7ca --- /dev/null +++ b/packages/kbn-securitysolution-t-grid/package.json @@ -0,0 +1,10 @@ +{ + "name": "@kbn/securitysolution-t-grid", + "version": "1.0.0", + "description": "security solution t-grid packages will allow sharing components between timelines and security_solution plugin until we transfer all functionality to timelines plugin", + "license": "SSPL-1.0 OR Elastic License 2.0", + "browser": "./target_web/browser.js", + "main": "./target_node/index.js", + "types": "./target_types/index.d.ts", + "private": true +} diff --git a/packages/kbn-securitysolution-t-grid/react/package.json b/packages/kbn-securitysolution-t-grid/react/package.json new file mode 100644 index 0000000000000..c29ddd45f084d --- /dev/null +++ b/packages/kbn-securitysolution-t-grid/react/package.json @@ -0,0 +1,5 @@ +{ + "browser": "../target_web/react", + "main": "../target_node/react", + "types": "../target_types/react/index.d.ts" +} \ No newline at end of file diff --git a/packages/kbn-securitysolution-t-grid/src/constants/index.ts b/packages/kbn-securitysolution-t-grid/src/constants/index.ts new file mode 100644 index 0000000000000..c03c0093d9839 --- /dev/null +++ b/packages/kbn-securitysolution-t-grid/src/constants/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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const HIGHLIGHTED_DROP_TARGET_CLASS_NAME = 'highlighted-drop-target'; +export const EMPTY_PROVIDERS_GROUP_CLASS_NAME = 'empty-providers-group'; + +/** The draggable will move this many pixels via the keyboard when the arrow key is pressed */ +export const KEYBOARD_DRAG_OFFSET = 20; + +export const DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME = 'draggable-keyboard-wrapper'; + +export const ROW_RENDERER_CLASS_NAME = 'row-renderer'; + +export const NOTES_CONTAINER_CLASS_NAME = 'notes-container'; + +export const NOTE_CONTENT_CLASS_NAME = 'note-content'; + +/** This class is added to the document body while dragging */ +export const IS_DRAGGING_CLASS_NAME = 'is-dragging'; + +export const HOVER_ACTIONS_ALWAYS_SHOW_CLASS_NAME = 'hover-actions-always-show'; diff --git a/packages/kbn-securitysolution-t-grid/src/index.ts b/packages/kbn-securitysolution-t-grid/src/index.ts new file mode 100644 index 0000000000000..0c2e9a7dbea8b --- /dev/null +++ b/packages/kbn-securitysolution-t-grid/src/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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './constants'; +export * from './utils'; +export * from './mock'; diff --git a/packages/kbn-securitysolution-t-grid/src/mock/index.ts b/packages/kbn-securitysolution-t-grid/src/mock/index.ts new file mode 100644 index 0000000000000..dc1b63dfc33b0 --- /dev/null +++ b/packages/kbn-securitysolution-t-grid/src/mock/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './mock_event_details'; diff --git a/x-pack/plugins/security_solution/common/utils/mock_event_details.ts b/packages/kbn-securitysolution-t-grid/src/mock/mock_event_details.ts similarity index 97% rename from x-pack/plugins/security_solution/common/utils/mock_event_details.ts rename to packages/kbn-securitysolution-t-grid/src/mock/mock_event_details.ts index 7dc257ebb3fef..167fc9dd17a2a 100644 --- a/x-pack/plugins/security_solution/common/utils/mock_event_details.ts +++ b/packages/kbn-securitysolution-t-grid/src/mock/mock_event_details.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ export const eventHit = { diff --git a/packages/kbn-securitysolution-t-grid/src/utils/api/index.ts b/packages/kbn-securitysolution-t-grid/src/utils/api/index.ts new file mode 100644 index 0000000000000..34e448419693b --- /dev/null +++ b/packages/kbn-securitysolution-t-grid/src/utils/api/index.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { has } from 'lodash/fp'; + +export interface AppError extends Error { + body: { + message: string; + }; +} + +export interface KibanaError extends AppError { + body: { + message: string; + statusCode: number; + }; +} + +export interface SecurityAppError extends AppError { + body: { + message: string; + status_code: number; + }; +} + +export const isKibanaError = (error: unknown): error is KibanaError => + has('message', error) && has('body.message', error) && has('body.statusCode', error); + +export const isSecurityAppError = (error: unknown): error is SecurityAppError => + has('message', error) && has('body.message', error) && has('body.status_code', error); + +export const isAppError = (error: unknown): error is AppError => + isKibanaError(error) || isSecurityAppError(error); + +export const isNotFoundError = (error: unknown) => + (isKibanaError(error) && error.body.statusCode === 404) || + (isSecurityAppError(error) && error.body.status_code === 404); diff --git a/packages/kbn-securitysolution-t-grid/src/utils/drag_and_drop/index.ts b/packages/kbn-securitysolution-t-grid/src/utils/drag_and_drop/index.ts new file mode 100644 index 0000000000000..91b2e88d97358 --- /dev/null +++ b/packages/kbn-securitysolution-t-grid/src/utils/drag_and_drop/index.ts @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { DropResult } from 'react-beautiful-dnd'; + +export const draggableIdPrefix = 'draggableId'; + +export const droppableIdPrefix = 'droppableId'; + +export const draggableContentPrefix = `${draggableIdPrefix}.content.`; + +export const draggableTimelineProvidersPrefix = `${draggableIdPrefix}.timelineProviders.`; + +export const draggableFieldPrefix = `${draggableIdPrefix}.field.`; + +export const droppableContentPrefix = `${droppableIdPrefix}.content.`; + +export const droppableFieldPrefix = `${droppableIdPrefix}.field.`; + +export const droppableTimelineProvidersPrefix = `${droppableIdPrefix}.timelineProviders.`; + +export const droppableTimelineColumnsPrefix = `${droppableIdPrefix}.timelineColumns.`; + +export const droppableTimelineFlyoutBottomBarPrefix = `${droppableIdPrefix}.flyoutButton.`; + +export const getDraggableId = (dataProviderId: string): string => + `${draggableContentPrefix}${dataProviderId}`; + +export const getDraggableFieldId = ({ + contextId, + fieldId, +}: { + contextId: string; + fieldId: string; +}): string => `${draggableFieldPrefix}${escapeContextId(contextId)}.${escapeFieldId(fieldId)}`; + +export const getTimelineProviderDroppableId = ({ + groupIndex, + timelineId, +}: { + groupIndex: number; + timelineId: string; +}): string => `${droppableTimelineProvidersPrefix}${timelineId}.group.${groupIndex}`; + +export const getTimelineProviderDraggableId = ({ + dataProviderId, + groupIndex, + timelineId, +}: { + dataProviderId: string; + groupIndex: number; + timelineId: string; +}): string => + `${draggableTimelineProvidersPrefix}${timelineId}.group.${groupIndex}.${dataProviderId}`; + +export const getDroppableId = (visualizationPlaceholderId: string): string => + `${droppableContentPrefix}${visualizationPlaceholderId}`; + +export const sourceIsContent = (result: DropResult): boolean => + result.source.droppableId.startsWith(droppableContentPrefix); + +export const sourceAndDestinationAreSameTimelineProviders = (result: DropResult): boolean => { + const regex = /^droppableId\.timelineProviders\.(\S+)\./; + const sourceMatches = result.source.droppableId.match(regex) || []; + const destinationMatches = + (result.destination && result.destination.droppableId.match(regex)) || []; + + return ( + sourceMatches.length >= 2 && + destinationMatches.length >= 2 && + sourceMatches[1] === destinationMatches[1] + ); +}; + +export const draggableIsContent = (result: DropResult | { draggableId: string }): boolean => + result.draggableId.startsWith(draggableContentPrefix); + +export const draggableIsField = (result: DropResult | { draggableId: string }): boolean => + result.draggableId.startsWith(draggableFieldPrefix); + +export const reasonIsDrop = (result: DropResult): boolean => result.reason === 'DROP'; + +export const destinationIsTimelineProviders = (result: DropResult): boolean => + result.destination != null && + result.destination.droppableId.startsWith(droppableTimelineProvidersPrefix); + +export const destinationIsTimelineColumns = (result: DropResult): boolean => + result.destination != null && + result.destination.droppableId.startsWith(droppableTimelineColumnsPrefix); + +export const destinationIsTimelineButton = (result: DropResult): boolean => + result.destination != null && + result.destination.droppableId.startsWith(droppableTimelineFlyoutBottomBarPrefix); + +export const getProviderIdFromDraggable = (result: DropResult): string => + result.draggableId.substring(result.draggableId.lastIndexOf('.') + 1); + +export const getFieldIdFromDraggable = (result: DropResult): string => + unEscapeFieldId(result.draggableId.substring(result.draggableId.lastIndexOf('.') + 1)); + +export const escapeDataProviderId = (path: string) => path.replace(/\./g, '_'); + +export const escapeContextId = (path: string) => path.replace(/\./g, '_'); + +export const escapeFieldId = (path: string) => path.replace(/\./g, '!!!DOT!!!'); + +export const unEscapeFieldId = (path: string) => path.replace(/!!!DOT!!!/g, '.'); + +export const providerWasDroppedOnTimeline = (result: DropResult): boolean => + reasonIsDrop(result) && + draggableIsContent(result) && + sourceIsContent(result) && + destinationIsTimelineProviders(result); + +export const userIsReArrangingProviders = (result: DropResult): boolean => + reasonIsDrop(result) && sourceAndDestinationAreSameTimelineProviders(result); + +export const fieldWasDroppedOnTimelineColumns = (result: DropResult): boolean => + reasonIsDrop(result) && draggableIsField(result) && destinationIsTimelineColumns(result); + +/** + * Prevents fields from being dragged or dropped to any area other than column + * header drop zone in the timeline + */ +export const DRAG_TYPE_FIELD = 'drag-type-field'; + +/** This class is added to the document body while timeline field dragging */ +export const IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME = 'is-timeline-field-dragging'; diff --git a/packages/kbn-securitysolution-t-grid/src/utils/index.ts b/packages/kbn-securitysolution-t-grid/src/utils/index.ts new file mode 100644 index 0000000000000..39629a990c539 --- /dev/null +++ b/packages/kbn-securitysolution-t-grid/src/utils/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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './api'; +export * from './drag_and_drop'; diff --git a/packages/kbn-securitysolution-t-grid/tsconfig.browser.json b/packages/kbn-securitysolution-t-grid/tsconfig.browser.json new file mode 100644 index 0000000000000..a5183ba4fd457 --- /dev/null +++ b/packages/kbn-securitysolution-t-grid/tsconfig.browser.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.browser.json", + "compilerOptions": { + "allowJs": true, + "incremental": true, + "outDir": "./target_web", + "declaration": false, + "isolatedModules": true, + "sourceMap": true, + "sourceRoot": "../../../../../packages/kbn-securitysolution-t-grid/src", + "types": [ + "jest", + "node" + ], + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + ], + "exclude": [ + "**/__fixtures__/**/*" + ] +} diff --git a/packages/kbn-securitysolution-t-grid/tsconfig.json b/packages/kbn-securitysolution-t-grid/tsconfig.json new file mode 100644 index 0000000000000..8cda578edede4 --- /dev/null +++ b/packages/kbn-securitysolution-t-grid/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "incremental": true, + "outDir": "target", + "rootDir": "src", + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-securitysolution-t-grid/src", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js index 225f93d487823..5baff607704c7 100644 --- a/packages/kbn-test/jest-preset.js +++ b/packages/kbn-test/jest-preset.js @@ -94,7 +94,7 @@ module.exports = { transformIgnorePatterns: [ // ignore all node_modules except monaco-editor and react-monaco-editor which requires babel transforms to handle dynamic import() // since ESM modules are not natively supported in Jest yet (https://github.com/facebook/jest/issues/4842) - '[/\\\\]node_modules(?)[/\\\\].+\\.js$', + '[/\\\\]node_modules(?)[/\\\\].+\\.js$', 'packages/kbn-pm/dist/index.js', ], diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json index 275d9fac73c58..aaff513f1591f 100644 --- a/packages/kbn-test/package.json +++ b/packages/kbn-test/package.json @@ -12,8 +12,5 @@ }, "kibana": { "devOnly": true - }, - "dependencies": { - "@kbn/optimizer": "link:../kbn-optimizer" } } \ No newline at end of file diff --git a/packages/kbn-test/src/jest/setup/babel_polyfill.js b/packages/kbn-test/src/jest/setup/babel_polyfill.js index d112e4d4fcb39..7dda4cceec65c 100644 --- a/packages/kbn-test/src/jest/setup/babel_polyfill.js +++ b/packages/kbn-test/src/jest/setup/babel_polyfill.js @@ -9,4 +9,4 @@ // Note: In theory importing the polyfill should not be needed, as Babel should // include the necessary polyfills when using `@babel/preset-env`, but for some // reason it did not work. See https://github.com/elastic/kibana/issues/14506 -import '@kbn/optimizer/src/node/polyfill'; +import '@kbn/optimizer/target/node/polyfill'; diff --git a/packages/kbn-tinymath/grammar/grammar.peggy b/packages/kbn-tinymath/grammar/grammar.peggy index 1c6f8c3334c23..414bc2fa11cb7 100644 --- a/packages/kbn-tinymath/grammar/grammar.peggy +++ b/packages/kbn-tinymath/grammar/grammar.peggy @@ -43,7 +43,7 @@ Literal "literal" // Quoted variables are interpreted as strings // but unquoted variables are more restrictive Variable - = _ [\'] chars:(ValidChar / Space / [\"])* [\'] _ { + = _ '"' chars:("\\\"" { return "\""; } / [^"])* '"' _ { return { type: 'variable', value: chars.join(''), @@ -51,7 +51,7 @@ Variable text: text() }; } - / _ [\"] chars:(ValidChar / Space / [\'])* [\"] _ { + / _ "'" chars:("\\\'" { return "\'"; } / [^'])* "'" _ { return { type: 'variable', value: chars.join(''), diff --git a/packages/kbn-tinymath/test/library.test.js b/packages/kbn-tinymath/test/library.test.js index bbc8503684fd4..9d87919c4f1ac 100644 --- a/packages/kbn-tinymath/test/library.test.js +++ b/packages/kbn-tinymath/test/library.test.js @@ -92,6 +92,7 @@ describe('Parser', () => { expect(parse('@foo0')).toEqual(variableEqual('@foo0')); expect(parse('.foo0')).toEqual(variableEqual('.foo0')); expect(parse('-foo0')).toEqual(variableEqual('-foo0')); + expect(() => parse(`foo😀\t')`)).toThrow('Failed to parse'); }); }); @@ -103,6 +104,7 @@ describe('Parser', () => { expect(parse('"foo bar fizz buzz"')).toEqual(variableEqual('foo bar fizz buzz')); expect(parse('"foo bar baby"')).toEqual(variableEqual('foo bar baby')); expect(parse(`"f'oo"`)).toEqual(variableEqual(`f'oo`)); + expect(parse(`"foo😀\t"`)).toEqual(variableEqual(`foo😀\t`)); }); it('strings with single quotes', () => { @@ -119,6 +121,7 @@ describe('Parser', () => { expect(parse("'foo bar '")).toEqual(variableEqual("foo bar ")); expect(parse("'0foo'")).toEqual(variableEqual("0foo")); expect(parse(`'f"oo'`)).toEqual(variableEqual(`f"oo`)); + expect(parse(`'foo😀\t'`)).toEqual(variableEqual(`foo😀\t`)); /* eslint-enable prettier/prettier */ }); diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index 0264c8a1acf75..92f5a854f6b00 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -53,8 +53,21 @@ function defaultStartDeps(availableApps?: App[]) { return deps; } +function defaultStartTestOptions({ + browserSupportsCsp = true, + kibanaVersion = 'version', +}: { + browserSupportsCsp?: boolean; + kibanaVersion?: string; +}): any { + return { + browserSupportsCsp, + kibanaVersion, + }; +} + async function start({ - options = { browserSupportsCsp: true }, + options = defaultStartTestOptions({}), cspConfigMock = { warnLegacyBrowsers: true }, startDeps = defaultStartDeps(), }: { options?: any; cspConfigMock?: any; startDeps?: ReturnType<typeof defaultStartDeps> } = {}) { @@ -82,7 +95,9 @@ afterAll(() => { describe('start', () => { it('adds legacy browser warning if browserSupportsCsp is disabled and warnLegacyBrowsers is enabled', async () => { - const { startDeps } = await start({ options: { browserSupportsCsp: false } }); + const { startDeps } = await start({ + options: { browserSupportsCsp: false, kibanaVersion: '7.0.0' }, + }); expect(startDeps.notifications.toasts.addWarning.mock.calls).toMatchInlineSnapshot(` Array [ @@ -95,6 +110,41 @@ describe('start', () => { `); }); + it('adds the kibana versioned class to the document body', async () => { + const { chrome, service } = await start({ + options: { browserSupportsCsp: false, kibanaVersion: '1.2.3' }, + }); + const promise = chrome.getBodyClasses$().pipe(toArray()).toPromise(); + service.stop(); + await expect(promise).resolves.toMatchInlineSnapshot(` + Array [ + Array [ + "kbnBody", + "kbnBody--noHeaderBanner", + "kbnBody--chromeHidden", + "kbnVersion-1-2-3", + ], + ] + `); + }); + it('strips off "snapshot" from the kibana version if present', async () => { + const { chrome, service } = await start({ + options: { browserSupportsCsp: false, kibanaVersion: '8.0.0-SnAPshot' }, + }); + const promise = chrome.getBodyClasses$().pipe(toArray()).toPromise(); + service.stop(); + await expect(promise).resolves.toMatchInlineSnapshot(` + Array [ + Array [ + "kbnBody", + "kbnBody--noHeaderBanner", + "kbnBody--chromeHidden", + "kbnVersion-8-0-0", + ], + ] + `); + }); + it('does not add legacy browser warning if browser supports CSP', async () => { const { startDeps } = await start(); diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 5ed447edde75a..f1381c52ce779 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -37,9 +37,11 @@ import { export type { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle }; const IS_LOCKED_KEY = 'core.chrome.isLocked'; +const SNAPSHOT_REGEX = /-snapshot/i; interface ConstructorParams { browserSupportsCsp: boolean; + kibanaVersion: string; } interface StartDeps { @@ -116,6 +118,16 @@ export class ChromeService { const helpSupportUrl$ = new BehaviorSubject<string>(KIBANA_ASK_ELASTIC_LINK); const isNavDrawerLocked$ = new BehaviorSubject(localStorage.getItem(IS_LOCKED_KEY) === 'true'); + const getKbnVersionClass = () => { + // we assume that the version is valid and has the form 'X.X.X' + // strip out `SNAPSHOT` and reformat to 'X-X-X' + const formattedVersionClass = this.params.kibanaVersion + .replace(SNAPSHOT_REGEX, '') + .split('.') + .join('-'); + return `kbnVersion-${formattedVersionClass}`; + }; + const headerBanner$ = new BehaviorSubject<ChromeUserBanner | undefined>(undefined); const bodyClasses$ = combineLatest([headerBanner$, this.isVisible$!]).pipe( map(([headerBanner, isVisible]) => { @@ -123,6 +135,7 @@ export class ChromeService { 'kbnBody', headerBanner ? 'kbnBody--hasHeaderBanner' : 'kbnBody--noHeaderBanner', isVisible ? 'kbnBody--chromeVisible' : 'kbnBody--chromeHidden', + getKbnVersionClass(), ]; }) ); diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 3668829a6888c..0b10209bc13e5 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -370,54 +370,62 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` isOpen={true} onClose={[Function]} > - <EuiWindowEvent - event="keydown" - handler={[Function]} - /> - <EuiOverlayMask - headerZindexLocation="below" - onClick={[Function]} + <EuiFlyout + aria-label="Primary" + as="nav" + className="euiCollapsibleNav" + closeButtonPosition="outside" + data-test-subj="collapsibleNav" + hideCloseButton={false} + id="collapsibe-nav" + onClose={[Function]} + outsideClickCloses={true} + ownFocus={true} + paddingSize="none" + pushMinBreakpoint="l" + role={null} + side="left" + size={320} + type="overlay" > - <Portal - containerInfo={ - <div - class="euiOverlayMask euiOverlayMask--belowHeader" - /> - } - /> - </EuiOverlayMask> - <EuiFocusTrap - clickOutsideDisables={true} - disabled={false} - > - <div - data-eui="EuiFocusTrap" + <nav + data-eui="EuiFlyout" + data-test-subj="collapsibleNav" + role={null} > - <nav - aria-label="Primary" - className="euiCollapsibleNav" - data-test-subj="collapsibleNav" - id="collapsibe-nav" + <button + data-test-subj="euiFlyoutCloseButton" + onClick={[Function]} + type="button" + /> + <EuiFlexItem + grow={false} + style={ + Object { + "flexShrink": 0, + } + } > - <EuiFlexItem - grow={false} + <div + className="euiFlexItem euiFlexItem--flexGrowZero" style={ Object { "flexShrink": 0, } } > - <div - className="euiFlexItem euiFlexItem--flexGrowZero" + <EuiCollapsibleNavGroup + background="light" + className="eui-yScroll" style={ Object { - "flexShrink": 0, + "maxHeight": "40vh", } } > - <EuiCollapsibleNavGroup - background="light" - className="eui-yScroll" + <div + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light eui-yScroll" + id="generated-id" style={ Object { "maxHeight": "40vh", @@ -425,109 +433,109 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` } > <div - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light eui-yScroll" - id="mockId" - style={ - Object { - "maxHeight": "40vh", - } - } + className="euiCollapsibleNavGroup__children" > - <div - className="euiCollapsibleNavGroup__children" + <EuiListGroup + color="text" + gutterSize="none" + listItems={ + Array [ + Object { + "data-test-subj": "collapsibleNavCustomNavLink", + "href": "Custom link", + "icon": undefined, + "iconType": undefined, + "isActive": false, + "isDisabled": undefined, + "label": "Custom link", + "onClick": [Function], + }, + ] + } + maxWidth="none" + size="s" > - <EuiListGroup - color="text" - gutterSize="none" - listItems={ - Array [ - Object { - "data-test-subj": "collapsibleNavCustomNavLink", - "href": "Custom link", - "icon": undefined, - "iconType": undefined, - "isActive": false, - "isDisabled": undefined, - "label": "Custom link", - "onClick": [Function], - }, - ] + <ul + className="euiListGroup" + style={ + Object { + "maxWidth": "none", + } } - maxWidth="none" - size="s" > - <ul - className="euiListGroup" - style={ - Object { - "maxWidth": "none", - } - } + <EuiListGroupItem + color="text" + data-test-subj="collapsibleNavCustomNavLink" + href="Custom link" + isActive={false} + key="title-0" + label="Custom link" + onClick={[Function]} + showToolTip={false} + size="s" + wrapText={false} > - <EuiListGroupItem - color="text" - data-test-subj="collapsibleNavCustomNavLink" - href="Custom link" - isActive={false} - key="title-0" - label="Custom link" - onClick={[Function]} - showToolTip={false} - size="s" - wrapText={false} + <li + className="euiListGroupItem euiListGroupItem--small euiListGroupItem--text euiListGroupItem-isClickable" > - <li - className="euiListGroupItem euiListGroupItem--small euiListGroupItem--text euiListGroupItem-isClickable" + <a + className="euiListGroupItem__button" + data-test-subj="collapsibleNavCustomNavLink" + href="Custom link" + onClick={[Function]} + rel="noreferrer" > - <a - className="euiListGroupItem__button" - data-test-subj="collapsibleNavCustomNavLink" - href="Custom link" - onClick={[Function]} - rel="noreferrer" + <span + className="euiListGroupItem__label" + title="Custom link" > - <span - className="euiListGroupItem__label" - title="Custom link" - > - Custom link - </span> - </a> - </li> - </EuiListGroupItem> - </ul> - </EuiListGroup> - </div> + Custom link + </span> + </a> + </li> + </EuiListGroupItem> + </ul> + </EuiListGroup> </div> - </EuiCollapsibleNavGroup> - </div> - </EuiFlexItem> - <EuiHorizontalRule - margin="none" - > - <hr - className="euiHorizontalRule euiHorizontalRule--full" - /> - </EuiHorizontalRule> - <EuiFlexItem - grow={false} + </div> + </EuiCollapsibleNavGroup> + </div> + </EuiFlexItem> + <EuiHorizontalRule + margin="none" + > + <hr + className="euiHorizontalRule euiHorizontalRule--full" + /> + </EuiHorizontalRule> + <EuiFlexItem + grow={false} + style={ + Object { + "flexShrink": 0, + } + } + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" style={ Object { "flexShrink": 0, } } > - <div - className="euiFlexItem euiFlexItem--flexGrowZero" + <EuiCollapsibleNavGroup + background="light" + className="eui-yScroll" style={ Object { - "flexShrink": 0, + "maxHeight": "40vh", } } > - <EuiCollapsibleNavGroup - background="light" - className="eui-yScroll" + <div + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light eui-yScroll" + id="generated-id" style={ Object { "maxHeight": "40vh", @@ -535,1527 +543,1455 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` } > <div - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light eui-yScroll" - id="mockId" - style={ - Object { - "maxHeight": "40vh", - } - } + className="euiCollapsibleNavGroup__children" > - <div - className="euiCollapsibleNavGroup__children" + <EuiListGroup + aria-label="Pinned links" + color="text" + gutterSize="none" + listItems={ + Array [ + Object { + "data-test-subj": "homeLink", + "href": "/", + "iconType": "home", + "label": "Home", + "onClick": [Function], + }, + ] + } + maxWidth="none" + size="s" > - <EuiListGroup + <ul aria-label="Pinned links" - color="text" - gutterSize="none" - listItems={ - Array [ - Object { - "data-test-subj": "homeLink", - "href": "/", - "iconType": "home", - "label": "Home", - "onClick": [Function], - }, - ] + className="euiListGroup" + style={ + Object { + "maxWidth": "none", + } } - maxWidth="none" - size="s" > - <ul - aria-label="Pinned links" - className="euiListGroup" - style={ - Object { - "maxWidth": "none", - } - } + <EuiListGroupItem + color="text" + data-test-subj="homeLink" + href="/" + iconType="home" + key="title-0" + label="Home" + onClick={[Function]} + showToolTip={false} + size="s" + wrapText={false} > - <EuiListGroupItem - color="text" - data-test-subj="homeLink" - href="/" - iconType="home" - key="title-0" - label="Home" - onClick={[Function]} - showToolTip={false} - size="s" - wrapText={false} + <li + className="euiListGroupItem euiListGroupItem--small euiListGroupItem--text euiListGroupItem-isClickable" > - <li - className="euiListGroupItem euiListGroupItem--small euiListGroupItem--text euiListGroupItem-isClickable" + <a + className="euiListGroupItem__button" + data-test-subj="homeLink" + href="/" + onClick={[Function]} + rel="noreferrer" > - <a - className="euiListGroupItem__button" - data-test-subj="homeLink" - href="/" - onClick={[Function]} - rel="noreferrer" + <EuiIcon + className="euiListGroupItem__icon" + color="inherit" + type="home" > - <EuiIcon + <span className="euiListGroupItem__icon" color="inherit" - type="home" - > - <span - className="euiListGroupItem__icon" - color="inherit" - data-euiicon-type="home" - /> - </EuiIcon> - <span - className="euiListGroupItem__label" - title="Home" - > - Home - </span> - </a> - </li> - </EuiListGroupItem> - </ul> - </EuiListGroup> - </div> + data-euiicon-type="home" + /> + </EuiIcon> + <span + className="euiListGroupItem__label" + title="Home" + > + Home + </span> + </a> + </li> + </EuiListGroupItem> + </ul> + </EuiListGroup> </div> - </EuiCollapsibleNavGroup> - </div> - </EuiFlexItem> - <EuiCollapsibleNavGroup - background="light" + </div> + </EuiCollapsibleNavGroup> + </div> + </EuiFlexItem> + <EuiCollapsibleNavGroup + background="light" + data-test-subj="collapsibleNavGroup-recentlyViewed" + initialIsOpen={true} + isCollapsible={true} + key="recentlyViewed" + onToggle={[Function]} + title="Recently viewed" + > + <EuiAccordion + arrowDisplay="right" + buttonClassName="euiCollapsibleNavGroup__heading" + buttonContent={ + <EuiFlexGroup + alignItems="center" + gutterSize="m" + responsive={false} + > + <EuiFlexItem> + <EuiTitle + size="xxs" + > + <h3 + className="euiCollapsibleNavGroup__title" + id="generated-id__title" + > + Recently viewed + </h3> + </EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-recentlyViewed" + id="generated-id" initialIsOpen={true} - isCollapsible={true} - key="recentlyViewed" + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} - title="Recently viewed" + paddingSize="none" > - <EuiAccordion - arrowDisplay="right" - buttonClassName="euiCollapsibleNavGroup__heading" - buttonContent={ - <EuiFlexGroup - alignItems="center" - gutterSize="m" - responsive={false} - > - <EuiFlexItem> - <EuiTitle - size="xxs" - > - <h3 - className="euiCollapsibleNavGroup__title" - id="mockId__title" - > - Recently viewed - </h3> - </EuiTitle> - </EuiFlexItem> - </EuiFlexGroup> - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" + <div + className="euiAccordion euiAccordion-isOpen euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-recentlyViewed" - id="mockId" - initialIsOpen={true} - isLoading={false} - isLoadingMessage={false} onToggle={[Function]} - paddingSize="none" > <div - className="euiAccordion euiAccordion-isOpen euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-recentlyViewed" - onToggle={[Function]} + className="euiAccordion__triggerWrapper" > - <div - className="euiAccordion__triggerWrapper" + <button + aria-controls="generated-id" + aria-expanded={true} + className="euiAccordion__button euiAccordion__buttonReverse euiCollapsibleNavGroup__heading" + id="generated-id" + onClick={[Function]} + type="button" > - <button - aria-controls="mockId" - aria-expanded={true} - className="euiAccordion__button euiAccordion__buttonReverse euiCollapsibleNavGroup__heading" - id="mockId" - onClick={[Function]} - type="button" + <span + className="euiAccordion__iconWrapper" > - <span - className="euiAccordion__iconWrapper" + <EuiIcon + className="euiAccordion__icon euiAccordion__icon-isOpen" + size="m" + type="arrowRight" > - <EuiIcon + <span className="euiAccordion__icon euiAccordion__icon-isOpen" + data-euiicon-type="arrowRight" size="m" - type="arrowRight" - > - <span - className="euiAccordion__icon euiAccordion__icon-isOpen" - data-euiicon-type="arrowRight" - size="m" - /> - </EuiIcon> - </span> - <span - className="euiIEFlexWrapFix" + /> + </EuiIcon> + </span> + <span + className="euiIEFlexWrapFix" + > + <EuiFlexGroup + alignItems="center" + gutterSize="m" + responsive={false} > - <EuiFlexGroup - alignItems="center" - gutterSize="m" - responsive={false} + <div + className="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow" > - <div - className="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow" - > - <EuiFlexItem> - <div - className="euiFlexItem" + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiTitle + size="xxs" > - <EuiTitle - size="xxs" + <h3 + className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" + id="generated-id__title" > - <h3 - className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" - id="mockId__title" - > - Recently viewed - </h3> - </EuiTitle> - </div> - </EuiFlexItem> - </div> - </EuiFlexGroup> - </span> - </button> - </div> - <div - aria-labelledby="mockId" - className="euiAccordion__childWrapper" - id="mockId" - role="region" - tabIndex={-1} + Recently viewed + </h3> + </EuiTitle> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + </span> + </button> + </div> + <div + aria-labelledby="generated-id" + className="euiAccordion__childWrapper" + id="generated-id" + role="region" + tabIndex={-1} + > + <EuiResizeObserver + onResize={[Function]} > - <EuiResizeObserver - onResize={[Function]} - > - <div> + <div> + <div + className="" + > <div - className="" + className="euiCollapsibleNavGroup__children" > - <div - className="euiCollapsibleNavGroup__children" + <EuiListGroup + aria-label="Recently viewed links" + className="kbnCollapsibleNav__recentsListGroup" + color="subdued" + gutterSize="none" + listItems={ + Array [ + Object { + "aria-label": "recent 1", + "data-test-subj": "collapsibleNavAppLink--recent", + "href": "http://localhost/recent%201", + "label": "recent 1", + "onClick": [Function], + "title": "recent 1", + }, + Object { + "aria-label": "recent 2", + "data-test-subj": "collapsibleNavAppLink--recent", + "href": "http://localhost/recent%202", + "label": "recent 2", + "onClick": [Function], + "title": "recent 2", + }, + ] + } + maxWidth="none" + size="s" > - <EuiListGroup + <ul aria-label="Recently viewed links" - className="kbnCollapsibleNav__recentsListGroup" - color="subdued" - gutterSize="none" - listItems={ - Array [ - Object { - "aria-label": "recent 1", - "data-test-subj": "collapsibleNavAppLink--recent", - "href": "http://localhost/recent%201", - "label": "recent 1", - "onClick": [Function], - "title": "recent 1", - }, - Object { - "aria-label": "recent 2", - "data-test-subj": "collapsibleNavAppLink--recent", - "href": "http://localhost/recent%202", - "label": "recent 2", - "onClick": [Function], - "title": "recent 2", - }, - ] + className="euiListGroup kbnCollapsibleNav__recentsListGroup" + style={ + Object { + "maxWidth": "none", + } } - maxWidth="none" - size="s" > - <ul - aria-label="Recently viewed links" - className="euiListGroup kbnCollapsibleNav__recentsListGroup" - style={ - Object { - "maxWidth": "none", - } - } + <EuiListGroupItem + aria-label="recent 1" + color="subdued" + data-test-subj="collapsibleNavAppLink--recent" + href="http://localhost/recent%201" + key="title-0" + label="recent 1" + onClick={[Function]} + showToolTip={false} + size="s" + title="recent 1" + wrapText={false} > - <EuiListGroupItem - aria-label="recent 1" - color="subdued" - data-test-subj="collapsibleNavAppLink--recent" - href="http://localhost/recent%201" - key="title-0" - label="recent 1" - onClick={[Function]} - showToolTip={false} - size="s" - title="recent 1" - wrapText={false} + <li + className="euiListGroupItem euiListGroupItem--small euiListGroupItem--subdued euiListGroupItem-isClickable" > - <li - className="euiListGroupItem euiListGroupItem--small euiListGroupItem--subdued euiListGroupItem-isClickable" + <a + aria-label="recent 1" + className="euiListGroupItem__button" + data-test-subj="collapsibleNavAppLink--recent" + href="http://localhost/recent%201" + onClick={[Function]} + rel="noreferrer" + title="recent 1" > - <a - aria-label="recent 1" - className="euiListGroupItem__button" - data-test-subj="collapsibleNavAppLink--recent" - href="http://localhost/recent%201" - onClick={[Function]} - rel="noreferrer" + <span + className="euiListGroupItem__label" title="recent 1" > - <span - className="euiListGroupItem__label" - title="recent 1" - > - recent 1 - </span> - </a> - </li> - </EuiListGroupItem> - <EuiListGroupItem - aria-label="recent 2" - color="subdued" - data-test-subj="collapsibleNavAppLink--recent" - href="http://localhost/recent%202" - key="title-1" - label="recent 2" - onClick={[Function]} - showToolTip={false} - size="s" - title="recent 2" - wrapText={false} + recent 1 + </span> + </a> + </li> + </EuiListGroupItem> + <EuiListGroupItem + aria-label="recent 2" + color="subdued" + data-test-subj="collapsibleNavAppLink--recent" + href="http://localhost/recent%202" + key="title-1" + label="recent 2" + onClick={[Function]} + showToolTip={false} + size="s" + title="recent 2" + wrapText={false} + > + <li + className="euiListGroupItem euiListGroupItem--small euiListGroupItem--subdued euiListGroupItem-isClickable" > - <li - className="euiListGroupItem euiListGroupItem--small euiListGroupItem--subdued euiListGroupItem-isClickable" + <a + aria-label="recent 2" + className="euiListGroupItem__button" + data-test-subj="collapsibleNavAppLink--recent" + href="http://localhost/recent%202" + onClick={[Function]} + rel="noreferrer" + title="recent 2" > - <a - aria-label="recent 2" - className="euiListGroupItem__button" - data-test-subj="collapsibleNavAppLink--recent" - href="http://localhost/recent%202" - onClick={[Function]} - rel="noreferrer" + <span + className="euiListGroupItem__label" title="recent 2" > - <span - className="euiListGroupItem__label" - title="recent 2" - > - recent 2 - </span> - </a> - </li> - </EuiListGroupItem> - </ul> - </EuiListGroup> - </div> + recent 2 + </span> + </a> + </li> + </EuiListGroupItem> + </ul> + </EuiListGroup> </div> </div> - </EuiResizeObserver> - </div> + </div> + </EuiResizeObserver> </div> - </EuiAccordion> - </EuiCollapsibleNavGroup> - <EuiHorizontalRule - margin="none" - > - <hr - className="euiHorizontalRule euiHorizontalRule--full" - /> - </EuiHorizontalRule> - <EuiFlexItem - className="eui-yScroll" + </div> + </EuiAccordion> + </EuiCollapsibleNavGroup> + <EuiHorizontalRule + margin="none" + > + <hr + className="euiHorizontalRule euiHorizontalRule--full" + /> + </EuiHorizontalRule> + <EuiFlexItem + className="eui-yScroll" + > + <div + className="euiFlexItem eui-yScroll" > - <div - className="euiFlexItem eui-yScroll" + <EuiCollapsibleNavGroup + data-test-subj="collapsibleNavGroup-kibana" + iconType="logoKibana" + initialIsOpen={true} + isCollapsible={true} + key="kibana" + onToggle={[Function]} + title="Analytics" > - <EuiCollapsibleNavGroup + <EuiAccordion + arrowDisplay="right" + buttonClassName="euiCollapsibleNavGroup__heading" + buttonContent={ + <EuiFlexGroup + alignItems="center" + gutterSize="m" + responsive={false} + > + <EuiFlexItem + grow={false} + > + <EuiIcon + size="l" + type="logoKibana" + /> + </EuiFlexItem> + <EuiFlexItem> + <EuiTitle + size="xxs" + > + <h3 + className="euiCollapsibleNavGroup__title" + id="generated-id__title" + > + Analytics + </h3> + </EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-kibana" - iconType="logoKibana" + id="generated-id" initialIsOpen={true} - isCollapsible={true} - key="kibana" + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} - title="Analytics" + paddingSize="none" > - <EuiAccordion - arrowDisplay="right" - buttonClassName="euiCollapsibleNavGroup__heading" - buttonContent={ - <EuiFlexGroup - alignItems="center" - gutterSize="m" - responsive={false} - > - <EuiFlexItem - grow={false} - > - <EuiIcon - size="l" - type="logoKibana" - /> - </EuiFlexItem> - <EuiFlexItem> - <EuiTitle - size="xxs" - > - <h3 - className="euiCollapsibleNavGroup__title" - id="mockId__title" - > - Analytics - </h3> - </EuiTitle> - </EuiFlexItem> - </EuiFlexGroup> - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + <div + className="euiAccordion euiAccordion-isOpen euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-kibana" - id="mockId" - initialIsOpen={true} - isLoading={false} - isLoadingMessage={false} onToggle={[Function]} - paddingSize="none" > <div - className="euiAccordion euiAccordion-isOpen euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-kibana" - onToggle={[Function]} + className="euiAccordion__triggerWrapper" > - <div - className="euiAccordion__triggerWrapper" + <button + aria-controls="generated-id" + aria-expanded={true} + className="euiAccordion__button euiAccordion__buttonReverse euiCollapsibleNavGroup__heading" + id="generated-id" + onClick={[Function]} + type="button" > - <button - aria-controls="mockId" - aria-expanded={true} - className="euiAccordion__button euiAccordion__buttonReverse euiCollapsibleNavGroup__heading" - id="mockId" - onClick={[Function]} - type="button" + <span + className="euiAccordion__iconWrapper" > - <span - className="euiAccordion__iconWrapper" + <EuiIcon + className="euiAccordion__icon euiAccordion__icon-isOpen" + size="m" + type="arrowRight" > - <EuiIcon + <span className="euiAccordion__icon euiAccordion__icon-isOpen" + data-euiicon-type="arrowRight" size="m" - type="arrowRight" - > - <span - className="euiAccordion__icon euiAccordion__icon-isOpen" - data-euiicon-type="arrowRight" - size="m" - /> - </EuiIcon> - </span> - <span - className="euiIEFlexWrapFix" + /> + </EuiIcon> + </span> + <span + className="euiIEFlexWrapFix" + > + <EuiFlexGroup + alignItems="center" + gutterSize="m" + responsive={false} > - <EuiFlexGroup - alignItems="center" - gutterSize="m" - responsive={false} + <div + className="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow" > - <div - className="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow" + <EuiFlexItem + grow={false} > - <EuiFlexItem - grow={false} + <div + className="euiFlexItem euiFlexItem--flexGrowZero" > - <div - className="euiFlexItem euiFlexItem--flexGrowZero" + <EuiIcon + size="l" + type="logoKibana" > - <EuiIcon + <span + data-euiicon-type="logoKibana" size="l" - type="logoKibana" - > - <span - data-euiicon-type="logoKibana" - size="l" - /> - </EuiIcon> - </div> - </EuiFlexItem> - <EuiFlexItem> - <div - className="euiFlexItem" + /> + </EuiIcon> + </div> + </EuiFlexItem> + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiTitle + size="xxs" > - <EuiTitle - size="xxs" + <h3 + className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" + id="generated-id__title" > - <h3 - className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" - id="mockId__title" - > - Analytics - </h3> - </EuiTitle> - </div> - </EuiFlexItem> - </div> - </EuiFlexGroup> - </span> - </button> - </div> - <div - aria-labelledby="mockId" - className="euiAccordion__childWrapper" - id="mockId" - role="region" - tabIndex={-1} + Analytics + </h3> + </EuiTitle> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + </span> + </button> + </div> + <div + aria-labelledby="generated-id" + className="euiAccordion__childWrapper" + id="generated-id" + role="region" + tabIndex={-1} + > + <EuiResizeObserver + onResize={[Function]} > - <EuiResizeObserver - onResize={[Function]} - > - <div> + <div> + <div + className="" + > <div - className="" + className="euiCollapsibleNavGroup__children" > - <div - className="euiCollapsibleNavGroup__children" + <EuiListGroup + aria-label="Primary navigation links, Analytics" + color="subdued" + gutterSize="none" + listItems={ + Array [ + Object { + "data-test-subj": "collapsibleNavAppLink", + "href": "discover", + "isActive": false, + "isDisabled": undefined, + "label": "discover", + "onClick": [Function], + }, + Object { + "data-test-subj": "collapsibleNavAppLink", + "href": "visualize", + "isActive": false, + "isDisabled": undefined, + "label": "visualize", + "onClick": [Function], + }, + Object { + "data-test-subj": "collapsibleNavAppLink", + "href": "dashboard", + "isActive": false, + "isDisabled": undefined, + "label": "dashboard", + "onClick": [Function], + }, + ] + } + maxWidth="none" + size="s" > - <EuiListGroup + <ul aria-label="Primary navigation links, Analytics" - color="subdued" - gutterSize="none" - listItems={ - Array [ - Object { - "data-test-subj": "collapsibleNavAppLink", - "href": "discover", - "isActive": false, - "isDisabled": undefined, - "label": "discover", - "onClick": [Function], - }, - Object { - "data-test-subj": "collapsibleNavAppLink", - "href": "visualize", - "isActive": false, - "isDisabled": undefined, - "label": "visualize", - "onClick": [Function], - }, - Object { - "data-test-subj": "collapsibleNavAppLink", - "href": "dashboard", - "isActive": false, - "isDisabled": undefined, - "label": "dashboard", - "onClick": [Function], - }, - ] + className="euiListGroup" + style={ + Object { + "maxWidth": "none", + } } - maxWidth="none" - size="s" > - <ul - aria-label="Primary navigation links, Analytics" - className="euiListGroup" - style={ - Object { - "maxWidth": "none", - } - } + <EuiListGroupItem + color="subdued" + data-test-subj="collapsibleNavAppLink" + href="discover" + isActive={false} + key="title-0" + label="discover" + onClick={[Function]} + showToolTip={false} + size="s" + wrapText={false} > - <EuiListGroupItem - color="subdued" - data-test-subj="collapsibleNavAppLink" - href="discover" - isActive={false} - key="title-0" - label="discover" - onClick={[Function]} - showToolTip={false} - size="s" - wrapText={false} + <li + className="euiListGroupItem euiListGroupItem--small euiListGroupItem--subdued euiListGroupItem-isClickable" > - <li - className="euiListGroupItem euiListGroupItem--small euiListGroupItem--subdued euiListGroupItem-isClickable" + <a + className="euiListGroupItem__button" + data-test-subj="collapsibleNavAppLink" + href="discover" + onClick={[Function]} + rel="noreferrer" > - <a - className="euiListGroupItem__button" - data-test-subj="collapsibleNavAppLink" - href="discover" - onClick={[Function]} - rel="noreferrer" + <span + className="euiListGroupItem__label" + title="discover" > - <span - className="euiListGroupItem__label" - title="discover" - > - discover - </span> - </a> - </li> - </EuiListGroupItem> - <EuiListGroupItem - color="subdued" - data-test-subj="collapsibleNavAppLink" - href="visualize" - isActive={false} - key="title-1" - label="visualize" - onClick={[Function]} - showToolTip={false} - size="s" - wrapText={false} + discover + </span> + </a> + </li> + </EuiListGroupItem> + <EuiListGroupItem + color="subdued" + data-test-subj="collapsibleNavAppLink" + href="visualize" + isActive={false} + key="title-1" + label="visualize" + onClick={[Function]} + showToolTip={false} + size="s" + wrapText={false} + > + <li + className="euiListGroupItem euiListGroupItem--small euiListGroupItem--subdued euiListGroupItem-isClickable" > - <li - className="euiListGroupItem euiListGroupItem--small euiListGroupItem--subdued euiListGroupItem-isClickable" + <a + className="euiListGroupItem__button" + data-test-subj="collapsibleNavAppLink" + href="visualize" + onClick={[Function]} + rel="noreferrer" > - <a - className="euiListGroupItem__button" - data-test-subj="collapsibleNavAppLink" - href="visualize" - onClick={[Function]} - rel="noreferrer" + <span + className="euiListGroupItem__label" + title="visualize" > - <span - className="euiListGroupItem__label" - title="visualize" - > - visualize - </span> - </a> - </li> - </EuiListGroupItem> - <EuiListGroupItem - color="subdued" - data-test-subj="collapsibleNavAppLink" - href="dashboard" - isActive={false} - key="title-2" - label="dashboard" - onClick={[Function]} - showToolTip={false} - size="s" - wrapText={false} + visualize + </span> + </a> + </li> + </EuiListGroupItem> + <EuiListGroupItem + color="subdued" + data-test-subj="collapsibleNavAppLink" + href="dashboard" + isActive={false} + key="title-2" + label="dashboard" + onClick={[Function]} + showToolTip={false} + size="s" + wrapText={false} + > + <li + className="euiListGroupItem euiListGroupItem--small euiListGroupItem--subdued euiListGroupItem-isClickable" > - <li - className="euiListGroupItem euiListGroupItem--small euiListGroupItem--subdued euiListGroupItem-isClickable" + <a + className="euiListGroupItem__button" + data-test-subj="collapsibleNavAppLink" + href="dashboard" + onClick={[Function]} + rel="noreferrer" > - <a - className="euiListGroupItem__button" - data-test-subj="collapsibleNavAppLink" - href="dashboard" - onClick={[Function]} - rel="noreferrer" + <span + className="euiListGroupItem__label" + title="dashboard" > - <span - className="euiListGroupItem__label" - title="dashboard" - > - dashboard - </span> - </a> - </li> - </EuiListGroupItem> - </ul> - </EuiListGroup> - </div> + dashboard + </span> + </a> + </li> + </EuiListGroupItem> + </ul> + </EuiListGroup> </div> </div> - </EuiResizeObserver> - </div> + </div> + </EuiResizeObserver> </div> - </EuiAccordion> - </EuiCollapsibleNavGroup> - <EuiCollapsibleNavGroup + </div> + </EuiAccordion> + </EuiCollapsibleNavGroup> + <EuiCollapsibleNavGroup + data-test-subj="collapsibleNavGroup-observability" + iconType="logoObservability" + initialIsOpen={true} + isCollapsible={true} + key="observability" + onToggle={[Function]} + title="Observability" + > + <EuiAccordion + arrowDisplay="right" + buttonClassName="euiCollapsibleNavGroup__heading" + buttonContent={ + <EuiFlexGroup + alignItems="center" + gutterSize="m" + responsive={false} + > + <EuiFlexItem + grow={false} + > + <EuiIcon + size="l" + type="logoObservability" + /> + </EuiFlexItem> + <EuiFlexItem> + <EuiTitle + size="xxs" + > + <h3 + className="euiCollapsibleNavGroup__title" + id="generated-id__title" + > + Observability + </h3> + </EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-observability" - iconType="logoObservability" + id="generated-id" initialIsOpen={true} - isCollapsible={true} - key="observability" + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} - title="Observability" + paddingSize="none" > - <EuiAccordion - arrowDisplay="right" - buttonClassName="euiCollapsibleNavGroup__heading" - buttonContent={ - <EuiFlexGroup - alignItems="center" - gutterSize="m" - responsive={false} - > - <EuiFlexItem - grow={false} - > - <EuiIcon - size="l" - type="logoObservability" - /> - </EuiFlexItem> - <EuiFlexItem> - <EuiTitle - size="xxs" - > - <h3 - className="euiCollapsibleNavGroup__title" - id="mockId__title" - > - Observability - </h3> - </EuiTitle> - </EuiFlexItem> - </EuiFlexGroup> - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + <div + className="euiAccordion euiAccordion-isOpen euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-observability" - id="mockId" - initialIsOpen={true} - isLoading={false} - isLoadingMessage={false} onToggle={[Function]} - paddingSize="none" > <div - className="euiAccordion euiAccordion-isOpen euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-observability" - onToggle={[Function]} + className="euiAccordion__triggerWrapper" > - <div - className="euiAccordion__triggerWrapper" + <button + aria-controls="generated-id" + aria-expanded={true} + className="euiAccordion__button euiAccordion__buttonReverse euiCollapsibleNavGroup__heading" + id="generated-id" + onClick={[Function]} + type="button" > - <button - aria-controls="mockId" - aria-expanded={true} - className="euiAccordion__button euiAccordion__buttonReverse euiCollapsibleNavGroup__heading" - id="mockId" - onClick={[Function]} - type="button" + <span + className="euiAccordion__iconWrapper" > - <span - className="euiAccordion__iconWrapper" + <EuiIcon + className="euiAccordion__icon euiAccordion__icon-isOpen" + size="m" + type="arrowRight" > - <EuiIcon + <span className="euiAccordion__icon euiAccordion__icon-isOpen" + data-euiicon-type="arrowRight" size="m" - type="arrowRight" - > - <span - className="euiAccordion__icon euiAccordion__icon-isOpen" - data-euiicon-type="arrowRight" - size="m" - /> - </EuiIcon> - </span> - <span - className="euiIEFlexWrapFix" + /> + </EuiIcon> + </span> + <span + className="euiIEFlexWrapFix" + > + <EuiFlexGroup + alignItems="center" + gutterSize="m" + responsive={false} > - <EuiFlexGroup - alignItems="center" - gutterSize="m" - responsive={false} + <div + className="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow" > - <div - className="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow" + <EuiFlexItem + grow={false} > - <EuiFlexItem - grow={false} + <div + className="euiFlexItem euiFlexItem--flexGrowZero" > - <div - className="euiFlexItem euiFlexItem--flexGrowZero" + <EuiIcon + size="l" + type="logoObservability" > - <EuiIcon + <span + data-euiicon-type="logoObservability" size="l" - type="logoObservability" - > - <span - data-euiicon-type="logoObservability" - size="l" - /> - </EuiIcon> - </div> - </EuiFlexItem> - <EuiFlexItem> - <div - className="euiFlexItem" + /> + </EuiIcon> + </div> + </EuiFlexItem> + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiTitle + size="xxs" > - <EuiTitle - size="xxs" + <h3 + className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" + id="generated-id__title" > - <h3 - className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" - id="mockId__title" - > - Observability - </h3> - </EuiTitle> - </div> - </EuiFlexItem> - </div> - </EuiFlexGroup> - </span> - </button> - </div> - <div - aria-labelledby="mockId" - className="euiAccordion__childWrapper" - id="mockId" - role="region" - tabIndex={-1} + Observability + </h3> + </EuiTitle> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + </span> + </button> + </div> + <div + aria-labelledby="generated-id" + className="euiAccordion__childWrapper" + id="generated-id" + role="region" + tabIndex={-1} + > + <EuiResizeObserver + onResize={[Function]} > - <EuiResizeObserver - onResize={[Function]} - > - <div> + <div> + <div + className="" + > <div - className="" + className="euiCollapsibleNavGroup__children" > - <div - className="euiCollapsibleNavGroup__children" + <EuiListGroup + aria-label="Primary navigation links, Observability" + color="subdued" + gutterSize="none" + listItems={ + Array [ + Object { + "data-test-subj": "collapsibleNavAppLink", + "href": "metrics", + "isActive": false, + "isDisabled": undefined, + "label": "metrics", + "onClick": [Function], + }, + Object { + "data-test-subj": "collapsibleNavAppLink", + "href": "logs", + "isActive": false, + "isDisabled": undefined, + "label": "logs", + "onClick": [Function], + }, + ] + } + maxWidth="none" + size="s" > - <EuiListGroup + <ul aria-label="Primary navigation links, Observability" - color="subdued" - gutterSize="none" - listItems={ - Array [ - Object { - "data-test-subj": "collapsibleNavAppLink", - "href": "metrics", - "isActive": false, - "isDisabled": undefined, - "label": "metrics", - "onClick": [Function], - }, - Object { - "data-test-subj": "collapsibleNavAppLink", - "href": "logs", - "isActive": false, - "isDisabled": undefined, - "label": "logs", - "onClick": [Function], - }, - ] + className="euiListGroup" + style={ + Object { + "maxWidth": "none", + } } - maxWidth="none" - size="s" > - <ul - aria-label="Primary navigation links, Observability" - className="euiListGroup" - style={ - Object { - "maxWidth": "none", - } - } + <EuiListGroupItem + color="subdued" + data-test-subj="collapsibleNavAppLink" + href="metrics" + isActive={false} + key="title-0" + label="metrics" + onClick={[Function]} + showToolTip={false} + size="s" + wrapText={false} > - <EuiListGroupItem - color="subdued" - data-test-subj="collapsibleNavAppLink" - href="metrics" - isActive={false} - key="title-0" - label="metrics" - onClick={[Function]} - showToolTip={false} - size="s" - wrapText={false} + <li + className="euiListGroupItem euiListGroupItem--small euiListGroupItem--subdued euiListGroupItem-isClickable" > - <li - className="euiListGroupItem euiListGroupItem--small euiListGroupItem--subdued euiListGroupItem-isClickable" + <a + className="euiListGroupItem__button" + data-test-subj="collapsibleNavAppLink" + href="metrics" + onClick={[Function]} + rel="noreferrer" > - <a - className="euiListGroupItem__button" - data-test-subj="collapsibleNavAppLink" - href="metrics" - onClick={[Function]} - rel="noreferrer" + <span + className="euiListGroupItem__label" + title="metrics" > - <span - className="euiListGroupItem__label" - title="metrics" - > - metrics - </span> - </a> - </li> - </EuiListGroupItem> - <EuiListGroupItem - color="subdued" - data-test-subj="collapsibleNavAppLink" - href="logs" - isActive={false} - key="title-1" - label="logs" - onClick={[Function]} - showToolTip={false} - size="s" - wrapText={false} + metrics + </span> + </a> + </li> + </EuiListGroupItem> + <EuiListGroupItem + color="subdued" + data-test-subj="collapsibleNavAppLink" + href="logs" + isActive={false} + key="title-1" + label="logs" + onClick={[Function]} + showToolTip={false} + size="s" + wrapText={false} + > + <li + className="euiListGroupItem euiListGroupItem--small euiListGroupItem--subdued euiListGroupItem-isClickable" > - <li - className="euiListGroupItem euiListGroupItem--small euiListGroupItem--subdued euiListGroupItem-isClickable" + <a + className="euiListGroupItem__button" + data-test-subj="collapsibleNavAppLink" + href="logs" + onClick={[Function]} + rel="noreferrer" > - <a - className="euiListGroupItem__button" - data-test-subj="collapsibleNavAppLink" - href="logs" - onClick={[Function]} - rel="noreferrer" + <span + className="euiListGroupItem__label" + title="logs" > - <span - className="euiListGroupItem__label" - title="logs" - > - logs - </span> - </a> - </li> - </EuiListGroupItem> - </ul> - </EuiListGroup> - </div> + logs + </span> + </a> + </li> + </EuiListGroupItem> + </ul> + </EuiListGroup> </div> </div> - </EuiResizeObserver> - </div> + </div> + </EuiResizeObserver> </div> - </EuiAccordion> - </EuiCollapsibleNavGroup> - <EuiCollapsibleNavGroup + </div> + </EuiAccordion> + </EuiCollapsibleNavGroup> + <EuiCollapsibleNavGroup + data-test-subj="collapsibleNavGroup-securitySolution" + iconType="logoSecurity" + initialIsOpen={true} + isCollapsible={true} + key="securitySolution" + onToggle={[Function]} + title="Security" + > + <EuiAccordion + arrowDisplay="right" + buttonClassName="euiCollapsibleNavGroup__heading" + buttonContent={ + <EuiFlexGroup + alignItems="center" + gutterSize="m" + responsive={false} + > + <EuiFlexItem + grow={false} + > + <EuiIcon + size="l" + type="logoSecurity" + /> + </EuiFlexItem> + <EuiFlexItem> + <EuiTitle + size="xxs" + > + <h3 + className="euiCollapsibleNavGroup__title" + id="generated-id__title" + > + Security + </h3> + </EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-securitySolution" - iconType="logoSecurity" + id="generated-id" initialIsOpen={true} - isCollapsible={true} - key="securitySolution" + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} - title="Security" + paddingSize="none" > - <EuiAccordion - arrowDisplay="right" - buttonClassName="euiCollapsibleNavGroup__heading" - buttonContent={ - <EuiFlexGroup - alignItems="center" - gutterSize="m" - responsive={false} - > - <EuiFlexItem - grow={false} - > - <EuiIcon - size="l" - type="logoSecurity" - /> - </EuiFlexItem> - <EuiFlexItem> - <EuiTitle - size="xxs" - > - <h3 - className="euiCollapsibleNavGroup__title" - id="mockId__title" - > - Security - </h3> - </EuiTitle> - </EuiFlexItem> - </EuiFlexGroup> - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + <div + className="euiAccordion euiAccordion-isOpen euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-securitySolution" - id="mockId" - initialIsOpen={true} - isLoading={false} - isLoadingMessage={false} onToggle={[Function]} - paddingSize="none" > <div - className="euiAccordion euiAccordion-isOpen euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-securitySolution" - onToggle={[Function]} + className="euiAccordion__triggerWrapper" > - <div - className="euiAccordion__triggerWrapper" + <button + aria-controls="generated-id" + aria-expanded={true} + className="euiAccordion__button euiAccordion__buttonReverse euiCollapsibleNavGroup__heading" + id="generated-id" + onClick={[Function]} + type="button" > - <button - aria-controls="mockId" - aria-expanded={true} - className="euiAccordion__button euiAccordion__buttonReverse euiCollapsibleNavGroup__heading" - id="mockId" - onClick={[Function]} - type="button" + <span + className="euiAccordion__iconWrapper" > - <span - className="euiAccordion__iconWrapper" + <EuiIcon + className="euiAccordion__icon euiAccordion__icon-isOpen" + size="m" + type="arrowRight" > - <EuiIcon + <span className="euiAccordion__icon euiAccordion__icon-isOpen" + data-euiicon-type="arrowRight" size="m" - type="arrowRight" - > - <span - className="euiAccordion__icon euiAccordion__icon-isOpen" - data-euiicon-type="arrowRight" - size="m" - /> - </EuiIcon> - </span> - <span - className="euiIEFlexWrapFix" + /> + </EuiIcon> + </span> + <span + className="euiIEFlexWrapFix" + > + <EuiFlexGroup + alignItems="center" + gutterSize="m" + responsive={false} > - <EuiFlexGroup - alignItems="center" - gutterSize="m" - responsive={false} + <div + className="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow" > - <div - className="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow" + <EuiFlexItem + grow={false} > - <EuiFlexItem - grow={false} + <div + className="euiFlexItem euiFlexItem--flexGrowZero" > - <div - className="euiFlexItem euiFlexItem--flexGrowZero" + <EuiIcon + size="l" + type="logoSecurity" > - <EuiIcon + <span + data-euiicon-type="logoSecurity" size="l" - type="logoSecurity" - > - <span - data-euiicon-type="logoSecurity" - size="l" - /> - </EuiIcon> - </div> - </EuiFlexItem> - <EuiFlexItem> - <div - className="euiFlexItem" + /> + </EuiIcon> + </div> + </EuiFlexItem> + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiTitle + size="xxs" > - <EuiTitle - size="xxs" + <h3 + className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" + id="generated-id__title" > - <h3 - className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" - id="mockId__title" - > - Security - </h3> - </EuiTitle> - </div> - </EuiFlexItem> - </div> - </EuiFlexGroup> - </span> - </button> - </div> - <div - aria-labelledby="mockId" - className="euiAccordion__childWrapper" - id="mockId" - role="region" - tabIndex={-1} + Security + </h3> + </EuiTitle> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + </span> + </button> + </div> + <div + aria-labelledby="generated-id" + className="euiAccordion__childWrapper" + id="generated-id" + role="region" + tabIndex={-1} + > + <EuiResizeObserver + onResize={[Function]} > - <EuiResizeObserver - onResize={[Function]} - > - <div> + <div> + <div + className="" + > <div - className="" + className="euiCollapsibleNavGroup__children" > - <div - className="euiCollapsibleNavGroup__children" + <EuiListGroup + aria-label="Primary navigation links, Security" + color="subdued" + gutterSize="none" + listItems={ + Array [ + Object { + "data-test-subj": "collapsibleNavAppLink", + "href": "siem", + "isActive": false, + "isDisabled": undefined, + "label": "siem", + "onClick": [Function], + }, + ] + } + maxWidth="none" + size="s" > - <EuiListGroup + <ul aria-label="Primary navigation links, Security" - color="subdued" - gutterSize="none" - listItems={ - Array [ - Object { - "data-test-subj": "collapsibleNavAppLink", - "href": "siem", - "isActive": false, - "isDisabled": undefined, - "label": "siem", - "onClick": [Function], - }, - ] + className="euiListGroup" + style={ + Object { + "maxWidth": "none", + } } - maxWidth="none" - size="s" > - <ul - aria-label="Primary navigation links, Security" - className="euiListGroup" - style={ - Object { - "maxWidth": "none", - } - } + <EuiListGroupItem + color="subdued" + data-test-subj="collapsibleNavAppLink" + href="siem" + isActive={false} + key="title-0" + label="siem" + onClick={[Function]} + showToolTip={false} + size="s" + wrapText={false} > - <EuiListGroupItem - color="subdued" - data-test-subj="collapsibleNavAppLink" - href="siem" - isActive={false} - key="title-0" - label="siem" - onClick={[Function]} - showToolTip={false} - size="s" - wrapText={false} + <li + className="euiListGroupItem euiListGroupItem--small euiListGroupItem--subdued euiListGroupItem-isClickable" > - <li - className="euiListGroupItem euiListGroupItem--small euiListGroupItem--subdued euiListGroupItem-isClickable" + <a + className="euiListGroupItem__button" + data-test-subj="collapsibleNavAppLink" + href="siem" + onClick={[Function]} + rel="noreferrer" > - <a - className="euiListGroupItem__button" - data-test-subj="collapsibleNavAppLink" - href="siem" - onClick={[Function]} - rel="noreferrer" + <span + className="euiListGroupItem__label" + title="siem" > - <span - className="euiListGroupItem__label" - title="siem" - > - siem - </span> - </a> - </li> - </EuiListGroupItem> - </ul> - </EuiListGroup> - </div> + siem + </span> + </a> + </li> + </EuiListGroupItem> + </ul> + </EuiListGroup> </div> </div> - </EuiResizeObserver> - </div> + </div> + </EuiResizeObserver> </div> - </EuiAccordion> - </EuiCollapsibleNavGroup> - <EuiCollapsibleNavGroup + </div> + </EuiAccordion> + </EuiCollapsibleNavGroup> + <EuiCollapsibleNavGroup + data-test-subj="collapsibleNavGroup-management" + iconType="managementApp" + initialIsOpen={true} + isCollapsible={true} + key="management" + onToggle={[Function]} + title="Management" + > + <EuiAccordion + arrowDisplay="right" + buttonClassName="euiCollapsibleNavGroup__heading" + buttonContent={ + <EuiFlexGroup + alignItems="center" + gutterSize="m" + responsive={false} + > + <EuiFlexItem + grow={false} + > + <EuiIcon + size="l" + type="managementApp" + /> + </EuiFlexItem> + <EuiFlexItem> + <EuiTitle + size="xxs" + > + <h3 + className="euiCollapsibleNavGroup__title" + id="generated-id__title" + > + Management + </h3> + </EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-management" - iconType="managementApp" + id="generated-id" initialIsOpen={true} - isCollapsible={true} - key="management" + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} - title="Management" + paddingSize="none" > - <EuiAccordion - arrowDisplay="right" - buttonClassName="euiCollapsibleNavGroup__heading" - buttonContent={ - <EuiFlexGroup - alignItems="center" - gutterSize="m" - responsive={false} - > - <EuiFlexItem - grow={false} - > - <EuiIcon - size="l" - type="managementApp" - /> - </EuiFlexItem> - <EuiFlexItem> - <EuiTitle - size="xxs" - > - <h3 - className="euiCollapsibleNavGroup__title" - id="mockId__title" - > - Management - </h3> - </EuiTitle> - </EuiFlexItem> - </EuiFlexGroup> - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" + <div + className="euiAccordion euiAccordion-isOpen euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-management" - id="mockId" - initialIsOpen={true} - isLoading={false} - isLoadingMessage={false} onToggle={[Function]} - paddingSize="none" > <div - className="euiAccordion euiAccordion-isOpen euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-management" - onToggle={[Function]} + className="euiAccordion__triggerWrapper" > - <div - className="euiAccordion__triggerWrapper" + <button + aria-controls="generated-id" + aria-expanded={true} + className="euiAccordion__button euiAccordion__buttonReverse euiCollapsibleNavGroup__heading" + id="generated-id" + onClick={[Function]} + type="button" > - <button - aria-controls="mockId" - aria-expanded={true} - className="euiAccordion__button euiAccordion__buttonReverse euiCollapsibleNavGroup__heading" - id="mockId" - onClick={[Function]} - type="button" + <span + className="euiAccordion__iconWrapper" > - <span - className="euiAccordion__iconWrapper" + <EuiIcon + className="euiAccordion__icon euiAccordion__icon-isOpen" + size="m" + type="arrowRight" > - <EuiIcon + <span className="euiAccordion__icon euiAccordion__icon-isOpen" + data-euiicon-type="arrowRight" size="m" - type="arrowRight" - > - <span - className="euiAccordion__icon euiAccordion__icon-isOpen" - data-euiicon-type="arrowRight" - size="m" - /> - </EuiIcon> - </span> - <span - className="euiIEFlexWrapFix" + /> + </EuiIcon> + </span> + <span + className="euiIEFlexWrapFix" + > + <EuiFlexGroup + alignItems="center" + gutterSize="m" + responsive={false} > - <EuiFlexGroup - alignItems="center" - gutterSize="m" - responsive={false} + <div + className="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow" > - <div - className="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow" + <EuiFlexItem + grow={false} > - <EuiFlexItem - grow={false} + <div + className="euiFlexItem euiFlexItem--flexGrowZero" > - <div - className="euiFlexItem euiFlexItem--flexGrowZero" + <EuiIcon + size="l" + type="managementApp" > - <EuiIcon + <span + data-euiicon-type="managementApp" size="l" - type="managementApp" - > - <span - data-euiicon-type="managementApp" - size="l" - /> - </EuiIcon> - </div> - </EuiFlexItem> - <EuiFlexItem> - <div - className="euiFlexItem" + /> + </EuiIcon> + </div> + </EuiFlexItem> + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiTitle + size="xxs" > - <EuiTitle - size="xxs" + <h3 + className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" + id="generated-id__title" > - <h3 - className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" - id="mockId__title" - > - Management - </h3> - </EuiTitle> - </div> - </EuiFlexItem> - </div> - </EuiFlexGroup> - </span> - </button> - </div> - <div - aria-labelledby="mockId" - className="euiAccordion__childWrapper" - id="mockId" - role="region" - tabIndex={-1} + Management + </h3> + </EuiTitle> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + </span> + </button> + </div> + <div + aria-labelledby="generated-id" + className="euiAccordion__childWrapper" + id="generated-id" + role="region" + tabIndex={-1} + > + <EuiResizeObserver + onResize={[Function]} > - <EuiResizeObserver - onResize={[Function]} - > - <div> + <div> + <div + className="" + > <div - className="" + className="euiCollapsibleNavGroup__children" > - <div - className="euiCollapsibleNavGroup__children" + <EuiListGroup + aria-label="Primary navigation links, Management" + color="subdued" + gutterSize="none" + listItems={ + Array [ + Object { + "data-test-subj": "collapsibleNavAppLink", + "href": "monitoring", + "isActive": false, + "isDisabled": undefined, + "label": "monitoring", + "onClick": [Function], + }, + ] + } + maxWidth="none" + size="s" > - <EuiListGroup + <ul aria-label="Primary navigation links, Management" - color="subdued" - gutterSize="none" - listItems={ - Array [ - Object { - "data-test-subj": "collapsibleNavAppLink", - "href": "monitoring", - "isActive": false, - "isDisabled": undefined, - "label": "monitoring", - "onClick": [Function], - }, - ] + className="euiListGroup" + style={ + Object { + "maxWidth": "none", + } } - maxWidth="none" - size="s" > - <ul - aria-label="Primary navigation links, Management" - className="euiListGroup" - style={ - Object { - "maxWidth": "none", - } - } + <EuiListGroupItem + color="subdued" + data-test-subj="collapsibleNavAppLink" + href="monitoring" + isActive={false} + key="title-0" + label="monitoring" + onClick={[Function]} + showToolTip={false} + size="s" + wrapText={false} > - <EuiListGroupItem - color="subdued" - data-test-subj="collapsibleNavAppLink" - href="monitoring" - isActive={false} - key="title-0" - label="monitoring" - onClick={[Function]} - showToolTip={false} - size="s" - wrapText={false} + <li + className="euiListGroupItem euiListGroupItem--small euiListGroupItem--subdued euiListGroupItem-isClickable" > - <li - className="euiListGroupItem euiListGroupItem--small euiListGroupItem--subdued euiListGroupItem-isClickable" + <a + className="euiListGroupItem__button" + data-test-subj="collapsibleNavAppLink" + href="monitoring" + onClick={[Function]} + rel="noreferrer" > - <a - className="euiListGroupItem__button" - data-test-subj="collapsibleNavAppLink" - href="monitoring" - onClick={[Function]} - rel="noreferrer" + <span + className="euiListGroupItem__label" + title="monitoring" > - <span - className="euiListGroupItem__label" - title="monitoring" - > - monitoring - </span> - </a> - </li> - </EuiListGroupItem> - </ul> - </EuiListGroup> - </div> + monitoring + </span> + </a> + </li> + </EuiListGroupItem> + </ul> + </EuiListGroup> </div> </div> - </EuiResizeObserver> - </div> + </div> + </EuiResizeObserver> </div> - </EuiAccordion> - </EuiCollapsibleNavGroup> - <EuiCollapsibleNavGroup + </div> + </EuiAccordion> + </EuiCollapsibleNavGroup> + <EuiCollapsibleNavGroup + data-test-subj="collapsibleNavGroup-noCategory" + key="0" + > + <div + className="euiCollapsibleNavGroup" data-test-subj="collapsibleNavGroup-noCategory" - key="0" + id="generated-id" > <div - className="euiCollapsibleNavGroup" - data-test-subj="collapsibleNavGroup-noCategory" - id="mockId" + className="euiCollapsibleNavGroup__children" > - <div - className="euiCollapsibleNavGroup__children" + <EuiListGroup + flush={true} > - <EuiListGroup - flush={true} + <ul + className="euiListGroup euiListGroup-flush euiListGroup--gutterSmall euiListGroup-maxWidthDefault" > - <ul - className="euiListGroup euiListGroup-flush euiListGroup--gutterSmall euiListGroup-maxWidthDefault" + <EuiListGroupItem + color="text" + data-test-subj="collapsibleNavAppLink" + href="canvas" + isActive={false} + label="canvas" + onClick={[Function]} + size="s" > - <EuiListGroupItem - color="text" - data-test-subj="collapsibleNavAppLink" - href="canvas" - isActive={false} - label="canvas" - onClick={[Function]} - size="s" + <li + className="euiListGroupItem euiListGroupItem--small euiListGroupItem--text euiListGroupItem-isClickable" > - <li - className="euiListGroupItem euiListGroupItem--small euiListGroupItem--text euiListGroupItem-isClickable" + <a + className="euiListGroupItem__button" + data-test-subj="collapsibleNavAppLink" + href="canvas" + onClick={[Function]} + rel="noreferrer" > - <a - className="euiListGroupItem__button" - data-test-subj="collapsibleNavAppLink" - href="canvas" - onClick={[Function]} - rel="noreferrer" + <span + className="euiListGroupItem__label" + title="canvas" > - <span - className="euiListGroupItem__label" - title="canvas" - > - canvas - </span> - </a> - </li> - </EuiListGroupItem> - </ul> - </EuiListGroup> - </div> + canvas + </span> + </a> + </li> + </EuiListGroupItem> + </ul> + </EuiListGroup> </div> - </EuiCollapsibleNavGroup> - <EuiShowFor - sizes={ - Array [ - "l", - "xl", - ] - } - > - <EuiCollapsibleNavGroup> + </div> + </EuiCollapsibleNavGroup> + <EuiShowFor + sizes={ + Array [ + "l", + "xl", + ] + } + > + <EuiCollapsibleNavGroup> + <div + className="euiCollapsibleNavGroup" + id="generated-id" + > <div - className="euiCollapsibleNavGroup" - id="mockId" + className="euiCollapsibleNavGroup__children" > - <div - className="euiCollapsibleNavGroup__children" + <EuiListGroup + flush={true} > - <EuiListGroup - flush={true} - > - <ul - className="euiListGroup euiListGroup-flush euiListGroup--gutterSmall euiListGroup-maxWidthDefault" - > - <EuiListGroupItem - aria-label="Dock primary navigation" - buttonRef={ - Object { - "current": <button - aria-label="Dock primary navigation" - class="euiListGroupItem__button" - data-test-subj="collapsible-nav-lock" - type="button" - > - <span - class="euiListGroupItem__icon" - color="inherit" - data-euiicon-type="lockOpen" - /> - <span - class="euiListGroupItem__label" - title="Dock navigation" - > - Dock navigation - </span> - </button>, - } - } - color="subdued" - data-test-subj="collapsible-nav-lock" - iconType="lockOpen" - label="Dock navigation" - onClick={[Function]} - size="xs" - > - <li - className="euiListGroupItem euiListGroupItem--xSmall euiListGroupItem--subdued euiListGroupItem-isClickable" - > - <button + <ul + className="euiListGroup euiListGroup-flush euiListGroup--gutterSmall euiListGroup-maxWidthDefault" + > + <EuiListGroupItem + aria-label="Dock primary navigation" + buttonRef={ + Object { + "current": <button aria-label="Dock primary navigation" - className="euiListGroupItem__button" + class="euiListGroupItem__button" data-test-subj="collapsible-nav-lock" - disabled={false} - onClick={[Function]} type="button" > - <EuiIcon - className="euiListGroupItem__icon" + <span + class="euiListGroupItem__icon" color="inherit" - type="lockOpen" - > - <span - className="euiListGroupItem__icon" - color="inherit" - data-euiicon-type="lockOpen" - /> - </EuiIcon> + data-euiicon-type="lockOpen" + /> <span - className="euiListGroupItem__label" + class="euiListGroupItem__label" title="Dock navigation" > Dock navigation </span> - </button> - </li> - </EuiListGroupItem> - </ul> - </EuiListGroup> - </div> + </button>, + } + } + color="subdued" + data-test-subj="collapsible-nav-lock" + iconType="lockOpen" + label="Dock navigation" + onClick={[Function]} + size="xs" + > + <li + className="euiListGroupItem euiListGroupItem--xSmall euiListGroupItem--subdued euiListGroupItem-isClickable" + > + <button + aria-label="Dock primary navigation" + className="euiListGroupItem__button" + data-test-subj="collapsible-nav-lock" + disabled={false} + onClick={[Function]} + type="button" + > + <EuiIcon + className="euiListGroupItem__icon" + color="inherit" + type="lockOpen" + > + <span + className="euiListGroupItem__icon" + color="inherit" + data-euiicon-type="lockOpen" + /> + </EuiIcon> + <span + className="euiListGroupItem__label" + title="Dock navigation" + > + Dock navigation + </span> + </button> + </li> + </EuiListGroupItem> + </ul> + </EuiListGroup> </div> - </EuiCollapsibleNavGroup> - </EuiShowFor> - </div> - </EuiFlexItem> - <EuiScreenReaderOnly - showOnFocus={true} - > - <EuiButtonEmpty - className="euiScreenReaderOnly--showOnFocus euiCollapsibleNav__closeButton" - iconType="cross" - onClick={[Function]} - size="xs" - textProps={ - Object { - "className": "euiCollapsibleNav__closeButtonText", - } - } - > - <button - className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall euiScreenReaderOnly--showOnFocus euiCollapsibleNav__closeButton" - disabled={false} - onClick={[Function]} - type="button" - > - <EuiButtonContent - className="euiButtonEmpty__content" - iconSide="left" - iconSize="s" - iconType="cross" - textProps={ - Object { - "className": "euiButtonEmpty__text euiCollapsibleNav__closeButtonText", - } - } - > - <span - className="euiButtonContent euiButtonEmpty__content" - > - <EuiIcon - className="euiButtonContent__icon" - color="inherit" - size="s" - type="cross" - > - <span - className="euiButtonContent__icon" - color="inherit" - data-euiicon-type="cross" - size="s" - /> - </EuiIcon> - <span - className="euiButtonEmpty__text euiCollapsibleNav__closeButtonText" - > - <EuiI18n - default="close" - token="euiCollapsibleNav.closeButtonLabel" - > - close - </EuiI18n> - </span> - </span> - </EuiButtonContent> - </button> - </EuiButtonEmpty> - </EuiScreenReaderOnly> - </nav> - </div> - </EuiFocusTrap> + </div> + </EuiCollapsibleNavGroup> + </EuiShowFor> + </div> + </EuiFlexItem> + </nav> + </EuiFlyout> </EuiCollapsibleNav> </CollapsibleNav> `; @@ -2770,42 +2706,57 @@ exports[`CollapsibleNav renders the default nav 3`] = ` isOpen={false} onClose={[Function]} > - <EuiWindowEvent - event="keydown" - handler={[Function]} - /> - <EuiFocusTrap - clickOutsideDisables={true} - disabled={true} + <EuiFlyout + aria-label="Primary" + as="nav" + className="euiCollapsibleNav" + closeButtonPosition="outside" + data-test-subj="collapsibleNav" + hideCloseButton={true} + id="collapsibe-nav" + onClose={[Function]} + outsideClickCloses={true} + ownFocus={true} + paddingSize="none" + pushMinBreakpoint="l" + role={null} + side="left" + size={320} + type="push" > - <div - data-eui="EuiFocusTrap" + <nav + data-eui="EuiFlyout" + data-test-subj="collapsibleNav" + role={null} > - <nav - aria-label="Primary" - className="euiCollapsibleNav euiCollapsibleNav--isDocked" - data-test-subj="collapsibleNav" - id="collapsibe-nav" + <EuiFlexItem + grow={false} + style={ + Object { + "flexShrink": 0, + } + } > - <EuiFlexItem - grow={false} + <div + className="euiFlexItem euiFlexItem--flexGrowZero" style={ Object { "flexShrink": 0, } } > - <div - className="euiFlexItem euiFlexItem--flexGrowZero" + <EuiCollapsibleNavGroup + background="light" + className="eui-yScroll" style={ Object { - "flexShrink": 0, + "maxHeight": "40vh", } } > - <EuiCollapsibleNavGroup - background="light" - className="eui-yScroll" + <div + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light eui-yScroll" + id="generated-id" style={ Object { "maxHeight": "40vh", @@ -2813,423 +2764,351 @@ exports[`CollapsibleNav renders the default nav 3`] = ` } > <div - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light eui-yScroll" - id="mockId" - style={ - Object { - "maxHeight": "40vh", - } - } + className="euiCollapsibleNavGroup__children" > - <div - className="euiCollapsibleNavGroup__children" + <EuiListGroup + aria-label="Pinned links" + color="text" + gutterSize="none" + listItems={ + Array [ + Object { + "data-test-subj": "homeLink", + "href": "/", + "iconType": "home", + "label": "Home", + "onClick": [Function], + }, + ] + } + maxWidth="none" + size="s" > - <EuiListGroup + <ul aria-label="Pinned links" - color="text" - gutterSize="none" - listItems={ - Array [ - Object { - "data-test-subj": "homeLink", - "href": "/", - "iconType": "home", - "label": "Home", - "onClick": [Function], - }, - ] + className="euiListGroup" + style={ + Object { + "maxWidth": "none", + } } - maxWidth="none" - size="s" > - <ul - aria-label="Pinned links" - className="euiListGroup" - style={ - Object { - "maxWidth": "none", - } - } + <EuiListGroupItem + color="text" + data-test-subj="homeLink" + href="/" + iconType="home" + key="title-0" + label="Home" + onClick={[Function]} + showToolTip={false} + size="s" + wrapText={false} > - <EuiListGroupItem - color="text" - data-test-subj="homeLink" - href="/" - iconType="home" - key="title-0" - label="Home" - onClick={[Function]} - showToolTip={false} - size="s" - wrapText={false} + <li + className="euiListGroupItem euiListGroupItem--small euiListGroupItem--text euiListGroupItem-isClickable" > - <li - className="euiListGroupItem euiListGroupItem--small euiListGroupItem--text euiListGroupItem-isClickable" + <a + className="euiListGroupItem__button" + data-test-subj="homeLink" + href="/" + onClick={[Function]} + rel="noreferrer" > - <a - className="euiListGroupItem__button" - data-test-subj="homeLink" - href="/" - onClick={[Function]} - rel="noreferrer" + <EuiIcon + className="euiListGroupItem__icon" + color="inherit" + type="home" > - <EuiIcon + <span className="euiListGroupItem__icon" color="inherit" - type="home" - > - <span - className="euiListGroupItem__icon" - color="inherit" - data-euiicon-type="home" - /> - </EuiIcon> - <span - className="euiListGroupItem__label" - title="Home" - > - Home - </span> - </a> - </li> - </EuiListGroupItem> - </ul> - </EuiListGroup> - </div> + data-euiicon-type="home" + /> + </EuiIcon> + <span + className="euiListGroupItem__label" + title="Home" + > + Home + </span> + </a> + </li> + </EuiListGroupItem> + </ul> + </EuiListGroup> </div> - </EuiCollapsibleNavGroup> - </div> - </EuiFlexItem> - <EuiCollapsibleNavGroup - background="light" + </div> + </EuiCollapsibleNavGroup> + </div> + </EuiFlexItem> + <EuiCollapsibleNavGroup + background="light" + data-test-subj="collapsibleNavGroup-recentlyViewed" + initialIsOpen={true} + isCollapsible={true} + key="recentlyViewed" + onToggle={[Function]} + title="Recently viewed" + > + <EuiAccordion + arrowDisplay="right" + buttonClassName="euiCollapsibleNavGroup__heading" + buttonContent={ + <EuiFlexGroup + alignItems="center" + gutterSize="m" + responsive={false} + > + <EuiFlexItem> + <EuiTitle + size="xxs" + > + <h3 + className="euiCollapsibleNavGroup__title" + id="generated-id__title" + > + Recently viewed + </h3> + </EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-recentlyViewed" + id="generated-id" initialIsOpen={true} - isCollapsible={true} - key="recentlyViewed" + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} - title="Recently viewed" + paddingSize="none" > - <EuiAccordion - arrowDisplay="right" - buttonClassName="euiCollapsibleNavGroup__heading" - buttonContent={ - <EuiFlexGroup - alignItems="center" - gutterSize="m" - responsive={false} - > - <EuiFlexItem> - <EuiTitle - size="xxs" - > - <h3 - className="euiCollapsibleNavGroup__title" - id="mockId__title" - > - Recently viewed - </h3> - </EuiTitle> - </EuiFlexItem> - </EuiFlexGroup> - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" + <div + className="euiAccordion euiAccordion-isOpen euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-recentlyViewed" - id="mockId" - initialIsOpen={true} - isLoading={false} - isLoadingMessage={false} onToggle={[Function]} - paddingSize="none" > <div - className="euiAccordion euiAccordion-isOpen euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-recentlyViewed" - onToggle={[Function]} + className="euiAccordion__triggerWrapper" > - <div - className="euiAccordion__triggerWrapper" + <button + aria-controls="generated-id" + aria-expanded={true} + className="euiAccordion__button euiAccordion__buttonReverse euiCollapsibleNavGroup__heading" + id="generated-id" + onClick={[Function]} + type="button" > - <button - aria-controls="mockId" - aria-expanded={true} - className="euiAccordion__button euiAccordion__buttonReverse euiCollapsibleNavGroup__heading" - id="mockId" - onClick={[Function]} - type="button" + <span + className="euiAccordion__iconWrapper" > - <span - className="euiAccordion__iconWrapper" + <EuiIcon + className="euiAccordion__icon euiAccordion__icon-isOpen" + size="m" + type="arrowRight" > - <EuiIcon + <span className="euiAccordion__icon euiAccordion__icon-isOpen" + data-euiicon-type="arrowRight" size="m" - type="arrowRight" - > - <span - className="euiAccordion__icon euiAccordion__icon-isOpen" - data-euiicon-type="arrowRight" - size="m" - /> - </EuiIcon> - </span> - <span - className="euiIEFlexWrapFix" + /> + </EuiIcon> + </span> + <span + className="euiIEFlexWrapFix" + > + <EuiFlexGroup + alignItems="center" + gutterSize="m" + responsive={false} > - <EuiFlexGroup - alignItems="center" - gutterSize="m" - responsive={false} + <div + className="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow" > - <div - className="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow" - > - <EuiFlexItem> - <div - className="euiFlexItem" + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiTitle + size="xxs" > - <EuiTitle - size="xxs" + <h3 + className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" + id="generated-id__title" > - <h3 - className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" - id="mockId__title" - > - Recently viewed - </h3> - </EuiTitle> - </div> - </EuiFlexItem> - </div> - </EuiFlexGroup> - </span> - </button> - </div> - <div - aria-labelledby="mockId" - className="euiAccordion__childWrapper" - id="mockId" - role="region" - tabIndex={-1} + Recently viewed + </h3> + </EuiTitle> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + </span> + </button> + </div> + <div + aria-labelledby="generated-id" + className="euiAccordion__childWrapper" + id="generated-id" + role="region" + tabIndex={-1} + > + <EuiResizeObserver + onResize={[Function]} > - <EuiResizeObserver - onResize={[Function]} - > - <div> + <div> + <div + className="" + > <div - className="" + className="euiCollapsibleNavGroup__children" > - <div - className="euiCollapsibleNavGroup__children" + <EuiText + color="subdued" + size="s" + style={ + Object { + "padding": "0 8px 8px", + } + } > - <EuiText - color="subdued" - size="s" + <div + className="euiText euiText--small" style={ Object { "padding": "0 8px 8px", } } > - <div - className="euiText euiText--small" - style={ - Object { - "padding": "0 8px 8px", - } - } + <EuiTextColor + color="subdued" + component="div" > - <EuiTextColor - color="subdued" - component="div" + <div + className="euiTextColor euiTextColor--subdued" > - <div - className="euiTextColor euiTextColor--subdued" - > - <p> - No recently viewed items - </p> - </div> - </EuiTextColor> - </div> - </EuiText> - </div> + <p> + No recently viewed items + </p> + </div> + </EuiTextColor> + </div> + </EuiText> </div> </div> - </EuiResizeObserver> - </div> + </div> + </EuiResizeObserver> </div> - </EuiAccordion> - </EuiCollapsibleNavGroup> - <EuiHorizontalRule - margin="none" - > - <hr - className="euiHorizontalRule euiHorizontalRule--full" - /> - </EuiHorizontalRule> - <EuiFlexItem - className="eui-yScroll" + </div> + </EuiAccordion> + </EuiCollapsibleNavGroup> + <EuiHorizontalRule + margin="none" + > + <hr + className="euiHorizontalRule euiHorizontalRule--full" + /> + </EuiHorizontalRule> + <EuiFlexItem + className="eui-yScroll" + > + <div + className="euiFlexItem eui-yScroll" > - <div - className="euiFlexItem eui-yScroll" + <EuiShowFor + sizes={ + Array [ + "l", + "xl", + ] + } > - <EuiShowFor - sizes={ - Array [ - "l", - "xl", - ] - } - > - <EuiCollapsibleNavGroup> + <EuiCollapsibleNavGroup> + <div + className="euiCollapsibleNavGroup" + id="generated-id" + > <div - className="euiCollapsibleNavGroup" - id="mockId" + className="euiCollapsibleNavGroup__children" > - <div - className="euiCollapsibleNavGroup__children" + <EuiListGroup + flush={true} > - <EuiListGroup - flush={true} + <ul + className="euiListGroup euiListGroup-flush euiListGroup--gutterSmall euiListGroup-maxWidthDefault" > - <ul - className="euiListGroup euiListGroup-flush euiListGroup--gutterSmall euiListGroup-maxWidthDefault" - > - <EuiListGroupItem - aria-label="Undock primary navigation" - buttonRef={ - Object { - "current": <button - aria-label="Undock primary navigation" - class="euiListGroupItem__button" - data-test-subj="collapsible-nav-lock" - type="button" - > - <span - class="euiListGroupItem__icon" - color="inherit" - data-euiicon-type="lock" - /> - <span - class="euiListGroupItem__label" - title="Undock navigation" - > - Undock navigation - </span> - </button>, - } - } - color="subdued" - data-test-subj="collapsible-nav-lock" - iconType="lock" - label="Undock navigation" - onClick={[Function]} - size="xs" - > - <li - className="euiListGroupItem euiListGroupItem--xSmall euiListGroupItem--subdued euiListGroupItem-isClickable" - > - <button + <EuiListGroupItem + aria-label="Undock primary navigation" + buttonRef={ + Object { + "current": <button aria-label="Undock primary navigation" - className="euiListGroupItem__button" + class="euiListGroupItem__button" data-test-subj="collapsible-nav-lock" - disabled={false} - onClick={[Function]} type="button" > - <EuiIcon - className="euiListGroupItem__icon" + <span + class="euiListGroupItem__icon" color="inherit" - type="lock" - > - <span - className="euiListGroupItem__icon" - color="inherit" - data-euiicon-type="lock" - /> - </EuiIcon> + data-euiicon-type="lock" + /> <span - className="euiListGroupItem__label" + class="euiListGroupItem__label" title="Undock navigation" > Undock navigation </span> - </button> - </li> - </EuiListGroupItem> - </ul> - </EuiListGroup> - </div> + </button>, + } + } + color="subdued" + data-test-subj="collapsible-nav-lock" + iconType="lock" + label="Undock navigation" + onClick={[Function]} + size="xs" + > + <li + className="euiListGroupItem euiListGroupItem--xSmall euiListGroupItem--subdued euiListGroupItem-isClickable" + > + <button + aria-label="Undock primary navigation" + className="euiListGroupItem__button" + data-test-subj="collapsible-nav-lock" + disabled={false} + onClick={[Function]} + type="button" + > + <EuiIcon + className="euiListGroupItem__icon" + color="inherit" + type="lock" + > + <span + className="euiListGroupItem__icon" + color="inherit" + data-euiicon-type="lock" + /> + </EuiIcon> + <span + className="euiListGroupItem__label" + title="Undock navigation" + > + Undock navigation + </span> + </button> + </li> + </EuiListGroupItem> + </ul> + </EuiListGroup> </div> - </EuiCollapsibleNavGroup> - </EuiShowFor> - </div> - </EuiFlexItem> - <EuiScreenReaderOnly - showOnFocus={true} - > - <EuiButtonEmpty - className="euiScreenReaderOnly--showOnFocus euiCollapsibleNav__closeButton" - iconType="cross" - onClick={[Function]} - size="xs" - textProps={ - Object { - "className": "euiCollapsibleNav__closeButtonText", - } - } - > - <button - className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall euiScreenReaderOnly--showOnFocus euiCollapsibleNav__closeButton" - disabled={false} - onClick={[Function]} - type="button" - > - <EuiButtonContent - className="euiButtonEmpty__content" - iconSide="left" - iconSize="s" - iconType="cross" - textProps={ - Object { - "className": "euiButtonEmpty__text euiCollapsibleNav__closeButtonText", - } - } - > - <span - className="euiButtonContent euiButtonEmpty__content" - > - <EuiIcon - className="euiButtonContent__icon" - color="inherit" - size="s" - type="cross" - > - <span - className="euiButtonContent__icon" - color="inherit" - data-euiicon-type="cross" - size="s" - /> - </EuiIcon> - <span - className="euiButtonEmpty__text euiCollapsibleNav__closeButtonText" - > - <EuiI18n - default="close" - token="euiCollapsibleNav.closeButtonLabel" - > - close - </EuiI18n> - </span> - </span> - </EuiButtonContent> - </button> - </EuiButtonEmpty> - </EuiScreenReaderOnly> - </nav> - </div> - </EuiFocusTrap> + </div> + </EuiCollapsibleNavGroup> + </EuiShowFor> + </div> + </EuiFlexItem> + </nav> + </EuiFlyout> </EuiCollapsibleNav> </CollapsibleNav> `; diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index 6ad1e2d3a1cc6..5aee9ca1b7c08 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -4947,42 +4947,57 @@ exports[`Header renders 1`] = ` isOpen={false} onClose={[Function]} > - <EuiWindowEvent - event="keydown" - handler={[Function]} - /> - <EuiFocusTrap - clickOutsideDisables={true} - disabled={true} + <EuiFlyout + aria-label="Primary" + as="nav" + className="euiCollapsibleNav" + closeButtonPosition="outside" + data-test-subj="collapsibleNav" + hideCloseButton={true} + id="mockId" + onClose={[Function]} + outsideClickCloses={true} + ownFocus={true} + paddingSize="none" + pushMinBreakpoint="l" + role={null} + side="left" + size={320} + type="push" > - <div - data-eui="EuiFocusTrap" + <nav + data-eui="EuiFlyout" + data-test-subj="collapsibleNav" + role={null} > - <nav - aria-label="Primary" - className="euiCollapsibleNav euiCollapsibleNav--isDocked" - data-test-subj="collapsibleNav" - id="mockId" + <EuiFlexItem + grow={false} + style={ + Object { + "flexShrink": 0, + } + } > - <EuiFlexItem - grow={false} + <div + className="euiFlexItem euiFlexItem--flexGrowZero" style={ Object { "flexShrink": 0, } } > - <div - className="euiFlexItem euiFlexItem--flexGrowZero" + <EuiCollapsibleNavGroup + background="light" + className="eui-yScroll" style={ Object { - "flexShrink": 0, + "maxHeight": "40vh", } } > - <EuiCollapsibleNavGroup - background="light" - className="eui-yScroll" + <div + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light eui-yScroll" + id="mockId" style={ Object { "maxHeight": "40vh", @@ -4990,109 +5005,109 @@ exports[`Header renders 1`] = ` } > <div - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light eui-yScroll" - id="mockId" - style={ - Object { - "maxHeight": "40vh", - } - } + className="euiCollapsibleNavGroup__children" > - <div - className="euiCollapsibleNavGroup__children" + <EuiListGroup + color="text" + gutterSize="none" + listItems={ + Array [ + Object { + "data-test-subj": "collapsibleNavCustomNavLink", + "href": "", + "icon": undefined, + "iconType": undefined, + "isActive": false, + "isDisabled": undefined, + "label": "Manage cloud deployment", + "onClick": [Function], + }, + ] + } + maxWidth="none" + size="s" > - <EuiListGroup - color="text" - gutterSize="none" - listItems={ - Array [ - Object { - "data-test-subj": "collapsibleNavCustomNavLink", - "href": "", - "icon": undefined, - "iconType": undefined, - "isActive": false, - "isDisabled": undefined, - "label": "Manage cloud deployment", - "onClick": [Function], - }, - ] + <ul + className="euiListGroup" + style={ + Object { + "maxWidth": "none", + } } - maxWidth="none" - size="s" > - <ul - className="euiListGroup" - style={ - Object { - "maxWidth": "none", - } - } + <EuiListGroupItem + color="text" + data-test-subj="collapsibleNavCustomNavLink" + href="" + isActive={false} + key="title-0" + label="Manage cloud deployment" + onClick={[Function]} + showToolTip={false} + size="s" + wrapText={false} > - <EuiListGroupItem - color="text" - data-test-subj="collapsibleNavCustomNavLink" - href="" - isActive={false} - key="title-0" - label="Manage cloud deployment" - onClick={[Function]} - showToolTip={false} - size="s" - wrapText={false} + <li + className="euiListGroupItem euiListGroupItem--small euiListGroupItem--text euiListGroupItem-isClickable" > - <li - className="euiListGroupItem euiListGroupItem--small euiListGroupItem--text euiListGroupItem-isClickable" + <button + className="euiListGroupItem__button" + data-test-subj="collapsibleNavCustomNavLink" + disabled={false} + onClick={[Function]} + type="button" > - <button - className="euiListGroupItem__button" - data-test-subj="collapsibleNavCustomNavLink" - disabled={false} - onClick={[Function]} - type="button" + <span + className="euiListGroupItem__label" + title="Manage cloud deployment" > - <span - className="euiListGroupItem__label" - title="Manage cloud deployment" - > - Manage cloud deployment - </span> - </button> - </li> - </EuiListGroupItem> - </ul> - </EuiListGroup> - </div> + Manage cloud deployment + </span> + </button> + </li> + </EuiListGroupItem> + </ul> + </EuiListGroup> </div> - </EuiCollapsibleNavGroup> - </div> - </EuiFlexItem> - <EuiHorizontalRule - margin="none" - > - <hr - className="euiHorizontalRule euiHorizontalRule--full" - /> - </EuiHorizontalRule> - <EuiFlexItem - grow={false} + </div> + </EuiCollapsibleNavGroup> + </div> + </EuiFlexItem> + <EuiHorizontalRule + margin="none" + > + <hr + className="euiHorizontalRule euiHorizontalRule--full" + /> + </EuiHorizontalRule> + <EuiFlexItem + grow={false} + style={ + Object { + "flexShrink": 0, + } + } + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" style={ Object { "flexShrink": 0, } } > - <div - className="euiFlexItem euiFlexItem--flexGrowZero" + <EuiCollapsibleNavGroup + background="light" + className="eui-yScroll" style={ Object { - "flexShrink": 0, + "maxHeight": "40vh", } } > - <EuiCollapsibleNavGroup - background="light" - className="eui-yScroll" + <div + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light eui-yScroll" + id="mockId" style={ Object { "maxHeight": "40vh", @@ -5100,307 +5115,353 @@ exports[`Header renders 1`] = ` } > <div - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light eui-yScroll" - id="mockId" - style={ - Object { - "maxHeight": "40vh", - } - } + className="euiCollapsibleNavGroup__children" > - <div - className="euiCollapsibleNavGroup__children" + <EuiListGroup + aria-label="Pinned links" + color="text" + gutterSize="none" + listItems={ + Array [ + Object { + "data-test-subj": "homeLink", + "href": "/", + "iconType": "home", + "label": "Home", + "onClick": [Function], + }, + ] + } + maxWidth="none" + size="s" > - <EuiListGroup + <ul aria-label="Pinned links" - color="text" - gutterSize="none" - listItems={ - Array [ - Object { - "data-test-subj": "homeLink", - "href": "/", - "iconType": "home", - "label": "Home", - "onClick": [Function], - }, - ] + className="euiListGroup" + style={ + Object { + "maxWidth": "none", + } } - maxWidth="none" - size="s" > - <ul - aria-label="Pinned links" - className="euiListGroup" - style={ - Object { - "maxWidth": "none", - } - } + <EuiListGroupItem + color="text" + data-test-subj="homeLink" + href="/" + iconType="home" + key="title-0" + label="Home" + onClick={[Function]} + showToolTip={false} + size="s" + wrapText={false} > - <EuiListGroupItem - color="text" - data-test-subj="homeLink" - href="/" - iconType="home" - key="title-0" - label="Home" - onClick={[Function]} - showToolTip={false} - size="s" - wrapText={false} + <li + className="euiListGroupItem euiListGroupItem--small euiListGroupItem--text euiListGroupItem-isClickable" > - <li - className="euiListGroupItem euiListGroupItem--small euiListGroupItem--text euiListGroupItem-isClickable" + <a + className="euiListGroupItem__button" + data-test-subj="homeLink" + href="/" + onClick={[Function]} + rel="noreferrer" > - <a - className="euiListGroupItem__button" - data-test-subj="homeLink" - href="/" - onClick={[Function]} - rel="noreferrer" + <EuiIcon + className="euiListGroupItem__icon" + color="inherit" + type="home" > - <EuiIcon + <span className="euiListGroupItem__icon" color="inherit" - type="home" - > - <span - className="euiListGroupItem__icon" - color="inherit" - data-euiicon-type="home" - /> - </EuiIcon> - <span - className="euiListGroupItem__label" - title="Home" - > - Home - </span> - </a> - </li> - </EuiListGroupItem> - </ul> - </EuiListGroup> - </div> + data-euiicon-type="home" + /> + </EuiIcon> + <span + className="euiListGroupItem__label" + title="Home" + > + Home + </span> + </a> + </li> + </EuiListGroupItem> + </ul> + </EuiListGroup> </div> - </EuiCollapsibleNavGroup> - </div> - </EuiFlexItem> - <EuiCollapsibleNavGroup - background="light" + </div> + </EuiCollapsibleNavGroup> + </div> + </EuiFlexItem> + <EuiCollapsibleNavGroup + background="light" + data-test-subj="collapsibleNavGroup-recentlyViewed" + initialIsOpen={true} + isCollapsible={true} + key="recentlyViewed" + onToggle={[Function]} + title="Recently viewed" + > + <EuiAccordion + arrowDisplay="right" + buttonClassName="euiCollapsibleNavGroup__heading" + buttonContent={ + <EuiFlexGroup + alignItems="center" + gutterSize="m" + responsive={false} + > + <EuiFlexItem> + <EuiTitle + size="xxs" + > + <h3 + className="euiCollapsibleNavGroup__title" + id="mockId__title" + > + Recently viewed + </h3> + </EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-recentlyViewed" + id="mockId" initialIsOpen={true} - isCollapsible={true} - key="recentlyViewed" + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} - title="Recently viewed" + paddingSize="none" > - <EuiAccordion - arrowDisplay="right" - buttonClassName="euiCollapsibleNavGroup__heading" - buttonContent={ - <EuiFlexGroup - alignItems="center" - gutterSize="m" - responsive={false} - > - <EuiFlexItem> - <EuiTitle - size="xxs" - > - <h3 - className="euiCollapsibleNavGroup__title" - id="mockId__title" - > - Recently viewed - </h3> - </EuiTitle> - </EuiFlexItem> - </EuiFlexGroup> - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" + <div + className="euiAccordion euiAccordion-isOpen euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-recentlyViewed" - id="mockId" - initialIsOpen={true} - isLoading={false} - isLoadingMessage={false} onToggle={[Function]} - paddingSize="none" > <div - className="euiAccordion euiAccordion-isOpen euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" - data-test-subj="collapsibleNavGroup-recentlyViewed" - onToggle={[Function]} + className="euiAccordion__triggerWrapper" > - <div - className="euiAccordion__triggerWrapper" + <button + aria-controls="mockId" + aria-expanded={true} + className="euiAccordion__button euiAccordion__buttonReverse euiCollapsibleNavGroup__heading" + id="mockId" + onClick={[Function]} + type="button" > - <button - aria-controls="mockId" - aria-expanded={true} - className="euiAccordion__button euiAccordion__buttonReverse euiCollapsibleNavGroup__heading" - id="mockId" - onClick={[Function]} - type="button" + <span + className="euiAccordion__iconWrapper" > - <span - className="euiAccordion__iconWrapper" + <EuiIcon + className="euiAccordion__icon euiAccordion__icon-isOpen" + size="m" + type="arrowRight" > - <EuiIcon + <span className="euiAccordion__icon euiAccordion__icon-isOpen" + data-euiicon-type="arrowRight" size="m" - type="arrowRight" - > - <span - className="euiAccordion__icon euiAccordion__icon-isOpen" - data-euiicon-type="arrowRight" - size="m" - /> - </EuiIcon> - </span> - <span - className="euiIEFlexWrapFix" + /> + </EuiIcon> + </span> + <span + className="euiIEFlexWrapFix" + > + <EuiFlexGroup + alignItems="center" + gutterSize="m" + responsive={false} > - <EuiFlexGroup - alignItems="center" - gutterSize="m" - responsive={false} + <div + className="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow" > - <div - className="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow" - > - <EuiFlexItem> - <div - className="euiFlexItem" + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiTitle + size="xxs" > - <EuiTitle - size="xxs" + <h3 + className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" + id="mockId__title" > - <h3 - className="euiTitle euiTitle--xxsmall euiCollapsibleNavGroup__title" - id="mockId__title" - > - Recently viewed - </h3> - </EuiTitle> - </div> - </EuiFlexItem> - </div> - </EuiFlexGroup> - </span> - </button> - </div> - <div - aria-labelledby="mockId" - className="euiAccordion__childWrapper" - id="mockId" - role="region" - tabIndex={-1} + Recently viewed + </h3> + </EuiTitle> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + </span> + </button> + </div> + <div + aria-labelledby="mockId" + className="euiAccordion__childWrapper" + id="mockId" + role="region" + tabIndex={-1} + > + <EuiResizeObserver + onResize={[Function]} > - <EuiResizeObserver - onResize={[Function]} - > - <div> + <div> + <div + className="" + > <div - className="" + className="euiCollapsibleNavGroup__children" > - <div - className="euiCollapsibleNavGroup__children" + <EuiListGroup + aria-label="Recently viewed links" + className="kbnCollapsibleNav__recentsListGroup" + color="subdued" + gutterSize="none" + listItems={ + Array [ + Object { + "aria-label": "dashboard, type: kibana", + "data-test-subj": "collapsibleNavAppLink--recent", + "href": "http://localhost/", + "label": "dashboard", + "onClick": [Function], + "title": "dashboard, type: kibana", + }, + ] + } + maxWidth="none" + size="s" > - <EuiListGroup + <ul aria-label="Recently viewed links" - className="kbnCollapsibleNav__recentsListGroup" - color="subdued" - gutterSize="none" - listItems={ - Array [ - Object { - "aria-label": "dashboard, type: kibana", - "data-test-subj": "collapsibleNavAppLink--recent", - "href": "http://localhost/", - "label": "dashboard", - "onClick": [Function], - "title": "dashboard, type: kibana", - }, - ] + className="euiListGroup kbnCollapsibleNav__recentsListGroup" + style={ + Object { + "maxWidth": "none", + } } - maxWidth="none" - size="s" > - <ul - aria-label="Recently viewed links" - className="euiListGroup kbnCollapsibleNav__recentsListGroup" - style={ - Object { - "maxWidth": "none", - } - } + <EuiListGroupItem + aria-label="dashboard, type: kibana" + color="subdued" + data-test-subj="collapsibleNavAppLink--recent" + href="http://localhost/" + key="title-0" + label="dashboard" + onClick={[Function]} + showToolTip={false} + size="s" + title="dashboard, type: kibana" + wrapText={false} > - <EuiListGroupItem - aria-label="dashboard, type: kibana" - color="subdued" - data-test-subj="collapsibleNavAppLink--recent" - href="http://localhost/" - key="title-0" - label="dashboard" - onClick={[Function]} - showToolTip={false} - size="s" - title="dashboard, type: kibana" - wrapText={false} + <li + className="euiListGroupItem euiListGroupItem--small euiListGroupItem--subdued euiListGroupItem-isClickable" > - <li - className="euiListGroupItem euiListGroupItem--small euiListGroupItem--subdued euiListGroupItem-isClickable" + <a + aria-label="dashboard, type: kibana" + className="euiListGroupItem__button" + data-test-subj="collapsibleNavAppLink--recent" + href="http://localhost/" + onClick={[Function]} + rel="noreferrer" + title="dashboard, type: kibana" > - <a - aria-label="dashboard, type: kibana" - className="euiListGroupItem__button" - data-test-subj="collapsibleNavAppLink--recent" - href="http://localhost/" - onClick={[Function]} - rel="noreferrer" - title="dashboard, type: kibana" + <span + className="euiListGroupItem__label" + title="dashboard" > - <span - className="euiListGroupItem__label" - title="dashboard" - > - dashboard - </span> - </a> - </li> - </EuiListGroupItem> - </ul> - </EuiListGroup> - </div> + dashboard + </span> + </a> + </li> + </EuiListGroupItem> + </ul> + </EuiListGroup> </div> </div> - </EuiResizeObserver> - </div> + </div> + </EuiResizeObserver> </div> - </EuiAccordion> - </EuiCollapsibleNavGroup> - <EuiHorizontalRule - margin="none" - > - <hr - className="euiHorizontalRule euiHorizontalRule--full" - /> - </EuiHorizontalRule> - <EuiFlexItem - className="eui-yScroll" + </div> + </EuiAccordion> + </EuiCollapsibleNavGroup> + <EuiHorizontalRule + margin="none" + > + <hr + className="euiHorizontalRule euiHorizontalRule--full" + /> + </EuiHorizontalRule> + <EuiFlexItem + className="eui-yScroll" + > + <div + className="euiFlexItem eui-yScroll" > - <div - className="euiFlexItem eui-yScroll" + <EuiCollapsibleNavGroup + data-test-subj="collapsibleNavGroup-noCategory" + key="0" > - <EuiCollapsibleNavGroup + <div + className="euiCollapsibleNavGroup" data-test-subj="collapsibleNavGroup-noCategory" - key="0" + id="mockId" > + <div + className="euiCollapsibleNavGroup__children" + > + <EuiListGroup + flush={true} + > + <ul + className="euiListGroup euiListGroup-flush euiListGroup--gutterSmall euiListGroup-maxWidthDefault" + > + <EuiListGroupItem + color="text" + data-test-subj="collapsibleNavAppLink" + href="" + isActive={false} + label="kibana" + onClick={[Function]} + size="s" + > + <li + className="euiListGroupItem euiListGroupItem--small euiListGroupItem--text euiListGroupItem-isClickable" + > + <button + className="euiListGroupItem__button" + data-test-subj="collapsibleNavAppLink" + disabled={false} + onClick={[Function]} + type="button" + > + <span + className="euiListGroupItem__label" + title="kibana" + > + kibana + </span> + </button> + </li> + </EuiListGroupItem> + </ul> + </EuiListGroup> + </div> + </div> + </EuiCollapsibleNavGroup> + <EuiShowFor + sizes={ + Array [ + "l", + "xl", + ] + } + > + <EuiCollapsibleNavGroup> <div className="euiCollapsibleNavGroup" - data-test-subj="collapsibleNavGroup-noCategory" id="mockId" > <div @@ -5413,29 +5474,63 @@ exports[`Header renders 1`] = ` className="euiListGroup euiListGroup-flush euiListGroup--gutterSmall euiListGroup-maxWidthDefault" > <EuiListGroupItem - color="text" - data-test-subj="collapsibleNavAppLink" - href="" - isActive={false} - label="kibana" + aria-label="Undock primary navigation" + buttonRef={ + Object { + "current": <button + aria-label="Undock primary navigation" + class="euiListGroupItem__button" + data-test-subj="collapsible-nav-lock" + type="button" + > + <span + class="euiListGroupItem__icon" + color="inherit" + data-euiicon-type="lock" + /> + <span + class="euiListGroupItem__label" + title="Undock navigation" + > + Undock navigation + </span> + </button>, + } + } + color="subdued" + data-test-subj="collapsible-nav-lock" + iconType="lock" + label="Undock navigation" onClick={[Function]} - size="s" + size="xs" > <li - className="euiListGroupItem euiListGroupItem--small euiListGroupItem--text euiListGroupItem-isClickable" + className="euiListGroupItem euiListGroupItem--xSmall euiListGroupItem--subdued euiListGroupItem-isClickable" > <button + aria-label="Undock primary navigation" className="euiListGroupItem__button" - data-test-subj="collapsibleNavAppLink" + data-test-subj="collapsible-nav-lock" disabled={false} onClick={[Function]} type="button" > + <EuiIcon + className="euiListGroupItem__icon" + color="inherit" + type="lock" + > + <span + className="euiListGroupItem__icon" + color="inherit" + data-euiicon-type="lock" + /> + </EuiIcon> <span className="euiListGroupItem__label" - title="kibana" + title="Undock navigation" > - kibana + Undock navigation </span> </button> </li> @@ -5445,163 +5540,11 @@ exports[`Header renders 1`] = ` </div> </div> </EuiCollapsibleNavGroup> - <EuiShowFor - sizes={ - Array [ - "l", - "xl", - ] - } - > - <EuiCollapsibleNavGroup> - <div - className="euiCollapsibleNavGroup" - id="mockId" - > - <div - className="euiCollapsibleNavGroup__children" - > - <EuiListGroup - flush={true} - > - <ul - className="euiListGroup euiListGroup-flush euiListGroup--gutterSmall euiListGroup-maxWidthDefault" - > - <EuiListGroupItem - aria-label="Undock primary navigation" - buttonRef={ - Object { - "current": <button - aria-label="Undock primary navigation" - class="euiListGroupItem__button" - data-test-subj="collapsible-nav-lock" - type="button" - > - <span - class="euiListGroupItem__icon" - color="inherit" - data-euiicon-type="lock" - /> - <span - class="euiListGroupItem__label" - title="Undock navigation" - > - Undock navigation - </span> - </button>, - } - } - color="subdued" - data-test-subj="collapsible-nav-lock" - iconType="lock" - label="Undock navigation" - onClick={[Function]} - size="xs" - > - <li - className="euiListGroupItem euiListGroupItem--xSmall euiListGroupItem--subdued euiListGroupItem-isClickable" - > - <button - aria-label="Undock primary navigation" - className="euiListGroupItem__button" - data-test-subj="collapsible-nav-lock" - disabled={false} - onClick={[Function]} - type="button" - > - <EuiIcon - className="euiListGroupItem__icon" - color="inherit" - type="lock" - > - <span - className="euiListGroupItem__icon" - color="inherit" - data-euiicon-type="lock" - /> - </EuiIcon> - <span - className="euiListGroupItem__label" - title="Undock navigation" - > - Undock navigation - </span> - </button> - </li> - </EuiListGroupItem> - </ul> - </EuiListGroup> - </div> - </div> - </EuiCollapsibleNavGroup> - </EuiShowFor> - </div> - </EuiFlexItem> - <EuiScreenReaderOnly - showOnFocus={true} - > - <EuiButtonEmpty - className="euiScreenReaderOnly--showOnFocus euiCollapsibleNav__closeButton" - iconType="cross" - onClick={[Function]} - size="xs" - textProps={ - Object { - "className": "euiCollapsibleNav__closeButtonText", - } - } - > - <button - className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall euiScreenReaderOnly--showOnFocus euiCollapsibleNav__closeButton" - disabled={false} - onClick={[Function]} - type="button" - > - <EuiButtonContent - className="euiButtonEmpty__content" - iconSide="left" - iconSize="s" - iconType="cross" - textProps={ - Object { - "className": "euiButtonEmpty__text euiCollapsibleNav__closeButtonText", - } - } - > - <span - className="euiButtonContent euiButtonEmpty__content" - > - <EuiIcon - className="euiButtonContent__icon" - color="inherit" - size="s" - type="cross" - > - <span - className="euiButtonContent__icon" - color="inherit" - data-euiicon-type="cross" - size="s" - /> - </EuiIcon> - <span - className="euiButtonEmpty__text euiCollapsibleNav__closeButtonText" - > - <EuiI18n - default="close" - token="euiCollapsibleNav.closeButtonLabel" - > - close - </EuiI18n> - </span> - </span> - </EuiButtonContent> - </button> - </EuiButtonEmpty> - </EuiScreenReaderOnly> - </nav> - </div> - </EuiFocusTrap> + </EuiShowFor> + </div> + </EuiFlexItem> + </nav> + </EuiFlyout> </EuiCollapsibleNav> </CollapsibleNav> </header> diff --git a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx index 7f338a859e7b4..460770744d53a 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx @@ -16,10 +16,6 @@ import { httpServiceMock } from '../../../http/http_service.mock'; import { ChromeRecentlyAccessedHistoryItem } from '../../recently_accessed'; import { CollapsibleNav } from './collapsible_nav'; -jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ - htmlIdGenerator: () => () => 'mockId', -})); - const { kibana, observability, security, management } = DEFAULT_APP_CATEGORIES; function mockLink({ title = 'discover', category }: Partial<ChromeNavLink>) { diff --git a/src/core/public/chrome/ui/header/header.test.tsx b/src/core/public/chrome/ui/header/header.test.tsx index fdbdde8556eeb..a3a0197b4017e 100644 --- a/src/core/public/chrome/ui/header/header.test.tsx +++ b/src/core/public/chrome/ui/header/header.test.tsx @@ -99,7 +99,7 @@ describe('Header', () => { act(() => isLocked$.next(true)); component.update(); - expect(component.find('nav[aria-label="Primary"]').exists()).toBeTruthy(); + expect(component.find('[data-test-subj="collapsibleNav"]').exists()).toBeTruthy(); expect(component).toMatchSnapshot(); act(() => diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 67cdd24aae848..246ca83ef5ade 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -87,6 +87,7 @@ export function Header({ const isVisible = useObservable(observables.isVisible$, false); const isLocked = useObservable(observables.isLocked$, false); const [isNavOpen, setIsNavOpen] = useState(false); + const [navId] = useState(htmlIdGenerator()()); const breadcrumbsAppendExtension = useObservable(breadcrumbsAppendExtension$); if (!isVisible) { @@ -99,7 +100,6 @@ export function Header({ } const toggleCollapsibleNavRef = createRef<HTMLButtonElement & { euiAnimate: () => void }>(); - const navId = htmlIdGenerator()(); const className = classnames('hide-for-sharing', 'headerGlobalNav'); const Breadcrumbs = ( diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index 1c4e78f0a5c2e..8ead0f50785bd 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -46,6 +46,7 @@ const defaultCoreSystemParams = { csp: { warnLegacyBrowsers: true, }, + version: 'version', } as any, }; @@ -91,12 +92,12 @@ describe('constructor', () => { }); }); - it('passes browserSupportsCsp to ChromeService', () => { + it('passes browserSupportsCsp and coreContext to ChromeService', () => { createCoreSystem(); - expect(ChromeServiceConstructor).toHaveBeenCalledTimes(1); expect(ChromeServiceConstructor).toHaveBeenCalledWith({ - browserSupportsCsp: expect.any(Boolean), + browserSupportsCsp: true, + kibanaVersion: 'version', }); }); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index f0ea1e62fc33f..9a28bf45df927 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -5,7 +5,6 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - import { CoreId } from '../server'; import { PackageInfo, EnvironmentMode } from '../server/types'; import { CoreSetup, CoreStart } from '.'; @@ -98,6 +97,7 @@ export class CoreSystem { this.injectedMetadata = new InjectedMetadataService({ injectedMetadata, }); + this.coreContext = { coreId: Symbol('core'), env: injectedMetadata.env }; this.fatalErrors = new FatalErrorsService(rootDomElement, () => { // Stop Core before rendering any fatal errors into the DOM @@ -109,14 +109,16 @@ export class CoreSystem { this.savedObjects = new SavedObjectsService(); this.uiSettings = new UiSettingsService(); this.overlay = new OverlayService(); - this.chrome = new ChromeService({ browserSupportsCsp }); + this.chrome = new ChromeService({ + browserSupportsCsp, + kibanaVersion: injectedMetadata.version, + }); this.docLinks = new DocLinksService(); this.rendering = new RenderingService(); this.application = new ApplicationService(); this.integrations = new IntegrationsService(); this.deprecations = new DeprecationsService(); - this.coreContext = { coreId: Symbol('core'), env: injectedMetadata.env }; this.plugins = new PluginsService(this.coreContext, injectedMetadata.uiPlugins); this.coreApp = new CoreApp(this.coreContext); } diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 8c52d09f82159..502b22a6f8e89 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -204,6 +204,7 @@ export class DocLinksService { }, search: { sessions: `${KIBANA_DOCS}search-sessions.html`, + sessionLimits: `${KIBANA_DOCS}search-sessions.html#_limitations`, }, date: { dateMath: `${ELASTICSEARCH_DOCS}common-options.html#date-math`, @@ -523,6 +524,7 @@ export interface DocLinksStart { }; readonly search: { readonly sessions: string; + readonly sessionLimits: string; }; readonly indexPatterns: { readonly introduction: string; diff --git a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap index f5a1c51ccbe15..fbd09f3096854 100644 --- a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap +++ b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap @@ -26,7 +26,7 @@ Array [ ] `; -exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"<div data-eui=\\"EuiFocusTrap\\"><div role=\\"dialog\\" class=\\"euiFlyout euiFlyout--medium euiFlyout--paddingLarge\\" tabindex=\\"0\\"><button class=\\"euiButtonIcon euiButtonIcon--text euiButtonIcon--empty euiButtonIcon--xSmall euiFlyout__closeButton\\" type=\\"button\\" aria-label=\\"Close this dialog\\" data-test-subj=\\"euiFlyoutCloseButton\\"><span data-euiicon-type=\\"cross\\" class=\\"euiButtonIcon__icon\\" aria-hidden=\\"true\\" color=\\"inherit\\"></span></button><div class=\\"kbnOverlayMountWrapper\\"><span>Flyout content</span></div></div></div>"`; +exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"<div data-eui=\\"EuiFlyout\\" role=\\"dialog\\"><button type=\\"button\\" data-test-subj=\\"euiFlyoutCloseButton\\"></button><div class=\\"kbnOverlayMountWrapper\\"><span>Flyout content</span></div></div>"`; exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 1`] = ` Array [ @@ -59,4 +59,4 @@ Array [ ] `; -exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"<div data-eui=\\"EuiFocusTrap\\"><div role=\\"dialog\\" class=\\"euiFlyout euiFlyout--medium euiFlyout--paddingLarge\\" tabindex=\\"0\\"><button class=\\"euiButtonIcon euiButtonIcon--text euiButtonIcon--empty euiButtonIcon--xSmall euiFlyout__closeButton\\" type=\\"button\\" aria-label=\\"Close this dialog\\" data-test-subj=\\"euiFlyoutCloseButton\\"><span data-euiicon-type=\\"cross\\" class=\\"euiButtonIcon__icon\\" aria-hidden=\\"true\\" color=\\"inherit\\"></span></button><div class=\\"kbnOverlayMountWrapper\\"><span>Flyout content 2</span></div></div></div>"`; +exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"<div data-eui=\\"EuiFlyout\\" role=\\"dialog\\"><button type=\\"button\\" data-test-subj=\\"euiFlyoutCloseButton\\"></button><div class=\\"kbnOverlayMountWrapper\\"><span>Flyout content 2</span></div></div>"`; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 27569935bcc65..ca95b253f9cdb 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -585,6 +585,7 @@ export interface DocLinksStart { }; readonly search: { readonly sessions: string; + readonly sessionLimits: string; }; readonly indexPatterns: { readonly introduction: string; @@ -1631,6 +1632,6 @@ export interface UserProvidedValues<T = any> { // Warnings were encountered during analysis: // -// src/core/public/core_system.ts:166:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts +// src/core/public/core_system.ts:168:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/public/rendering/_base.scss b/src/core/public/rendering/_base.scss index 4bd6afe90d342..92ba28ff70887 100644 --- a/src/core/public/rendering/_base.scss +++ b/src/core/public/rendering/_base.scss @@ -38,6 +38,7 @@ @mixin kbnAffordForHeader($headerHeight) { @include euiHeaderAffordForFixed($headerHeight); + #securitySolutionStickyKQL, #app-fixed-viewport { top: $headerHeight; } diff --git a/src/core/public/styles/_base.scss b/src/core/public/styles/_base.scss index 3386fa73f328a..de138cdf402e6 100644 --- a/src/core/public/styles/_base.scss +++ b/src/core/public/styles/_base.scss @@ -26,7 +26,7 @@ } .euiBody--collapsibleNavIsDocked .euiBottomBar { - margin-left: $euiCollapsibleNavWidth; + margin-left: 320px; // Hard-coded for now -- @cchaos } // Temporary fix for EuiPageHeader with a bottom border but no tabs or padding diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index 534d7df9d9466..e1986c5bf1d92 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -114,6 +114,7 @@ test('runs services on "start"', async () => { expect(mockSavedObjectsService.start).not.toHaveBeenCalled(); expect(mockUiSettingsService.start).not.toHaveBeenCalled(); expect(mockMetricsService.start).not.toHaveBeenCalled(); + expect(mockStatusService.start).not.toHaveBeenCalled(); await server.start(); @@ -121,6 +122,7 @@ test('runs services on "start"', async () => { expect(mockSavedObjectsService.start).toHaveBeenCalledTimes(1); expect(mockUiSettingsService.start).toHaveBeenCalledTimes(1); expect(mockMetricsService.start).toHaveBeenCalledTimes(1); + expect(mockStatusService.start).toHaveBeenCalledTimes(1); }); test('does not fail on "setup" if there are unused paths detected', async () => { diff --git a/src/core/server/server.ts b/src/core/server/server.ts index adf794c390338..3f553dd90678e 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -248,6 +248,7 @@ export class Server { savedObjects: savedObjectsStart, exposedConfigsToUsage: this.plugins.getExposedPluginConfigsToUsage(), }); + this.status.start(); this.coreStart = { capabilities: capabilitiesStart, @@ -261,7 +262,6 @@ export class Server { await this.plugins.start(this.coreStart); - this.status.start(); await this.http.start(); startTransaction?.end(); diff --git a/src/core/server/status/plugins_status.test.ts b/src/core/server/status/plugins_status.test.ts index b0d9e47876940..9dc1ddcddca3e 100644 --- a/src/core/server/status/plugins_status.test.ts +++ b/src/core/server/status/plugins_status.test.ts @@ -8,7 +8,7 @@ import { PluginName } from '../plugins'; import { PluginsStatusService } from './plugins_status'; -import { of, Observable, BehaviorSubject } from 'rxjs'; +import { of, Observable, BehaviorSubject, ReplaySubject } from 'rxjs'; import { ServiceStatusLevels, CoreStatus, ServiceStatus } from './types'; import { first } from 'rxjs/operators'; import { ServiceStatusLevelSnapshotSerializer } from './test_utils'; @@ -34,6 +34,28 @@ describe('PluginStatusService', () => { ['c', ['a', 'b']], ]); + describe('set', () => { + it('throws an exception if called after registrations are blocked', () => { + const service = new PluginsStatusService({ + core$: coreAllAvailable$, + pluginDependencies, + }); + + service.blockNewRegistrations(); + expect(() => { + service.set( + 'a', + of({ + level: ServiceStatusLevels.available, + summary: 'fail!', + }) + ); + }).toThrowErrorMatchingInlineSnapshot( + `"Custom statuses cannot be registered after setup, plugin [a] attempted"` + ); + }); + }); + describe('getDerivedStatus$', () => { it(`defaults to core's most severe status`, async () => { const serviceAvailable = new PluginsStatusService({ @@ -231,6 +253,75 @@ describe('PluginStatusService', () => { { a: { level: ServiceStatusLevels.available, summary: 'a available' } }, ]); }); + + it('updates when a plugin status observable emits', async () => { + const service = new PluginsStatusService({ + core$: coreAllAvailable$, + pluginDependencies: new Map([['a', []]]), + }); + const statusUpdates: Array<Record<PluginName, ServiceStatus>> = []; + const subscription = service + .getAll$() + .subscribe((pluginStatuses) => statusUpdates.push(pluginStatuses)); + + const aStatus$ = new BehaviorSubject<ServiceStatus>({ + level: ServiceStatusLevels.degraded, + summary: 'a degraded', + }); + service.set('a', aStatus$); + aStatus$.next({ level: ServiceStatusLevels.unavailable, summary: 'a unavailable' }); + aStatus$.next({ level: ServiceStatusLevels.available, summary: 'a available' }); + subscription.unsubscribe(); + + expect(statusUpdates).toEqual([ + { a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' } }, + { a: { level: ServiceStatusLevels.degraded, summary: 'a degraded' } }, + { a: { level: ServiceStatusLevels.unavailable, summary: 'a unavailable' } }, + { a: { level: ServiceStatusLevels.available, summary: 'a available' } }, + ]); + }); + + it('emits an unavailable status if first emission times out, then continues future emissions', async () => { + jest.useFakeTimers(); + const service = new PluginsStatusService({ + core$: coreAllAvailable$, + pluginDependencies: new Map([ + ['a', []], + ['b', ['a']], + ]), + }); + + const pluginA$ = new ReplaySubject<ServiceStatus>(1); + service.set('a', pluginA$); + const firstEmission = service.getAll$().pipe(first()).toPromise(); + jest.runAllTimers(); + + expect(await firstEmission).toEqual({ + a: { level: ServiceStatusLevels.unavailable, summary: 'Status check timed out after 30s' }, + b: { + level: ServiceStatusLevels.unavailable, + summary: '[a]: Status check timed out after 30s', + detail: 'See the status page for more information', + meta: { + affectedServices: { + a: { + level: ServiceStatusLevels.unavailable, + summary: 'Status check timed out after 30s', + }, + }, + }, + }, + }); + + pluginA$.next({ level: ServiceStatusLevels.available, summary: 'a available' }); + const secondEmission = service.getAll$().pipe(first()).toPromise(); + jest.runAllTimers(); + expect(await secondEmission).toEqual({ + a: { level: ServiceStatusLevels.available, summary: 'a available' }, + b: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, + }); + jest.useRealTimers(); + }); }); describe('getDependenciesStatus$', () => { diff --git a/src/core/server/status/plugins_status.ts b/src/core/server/status/plugins_status.ts index 1aacbf3be56db..6a8ef1081e165 100644 --- a/src/core/server/status/plugins_status.ts +++ b/src/core/server/status/plugins_status.ts @@ -7,13 +7,22 @@ */ import { BehaviorSubject, Observable, combineLatest, of } from 'rxjs'; -import { map, distinctUntilChanged, switchMap, debounceTime } from 'rxjs/operators'; +import { + map, + distinctUntilChanged, + switchMap, + debounceTime, + timeoutWith, + startWith, +} from 'rxjs/operators'; import { isDeepStrictEqual } from 'util'; import { PluginName } from '../plugins'; -import { ServiceStatus, CoreStatus } from './types'; +import { ServiceStatus, CoreStatus, ServiceStatusLevels } from './types'; import { getSummaryStatus } from './get_summary_status'; +const STATUS_TIMEOUT_MS = 30 * 1000; // 30 seconds + interface Deps { core$: Observable<CoreStatus>; pluginDependencies: ReadonlyMap<PluginName, PluginName[]>; @@ -23,6 +32,7 @@ export class PluginsStatusService { private readonly pluginStatuses = new Map<PluginName, Observable<ServiceStatus>>(); private readonly update$ = new BehaviorSubject(true); private readonly defaultInheritedStatus$: Observable<ServiceStatus>; + private newRegistrationsAllowed = true; constructor(private readonly deps: Deps) { this.defaultInheritedStatus$ = this.deps.core$.pipe( @@ -35,10 +45,19 @@ export class PluginsStatusService { } public set(plugin: PluginName, status$: Observable<ServiceStatus>) { + if (!this.newRegistrationsAllowed) { + throw new Error( + `Custom statuses cannot be registered after setup, plugin [${plugin}] attempted` + ); + } this.pluginStatuses.set(plugin, status$); this.update$.next(true); // trigger all existing Observables to update from the new source Observable } + public blockNewRegistrations() { + this.newRegistrationsAllowed = false; + } + public getAll$(): Observable<Record<PluginName, ServiceStatus>> { return this.getPluginStatuses$([...this.deps.pluginDependencies.keys()]); } @@ -86,13 +105,22 @@ export class PluginsStatusService { return this.update$.pipe( switchMap(() => { const pluginStatuses = plugins - .map( - (depName) => - [depName, this.pluginStatuses.get(depName) ?? this.getDerivedStatus$(depName)] as [ - PluginName, - Observable<ServiceStatus> - ] - ) + .map((depName) => { + const pluginStatus = this.pluginStatuses.get(depName) + ? this.pluginStatuses.get(depName)!.pipe( + timeoutWith( + STATUS_TIMEOUT_MS, + this.pluginStatuses.get(depName)!.pipe( + startWith({ + level: ServiceStatusLevels.unavailable, + summary: `Status check timed out after ${STATUS_TIMEOUT_MS / 1000}s`, + }) + ) + ) + ) + : this.getDerivedStatus$(depName); + return [depName, pluginStatus] as [PluginName, Observable<ServiceStatus>]; + }) .map(([pName, status$]) => status$.pipe(map((status) => [pName, status] as [PluginName, ServiceStatus])) ); diff --git a/src/core/server/status/status_service.ts b/src/core/server/status/status_service.ts index b8c19508a5d61..d4dc8ed3d4d72 100644 --- a/src/core/server/status/status_service.ts +++ b/src/core/server/status/status_service.ts @@ -135,9 +135,11 @@ export class StatusService implements CoreService<InternalStatusServiceSetup> { } public start() { - if (!this.overall$) { - throw new Error('cannot call `start` before `setup`'); + if (!this.pluginsStatus || !this.overall$) { + throw new Error(`StatusService#setup must be called before #start`); } + this.pluginsStatus.blockNewRegistrations(); + getOverallStatusChanges(this.overall$, this.stop$).subscribe((message) => { this.logger.info(message); }); diff --git a/src/core/server/status/types.ts b/src/core/server/status/types.ts index 411b942c8eb33..bfca4c74d9365 100644 --- a/src/core/server/status/types.ts +++ b/src/core/server/status/types.ts @@ -196,6 +196,9 @@ export interface StatusServiceSetup { * Completely overrides the default inherited status. * * @remarks + * The first emission from this Observable should occur within 30s, else this plugin's status will fallback to + * `unavailable` until the first emission. + * * See the {@link StatusServiceSetup.derivedStatus$} API for leveraging the default status * calculation that is provided by Core. */ diff --git a/src/dev/build/tasks/bin/scripts/kibana-encryption-keys.bat b/src/dev/build/tasks/bin/scripts/kibana-encryption-keys.bat new file mode 100755 index 0000000000000..9221af3142e61 --- /dev/null +++ b/src/dev/build/tasks/bin/scripts/kibana-encryption-keys.bat @@ -0,0 +1,35 @@ +@echo off + +SETLOCAL ENABLEDELAYEDEXPANSION + +set SCRIPT_DIR=%~dp0 +for %%I in ("%SCRIPT_DIR%..") do set DIR=%%~dpfI + +set NODE=%DIR%\node\node.exe + +If Not Exist "%NODE%" ( + Echo unable to find usable node.js executable. + Exit /B 1 +) + +set CONFIG_DIR=%KBN_PATH_CONF% +If [%KBN_PATH_CONF%] == [] ( + set "CONFIG_DIR=%DIR%\config" +) + +IF EXIST "%CONFIG_DIR%\node.options" ( + for /F "usebackq eol=# tokens=*" %%i in ("%CONFIG_DIR%\node.options") do ( + If [!NODE_OPTIONS!] == [] ( + set "NODE_OPTIONS=%%i" + ) Else ( + set "NODE_OPTIONS=!NODE_OPTIONS! %%i" + ) + ) +) + +TITLE Kibana Encryption Keys +"%NODE%" "%DIR%\src\cli_encryption_keys\dist" %* + +:finally + +ENDLOCAL diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index a9b2dd6aefdda..d109a824ca81d 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -69,7 +69,6 @@ kibana_vars=( logging.appenders logging.appenders.console logging.appenders.file - logging.appenders.rolling-file logging.dest logging.json logging.loggers @@ -204,8 +203,8 @@ kibana_vars=( xpack.actions.proxyUrl xpack.actions.rejectUnauthorized xpack.actions.responseTimeout - xpack.actions.tls.proxyVerificationMode - xpack.actions.tls.verificationMode + xpack.actions.ssl.proxyVerificationMode + xpack.actions.ssl.verificationMode xpack.alerting.healthCheck.interval xpack.alerting.invalidateApiKeysTask.interval xpack.alerting.invalidateApiKeysTask.removalDelay diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts index 050743114f657..2c54bb8dba179 100644 --- a/src/dev/typescript/projects.ts +++ b/src/dev/typescript/projects.ts @@ -22,6 +22,9 @@ export const PROJECTS = [ new Project(resolve(REPO_ROOT, 'x-pack/plugins/security_solution/cypress/tsconfig.json'), { name: 'security_solution/cypress', }), + new Project(resolve(REPO_ROOT, 'x-pack/plugins/osquery/cypress/tsconfig.json'), { + name: 'osquery/cypress', + }), new Project(resolve(REPO_ROOT, 'x-pack/plugins/apm/e2e/tsconfig.json'), { name: 'apm/cypress', disableTypeCheck: true, @@ -55,6 +58,9 @@ export const PROJECTS = [ ...glob .sync('test/interpreter_functional/plugins/*/tsconfig.json', { cwd: REPO_ROOT }) .map((path) => new Project(resolve(REPO_ROOT, path))), + ...glob + .sync('test/server_integration/__fixtures__/plugins/*/tsconfig.json', { cwd: REPO_ROOT }) + .map((path) => new Project(resolve(REPO_ROOT, path))), ]; export function filterProjectsByFlag(projectFlag?: string) { diff --git a/src/plugins/console/public/application/components/welcome_panel.tsx b/src/plugins/console/public/application/components/welcome_panel.tsx index eb746e313d228..8514d41c04a51 100644 --- a/src/plugins/console/public/application/components/welcome_panel.tsx +++ b/src/plugins/console/public/application/components/welcome_panel.tsx @@ -27,7 +27,7 @@ interface Props { export function WelcomePanel(props: Props) { return ( - <EuiFlyout onClose={props.onDismiss} data-test-subj="welcomePanel" size="s"> + <EuiFlyout onClose={props.onDismiss} data-test-subj="welcomePanel" size="s" ownFocus={false}> <EuiFlyoutHeader hasBorder> <EuiTitle size="m"> <h2> diff --git a/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap index 9f56740fdac22..afe339f3f43a2 100644 --- a/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap +++ b/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap @@ -603,7 +603,7 @@ exports[`DashboardEmptyScreen renders correctly with readonly mode 1`] = ` } > <EuiPageBody> - <main + <div className="euiPageBody euiPageBody--borderRadiusNone" > <EuiPageContent @@ -675,7 +675,7 @@ exports[`DashboardEmptyScreen renders correctly with readonly mode 1`] = ` </div> </EuiPanel> </EuiPageContent> - </main> + </div> </EuiPageBody> </div> </EuiPage> @@ -950,7 +950,7 @@ exports[`DashboardEmptyScreen renders correctly with view mode 1`] = ` } > <EuiPageBody> - <main + <div className="euiPageBody euiPageBody--borderRadiusNone" > <EuiPageContent @@ -1035,7 +1035,7 @@ exports[`DashboardEmptyScreen renders correctly with view mode 1`] = ` </div> </EuiPanel> </EuiPageContent> - </main> + </div> </EuiPageBody> </div> </EuiPage> diff --git a/src/plugins/data/common/es_query/es_query/build_es_query.ts b/src/plugins/data/common/es_query/es_query/build_es_query.ts index 45724796c3518..d7b3c630d1a6e 100644 --- a/src/plugins/data/common/es_query/es_query/build_es_query.ts +++ b/src/plugins/data/common/es_query/es_query/build_es_query.ts @@ -10,9 +10,9 @@ import { groupBy, has, isEqual } from 'lodash'; import { buildQueryFromKuery } from './from_kuery'; import { buildQueryFromFilters } from './from_filters'; import { buildQueryFromLucene } from './from_lucene'; -import { IIndexPattern } from '../../index_patterns'; import { Filter } from '../filters'; import { Query } from '../../query/types'; +import { IndexPatternBase } from './types'; export interface EsQueryConfig { allowLeadingWildcards: boolean; @@ -36,7 +36,7 @@ function removeMatchAll<T>(filters: T[]) { * config contains dateformat:tz */ export function buildEsQuery( - indexPattern: IIndexPattern | undefined, + indexPattern: IndexPatternBase | undefined, queries: Query | Query[], filters: Filter | Filter[], config: EsQueryConfig = { diff --git a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts b/src/plugins/data/common/es_query/es_query/filter_matches_index.ts index 478263d5ce601..b376436756092 100644 --- a/src/plugins/data/common/es_query/es_query/filter_matches_index.ts +++ b/src/plugins/data/common/es_query/es_query/filter_matches_index.ts @@ -6,15 +6,16 @@ * Side Public License, v 1. */ -import { IIndexPattern, IFieldType } from '../../index_patterns'; +import { IFieldType } from '../../index_patterns'; import { Filter } from '../filters'; +import { IndexPatternBase } from './types'; /* * TODO: We should base this on something better than `filter.meta.key`. We should probably modify * this to check if `filter.meta.index` matches `indexPattern.id` instead, but that's a breaking * change. */ -export function filterMatchesIndex(filter: Filter, indexPattern?: IIndexPattern | null) { +export function filterMatchesIndex(filter: Filter, indexPattern?: IndexPatternBase | null) { if (!filter.meta?.key || !indexPattern) { return true; } diff --git a/src/plugins/data/common/es_query/es_query/from_filters.ts b/src/plugins/data/common/es_query/es_query/from_filters.ts index e50862235af1d..7b3c58d45a569 100644 --- a/src/plugins/data/common/es_query/es_query/from_filters.ts +++ b/src/plugins/data/common/es_query/es_query/from_filters.ts @@ -10,7 +10,7 @@ import { isUndefined } from 'lodash'; import { migrateFilter } from './migrate_filter'; import { filterMatchesIndex } from './filter_matches_index'; import { Filter, cleanFilter, isFilterDisabled } from '../filters'; -import { IIndexPattern } from '../../index_patterns'; +import { IndexPatternBase } from './types'; import { handleNestedFilter } from './handle_nested_filter'; /** @@ -45,7 +45,7 @@ const translateToQuery = (filter: Filter) => { export const buildQueryFromFilters = ( filters: Filter[] = [], - indexPattern: IIndexPattern | undefined, + indexPattern: IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex: boolean = false ) => { filters = filters.filter((filter) => filter && !isFilterDisabled(filter)); diff --git a/src/plugins/data/common/es_query/es_query/from_kuery.ts b/src/plugins/data/common/es_query/es_query/from_kuery.ts index afedaae45872b..3eccfd8776113 100644 --- a/src/plugins/data/common/es_query/es_query/from_kuery.ts +++ b/src/plugins/data/common/es_query/es_query/from_kuery.ts @@ -7,11 +7,11 @@ */ import { fromKueryExpression, toElasticsearchQuery, nodeTypes, KueryNode } from '../kuery'; -import { IIndexPattern } from '../../index_patterns'; +import { IndexPatternBase } from './types'; import { Query } from '../../query/types'; export function buildQueryFromKuery( - indexPattern: IIndexPattern | undefined, + indexPattern: IndexPatternBase | undefined, queries: Query[] = [], allowLeadingWildcards: boolean = false, dateFormatTZ?: string @@ -24,7 +24,7 @@ export function buildQueryFromKuery( } function buildQuery( - indexPattern: IIndexPattern | undefined, + indexPattern: IndexPatternBase | undefined, queryASTs: KueryNode[], config: Record<string, any> = {} ) { diff --git a/src/plugins/data/common/es_query/es_query/handle_nested_filter.test.ts b/src/plugins/data/common/es_query/es_query/handle_nested_filter.test.ts index ee5305132042a..d312d034df564 100644 --- a/src/plugins/data/common/es_query/es_query/handle_nested_filter.test.ts +++ b/src/plugins/data/common/es_query/es_query/handle_nested_filter.test.ts @@ -9,13 +9,14 @@ import { handleNestedFilter } from './handle_nested_filter'; import { fields } from '../../index_patterns/mocks'; import { buildPhraseFilter, buildQueryFilter } from '../filters'; -import { IFieldType, IIndexPattern } from '../../index_patterns'; +import { IndexPatternBase } from './types'; +import { IFieldType } from '../../index_patterns'; describe('handleNestedFilter', function () { - const indexPattern: IIndexPattern = ({ + const indexPattern: IndexPatternBase = { id: 'logstash-*', fields, - } as unknown) as IIndexPattern; + }; it("should return the filter's query wrapped in nested query if the target field is nested", () => { const field = getField('nestedField.child'); diff --git a/src/plugins/data/common/es_query/es_query/handle_nested_filter.ts b/src/plugins/data/common/es_query/es_query/handle_nested_filter.ts index 93927d81565ef..60e92769503fb 100644 --- a/src/plugins/data/common/es_query/es_query/handle_nested_filter.ts +++ b/src/plugins/data/common/es_query/es_query/handle_nested_filter.ts @@ -7,9 +7,9 @@ */ import { getFilterField, cleanFilter, Filter } from '../filters'; -import { IIndexPattern } from '../../index_patterns'; +import { IndexPatternBase } from './types'; -export const handleNestedFilter = (filter: Filter, indexPattern?: IIndexPattern) => { +export const handleNestedFilter = (filter: Filter, indexPattern?: IndexPatternBase) => { if (!indexPattern) return filter; const fieldName = getFilterField(filter); diff --git a/src/plugins/data/common/es_query/es_query/index.ts b/src/plugins/data/common/es_query/es_query/index.ts index 31529480c8ac9..c10ea5846ae3f 100644 --- a/src/plugins/data/common/es_query/es_query/index.ts +++ b/src/plugins/data/common/es_query/es_query/index.ts @@ -11,3 +11,4 @@ export { buildQueryFromFilters } from './from_filters'; export { luceneStringToDsl } from './lucene_string_to_dsl'; export { decorateQuery } from './decorate_query'; export { getEsQueryConfig } from './get_es_query_config'; +export { IndexPatternBase } from './types'; diff --git a/src/plugins/data/common/es_query/es_query/migrate_filter.ts b/src/plugins/data/common/es_query/es_query/migrate_filter.ts index c7c44d019a31c..9bd78b092fc18 100644 --- a/src/plugins/data/common/es_query/es_query/migrate_filter.ts +++ b/src/plugins/data/common/es_query/es_query/migrate_filter.ts @@ -9,7 +9,7 @@ import { get, omit } from 'lodash'; import { getConvertedValueForField } from '../filters'; import { Filter } from '../filters'; -import { IIndexPattern } from '../../index_patterns'; +import { IndexPatternBase } from './types'; export interface DeprecatedMatchPhraseFilter extends Filter { query: { @@ -28,7 +28,7 @@ function isDeprecatedMatchPhraseFilter(filter: any): filter is DeprecatedMatchPh return Boolean(fieldName && get(filter, ['query', 'match', fieldName, 'type']) === 'phrase'); } -export function migrateFilter(filter: Filter, indexPattern?: IIndexPattern) { +export function migrateFilter(filter: Filter, indexPattern?: IndexPatternBase) { if (isDeprecatedMatchPhraseFilter(filter)) { const fieldName = Object.keys(filter.query.match)[0]; const params: Record<string, any> = get(filter, ['query', 'match', fieldName]); diff --git a/src/plugins/data/common/es_query/es_query/types.ts b/src/plugins/data/common/es_query/es_query/types.ts new file mode 100644 index 0000000000000..2133736516049 --- /dev/null +++ b/src/plugins/data/common/es_query/es_query/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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IFieldType } from '../../index_patterns'; + +export interface IndexPatternBase { + fields: IFieldType[]; + id?: string; +} diff --git a/src/plugins/data/common/es_query/filters/build_filters.ts b/src/plugins/data/common/es_query/filters/build_filters.ts index ba1bd0a615493..369f9530fb92b 100644 --- a/src/plugins/data/common/es_query/filters/build_filters.ts +++ b/src/plugins/data/common/es_query/filters/build_filters.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { IIndexPattern, IFieldType } from '../..'; +import { IFieldType, IndexPatternBase } from '../..'; import { Filter, FILTERS, @@ -19,7 +19,7 @@ import { } from '.'; export function buildFilter( - indexPattern: IIndexPattern, + indexPattern: IndexPatternBase, field: IFieldType, type: FILTERS, negate: boolean, @@ -59,7 +59,7 @@ export function buildCustomFilter( } function buildBaseFilter( - indexPattern: IIndexPattern, + indexPattern: IndexPatternBase, field: IFieldType, type: FILTERS, params: any diff --git a/src/plugins/data/common/es_query/filters/exists_filter.ts b/src/plugins/data/common/es_query/filters/exists_filter.ts index 441a6bcb924b7..4836950c3bb27 100644 --- a/src/plugins/data/common/es_query/filters/exists_filter.ts +++ b/src/plugins/data/common/es_query/filters/exists_filter.ts @@ -7,7 +7,8 @@ */ import { Filter, FilterMeta } from './meta_filter'; -import { IIndexPattern, IFieldType } from '../../index_patterns'; +import { IFieldType } from '../../index_patterns'; +import { IndexPatternBase } from '..'; export type ExistsFilterMeta = FilterMeta; @@ -26,7 +27,7 @@ export const getExistsFilterField = (filter: ExistsFilter) => { return filter.exists && filter.exists.field; }; -export const buildExistsFilter = (field: IFieldType, indexPattern: IIndexPattern) => { +export const buildExistsFilter = (field: IFieldType, indexPattern: IndexPatternBase) => { return { meta: { index: indexPattern.id, diff --git a/src/plugins/data/common/es_query/filters/index.ts b/src/plugins/data/common/es_query/filters/index.ts index 133f5cd232e6f..fe7cdadabaee3 100644 --- a/src/plugins/data/common/es_query/filters/index.ts +++ b/src/plugins/data/common/es_query/filters/index.ts @@ -14,10 +14,8 @@ export * from './custom_filter'; export * from './exists_filter'; export * from './geo_bounding_box_filter'; export * from './geo_polygon_filter'; -export * from './get_display_value'; export * from './get_filter_field'; export * from './get_filter_params'; -export * from './get_index_pattern_from_filter'; export * from './match_all_filter'; export * from './meta_filter'; export * from './missing_filter'; diff --git a/src/plugins/data/common/es_query/filters/phrase_filter.ts b/src/plugins/data/common/es_query/filters/phrase_filter.ts index 85562435e68d0..27c1e85562097 100644 --- a/src/plugins/data/common/es_query/filters/phrase_filter.ts +++ b/src/plugins/data/common/es_query/filters/phrase_filter.ts @@ -8,7 +8,8 @@ import type { estypes } from '@elastic/elasticsearch'; import { get, isPlainObject } from 'lodash'; import { Filter, FilterMeta } from './meta_filter'; -import { IIndexPattern, IFieldType } from '../../index_patterns'; +import { IFieldType } from '../../index_patterns'; +import { IndexPatternBase } from '..'; export type PhraseFilterMeta = FilterMeta & { params?: { @@ -60,7 +61,7 @@ export const getPhraseFilterValue = (filter: PhraseFilter): PhraseFilterValue => export const buildPhraseFilter = ( field: IFieldType, value: any, - indexPattern: IIndexPattern + indexPattern: IndexPatternBase ): PhraseFilter => { const convertedValue = getConvertedValueForField(field, value); diff --git a/src/plugins/data/common/es_query/filters/phrases_filter.ts b/src/plugins/data/common/es_query/filters/phrases_filter.ts index 849c1b3faef2a..8a79472154493 100644 --- a/src/plugins/data/common/es_query/filters/phrases_filter.ts +++ b/src/plugins/data/common/es_query/filters/phrases_filter.ts @@ -9,7 +9,8 @@ import { Filter, FilterMeta } from './meta_filter'; import { getPhraseScript } from './phrase_filter'; import { FILTERS } from './index'; -import { IIndexPattern, IFieldType } from '../../index_patterns'; +import { IFieldType } from '../../index_patterns'; +import { IndexPatternBase } from '../es_query'; export type PhrasesFilterMeta = FilterMeta & { params: string[]; // The unformatted values @@ -34,7 +35,7 @@ export const getPhrasesFilterField = (filter: PhrasesFilter) => { export const buildPhrasesFilter = ( field: IFieldType, params: any[], - indexPattern: IIndexPattern + indexPattern: IndexPatternBase ) => { const index = indexPattern.id; const type = FILTERS.PHRASES; diff --git a/src/plugins/data/common/es_query/filters/range_filter.ts b/src/plugins/data/common/es_query/filters/range_filter.ts index a082b93c0a79a..7bc7a8cff7487 100644 --- a/src/plugins/data/common/es_query/filters/range_filter.ts +++ b/src/plugins/data/common/es_query/filters/range_filter.ts @@ -8,7 +8,8 @@ import type { estypes } from '@elastic/elasticsearch'; import { map, reduce, mapValues, get, keys, pickBy } from 'lodash'; import { Filter, FilterMeta } from './meta_filter'; -import { IIndexPattern, IFieldType } from '../../index_patterns'; +import { IFieldType } from '../../index_patterns'; +import { IndexPatternBase } from '..'; const OPERANDS_IN_RANGE = 2; @@ -93,7 +94,7 @@ const format = (field: IFieldType, value: any) => export const buildRangeFilter = ( field: IFieldType, params: RangeFilterParams, - indexPattern: IIndexPattern, + indexPattern: IndexPatternBase, formattedValue?: string ): RangeFilter => { const filter: any = { meta: { index: indexPattern.id, params: {} } }; diff --git a/src/plugins/data/common/es_query/kuery/ast/ast.ts b/src/plugins/data/common/es_query/kuery/ast/ast.ts index be82128969968..3e7b25897cab7 100644 --- a/src/plugins/data/common/es_query/kuery/ast/ast.ts +++ b/src/plugins/data/common/es_query/kuery/ast/ast.ts @@ -10,10 +10,10 @@ import { JsonObject } from '@kbn/common-utils'; import { nodeTypes } from '../node_types/index'; import { KQLSyntaxError } from '../kuery_syntax_error'; import { KueryNode, DslQuery, KueryParseOptions } from '../types'; -import { IIndexPattern } from '../../../index_patterns/types'; // @ts-ignore import { parse as parseKuery } from './_generated_/kuery'; +import { IndexPatternBase } from '../..'; const fromExpression = ( expression: string | DslQuery, @@ -65,7 +65,7 @@ export const fromKueryExpression = ( */ export const toElasticsearchQuery = ( node: KueryNode, - indexPattern?: IIndexPattern, + indexPattern?: IndexPatternBase, config?: Record<string, any>, context?: Record<string, any> ): JsonObject => { diff --git a/src/plugins/data/common/es_query/kuery/functions/and.ts b/src/plugins/data/common/es_query/kuery/functions/and.ts index 1989704cb627e..ba7d5d1f6645b 100644 --- a/src/plugins/data/common/es_query/kuery/functions/and.ts +++ b/src/plugins/data/common/es_query/kuery/functions/and.ts @@ -7,7 +7,7 @@ */ import * as ast from '../ast'; -import { IIndexPattern, KueryNode } from '../../..'; +import { IndexPatternBase, KueryNode } from '../../..'; export function buildNodeParams(children: KueryNode[]) { return { @@ -17,7 +17,7 @@ export function buildNodeParams(children: KueryNode[]) { export function toElasticsearchQuery( node: KueryNode, - indexPattern?: IIndexPattern, + indexPattern?: IndexPatternBase, config: Record<string, any> = {}, context: Record<string, any> = {} ) { diff --git a/src/plugins/data/common/es_query/kuery/functions/exists.ts b/src/plugins/data/common/es_query/kuery/functions/exists.ts index 5238fb1d8ee7f..fa6c37e6ba18f 100644 --- a/src/plugins/data/common/es_query/kuery/functions/exists.ts +++ b/src/plugins/data/common/es_query/kuery/functions/exists.ts @@ -8,7 +8,7 @@ import { get } from 'lodash'; import * as literal from '../node_types/literal'; -import { IIndexPattern, KueryNode, IFieldType } from '../../..'; +import { KueryNode, IFieldType, IndexPatternBase } from '../../..'; export function buildNodeParams(fieldName: string) { return { @@ -18,7 +18,7 @@ export function buildNodeParams(fieldName: string) { export function toElasticsearchQuery( node: KueryNode, - indexPattern?: IIndexPattern, + indexPattern?: IndexPatternBase, config: Record<string, any> = {}, context: Record<string, any> = {} ) { diff --git a/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.ts b/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.ts index f2498f3ea2ad4..38a433b1b80ab 100644 --- a/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.ts +++ b/src/plugins/data/common/es_query/kuery/functions/geo_bounding_box.ts @@ -9,7 +9,7 @@ import _ from 'lodash'; import { nodeTypes } from '../node_types'; import * as ast from '../ast'; -import { IIndexPattern, KueryNode, IFieldType, LatLon } from '../../..'; +import { IndexPatternBase, KueryNode, IFieldType, LatLon } from '../../..'; export function buildNodeParams(fieldName: string, params: any) { params = _.pick(params, 'topLeft', 'bottomRight'); @@ -26,7 +26,7 @@ export function buildNodeParams(fieldName: string, params: any) { export function toElasticsearchQuery( node: KueryNode, - indexPattern?: IIndexPattern, + indexPattern?: IndexPatternBase, config: Record<string, any> = {}, context: Record<string, any> = {} ) { diff --git a/src/plugins/data/common/es_query/kuery/functions/geo_polygon.ts b/src/plugins/data/common/es_query/kuery/functions/geo_polygon.ts index 584a315930d9c..69de7248a7b38 100644 --- a/src/plugins/data/common/es_query/kuery/functions/geo_polygon.ts +++ b/src/plugins/data/common/es_query/kuery/functions/geo_polygon.ts @@ -8,7 +8,7 @@ import { nodeTypes } from '../node_types'; import * as ast from '../ast'; -import { IIndexPattern, KueryNode, IFieldType, LatLon } from '../../..'; +import { IndexPatternBase, KueryNode, IFieldType, LatLon } from '../../..'; import { LiteralTypeBuildNode } from '../node_types/types'; export function buildNodeParams(fieldName: string, points: LatLon[]) { @@ -25,7 +25,7 @@ export function buildNodeParams(fieldName: string, points: LatLon[]) { export function toElasticsearchQuery( node: KueryNode, - indexPattern?: IIndexPattern, + indexPattern?: IndexPatternBase, config: Record<string, any> = {}, context: Record<string, any> = {} ) { diff --git a/src/plugins/data/common/es_query/kuery/functions/is.ts b/src/plugins/data/common/es_query/kuery/functions/is.ts index a18ad230c3cae..55d036c2156f9 100644 --- a/src/plugins/data/common/es_query/kuery/functions/is.ts +++ b/src/plugins/data/common/es_query/kuery/functions/is.ts @@ -11,7 +11,7 @@ import { getPhraseScript } from '../../filters'; import { getFields } from './utils/get_fields'; import { getTimeZoneFromSettings } from '../../utils'; import { getFullFieldNameNode } from './utils/get_full_field_name_node'; -import { IIndexPattern, KueryNode, IFieldType } from '../../..'; +import { IndexPatternBase, KueryNode, IFieldType } from '../../..'; import * as ast from '../ast'; @@ -39,7 +39,7 @@ export function buildNodeParams(fieldName: string, value: any, isPhrase: boolean export function toElasticsearchQuery( node: KueryNode, - indexPattern?: IIndexPattern, + indexPattern?: IndexPatternBase, config: Record<string, any> = {}, context: Record<string, any> = {} ) { diff --git a/src/plugins/data/common/es_query/kuery/functions/nested.ts b/src/plugins/data/common/es_query/kuery/functions/nested.ts index bfd01ef39764c..46ceeaf3e5de6 100644 --- a/src/plugins/data/common/es_query/kuery/functions/nested.ts +++ b/src/plugins/data/common/es_query/kuery/functions/nested.ts @@ -8,7 +8,7 @@ import * as ast from '../ast'; import * as literal from '../node_types/literal'; -import { IIndexPattern, KueryNode } from '../../..'; +import { IndexPatternBase, KueryNode } from '../../..'; export function buildNodeParams(path: any, child: any) { const pathNode = @@ -20,7 +20,7 @@ export function buildNodeParams(path: any, child: any) { export function toElasticsearchQuery( node: KueryNode, - indexPattern?: IIndexPattern, + indexPattern?: IndexPatternBase, config: Record<string, any> = {}, context: Record<string, any> = {} ) { diff --git a/src/plugins/data/common/es_query/kuery/functions/not.ts b/src/plugins/data/common/es_query/kuery/functions/not.ts index ef4456897bcdd..f837cd261c814 100644 --- a/src/plugins/data/common/es_query/kuery/functions/not.ts +++ b/src/plugins/data/common/es_query/kuery/functions/not.ts @@ -7,7 +7,7 @@ */ import * as ast from '../ast'; -import { IIndexPattern, KueryNode } from '../../..'; +import { IndexPatternBase, KueryNode } from '../../..'; export function buildNodeParams(child: KueryNode) { return { @@ -17,7 +17,7 @@ export function buildNodeParams(child: KueryNode) { export function toElasticsearchQuery( node: KueryNode, - indexPattern?: IIndexPattern, + indexPattern?: IndexPatternBase, config: Record<string, any> = {}, context: Record<string, any> = {} ) { diff --git a/src/plugins/data/common/es_query/kuery/functions/or.ts b/src/plugins/data/common/es_query/kuery/functions/or.ts index 416687e7cde9c..7365cc39595e6 100644 --- a/src/plugins/data/common/es_query/kuery/functions/or.ts +++ b/src/plugins/data/common/es_query/kuery/functions/or.ts @@ -7,7 +7,7 @@ */ import * as ast from '../ast'; -import { IIndexPattern, KueryNode } from '../../..'; +import { IndexPatternBase, KueryNode } from '../../..'; export function buildNodeParams(children: KueryNode[]) { return { @@ -17,7 +17,7 @@ export function buildNodeParams(children: KueryNode[]) { export function toElasticsearchQuery( node: KueryNode, - indexPattern?: IIndexPattern, + indexPattern?: IndexPatternBase, config: Record<string, any> = {}, context: Record<string, any> = {} ) { diff --git a/src/plugins/data/common/es_query/kuery/functions/range.ts b/src/plugins/data/common/es_query/kuery/functions/range.ts index 06b345e5821c3..caefa7e5373ca 100644 --- a/src/plugins/data/common/es_query/kuery/functions/range.ts +++ b/src/plugins/data/common/es_query/kuery/functions/range.ts @@ -13,7 +13,7 @@ import { getRangeScript, RangeFilterParams } from '../../filters'; import { getFields } from './utils/get_fields'; import { getTimeZoneFromSettings } from '../../utils'; import { getFullFieldNameNode } from './utils/get_full_field_name_node'; -import { IIndexPattern, KueryNode, IFieldType } from '../../..'; +import { IndexPatternBase, KueryNode, IFieldType } from '../../..'; export function buildNodeParams(fieldName: string, params: RangeFilterParams) { const paramsToMap = _.pick(params, 'gt', 'lt', 'gte', 'lte', 'format'); @@ -33,7 +33,7 @@ export function buildNodeParams(fieldName: string, params: RangeFilterParams) { export function toElasticsearchQuery( node: KueryNode, - indexPattern?: IIndexPattern, + indexPattern?: IndexPatternBase, config: Record<string, any> = {}, context: Record<string, any> = {} ) { diff --git a/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.ts b/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.ts index 4002a36648f04..7dac1262d5062 100644 --- a/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.ts +++ b/src/plugins/data/common/es_query/kuery/functions/utils/get_fields.ts @@ -8,10 +8,10 @@ import * as literal from '../../node_types/literal'; import * as wildcard from '../../node_types/wildcard'; -import { KueryNode, IIndexPattern } from '../../../..'; +import { KueryNode, IndexPatternBase } from '../../../..'; import { LiteralTypeBuildNode } from '../../node_types/types'; -export function getFields(node: KueryNode, indexPattern?: IIndexPattern) { +export function getFields(node: KueryNode, indexPattern?: IndexPatternBase) { if (!indexPattern) return []; if (node.type === 'literal') { const fieldName = literal.toElasticsearchQuery(node as LiteralTypeBuildNode); diff --git a/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.ts b/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.ts index e623579226861..644791637aa70 100644 --- a/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.ts +++ b/src/plugins/data/common/es_query/kuery/functions/utils/get_full_field_name_node.ts @@ -7,11 +7,11 @@ */ import { getFields } from './get_fields'; -import { IIndexPattern, IFieldType, KueryNode } from '../../../..'; +import { IndexPatternBase, IFieldType, KueryNode } from '../../../..'; export function getFullFieldNameNode( rootNameNode: any, - indexPattern?: IIndexPattern, + indexPattern?: IndexPatternBase, nestedPath?: string ): KueryNode { const fullFieldNameNode = { diff --git a/src/plugins/data/common/es_query/kuery/node_types/function.ts b/src/plugins/data/common/es_query/kuery/node_types/function.ts index b9b7379dfb23d..642089a101f31 100644 --- a/src/plugins/data/common/es_query/kuery/node_types/function.ts +++ b/src/plugins/data/common/es_query/kuery/node_types/function.ts @@ -9,7 +9,7 @@ import _ from 'lodash'; import { functions } from '../functions'; -import { IIndexPattern, KueryNode } from '../../..'; +import { IndexPatternBase, KueryNode } from '../../..'; import { FunctionName, FunctionTypeBuildNode } from './types'; export function buildNode(functionName: FunctionName, ...args: any[]) { @@ -45,7 +45,7 @@ export function buildNodeWithArgumentNodes( export function toElasticsearchQuery( node: KueryNode, - indexPattern?: IIndexPattern, + indexPattern?: IndexPatternBase, config?: Record<string, any>, context?: Record<string, any> ) { diff --git a/src/plugins/data/common/es_query/kuery/node_types/types.ts b/src/plugins/data/common/es_query/kuery/node_types/types.ts index b3247a0ad8dc2..ea8eb5e8a0618 100644 --- a/src/plugins/data/common/es_query/kuery/node_types/types.ts +++ b/src/plugins/data/common/es_query/kuery/node_types/types.ts @@ -11,8 +11,8 @@ */ import { JsonValue } from '@kbn/common-utils'; -import { IIndexPattern } from '../../../index_patterns'; import { KueryNode } from '..'; +import { IndexPatternBase } from '../..'; export type FunctionName = | 'is' @@ -30,7 +30,7 @@ interface FunctionType { buildNodeWithArgumentNodes: (functionName: FunctionName, args: any[]) => FunctionTypeBuildNode; toElasticsearchQuery: ( node: any, - indexPattern?: IIndexPattern, + indexPattern?: IndexPatternBase, config?: Record<string, any>, context?: Record<string, any> ) => JsonValue; diff --git a/src/plugins/data/common/field_formats/converters/string.ts b/src/plugins/data/common/field_formats/converters/string.ts index ec92d75910522..64367df5d90dd 100644 --- a/src/plugins/data/common/field_formats/converters/string.ts +++ b/src/plugins/data/common/field_formats/converters/string.ts @@ -13,6 +13,10 @@ import { FieldFormat } from '../field_format'; import { TextContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; import { shortenDottedString } from '../../utils'; +export const emptyLabel = i18n.translate('data.fieldFormats.string.emptyLabel', { + defaultMessage: '(empty)', +}); + const TRANSFORM_OPTIONS = [ { kind: false, @@ -103,6 +107,9 @@ export class StringFormat extends FieldFormat { } textConvert: TextContextTypeConvert = (val) => { + if (val === '') { + return emptyLabel; + } switch (this.param('transform')) { case 'lower': return String(val).toLowerCase(); diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 07aa8967b905e..a88f029c0c7cd 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -9,6 +9,7 @@ import type { estypes } from '@elastic/elasticsearch'; import { ToastInputFields, ErrorToastOptions } from 'src/core/public/notifications'; // eslint-disable-next-line import type { SavedObject } from 'src/core/server'; +import type { IndexPatternBase } from '../es_query'; import { IFieldType } from './fields'; import { RUNTIME_FIELD_TYPES } from './constants'; import { SerializedFieldFormat } from '../../../expressions/common'; @@ -29,10 +30,8 @@ export interface RuntimeField { * IIndexPattern allows for an IndexPattern OR an index pattern saved object * Use IndexPattern or IndexPatternSpec instead */ -export interface IIndexPattern { - fields: IFieldType[]; +export interface IIndexPattern extends IndexPatternBase { title: string; - id?: string; /** * Type is used for identifying rollup indices, otherwise left undefined */ diff --git a/src/plugins/data/common/search/aggs/utils/parse_time_shift.ts b/src/plugins/data/common/search/aggs/utils/parse_time_shift.ts index 4d8ee0f889173..91379ea054de3 100644 --- a/src/plugins/data/common/search/aggs/utils/parse_time_shift.ts +++ b/src/plugins/data/common/search/aggs/utils/parse_time_shift.ts @@ -20,7 +20,7 @@ export const parseTimeShift = (val: string): moment.Duration | 'previous' | 'inv if (trimmedVal === 'previous') { return 'previous'; } - const [, amount, unit] = trimmedVal.match(/^(\d+)(\w)$/) || []; + const [, amount, unit] = trimmedVal.match(/^(\d+)\s*(\w)$/) || []; const parsedAmount = Number(amount); if (Number.isNaN(parsedAmount) || !allowedUnits.includes(unit as AllowedUnit)) { return 'invalid'; diff --git a/src/plugins/data/common/search/types.ts b/src/plugins/data/common/search/types.ts index d1890ec97df4e..c5cf3f9f09e6c 100644 --- a/src/plugins/data/common/search/types.ts +++ b/src/plugins/data/common/search/types.ts @@ -65,6 +65,11 @@ export interface IKibanaSearchResponse<RawResponse = any> { */ isPartial?: boolean; + /** + * Indicates whether the results returned are from the async-search index + */ + isRestored?: boolean; + /** * The raw response returned by the internal search method (usually the raw ES response) */ diff --git a/src/plugins/data/config.ts b/src/plugins/data/config.ts index 9306b64019bbc..1b7bfbc09ad16 100644 --- a/src/plugins/data/config.ts +++ b/src/plugins/data/config.ts @@ -44,10 +44,20 @@ export const searchSessionsConfigSchema = schema.object({ */ pageSize: schema.number({ defaultValue: 100 }), /** - * trackingInterval controls how often we track search session objects progress + * trackingInterval controls how often we track persisted search session objects progress */ trackingInterval: schema.duration({ defaultValue: '10s' }), + /** + * cleanupInterval controls how often we track non-persisted search session objects for cleanup + */ + cleanupInterval: schema.duration({ defaultValue: '60s' }), + + /** + * expireInterval controls how often we track persisted search session objects for expiration + */ + expireInterval: schema.duration({ defaultValue: '60m' }), + /** * monitoringTaskTimeout controls for how long task manager waits for search session monitoring task to complete before considering it timed out, * If tasks timeouts it receives cancel signal and next task starts in "trackingInterval" time diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 078dd3a9b7c5a..d7667f20d517e 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -23,7 +23,6 @@ import { disableFilter, FILTERS, FilterStateStore, - getDisplayValueFromFilter, getPhraseFilterField, getPhraseFilterValue, isExistsFilter, @@ -43,6 +42,7 @@ import { FilterLabel } from './ui'; import { FilterItem } from './ui/filter_bar'; import { + getDisplayValueFromFilter, generateFilters, onlyDisabledFiltersChanged, changeTimeFilter, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 4d9c69b137a3e..2849b93b14483 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -808,11 +808,11 @@ export const esFilters: { FILTERS: typeof FILTERS; FilterStateStore: typeof FilterStateStore; buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("../common").Filter; - buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").IIndexPattern) => import("../common").PhrasesFilter; - buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").IIndexPattern) => import("../common").ExistsFilter; - buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").IIndexPattern) => import("../common").PhraseFilter; + buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").IndexPatternBase) => import("../common").PhrasesFilter; + buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").IndexPatternBase) => import("../common").ExistsFilter; + buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").IndexPatternBase) => import("../common").PhraseFilter; buildQueryFilter: (query: any, index: string, alias: string) => import("../common").QueryStringFilter; - buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").IIndexPattern, formattedValue?: string | undefined) => import("../common").RangeFilter; + buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").IndexPatternBase, formattedValue?: string | undefined) => import("../common").RangeFilter; isPhraseFilter: (filter: any) => filter is import("../common").PhraseFilter; isExistsFilter: (filter: any) => filter is import("../common").ExistsFilter; isPhrasesFilter: (filter: any) => filter is import("../common").PhrasesFilter; @@ -858,7 +858,7 @@ export const esFilters: { export const esKuery: { nodeTypes: import("../common/es_query/kuery/node_types").NodeTypes; fromKueryExpression: (expression: any, parseOptions?: Partial<import("../common").KueryParseOptions>) => import("../common").KueryNode; - toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IIndexPattern | undefined, config?: Record<string, any> | undefined, context?: Record<string, any> | undefined) => import("@kbn/common-utils").JsonObject; + toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IndexPatternBase | undefined, config?: Record<string, any> | undefined, context?: Record<string, any> | undefined) => import("@kbn/common-utils").JsonObject; }; // Warning: (ae-missing-release-tag) "esQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -867,7 +867,7 @@ export const esKuery: { export const esQuery: { buildEsQuery: typeof buildEsQuery; getEsQueryConfig: typeof getEsQueryConfig; - buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IIndexPattern | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => { + buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => { must: never[]; filter: import("../common").Filter[]; should: never[]; @@ -1286,22 +1286,19 @@ export interface IFieldType { visualizable?: boolean; } +// Warning: (ae-forgotten-export) The symbol "IndexPatternBase" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "IIndexPattern" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @deprecated (undocumented) -export interface IIndexPattern { +export interface IIndexPattern extends IndexPatternBase { // Warning: (ae-forgotten-export) The symbol "SerializedFieldFormat" needs to be exported by the entry point index.d.ts // // (undocumented) fieldFormatMap?: Record<string, SerializedFieldFormat<unknown> | undefined>; - // (undocumented) - fields: IFieldType[]; getFormatterForField?: (field: IndexPatternField | IndexPatternField['spec'] | IFieldType) => FieldFormat; // (undocumented) getTimeField?(): IFieldType | undefined; // (undocumented) - id?: string; - // (undocumented) timeFieldName?: string; // (undocumented) title: string; @@ -1351,6 +1348,7 @@ export interface IKibanaSearchRequest<Params = any> { export interface IKibanaSearchResponse<RawResponse = any> { id?: string; isPartial?: boolean; + isRestored?: boolean; isRunning?: boolean; loaded?: number; rawResponse: RawResponse; @@ -2730,13 +2728,13 @@ export interface WaitUntilNextSessionCompletesOptions { // Warnings were encountered during analysis: // -// src/plugins/data/common/es_query/filters/exists_filter.ts:19:3 - (ae-forgotten-export) The symbol "ExistsFilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/es_query/filters/exists_filter.ts:20:3 - (ae-forgotten-export) The symbol "FilterExistsProperty" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/es_query/filters/exists_filter.ts:20:3 - (ae-forgotten-export) The symbol "ExistsFilterMeta" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/es_query/filters/exists_filter.ts:21:3 - (ae-forgotten-export) The symbol "FilterExistsProperty" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/match_all_filter.ts:17:3 - (ae-forgotten-export) The symbol "MatchAllFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/meta_filter.ts:43:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/meta_filter.ts:44:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/es_query/filters/phrase_filter.ts:22:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/es_query/filters/phrases_filter.ts:20:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/es_query/filters/phrase_filter.ts:23:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/es_query/filters/phrases_filter.ts:21:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:65:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:138:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:169:7 - (ae-forgotten-export) The symbol "RuntimeField" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/query/filter_manager/index.ts b/src/plugins/data/public/query/filter_manager/index.ts index 327b9763541ac..55dba640b07b6 100644 --- a/src/plugins/data/public/query/filter_manager/index.ts +++ b/src/plugins/data/public/query/filter_manager/index.ts @@ -11,3 +11,5 @@ export { FilterManager } from './filter_manager'; export { mapAndFlattenFilters } from './lib/map_and_flatten_filters'; export { onlyDisabledFiltersChanged } from './lib/only_disabled'; export { generateFilters } from './lib/generate_filters'; +export { getDisplayValueFromFilter } from './lib/get_display_value'; +export { getIndexPatternFromFilter } from './lib/get_index_pattern_from_filter'; diff --git a/src/plugins/data/common/es_query/filters/get_display_value.ts b/src/plugins/data/public/query/filter_manager/lib/get_display_value.ts similarity index 95% rename from src/plugins/data/common/es_query/filters/get_display_value.ts rename to src/plugins/data/public/query/filter_manager/lib/get_display_value.ts index ee719843ae879..45c6167f600bc 100644 --- a/src/plugins/data/common/es_query/filters/get_display_value.ts +++ b/src/plugins/data/public/query/filter_manager/lib/get_display_value.ts @@ -7,9 +7,8 @@ */ import { i18n } from '@kbn/i18n'; -import { IIndexPattern } from '../..'; +import { Filter, IIndexPattern } from '../../../../common'; import { getIndexPatternFromFilter } from './get_index_pattern_from_filter'; -import { Filter } from '../filters'; function getValueFormatter(indexPattern?: IIndexPattern, key?: string) { // checking getFormatterForField exists because there is at least once case where an index pattern diff --git a/src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.test.ts b/src/plugins/data/public/query/filter_manager/lib/get_index_pattern_from_filter.test.ts similarity index 100% rename from src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.test.ts rename to src/plugins/data/public/query/filter_manager/lib/get_index_pattern_from_filter.test.ts diff --git a/src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.ts b/src/plugins/data/public/query/filter_manager/lib/get_index_pattern_from_filter.ts similarity index 88% rename from src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.ts rename to src/plugins/data/public/query/filter_manager/lib/get_index_pattern_from_filter.ts index bceeb5f2793ec..7a2ce29102e51 100644 --- a/src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.ts +++ b/src/plugins/data/public/query/filter_manager/lib/get_index_pattern_from_filter.ts @@ -6,8 +6,7 @@ * Side Public License, v 1. */ -import { Filter } from '../filters'; -import { IIndexPattern } from '../..'; +import { Filter, IIndexPattern } from '../../../../common'; export function getIndexPatternFromFilter( filter: Filter, diff --git a/src/plugins/data/public/search/errors/index.ts b/src/plugins/data/public/search/errors/index.ts index 82c9e04b79798..fcdea8dec1c2e 100644 --- a/src/plugins/data/public/search/errors/index.ts +++ b/src/plugins/data/public/search/errors/index.ts @@ -12,3 +12,4 @@ export * from './timeout_error'; export * from './utils'; export * from './types'; export * from './http_error'; +export * from './search_session_incomplete_warning'; diff --git a/src/plugins/data/public/search/errors/search_session_incomplete_warning.tsx b/src/plugins/data/public/search/errors/search_session_incomplete_warning.tsx new file mode 100644 index 0000000000000..c5c5c37f31cf8 --- /dev/null +++ b/src/plugins/data/public/search/errors/search_session_incomplete_warning.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; +import { CoreStart } from 'kibana/public'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export const SearchSessionIncompleteWarning = (docLinks: CoreStart['docLinks']) => ( + <> + <EuiSpacer size="s" /> + It needs more time to fully render. You can wait here or come back to it later. + <EuiSpacer size="m" /> + <EuiText textAlign="right"> + <EuiLink + href={docLinks.links.search.sessionLimits} + color="warning" + target="_blank" + data-test-subj="searchSessionIncompleteWarning" + external + > + <FormattedMessage id="data.searchSession.warning.readDocs" defaultMessage="Read More" /> + </EuiLink> + </EuiText> + </> +); diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts index fe66d4b6e9937..155638250a2a4 100644 --- a/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts +++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.test.ts @@ -29,6 +29,12 @@ jest.mock('./utils', () => ({ }), })); +jest.mock('../errors/search_session_incomplete_warning', () => ({ + SearchSessionIncompleteWarning: jest.fn(), +})); + +import { SearchSessionIncompleteWarning } from '../errors/search_session_incomplete_warning'; + let searchInterceptor: SearchInterceptor; let mockCoreSetup: MockedKeys<CoreSetup>; let bfetchSetup: jest.Mocked<BfetchPublicSetup>; @@ -508,6 +514,7 @@ describe('SearchInterceptor', () => { } : null ); + sessionServiceMock.isRestore.mockReturnValue(!!opts?.isRestore); fetchMock.mockResolvedValue({ result: 200 }); }; @@ -562,6 +569,92 @@ describe('SearchInterceptor', () => { (sessionService as jest.Mocked<ISessionService>).getSearchOptions ).toHaveBeenCalledWith(sessionId); }); + + test('should not show warning if a search is available during restore', async () => { + setup({ + isRestore: true, + isStored: true, + sessionId: '123', + }); + + const responses = [ + { + time: 10, + value: { + isPartial: false, + isRunning: false, + isRestored: true, + id: 1, + rawResponse: { + took: 1, + }, + }, + }, + ]; + mockFetchImplementation(responses); + + const response = searchInterceptor.search( + {}, + { + sessionId: '123', + } + ); + response.subscribe({ next, error, complete }); + + await timeTravel(10); + + expect(SearchSessionIncompleteWarning).toBeCalledTimes(0); + }); + + test('should show warning once if a search is not available during restore', async () => { + setup({ + isRestore: true, + isStored: true, + sessionId: '123', + }); + + const responses = [ + { + time: 10, + value: { + isPartial: false, + isRunning: false, + isRestored: false, + id: 1, + rawResponse: { + took: 1, + }, + }, + }, + ]; + mockFetchImplementation(responses); + + searchInterceptor + .search( + {}, + { + sessionId: '123', + } + ) + .subscribe({ next, error, complete }); + + await timeTravel(10); + + expect(SearchSessionIncompleteWarning).toBeCalledTimes(1); + + searchInterceptor + .search( + {}, + { + sessionId: '123', + } + ) + .subscribe({ next, error, complete }); + + await timeTravel(10); + + expect(SearchSessionIncompleteWarning).toBeCalledTimes(1); + }); }); describe('Session tracking', () => { diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts index 57b156a9b3c00..e0e1df65101c7 100644 --- a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts @@ -43,6 +43,7 @@ import { PainlessError, SearchTimeoutError, TimeoutErrorMode, + SearchSessionIncompleteWarning, } from '../errors'; import { toMountPoint } from '../../../../kibana_react/public'; import { AbortError, KibanaServerError } from '../../../../kibana_utils/public'; @@ -82,6 +83,7 @@ export class SearchInterceptor { * @internal */ private application!: CoreStart['application']; + private docLinks!: CoreStart['docLinks']; private batchedFetch!: BatchedFunc< { request: IKibanaSearchRequest; options: ISearchOptionsSerializable }, IKibanaSearchResponse @@ -95,6 +97,7 @@ export class SearchInterceptor { this.deps.startServices.then(([coreStart]) => { this.application = coreStart.application; + this.docLinks = coreStart.docLinks; }); this.batchedFetch = deps.bfetch.batchedFunction({ @@ -345,6 +348,11 @@ export class SearchInterceptor { this.handleSearchError(e, searchOptions, searchAbortController.isTimeout()) ); }), + tap((response) => { + if (this.deps.session.isRestore() && response.isRestored === false) { + this.showRestoreWarning(this.deps.session.getSessionId()); + } + }), finalize(() => { this.pendingCount$.next(this.pendingCount$.getValue() - 1); if (untrackSearch && this.deps.session.isCurrentSession(sessionId)) { @@ -371,6 +379,25 @@ export class SearchInterceptor { } ); + private showRestoreWarningToast = (sessionId?: string) => { + this.deps.toasts.addWarning( + { + title: 'Your search session is still running', + text: toMountPoint(SearchSessionIncompleteWarning(this.docLinks)), + }, + { + toastLifeTimeMs: 60000, + } + ); + }; + + private showRestoreWarning = memoize( + this.showRestoreWarningToast, + (_: SearchTimeoutError, sessionId: string) => { + return sessionId; + } + ); + /** * Show one error notification per session. * @internal diff --git a/src/plugins/data/public/search/session/session_service.test.ts b/src/plugins/data/public/search/session/session_service.test.ts index 39680c4948366..7f388a29cd454 100644 --- a/src/plugins/data/public/search/session/session_service.test.ts +++ b/src/plugins/data/public/search/session/session_service.test.ts @@ -98,6 +98,14 @@ describe('Session service', () => { expect(nowProvider.reset).toHaveBeenCalled(); }); + it("Can clear other apps' session", async () => { + sessionService.start(); + expect(sessionService.getSessionId()).not.toBeUndefined(); + currentAppId$.next('change'); + sessionService.clear(); + expect(sessionService.getSessionId()).toBeUndefined(); + }); + it("Can start a new session in case there is other apps' stale session", async () => { const s1 = sessionService.start(); expect(sessionService.getSessionId()).not.toBeUndefined(); diff --git a/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx b/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx index 23de8327ce1f1..9cc9af04409f1 100644 --- a/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx +++ b/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx @@ -20,9 +20,9 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component } from 'react'; import { IIndexPattern } from '../..'; -import { getDisplayValueFromFilter, Filter } from '../../../common'; +import { Filter } from '../../../common'; import { FilterLabel } from '../filter_bar'; -import { mapAndFlattenFilters } from '../../query'; +import { mapAndFlattenFilters, getDisplayValueFromFilter } from '../../query'; interface Props { filters: Filter[]; diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx index 2b8978a125bca..734161ea87232 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx @@ -37,10 +37,10 @@ import { Operator } from './lib/filter_operators'; import { PhraseValueInput } from './phrase_value_input'; import { PhrasesValuesInput } from './phrases_values_input'; import { RangeValueInput } from './range_value_input'; +import { getIndexPatternFromFilter } from '../../../query'; import { IIndexPattern, IFieldType } from '../../..'; import { Filter, - getIndexPatternFromFilter, FieldFilter, buildFilter, buildCustomFilter, diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx index 9e5090f945182..09e0571c2a870 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -14,14 +14,13 @@ import { IUiSettingsClient } from 'src/core/public'; import { FilterEditor } from './filter_editor'; import { FilterView } from './filter_view'; import { IIndexPattern } from '../..'; +import { getDisplayValueFromFilter, getIndexPatternFromFilter } from '../../query'; import { Filter, isFilterPinned, - getDisplayValueFromFilter, toggleFilterNegated, toggleFilterPinned, toggleFilterDisabled, - getIndexPatternFromFilter, } from '../../../common'; import { getIndexPatterns } from '../../services'; diff --git a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap index a0a7e54d27532..0ab3f8a4e3466 100644 --- a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap +++ b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap @@ -176,27 +176,27 @@ exports[`Inspector Data View component should render empty state 1`] = ` <div className="euiEmptyPrompt" > + <EuiTitle + size="m" + > + <h2 + className="euiTitle euiTitle--medium" + > + <FormattedMessage + defaultMessage="No data available" + id="data.inspector.table.noDataAvailableTitle" + values={Object {}} + > + No data available + </FormattedMessage> + </h2> + </EuiTitle> <EuiTextColor color="subdued" > <span className="euiTextColor euiTextColor--subdued" > - <EuiTitle - size="m" - > - <h2 - className="euiTitle euiTitle--medium" - > - <FormattedMessage - defaultMessage="No data available" - id="data.inspector.table.noDataAvailableTitle" - values={Object {}} - > - No data available - </FormattedMessage> - </h2> - </EuiTitle> <EuiSpacer size="m" > diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 0764f4f441e42..dd60951e6d228 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -238,6 +238,7 @@ export { DataRequestHandlerContext, AsyncSearchResponse, AsyncSearchStatusResponse, + NoSearchIdInSessionError, } from './search'; // Search namespace diff --git a/src/plugins/data/server/search/errors/no_search_id_in_session.ts b/src/plugins/data/server/search/errors/no_search_id_in_session.ts new file mode 100644 index 0000000000000..b291df1cee5ba --- /dev/null +++ b/src/plugins/data/server/search/errors/no_search_id_in_session.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { KbnError } from '../../../../kibana_utils/common'; + +export class NoSearchIdInSessionError extends KbnError { + constructor() { + super('No search ID in this session matching the given search request'); + } +} diff --git a/src/plugins/data/server/search/index.ts b/src/plugins/data/server/search/index.ts index 812f3171aef99..b9affe96ea2dd 100644 --- a/src/plugins/data/server/search/index.ts +++ b/src/plugins/data/server/search/index.ts @@ -13,3 +13,4 @@ export * from './strategies/eql_search'; export { usageProvider, SearchUsage, searchUsageObserver } from './collectors'; export * from './aggs'; export * from './session'; +export * from './errors/no_search_id_in_session'; diff --git a/src/plugins/data/server/search/search_service.test.ts b/src/plugins/data/server/search/search_service.test.ts index 52ee8e60a5b26..314cb2c3acbf8 100644 --- a/src/plugins/data/server/search/search_service.test.ts +++ b/src/plugins/data/server/search/search_service.test.ts @@ -25,6 +25,7 @@ import { ISearchSessionService, ISearchStart, ISearchStrategy, + NoSearchIdInSessionError, } from '.'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { expressionsPluginMock } from '../../../expressions/public/mocks'; @@ -175,6 +176,22 @@ describe('Search service', () => { expect(request).toStrictEqual({ ...searchRequest, id: 'my_id' }); }); + it('searches even if id is not found in session during restore', async () => { + const searchRequest = { params: {} }; + const options = { sessionId, isStored: true, isRestore: true }; + + mockSessionClient.getId = jest.fn().mockImplementation(() => { + throw new NoSearchIdInSessionError(); + }); + + const res = await mockScopedClient.search(searchRequest, options).toPromise(); + + const [request, callOptions] = mockStrategy.search.mock.calls[0]; + expect(callOptions).toBe(options); + expect(request).toStrictEqual({ ...searchRequest }); + expect(res.isRestored).toBe(false); + }); + it('does not fail if `trackId` throws', async () => { const searchRequest = { params: {} }; const options = { sessionId, isStored: false, isRestore: false }; diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index a651d7b3bf105..00dffefa5e3a6 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -19,7 +19,7 @@ import { SharedGlobalConfig, StartServicesAccessor, } from 'src/core/server'; -import { first, switchMap, tap } from 'rxjs/operators'; +import { first, map, switchMap, tap, withLatestFrom } from 'rxjs/operators'; import { BfetchServerSetup } from 'src/plugins/bfetch/server'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import type { @@ -80,6 +80,7 @@ import { registerBsearchRoute } from './routes/bsearch'; import { getKibanaContext } from './expressions/kibana_context'; import { enhancedEsSearchStrategyProvider } from './strategies/ese_search'; import { eqlSearchStrategyProvider } from './strategies/eql_search'; +import { NoSearchIdInSessionError } from './errors/no_search_id_in_session'; type StrategyMap = Record<string, ISearchStrategy<any, any>>; @@ -287,24 +288,48 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> { options.strategy ); - const getSearchRequest = async () => - !options.sessionId || !options.isRestore || request.id - ? request - : { + const getSearchRequest = async () => { + if (!options.sessionId || !options.isRestore || request.id) { + return request; + } else { + try { + const id = await deps.searchSessionsClient.getId(request, options); + this.logger.debug(`Found search session id for request ${id}`); + return { ...request, - id: await deps.searchSessionsClient.getId(request, options), + id, }; + } catch (e) { + if (e instanceof NoSearchIdInSessionError) { + this.logger.debug('Ignoring missing search ID'); + return request; + } else { + throw e; + } + } + } + }; - return from(getSearchRequest()).pipe( + const searchRequest$ = from(getSearchRequest()); + const search$ = searchRequest$.pipe( switchMap((searchRequest) => strategy.search(searchRequest, options, deps)), - tap((response) => { - if (!options.sessionId || !response.id || options.isRestore) return; + withLatestFrom(searchRequest$), + tap(([response, requestWithId]) => { + if (!options.sessionId || !response.id || (options.isRestore && requestWithId.id)) return; // intentionally swallow tracking error, as it shouldn't fail the search deps.searchSessionsClient.trackId(request, response.id, options).catch((trackErr) => { this.logger.error(trackErr); }); + }), + map(([response, requestWithId]) => { + return { + ...response, + isRestored: !!requestWithId.id, + }; }) ); + + return search$; } catch (e) { return throwError(e); } diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index c2b533bc42dc6..5ca19f9e1e509 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -447,11 +447,11 @@ export const esFilters: { buildQueryFilter: (query: any, index: string, alias: string) => import("../common").QueryStringFilter; buildCustomFilter: typeof buildCustomFilter; buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("../common").Filter; - buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").IIndexPattern) => import("../common").ExistsFilter; + buildExistsFilter: (field: import("../common").IFieldType, indexPattern: import("../common").IndexPatternBase) => import("../common").ExistsFilter; buildFilter: typeof buildFilter; - buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").IIndexPattern) => import("../common").PhraseFilter; - buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").IIndexPattern) => import("../common").PhrasesFilter; - buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").IIndexPattern, formattedValue?: string | undefined) => import("../common").RangeFilter; + buildPhraseFilter: (field: import("../common").IFieldType, value: any, indexPattern: import("../common").IndexPatternBase) => import("../common").PhraseFilter; + buildPhrasesFilter: (field: import("../common").IFieldType, params: any[], indexPattern: import("../common").IndexPatternBase) => import("../common").PhrasesFilter; + buildRangeFilter: (field: import("../common").IFieldType, params: import("../common").RangeFilterParams, indexPattern: import("../common").IndexPatternBase, formattedValue?: string | undefined) => import("../common").RangeFilter; isFilterDisabled: (filter: import("../common").Filter) => boolean; }; @@ -461,14 +461,14 @@ export const esFilters: { export const esKuery: { nodeTypes: import("../common/es_query/kuery/node_types").NodeTypes; fromKueryExpression: (expression: any, parseOptions?: Partial<import("../common").KueryParseOptions>) => import("../common").KueryNode; - toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IIndexPattern | undefined, config?: Record<string, any> | undefined, context?: Record<string, any> | undefined) => import("@kbn/common-utils").JsonObject; + toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IndexPatternBase | undefined, config?: Record<string, any> | undefined, context?: Record<string, any> | undefined) => import("@kbn/common-utils").JsonObject; }; // Warning: (ae-missing-release-tag) "esQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export const esQuery: { - buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IIndexPattern | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => { + buildQueryFromFilters: (filters: import("../common").Filter[] | undefined, indexPattern: import("../common").IndexPatternBase | undefined, ignoreFilterIfFieldNotInIndex?: boolean) => { must: never[]; filter: import("../common").Filter[]; should: never[]; @@ -1205,6 +1205,14 @@ export enum METRIC_TYPES { TOP_HITS = "top_hits" } +// Warning: (ae-forgotten-export) The symbol "KbnError" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "NoSearchIdInSessionError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class NoSearchIdInSessionError extends KbnError { + constructor(); +} + // Warning: (ae-missing-release-tag) "OptionedParamType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1537,18 +1545,18 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "HistogramFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:128:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:128:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:244:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:244:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:246:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:247:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:256:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:258:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:262:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:263:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:267:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:270:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:245:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:245:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:247:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:248:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:258:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:259:1 - (ae-forgotten-export) The symbol "IpAddress" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:263:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:264:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:268:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:272:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:81:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts // src/plugins/data/server/search/types.ts:115:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.test.tsx new file mode 100644 index 0000000000000..8c32942740a76 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; +import { EuiSelectable } from '@elastic/eui'; +import { ShallowWrapper } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import { shallowWithIntl } from '@kbn/test/jest'; +import { ChangeIndexPattern } from './change_indexpattern'; +import { indexPatternMock } from '../../../../../__mocks__/index_pattern'; +import { indexPatternWithTimefieldMock } from '../../../../../__mocks__/index_pattern_with_timefield'; +import { IndexPatternRef } from './types'; + +function getProps() { + return { + indexPatternId: indexPatternMock.id, + indexPatternRefs: [ + indexPatternMock as IndexPatternRef, + indexPatternWithTimefieldMock as IndexPatternRef, + ], + onChangeIndexPattern: jest.fn(), + trigger: { + label: indexPatternMock.title, + title: indexPatternMock.title, + 'data-test-subj': 'indexPattern-switch-link', + }, + }; +} + +function getIndexPatternPickerList(instance: ShallowWrapper) { + return instance.find(EuiSelectable).first(); +} + +function getIndexPatternPickerOptions(instance: ShallowWrapper) { + return getIndexPatternPickerList(instance).prop('options'); +} + +export function selectIndexPatternPickerOption(instance: ShallowWrapper, selectedLabel: string) { + const options: Array<{ label: string; checked?: 'on' | 'off' }> = getIndexPatternPickerOptions( + instance + ).map((option: { label: string }) => + option.label === selectedLabel + ? { ...option, checked: 'on' } + : { ...option, checked: undefined } + ); + return getIndexPatternPickerList(instance).prop('onChange')!(options); +} + +describe('ChangeIndexPattern', () => { + test('switching index pattern to the same index pattern does not trigger onChangeIndexPattern', async () => { + const props = getProps(); + const comp = shallowWithIntl(<ChangeIndexPattern {...props} />); + await act(async () => { + selectIndexPatternPickerOption(comp, indexPatternMock.title); + }); + expect(props.onChangeIndexPattern).toHaveBeenCalledTimes(0); + }); + test('switching index pattern to a different index pattern triggers onChangeIndexPattern', async () => { + const props = getProps(); + const comp = shallowWithIntl(<ChangeIndexPattern {...props} />); + await act(async () => { + selectIndexPatternPickerOption(comp, indexPatternWithTimefieldMock.title); + }); + expect(props.onChangeIndexPattern).toHaveBeenCalledTimes(1); + expect(props.onChangeIndexPattern).toHaveBeenCalledWith(indexPatternWithTimefieldMock.id); + }); +}); diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.tsx index d5076e4daa990..5f2f35e2419dd 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/change_indexpattern.tsx @@ -26,17 +26,17 @@ export type ChangeIndexPatternTriggerProps = EuiButtonProps & { // TODO: refactor to shared component with ../../../../../../../../x-pack/legacy/plugins/lens/public/indexpattern_plugin/change_indexpattern export function ChangeIndexPattern({ - indexPatternRefs, indexPatternId, + indexPatternRefs, onChangeIndexPattern, - trigger, selectableProps, + trigger, }: { - trigger: ChangeIndexPatternTriggerProps; + indexPatternId?: string; indexPatternRefs: IndexPatternRef[]; onChangeIndexPattern: (newId: string) => void; - indexPatternId?: string; selectableProps?: EuiSelectableProps<{ value: string }>; + trigger: ChangeIndexPatternTriggerProps; }) { const [isPopoverOpen, setPopoverIsOpen] = useState(false); @@ -86,7 +86,9 @@ export function ChangeIndexPattern({ const choice = (choices.find(({ checked }) => checked) as unknown) as { value: string; }; - onChangeIndexPattern(choice.value); + if (choice.value !== indexPatternId) { + onChangeIndexPattern(choice.value); + } setPopoverIsOpen(false); }} searchProps={{ diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.tsx index e11c1716efe6b..4abfa6ecea55a 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_search.tsx @@ -204,15 +204,21 @@ export function DiscoverFieldSearch({ onChange, value, types, useNewFieldsApi }: return [ { id: `${id}-any`, - label: 'any', + label: i18n.translate('discover.fieldChooser.filter.toggleButton.any', { + defaultMessage: 'any', + }), }, { id: `${id}-true`, - label: 'yes', + label: i18n.translate('discover.fieldChooser.filter.toggleButton.yes', { + defaultMessage: 'yes', + }), }, { id: `${id}-false`, - label: 'no', + label: i18n.translate('discover.fieldChooser.filter.toggleButton.no', { + defaultMessage: 'no', + }), }, ]; }; diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.test.tsx index 965d3cb6a30c4..de3c55ad7a869 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.test.tsx @@ -9,14 +9,21 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; import { findTestSubject } from '@elastic/eui/lib/test'; -import { FilterInBtn, FilterOutBtn } from './discover_grid_cell_actions'; +import { FilterInBtn, FilterOutBtn, buildCellActions } from './discover_grid_cell_actions'; import { DiscoverGridContext } from './discover_grid_context'; import { indexPatternMock } from '../../../__mocks__/index_pattern'; import { esHits } from '../../../__mocks__/es_hits'; import { EuiButton } from '@elastic/eui'; +import { IndexPatternField } from 'src/plugins/data/common'; describe('Discover cell actions ', function () { + it('should not show cell actions for unfilterable fields', async () => { + expect( + buildCellActions({ name: 'foo', filterable: false } as IndexPatternField) + ).toBeUndefined(); + }); + it('triggers filter function when FilterInBtn is clicked', async () => { const contextMock = { expanded: undefined, diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.tsx index 4e9218f0881cd..ab80cd3e7b461 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.tsx @@ -79,7 +79,7 @@ export const FilterOutBtn = ({ }; export function buildCellActions(field: IndexPatternField) { - if (!field.aggregatable && !field.searchable) { + if (!field.filterable) { return undefined; } diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx index 60841799b1398..50be2473a441e 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx @@ -144,7 +144,9 @@ describe('Discover flyout', function () { expect(props.setExpandedDoc.mock.calls[0][0]._id).toBe('4'); }); - it('allows navigating with arrow keys through documents', () => { + // EuiFlyout is mocked in Jest environments. + // EUI team to reinstate `onKeyDown`: https://github.com/elastic/eui/issues/4883 + it.skip('allows navigating with arrow keys through documents', () => { const props = getProps(); const component = mountWithIntl(<DiscoverGridFlyout {...props} />); findTestSubject(component, 'docTableDetailsFlyout').simulate('keydown', { key: 'ArrowRight' }); diff --git a/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap b/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap index f40dbbbae1f87..68786871825ac 100644 --- a/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap +++ b/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap @@ -147,27 +147,27 @@ exports[`Source Viewer component renders error state 1`] = ` /> </EuiIcon> <EuiSpacer - size="s" + size="m" > <div - className="euiSpacer euiSpacer--s" + className="euiSpacer euiSpacer--m" /> </EuiSpacer> + <EuiTitle + size="m" + > + <h2 + className="euiTitle euiTitle--medium" + > + An Error Occurred + </h2> + </EuiTitle> <EuiTextColor color="subdued" > <span className="euiTextColor euiTextColor--subdued" > - <EuiTitle - size="m" - > - <h2 - className="euiTitle euiTitle--medium" - > - An Error Occurred - </h2> - </EuiTitle> <EuiSpacer size="m" > diff --git a/src/plugins/discover/public/index.ts b/src/plugins/discover/public/index.ts index fbe853ec6deb5..3840df4353faf 100644 --- a/src/plugins/discover/public/index.ts +++ b/src/plugins/discover/public/index.ts @@ -17,4 +17,6 @@ export function plugin(initializerContext: PluginInitializerContext) { export { SavedSearch, SavedSearchLoader, createSavedSearchesLoader } from './saved_searches'; export { ISearchEmbeddable, SEARCH_EMBEDDABLE_TYPE, SearchInput } from './application/embeddable'; export { loadSharingDataHelpers } from './shared'; + export { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from './url_generator'; +export { DiscoverAppLocator, DiscoverAppLocatorParams } from './locator'; diff --git a/src/plugins/discover/public/locator.test.ts b/src/plugins/discover/public/locator.test.ts new file mode 100644 index 0000000000000..edbb0663d4aa3 --- /dev/null +++ b/src/plugins/discover/public/locator.test.ts @@ -0,0 +1,270 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { hashedItemStore, getStatesFromKbnUrl } from '../../kibana_utils/public'; +import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock'; +import { FilterStateStore } from '../../data/common'; +import { DiscoverAppLocatorDefinition } from './locator'; +import { SerializableState } from 'src/plugins/kibana_utils/common'; + +const indexPatternId: string = 'c367b774-a4c2-11ea-bb37-0242ac130002'; +const savedSearchId: string = '571aaf70-4c88-11e8-b3d7-01146121b73d'; + +interface SetupParams { + useHash?: boolean; +} + +const setup = async ({ useHash = false }: SetupParams = {}) => { + const locator = new DiscoverAppLocatorDefinition({ + useHash, + }); + + return { + locator, + }; +}; + +beforeEach(() => { + // @ts-expect-error + hashedItemStore.storage = mockStorage; +}); + +describe('Discover url generator', () => { + test('can create a link to Discover with no state and no saved search', async () => { + const { locator } = await setup(); + const { app, path } = await locator.getLocation({}); + const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']); + + expect(app).toBe('discover'); + expect(_a).toEqual({}); + expect(_g).toEqual({}); + }); + + test('can create a link to a saved search in Discover', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ savedSearchId }); + const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']); + + expect(path.startsWith(`#/view/${savedSearchId}`)).toBe(true); + expect(_a).toEqual({}); + expect(_g).toEqual({}); + }); + + test('can specify specific index pattern', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + indexPatternId, + }); + const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']); + + expect(_a).toEqual({ + index: indexPatternId, + }); + expect(_g).toEqual({}); + }); + + test('can specify specific time range', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, + }); + const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']); + + expect(_a).toEqual({}); + expect(_g).toEqual({ + time: { + from: 'now-15m', + mode: 'relative', + to: 'now', + }, + }); + }); + + test('can specify query', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + query: { + language: 'kuery', + query: 'foo', + }, + }); + const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']); + + expect(_a).toEqual({ + query: { + language: 'kuery', + query: 'foo', + }, + }); + expect(_g).toEqual({}); + }); + + test('can specify local and global filters', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + filters: [ + { + meta: { + alias: 'foo', + disabled: false, + negate: false, + }, + $state: { + store: FilterStateStore.APP_STATE, + }, + }, + { + meta: { + alias: 'bar', + disabled: false, + negate: false, + }, + $state: { + store: FilterStateStore.GLOBAL_STATE, + }, + }, + ], + }); + const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']); + + expect(_a).toEqual({ + filters: [ + { + $state: { + store: 'appState', + }, + meta: { + alias: 'foo', + disabled: false, + negate: false, + }, + }, + ], + }); + expect(_g).toEqual({ + filters: [ + { + $state: { + store: 'globalState', + }, + meta: { + alias: 'bar', + disabled: false, + negate: false, + }, + }, + ], + }); + }); + + test('can set refresh interval', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + refreshInterval: { + pause: false, + value: 666, + }, + }); + const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']); + + expect(_a).toEqual({}); + expect(_g).toEqual({ + refreshInterval: { + pause: false, + value: 666, + }, + }); + }); + + test('can set time range', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + timeRange: { + from: 'now-3h', + to: 'now', + }, + }); + const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g']); + + expect(_a).toEqual({}); + expect(_g).toEqual({ + time: { + from: 'now-3h', + to: 'now', + }, + }); + }); + + test('can specify a search session id', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + searchSessionId: '__test__', + }); + + expect(path).toMatchInlineSnapshot(`"#/?_g=()&_a=()&searchSessionId=__test__"`); + expect(path).toContain('__test__'); + }); + + test('can specify columns, interval, sort and savedQuery', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + columns: ['_source'], + interval: 'auto', + sort: [['timestamp, asc']] as string[][] & SerializableState, + savedQuery: '__savedQueryId__', + }); + + expect(path).toMatchInlineSnapshot( + `"#/?_g=()&_a=(columns:!(_source),interval:auto,savedQuery:__savedQueryId__,sort:!(!('timestamp,%20asc')))"` + ); + }); + + describe('useHash property', () => { + describe('when default useHash is set to false', () => { + test('when using default, sets index pattern ID in the generated URL', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + indexPatternId, + }); + + expect(path.indexOf(indexPatternId) > -1).toBe(true); + }); + + test('when enabling useHash, does not set index pattern ID in the generated URL', async () => { + const { locator } = await setup(); + const { path } = await locator.getLocation({ + useHash: true, + indexPatternId, + }); + + expect(path.indexOf(indexPatternId) > -1).toBe(false); + }); + }); + + describe('when default useHash is set to true', () => { + test('when using default, does not set index pattern ID in the generated URL', async () => { + const { locator } = await setup({ useHash: true }); + const { path } = await locator.getLocation({ + indexPatternId, + }); + + expect(path.indexOf(indexPatternId) > -1).toBe(false); + }); + + test('when disabling useHash, sets index pattern ID in the generated URL', async () => { + const { locator } = await setup({ useHash: true }); + const { path } = await locator.getLocation({ + useHash: false, + indexPatternId, + }); + + expect(path.indexOf(indexPatternId) > -1).toBe(true); + }); + }); + }); +}); diff --git a/src/plugins/discover/public/locator.ts b/src/plugins/discover/public/locator.ts new file mode 100644 index 0000000000000..fff89903bc465 --- /dev/null +++ b/src/plugins/discover/public/locator.ts @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { TimeRange, Filter, Query, QueryState, RefreshInterval } from '../../data/public'; +import type { LocatorDefinition, LocatorPublic } from '../../share/public'; +import { esFilters } from '../../data/public'; +import { setStateToKbnUrl } from '../../kibana_utils/public'; + +export const DISCOVER_APP_LOCATOR = 'DISCOVER_APP_LOCATOR'; + +export interface DiscoverAppLocatorParams extends SerializableState { + /** + * Optionally set saved search ID. + */ + savedSearchId?: string; + + /** + * Optionally set index pattern ID. + */ + indexPatternId?: string; + + /** + * Optionally set the time range in the time picker. + */ + timeRange?: TimeRange; + + /** + * Optionally set the refresh interval. + */ + refreshInterval?: RefreshInterval & SerializableState; + + /** + * Optionally apply filters. + */ + filters?: Filter[]; + + /** + * Optionally set a query. + */ + query?: Query; + + /** + * If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines + * whether to hash the data in the url to avoid url length issues. + */ + useHash?: boolean; + + /** + * Background search session id + */ + searchSessionId?: string; + + /** + * Columns displayed in the table + */ + columns?: string[]; + + /** + * Used interval of the histogram + */ + interval?: string; + + /** + * Array of the used sorting [[field,direction],...] + */ + sort?: string[][] & SerializableState; + + /** + * id of the used saved query + */ + savedQuery?: string; +} + +export type DiscoverAppLocator = LocatorPublic<DiscoverAppLocatorParams>; + +export interface DiscoverAppLocatorDependencies { + useHash: boolean; +} + +export class DiscoverAppLocatorDefinition implements LocatorDefinition<DiscoverAppLocatorParams> { + public readonly id = DISCOVER_APP_LOCATOR; + + constructor(protected readonly deps: DiscoverAppLocatorDependencies) {} + + public readonly getLocation = async (params: DiscoverAppLocatorParams) => { + const { + useHash = this.deps.useHash, + filters, + indexPatternId, + query, + refreshInterval, + savedSearchId, + timeRange, + searchSessionId, + columns, + savedQuery, + sort, + interval, + } = params; + const savedSearchPath = savedSearchId ? `view/${encodeURIComponent(savedSearchId)}` : ''; + const appState: { + query?: Query; + filters?: Filter[]; + index?: string; + columns?: string[]; + interval?: string; + sort?: string[][]; + savedQuery?: string; + } = {}; + const queryState: QueryState = {}; + + if (query) appState.query = query; + if (filters && filters.length) + appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f)); + if (indexPatternId) appState.index = indexPatternId; + if (columns) appState.columns = columns; + if (savedQuery) appState.savedQuery = savedQuery; + if (sort) appState.sort = sort; + if (interval) appState.interval = interval; + + if (timeRange) queryState.time = timeRange; + if (filters && filters.length) + queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f)); + if (refreshInterval) queryState.refreshInterval = refreshInterval; + + let path = `#/${savedSearchPath}`; + path = setStateToKbnUrl<QueryState>('_g', queryState, { useHash }, path); + path = setStateToKbnUrl('_a', appState, { useHash }, path); + + if (searchSessionId) { + path = `${path}&searchSessionId=${searchSessionId}`; + } + + return { + app: 'discover', + path, + state: {}, + }; + }; +} diff --git a/src/plugins/discover/public/mocks.ts b/src/plugins/discover/public/mocks.ts index 0f57c5c0fa138..53160df472a3c 100644 --- a/src/plugins/discover/public/mocks.ts +++ b/src/plugins/discover/public/mocks.ts @@ -16,6 +16,12 @@ const createSetupContract = (): Setup => { docViews: { addDocView: jest.fn(), }, + locator: { + getLocation: jest.fn(), + getUrl: jest.fn(), + useUrl: jest.fn(), + navigate: jest.fn(), + }, }; return setupContract; }; @@ -26,6 +32,12 @@ const createStartContract = (): Start => { urlGenerator: ({ createUrl: jest.fn(), } as unknown) as DiscoverStart['urlGenerator'], + locator: { + getLocation: jest.fn(), + getUrl: jest.fn(), + useUrl: jest.fn(), + navigate: jest.fn(), + }, }; return startContract; }; diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index 7b4e7bb67c00e..ec89f7516e92d 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -59,6 +59,7 @@ import { DiscoverUrlGenerator, SEARCH_SESSION_ID_QUERY_PARAM, } from './url_generator'; +import { DiscoverAppLocatorDefinition, DiscoverAppLocator } from './locator'; import { SearchEmbeddableFactory } from './application/embeddable'; import { UsageCollectionSetup } from '../../usage_collection/public'; import { replaceUrlHashQuery } from '../../kibana_utils/public/'; @@ -83,17 +84,68 @@ export interface DiscoverSetup { */ addDocView(docViewRaw: DocViewInput | DocViewInputFn): void; }; + + /** + * `share` plugin URL locator for Discover app. Use it to generate links into + * Discover application, for example, navigate: + * + * ```ts + * await plugins.discover.locator.navigate({ + * savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d', + * indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002', + * timeRange: { + * to: 'now', + * from: 'now-15m', + * mode: 'relative', + * }, + * }); + * ``` + * + * Generate a location: + * + * ```ts + * const location = await plugins.discover.locator.getLocation({ + * savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d', + * indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002', + * timeRange: { + * to: 'now', + * from: 'now-15m', + * mode: 'relative', + * }, + * }); + * ``` + */ + readonly locator: undefined | DiscoverAppLocator; } export interface DiscoverStart { savedSearchLoader: SavedObjectLoader; /** - * `share` plugin URL generator for Discover app. Use it to generate links into - * Discover application, example: + * @deprecated Use URL locator instead. URL generaotr will be removed. + */ + readonly urlGenerator: undefined | UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>; + + /** + * `share` plugin URL locator for Discover app. Use it to generate links into + * Discover application, for example, navigate: + * + * ```ts + * await plugins.discover.locator.navigate({ + * savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d', + * indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002', + * timeRange: { + * to: 'now', + * from: 'now-15m', + * mode: 'relative', + * }, + * }); + * ``` + * + * Generate a location: * * ```ts - * const url = await plugins.discover.urlGenerator.createUrl({ + * const location = await plugins.discover.locator.getLocation({ * savedSearchId: '571aaf70-4c88-11e8-b3d7-01146121b73d', * indexPatternId: 'c367b774-a4c2-11ea-bb37-0242ac130002', * timeRange: { @@ -104,7 +156,7 @@ export interface DiscoverStart { * }); * ``` */ - readonly urlGenerator: undefined | UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>; + readonly locator: undefined | DiscoverAppLocator; } /** @@ -156,7 +208,12 @@ export class DiscoverPlugin private stopUrlTracking: (() => void) | undefined = undefined; private servicesInitialized: boolean = false; private innerAngularInitialized: boolean = false; + + /** + * @deprecated + */ private urlGenerator?: DiscoverStart['urlGenerator']; + private locator?: DiscoverAppLocator; /** * why are those functions public? they are needed for some mocha tests @@ -179,6 +236,15 @@ export class DiscoverPlugin }) ); } + + if (plugins.share) { + this.locator = plugins.share.url.locators.create( + new DiscoverAppLocatorDefinition({ + useHash: core.uiSettings.get('state:storeInSessionStorage'), + }) + ); + } + this.docViewsRegistry = new DocViewsRegistry(); setDocViewsRegistry(this.docViewsRegistry); this.docViewsRegistry.addDocView({ @@ -323,6 +389,7 @@ export class DiscoverPlugin docViews: { addDocView: this.docViewsRegistry.addDocView.bind(this.docViewsRegistry), }, + locator: this.locator, }; } @@ -367,6 +434,7 @@ export class DiscoverPlugin return { urlGenerator: this.urlGenerator, + locator: this.locator, savedSearchLoader: createSavedSearchesLoader({ savedObjectsClient: core.savedObjects.client, savedObjects: plugins.savedObjects, diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/page_error.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/page_error.tsx index 0a27b4098681b..732aa35b05237 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/page_error.tsx +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/page_error.tsx @@ -13,7 +13,7 @@ import { Error } from '../types'; interface Props { title: React.ReactNode; - error: Error; + error?: Error; actions?: JSX.Element; isCentered?: boolean; } @@ -32,30 +32,30 @@ export const PageError: React.FunctionComponent<Props> = ({ isCentered, ...rest }) => { - const { - error: errorString, - cause, // wrapEsError() on the server adds a "cause" array - message, - } = error; + const errorString = error?.error; + const cause = error?.cause; // wrapEsError() on the server adds a "cause" array + const message = error?.message; const errorContent = ( <EuiPageContent verticalPosition="center" horizontalPosition="center" color="danger"> <EuiEmptyPrompt title={<h2>{title}</h2>} body={ - <> - {cause ? message || errorString : <p>{message || errorString}</p>} - {cause && ( - <> - <EuiSpacer size="s" /> - <ul> - {cause.map((causeMsg, i) => ( - <li key={i}>{causeMsg}</li> - ))} - </ul> - </> - )} - </> + error && ( + <> + {cause ? message || errorString : <p>{message || errorString}</p>} + {cause && ( + <> + <EuiSpacer size="s" /> + <ul> + {cause.map((causeMsg, i) => ( + <li key={i}>{causeMsg}</li> + ))} + </ul> + </> + )} + </> + ) } iconType="alert" actions={actions} diff --git a/src/plugins/es_ui_shared/public/components/page_loading/index.ts b/src/plugins/es_ui_shared/public/components/page_loading/index.ts new file mode 100644 index 0000000000000..3e7b93bb4e7c3 --- /dev/null +++ b/src/plugins/es_ui_shared/public/components/page_loading/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { PageLoading } from './page_loading'; diff --git a/src/plugins/es_ui_shared/public/components/page_loading/page_loading.tsx b/src/plugins/es_ui_shared/public/components/page_loading/page_loading.tsx new file mode 100644 index 0000000000000..2fb99208e58ac --- /dev/null +++ b/src/plugins/es_ui_shared/public/components/page_loading/page_loading.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiEmptyPrompt, EuiLoadingSpinner, EuiText, EuiPageContent } from '@elastic/eui'; + +export const PageLoading: React.FunctionComponent = ({ children }) => { + return ( + <EuiPageContent verticalPosition="center" horizontalPosition="center" color="subdued"> + <EuiEmptyPrompt + title={<EuiLoadingSpinner size="xl" />} + body={<EuiText color="subdued">{children}</EuiText>} + data-test-subj="sectionLoading" + /> + </EuiPageContent> + ); +}; diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts index 7b9013c043a0e..ef2e2daa25468 100644 --- a/src/plugins/es_ui_shared/public/index.ts +++ b/src/plugins/es_ui_shared/public/index.ts @@ -17,6 +17,7 @@ import * as XJson from './xjson'; export { JsonEditor, OnJsonEditorUpdateHandler, JsonEditorState } from './components/json_editor'; +export { PageLoading } from './components/page_loading'; export { SectionLoading } from './components/section_loading'; export { Frequency, CronEditor } from './components/cron_editor'; diff --git a/src/plugins/home/public/application/components/tutorial/instruction.js b/src/plugins/home/public/application/components/tutorial/instruction.js index e4b3b3f321bf9..b0b87ef438c96 100644 --- a/src/plugins/home/public/application/components/tutorial/instruction.js +++ b/src/plugins/home/public/application/components/tutorial/instruction.js @@ -115,6 +115,16 @@ export function Instruction({ {post} + {LazyCustomComponent && ( + <Suspense fallback={<EuiLoadingSpinner />}> + <LazyCustomComponent + basePath={getBasePath()} + isDarkTheme={uiSettings.get('theme:darkMode')} + http={http} + /> + </Suspense> + )} + <EuiSpacer /> </div> ); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx index fc25879b128ec..77ef0903bc6fc 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx @@ -216,7 +216,11 @@ const FieldEditorComponent = ({ Boolean(field?.type) && field?.type !== (updatedType && updatedType[0].value); return ( - <Form form={form} className="indexPatternFieldEditor__form"> + <Form + form={form} + className="indexPatternFieldEditor__form" + data-test-subj={'indexPatternFieldEditorForm'} + > <EuiFlexGroup> {/* Name */} <EuiFlexItem> diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap index 40170c39942e5..79c1a11cfef84 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap @@ -153,7 +153,7 @@ exports[`UrlFormatEditor should render normally 1`] = ` class="euiFormControlLayout__childrenWrapper" > <input - aria-describedby="generated-id-help" + aria-describedby="generated-id-help-0" class="euiFieldText" data-test-subj="urlEditorUrlTemplate" id="generated-id" @@ -164,7 +164,7 @@ exports[`UrlFormatEditor should render normally 1`] = ` </div> <div class="euiFormHelpText euiFormRow__text" - id="generated-id-help" + id="generated-id-help-0" > <a class="euiLink euiLink--primary" @@ -216,7 +216,7 @@ exports[`UrlFormatEditor should render normally 1`] = ` class="euiFormControlLayout__childrenWrapper" > <input - aria-describedby="generated-id-help" + aria-describedby="generated-id-help-0" class="euiFieldText" data-test-subj="urlEditorLabelTemplate" id="generated-id" @@ -227,7 +227,7 @@ exports[`UrlFormatEditor should render normally 1`] = ` </div> <div class="euiFormHelpText euiFormRow__text" - id="generated-id-help" + id="generated-id-help-0" > <a class="euiLink euiLink--primary" diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/url.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/url.test.tsx index 9f299a433aab1..1000d9d2b8650 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/url.test.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/url.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { FieldFormat } from 'src/plugins/data/public'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { UrlFormatEditor } from './url'; import { coreMock } from 'src/core/public/mocks'; import { createKibanaReactContext } from '../../../../../../kibana_react/public'; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/__snapshots__/header.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/__snapshots__/header.test.tsx.snap index ceb5b4f343568..5e5fbb7c5e99d 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/__snapshots__/header.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/__snapshots__/header.test.tsx.snap @@ -76,7 +76,7 @@ exports[`Header should render a different name, prompt, and beta tag if provided label="Beta" > <span - className="euiBetaBadge" + className="euiBetaBadge euiBetaBadge--hollow" title="Beta" > Beta diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_call_outs/__snapshots__/warning_call_out.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_call_outs/__snapshots__/warning_call_out.test.tsx.snap index e74bb671e7f4e..ba0f2aee0565f 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_call_outs/__snapshots__/warning_call_out.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_call_outs/__snapshots__/warning_call_out.test.tsx.snap @@ -140,62 +140,72 @@ exports[`ScriptingWarningCallOut should render normally 1`] = ` </span> </div> <EuiText + color="default" size="s" > <div className="euiText euiText--small" > - <EuiText - size="s" + <EuiTextColor + color="default" + component="div" > <div - className="euiText euiText--small" + className="euiTextColor euiTextColor--default" > - <p> - <FormattedMessage - defaultMessage="For greater flexibility and Painless script support, use {runtimeDocs}." - id="indexPatternManagement.scriptedFieldsDeprecatedBody" - values={ - Object { - "runtimeDocs": <EuiLink - target="_blank" - > - <FormattedMessage - defaultMessage="runtime fields" - id="indexPatternManagement.warningCallOutLabel.runtimeLink" - values={Object {}} - /> - </EuiLink>, - } - } + <EuiText + size="s" + > + <div + className="euiText euiText--small" > - <span> - For greater flexibility and Painless script support, use - <EuiLink - target="_blank" + <p> + <FormattedMessage + defaultMessage="For greater flexibility and Painless script support, use {runtimeDocs}." + id="indexPatternManagement.scriptedFieldsDeprecatedBody" + values={ + Object { + "runtimeDocs": <EuiLink + target="_blank" + > + <FormattedMessage + defaultMessage="runtime fields" + id="indexPatternManagement.warningCallOutLabel.runtimeLink" + values={Object {}} + /> + </EuiLink>, + } + } > - <button - className="euiLink euiLink--primary" - disabled={false} - type="button" - > - <FormattedMessage - defaultMessage="runtime fields" - id="indexPatternManagement.warningCallOutLabel.runtimeLink" - values={Object {}} + <span> + For greater flexibility and Painless script support, use + <EuiLink + target="_blank" > - <span> - runtime fields - </span> - </FormattedMessage> - </button> - </EuiLink> - . - </span> - </FormattedMessage> - </p> + <button + className="euiLink euiLink--primary" + disabled={false} + type="button" + > + <FormattedMessage + defaultMessage="runtime fields" + id="indexPatternManagement.warningCallOutLabel.runtimeLink" + values={Object {}} + > + <span> + runtime fields + </span> + </FormattedMessage> + </button> + </EuiLink> + . + </span> + </FormattedMessage> + </p> + </div> + </EuiText> </div> - </EuiText> + </EuiTextColor> </div> </EuiText> </div> diff --git a/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap b/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap index 5ad8205365146..67d2cf72c5375 100644 --- a/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap +++ b/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap @@ -329,6 +329,7 @@ exports[`InspectorPanel should render as expected 1`] = ` > <div className="euiFlyoutBody__overflow" + tabIndex={0} > <div className="euiFlyoutBody__overflowContent" diff --git a/src/plugins/kibana_react/public/page_template/solution_nav/__snapshots__/solution_nav.test.tsx.snap b/src/plugins/kibana_react/public/page_template/solution_nav/__snapshots__/solution_nav.test.tsx.snap index 0267357709534..352e1c80b3266 100644 --- a/src/plugins/kibana_react/public/page_template/solution_nav/__snapshots__/solution_nav.test.tsx.snap +++ b/src/plugins/kibana_react/public/page_template/solution_nav/__snapshots__/solution_nav.test.tsx.snap @@ -59,6 +59,12 @@ exports[`KibanaPageTemplateSolutionNav accepts EuiSideNavProps 1`] = ` }, ] } + mobileBreakpoints={ + Array [ + "xs", + "s", + ] + } mobileTitle={ <h2> <FormattedMessage @@ -135,6 +141,12 @@ exports[`KibanaPageTemplateSolutionNav renders 1`] = ` }, ] } + mobileBreakpoints={ + Array [ + "xs", + "s", + ] + } mobileTitle={ <h2> <FormattedMessage @@ -215,6 +227,12 @@ exports[`KibanaPageTemplateSolutionNav renders with icon 1`] = ` }, ] } + mobileBreakpoints={ + Array [ + "xs", + "s", + ] + } mobileTitle={ <h2> <KibanaPageTemplateSolutionNavAvatar diff --git a/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.tsx b/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.tsx index 4aa456f716dbd..bd9ee8eb4d0e8 100644 --- a/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.tsx +++ b/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.tsx @@ -17,7 +17,7 @@ import { KibanaPageTemplateSolutionNavAvatarProps, } from './solution_nav_avatar'; -export type KibanaPageTemplateSolutionNavProps = EuiSideNavProps<{}> & { +export type KibanaPageTemplateSolutionNavProps = Partial<EuiSideNavProps<{}>> & { /** * Name of the solution, i.e. "Observability" */ diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index a6b79a9e2c009..ff637b6686612 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -396,6 +396,10 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'visualization:visualize:legacyPieChartsLibrary': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, 'doc_table:legacy': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index 8448b359ce607..b59abc3aa7158 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -26,6 +26,7 @@ export interface UsageStats { 'autocomplete:useTimeRange': boolean; 'search:timeout': number; 'visualization:visualize:legacyChartsLibrary': boolean; + 'visualization:visualize:legacyPieChartsLibrary': boolean; 'doc_table:legacy': boolean; 'discover:modifyColumnsOnSwitch': boolean; 'discover:searchFieldsFromSource': boolean; diff --git a/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx b/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx index 5b424c7e95f18..1af85da983085 100644 --- a/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx +++ b/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx @@ -20,7 +20,6 @@ import { EuiFlexItem, EuiFlexGroup, EuiIcon, - EuiOverlayMask, } from '@elastic/eui'; import { SolutionName, ProjectStatus, ProjectID, Project, EnvironmentName } from '../../../common'; @@ -124,30 +123,32 @@ export const LabsFlyout = (props: Props) => { ); return ( - <EuiOverlayMask onClick={() => onClose()} headerZindexLocation="below"> - <EuiFlyout onClose={onClose} hideCloseButton={true}> - <EuiFlyoutHeader hasBorder> - <EuiTitle size="m"> - <h2> - <EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}> - <EuiFlexItem grow={false}> - <EuiIcon type="beaker" size="l" /> - </EuiFlexItem> - <EuiFlexItem>{strings.getTitleLabel()}</EuiFlexItem> - </EuiFlexGroup> - </h2> - </EuiTitle> - <EuiSpacer size="s" /> - <EuiText size="s" color="subdued"> - <p>{strings.getDescriptionMessage()}</p> - </EuiText> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <ProjectList {...{ projects, solutions, onStatusChange }} /> - </EuiFlyoutBody> - {footer} - </EuiFlyout> - </EuiOverlayMask> + <EuiFlyout + onClose={onClose} + hideCloseButton={true} + maskProps={{ headerZindexLocation: 'below' }} + > + <EuiFlyoutHeader hasBorder> + <EuiTitle size="m"> + <h2> + <EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}> + <EuiFlexItem grow={false}> + <EuiIcon type="beaker" size="l" /> + </EuiFlexItem> + <EuiFlexItem>{strings.getTitleLabel()}</EuiFlexItem> + </EuiFlexGroup> + </h2> + </EuiTitle> + <EuiSpacer size="s" /> + <EuiText size="s" color="subdued"> + <p>{strings.getDescriptionMessage()}</p> + </EuiText> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <ProjectList {...{ projects, solutions, onStatusChange }} /> + </EuiFlyoutBody> + {footer} + </EuiFlyout> ); }; diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/intro.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/intro.test.tsx.snap index 5239a92543539..5a8cd06b8ecc0 100644 --- a/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/intro.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/intro.test.tsx.snap @@ -47,20 +47,30 @@ exports[`Intro component renders correctly 1`] = ` </span> </div> <EuiText + color="default" size="s" > <div className="euiText euiText--small" > - <div> - <FormattedMessage - defaultMessage="Modifying objects is for advanced users only. Object properties are not validated and invalid objects could cause errors, data loss, or worse. Unless someone with intimate knowledge of the code told you to be in here, you probably shouldn’t be." - id="savedObjectsManagement.view.howToModifyObjectDescription" - values={Object {}} + <EuiTextColor + color="default" + component="div" + > + <div + className="euiTextColor euiTextColor--default" > - Modifying objects is for advanced users only. Object properties are not validated and invalid objects could cause errors, data loss, or worse. Unless someone with intimate knowledge of the code told you to be in here, you probably shouldn’t be. - </FormattedMessage> - </div> + <div> + <FormattedMessage + defaultMessage="Modifying objects is for advanced users only. Object properties are not validated and invalid objects could cause errors, data loss, or worse. Unless someone with intimate knowledge of the code told you to be in here, you probably shouldn’t be." + id="savedObjectsManagement.view.howToModifyObjectDescription" + values={Object {}} + > + Modifying objects is for advanced users only. Object properties are not validated and invalid objects could cause errors, data loss, or worse. Unless someone with intimate knowledge of the code told you to be in here, you probably shouldn’t be. + </FormattedMessage> + </div> + </div> + </EuiTextColor> </div> </EuiText> </div> diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/not_found_errors.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/not_found_errors.test.tsx.snap index bddfe000008d4..f977c17df41d3 100644 --- a/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/not_found_errors.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/not_found_errors.test.tsx.snap @@ -49,29 +49,39 @@ exports[`NotFoundErrors component renders correctly for index-pattern type 1`] = </span> </div> <EuiText + color="default" size="s" > <div className="euiText euiText--small" > - <div> - <FormattedMessage - defaultMessage="The index pattern associated with this object no longer exists." - id="savedObjectsManagement.view.indexPatternDoesNotExistErrorMessage" - values={Object {}} - > - The index pattern associated with this object no longer exists. - </FormattedMessage> - </div> - <div> - <FormattedMessage - defaultMessage="If you know what this error means, go ahead and fix it — otherwise click the delete button above." - id="savedObjectsManagement.view.howToFixErrorDescription" - values={Object {}} + <EuiTextColor + color="default" + component="div" + > + <div + className="euiTextColor euiTextColor--default" > - If you know what this error means, go ahead and fix it — otherwise click the delete button above. - </FormattedMessage> - </div> + <div> + <FormattedMessage + defaultMessage="The index pattern associated with this object no longer exists." + id="savedObjectsManagement.view.indexPatternDoesNotExistErrorMessage" + values={Object {}} + > + The index pattern associated with this object no longer exists. + </FormattedMessage> + </div> + <div> + <FormattedMessage + defaultMessage="If you know what this error means, go ahead and fix it — otherwise click the delete button above." + id="savedObjectsManagement.view.howToFixErrorDescription" + values={Object {}} + > + If you know what this error means, go ahead and fix it — otherwise click the delete button above. + </FormattedMessage> + </div> + </div> + </EuiTextColor> </div> </EuiText> </div> @@ -128,29 +138,39 @@ exports[`NotFoundErrors component renders correctly for index-pattern-field type </span> </div> <EuiText + color="default" size="s" > <div className="euiText euiText--small" > - <div> - <FormattedMessage - defaultMessage="A field associated with this object no longer exists in the index pattern." - id="savedObjectsManagement.view.fieldDoesNotExistErrorMessage" - values={Object {}} - > - A field associated with this object no longer exists in the index pattern. - </FormattedMessage> - </div> - <div> - <FormattedMessage - defaultMessage="If you know what this error means, go ahead and fix it — otherwise click the delete button above." - id="savedObjectsManagement.view.howToFixErrorDescription" - values={Object {}} + <EuiTextColor + color="default" + component="div" + > + <div + className="euiTextColor euiTextColor--default" > - If you know what this error means, go ahead and fix it — otherwise click the delete button above. - </FormattedMessage> - </div> + <div> + <FormattedMessage + defaultMessage="A field associated with this object no longer exists in the index pattern." + id="savedObjectsManagement.view.fieldDoesNotExistErrorMessage" + values={Object {}} + > + A field associated with this object no longer exists in the index pattern. + </FormattedMessage> + </div> + <div> + <FormattedMessage + defaultMessage="If you know what this error means, go ahead and fix it — otherwise click the delete button above." + id="savedObjectsManagement.view.howToFixErrorDescription" + values={Object {}} + > + If you know what this error means, go ahead and fix it — otherwise click the delete button above. + </FormattedMessage> + </div> + </div> + </EuiTextColor> </div> </EuiText> </div> @@ -207,29 +227,39 @@ exports[`NotFoundErrors component renders correctly for search type 1`] = ` </span> </div> <EuiText + color="default" size="s" > <div className="euiText euiText--small" > - <div> - <FormattedMessage - defaultMessage="The saved search associated with this object no longer exists." - id="savedObjectsManagement.view.savedSearchDoesNotExistErrorMessage" - values={Object {}} - > - The saved search associated with this object no longer exists. - </FormattedMessage> - </div> - <div> - <FormattedMessage - defaultMessage="If you know what this error means, go ahead and fix it — otherwise click the delete button above." - id="savedObjectsManagement.view.howToFixErrorDescription" - values={Object {}} + <EuiTextColor + color="default" + component="div" + > + <div + className="euiTextColor euiTextColor--default" > - If you know what this error means, go ahead and fix it — otherwise click the delete button above. - </FormattedMessage> - </div> + <div> + <FormattedMessage + defaultMessage="The saved search associated with this object no longer exists." + id="savedObjectsManagement.view.savedSearchDoesNotExistErrorMessage" + values={Object {}} + > + The saved search associated with this object no longer exists. + </FormattedMessage> + </div> + <div> + <FormattedMessage + defaultMessage="If you know what this error means, go ahead and fix it — otherwise click the delete button above." + id="savedObjectsManagement.view.howToFixErrorDescription" + values={Object {}} + > + If you know what this error means, go ahead and fix it — otherwise click the delete button above. + </FormattedMessage> + </div> + </div> + </EuiTextColor> </div> </EuiText> </div> @@ -286,21 +316,31 @@ exports[`NotFoundErrors component renders correctly for unknown type 1`] = ` </span> </div> <EuiText + color="default" size="s" > <div className="euiText euiText--small" > - <div /> - <div> - <FormattedMessage - defaultMessage="If you know what this error means, go ahead and fix it — otherwise click the delete button above." - id="savedObjectsManagement.view.howToFixErrorDescription" - values={Object {}} + <EuiTextColor + color="default" + component="div" + > + <div + className="euiTextColor euiTextColor--default" > - If you know what this error means, go ahead and fix it — otherwise click the delete button above. - </FormattedMessage> - </div> + <div /> + <div> + <FormattedMessage + defaultMessage="If you know what this error means, go ahead and fix it — otherwise click the delete button above." + id="savedObjectsManagement.view.howToFixErrorDescription" + values={Object {}} + > + If you know what this error means, go ahead and fix it — otherwise click the delete button above. + </FormattedMessage> + </div> + </div> + </EuiTextColor> </div> </EuiText> </div> diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap index a68e8891b5ad1..bd97f2e6bffb1 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap @@ -2,6 +2,7 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = ` <EuiFlyout + data-test-subj="importSavedObjectsFlyout" onClose={[MockFunction]} size="s" > @@ -277,6 +278,7 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = ` exports[`Flyout legacy conflicts should allow conflict resolution 1`] = ` <EuiFlyout + data-test-subj="importSavedObjectsFlyout" onClose={[MockFunction]} size="s" > @@ -548,6 +550,7 @@ Array [ exports[`Flyout should render import step 1`] = ` <EuiFlyout + data-test-subj="importSavedObjectsFlyout" onClose={[MockFunction]} size="s" > diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx index 62e0cd0504e8e..f6c8d5fb69408 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx @@ -960,7 +960,7 @@ export class Flyout extends Component<FlyoutProps, FlyoutState> { } return ( - <EuiFlyout onClose={close} size="s"> + <EuiFlyout onClose={close} size="s" data-test-subj="importSavedObjectsFlyout"> <EuiFlyoutHeader hasBorder> <EuiTitle size="m"> <h2> diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts index 8f5356f6a2201..5ee3156534c5e 100644 --- a/src/plugins/share/public/index.ts +++ b/src/plugins/share/public/index.ts @@ -7,7 +7,8 @@ */ export { CSV_QUOTE_VALUES_SETTING, CSV_SEPARATOR_SETTING } from '../common/constants'; -export { LocatorDefinition } from '../common/url_service'; + +export { LocatorDefinition, LocatorPublic, KibanaLocation } from '../common/url_service'; export { UrlGeneratorStateMapping } from './url_generators/url_generator_definition'; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 99c6dcb40e57d..496335a3b0dc8 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -8594,6 +8594,12 @@ "description": "Non-default value of setting." } }, + "visualization:visualize:legacyPieChartsLibrary": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } + }, "doc_table:legacy": { "type": "boolean", "_meta": { diff --git a/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx b/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx index a24673a4c1245..e757b5fe8f61d 100644 --- a/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx +++ b/src/plugins/vis_default_editor/public/components/sidebar/controls.tsx @@ -7,7 +7,14 @@ */ import React, { useCallback, useState } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiButtonEmpty, + EuiToolTip, + EuiIconTip, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import useDebounce from 'react-use/lib/useDebounce'; @@ -84,19 +91,32 @@ function DefaultEditorControls({ </EuiButton> </EuiToolTip> ) : ( - <EuiButton - data-test-subj="visualizeEditorRenderButton" - disabled={!isDirty} - fill - iconType="play" - onClick={applyChanges} - size="s" - > - <FormattedMessage - id="visDefaultEditor.sidebar.updateChartButtonLabel" - defaultMessage="Update" - /> - </EuiButton> + <EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}> + <EuiFlexItem grow={false}> + <EuiIconTip + content={i18n.translate('visDefaultEditor.sidebar.updateInfoTooltip', { + defaultMessage: 'CTRL + Enter is a shortcut for Update.', + })} + type="keyboardShortcut" + color="subdued" + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + data-test-subj="visualizeEditorRenderButton" + disabled={!isDirty} + fill + iconType="play" + onClick={applyChanges} + size="s" + > + <FormattedMessage + id="visDefaultEditor.sidebar.updateChartButtonLabel" + defaultMessage="Update" + /> + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> )} </EuiFlexItem> </EuiFlexGroup> diff --git a/src/plugins/vis_type_pie/common/index.ts b/src/plugins/vis_type_pie/common/index.ts index 1aa1680530b32..a02a2b2ba10f2 100644 --- a/src/plugins/vis_type_pie/common/index.ts +++ b/src/plugins/vis_type_pie/common/index.ts @@ -7,3 +7,4 @@ */ export const DEFAULT_PERCENT_DECIMALS = 2; +export const LEGACY_PIE_CHARTS_LIBRARY = 'visualization:visualize:legacyPieChartsLibrary'; diff --git a/src/plugins/vis_type_pie/kibana.json b/src/plugins/vis_type_pie/kibana.json index ee312fd19e8d5..eebefc42681b7 100644 --- a/src/plugins/vis_type_pie/kibana.json +++ b/src/plugins/vis_type_pie/kibana.json @@ -2,8 +2,10 @@ "id": "visTypePie", "version": "kibana", "ui": true, + "server": true, "requiredPlugins": ["charts", "data", "expressions", "visualizations", "usageCollection"], "requiredBundles": ["visDefaultEditor"], + "extraPublicDirs": ["common/index"], "owner": { "name": "Kibana App", "githubTeam": "kibana-app" diff --git a/src/plugins/vis_type_pie/public/plugin.ts b/src/plugins/vis_type_pie/public/plugin.ts index 440a3a75a2eb1..787f49c19aca3 100644 --- a/src/plugins/vis_type_pie/public/plugin.ts +++ b/src/plugins/vis_type_pie/public/plugin.ts @@ -12,7 +12,7 @@ import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; import { ChartsPluginSetup } from '../../charts/public'; import { UsageCollectionSetup } from '../../usage_collection/public'; import { DataPublicPluginStart } from '../../data/public'; -import { LEGACY_CHARTS_LIBRARY } from '../../visualizations/common/constants'; +import { LEGACY_PIE_CHARTS_LIBRARY } from '../common'; import { pieLabels as pieLabelsExpressionFunction } from './expression_functions/pie_labels'; import { createPieVisFn } from './pie_fn'; import { getPieVisRenderer } from './pie_renderer'; @@ -43,7 +43,7 @@ export class VisTypePiePlugin { core: CoreSetup<VisTypePiePluginStartDependencies>, { expressions, visualizations, charts, usageCollection }: VisTypePieSetupDependencies ) { - if (!core.uiSettings.get(LEGACY_CHARTS_LIBRARY, false)) { + if (!core.uiSettings.get(LEGACY_PIE_CHARTS_LIBRARY, false)) { const getStartDeps = async () => { const [coreStart, deps] = await core.getStartServices(); return { diff --git a/src/plugins/vis_type_pie/public/utils/get_layers.ts b/src/plugins/vis_type_pie/public/utils/get_layers.ts index 27dcf2d379811..5a82871bf3688 100644 --- a/src/plugins/vis_type_pie/public/utils/get_layers.ts +++ b/src/plugins/vis_type_pie/public/utils/get_layers.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import { i18n } from '@kbn/i18n'; import { Datum, PartitionFillLabel, @@ -125,11 +124,6 @@ export const getLayers = ( }, showAccessor: (d: Datum) => d !== EMPTY_SLICE, nodeLabel: (d: unknown) => { - if (d === '') { - return i18n.translate('visTypePie.emptyLabelValue', { - defaultMessage: '(empty)', - }); - } if (col.format) { const formattedLabel = formatter.deserialize(col.format).convert(d) ?? ''; if (visParams.labels.truncate && formattedLabel.length <= visParams.labels.truncate) { diff --git a/src/plugins/vis_type_pie/server/index.ts b/src/plugins/vis_type_pie/server/index.ts new file mode 100644 index 0000000000000..201071fbb5fca --- /dev/null +++ b/src/plugins/vis_type_pie/server/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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { VisTypePieServerPlugin } from './plugin'; + +export const plugin = () => new VisTypePieServerPlugin(); diff --git a/src/plugins/vis_type_pie/server/plugin.ts b/src/plugins/vis_type_pie/server/plugin.ts new file mode 100644 index 0000000000000..48576bdff5d33 --- /dev/null +++ b/src/plugins/vis_type_pie/server/plugin.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; + +import { CoreSetup, Plugin, UiSettingsParams } from 'kibana/server'; + +import { LEGACY_PIE_CHARTS_LIBRARY } from '../common'; + +export const getUiSettingsConfig: () => Record<string, UiSettingsParams<boolean>> = () => ({ + // TODO: Remove this when vis_type_vislib is removed + // https://github.com/elastic/kibana/issues/56143 + [LEGACY_PIE_CHARTS_LIBRARY]: { + name: i18n.translate('visTypePie.advancedSettings.visualization.legacyPieChartsLibrary.name', { + defaultMessage: 'Pie legacy charts library', + }), + requiresPageReload: true, + value: false, + description: i18n.translate( + 'visTypePie.advancedSettings.visualization.legacyPieChartsLibrary.description', + { + defaultMessage: 'Enables legacy charts library for pie charts in visualize.', + } + ), + deprecation: { + message: i18n.translate( + 'visTypePie.advancedSettings.visualization.legacyPieChartsLibrary.deprecation', + { + defaultMessage: + 'The legacy charts library for pie in visualize is deprecated and will not be supported as of 8.0.', + } + ), + docLinksKey: 'visualizationSettings', + }, + category: ['visualization'], + schema: schema.boolean(), + }, +}); + +export class VisTypePieServerPlugin implements Plugin<object, object> { + public setup(core: CoreSetup) { + core.uiSettings.register(getUiSettingsConfig()); + + return {}; + } + + public start() { + return {}; + } +} diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx index 7d42eb3f40ac5..610b4a91cfd14 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/field_select.tsx @@ -128,7 +128,7 @@ export function FieldSelect({ selectedOptions = [{ label: value!, id: 'INVALID_FIELD' }]; } } else { - if (value && !selectedOptions.length) { + if (value && fields[fieldsSelector] && !selectedOptions.length) { onChange([]); } } diff --git a/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx b/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx index 8e975f9904256..50d3e8c38e389 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx @@ -36,7 +36,7 @@ describe('ColorPicker', () => { const props = { ...defaultProps, value: '#68BC00' }; component = mount(<ColorPicker {...props} />); component.find('.tvbColorPicker button').simulate('click'); - const input = findTestSubject(component, 'topColorPickerInput'); + const input = findTestSubject(component, 'euiColorPickerInput_top'); expect(input.props().value).toBe('#68BC00'); }); @@ -44,7 +44,7 @@ describe('ColorPicker', () => { const props = { ...defaultProps, value: 'rgba(85,66,177,1)' }; component = mount(<ColorPicker {...props} />); component.find('.tvbColorPicker button').simulate('click'); - const input = findTestSubject(component, 'topColorPickerInput'); + const input = findTestSubject(component, 'euiColorPickerInput_top'); expect(input.props().value).toBe('85,66,177,1'); }); diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/reorder.js b/src/plugins/vis_type_timeseries/public/application/components/lib/reorder.ts similarity index 85% rename from src/plugins/vis_type_timeseries/public/application/components/lib/reorder.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/reorder.ts index 15c21e19af2a5..a026b5bb2051e 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/reorder.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/reorder.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -export const reorder = (list, startIndex, endIndex) => { +export const reorder = (list: unknown[], startIndex: number, endIndex: number) => { const result = Array.from(list); const [removed] = result.splice(startIndex, 1); result.splice(endIndex, 0, removed); diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.js b/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.ts similarity index 77% rename from src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.ts index 458866f2098a0..2862fe933bfb7 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.ts @@ -6,20 +6,30 @@ * Side Public License, v 1. */ -import _ from 'lodash'; -import handlebars from 'handlebars/dist/handlebars'; -import { emptyLabel } from '../../../../common/empty_label'; +import handlebars from 'handlebars'; import { i18n } from '@kbn/i18n'; +import { emptyLabel } from '../../../../common/empty_label'; + +type CompileOptions = Parameters<typeof handlebars.compile>[1]; -export function replaceVars(str, args = {}, vars = {}) { +export function replaceVars( + str: string, + args: Record<string, unknown> = {}, + vars: Record<string, unknown> = {}, + compileOptions: Partial<CompileOptions> = {} +) { try { - // we need add '[]' for emptyLabel because this value contains special characters. (https://handlebarsjs.com/guide/expressions.html#literal-segments) + /** we need add '[]' for emptyLabel because this value contains special characters. + * @see (https://handlebarsjs.com/guide/expressions.html#literal-segments) **/ const template = handlebars.compile(str.split(emptyLabel).join(`[${emptyLabel}]`), { strict: true, knownHelpersOnly: true, + ...compileOptions, + }); + const string = template({ + ...vars, + args, }); - - const string = template(_.assign({}, vars, { args })); return string; } catch (e) { diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js index 70529be78567d..c1d82a182e509 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import handlebars from 'handlebars/dist/handlebars'; import { isNumber } from 'lodash'; +import handlebars from 'handlebars'; import { isEmptyValue, DISPLAY_EMPTY_VALUE } from '../../../../common/last_value_utils'; import { inputFormats, outputFormats, isDuration } from '../lib/durations'; import { getFieldFormats } from '../../../services'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js index 8e59e8e1bb628..097b0a7b5e332 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js @@ -51,7 +51,9 @@ class TimeseriesVisualization extends Component { }; applyDocTo = (template) => (doc) => { - const vars = replaceVars(template, null, doc); + const vars = replaceVars(template, null, doc, { + noEscape: true, + }); if (vars instanceof Error) { this.showToastNotification = vars.error.caused_by; diff --git a/src/plugins/vis_type_vislib/public/plugin.ts b/src/plugins/vis_type_vislib/public/plugin.ts index 52faf8a74778c..cdc02aacafa3b 100644 --- a/src/plugins/vis_type_vislib/public/plugin.ts +++ b/src/plugins/vis_type_vislib/public/plugin.ts @@ -13,7 +13,8 @@ import { VisualizationsSetup } from '../../visualizations/public'; import { ChartsPluginSetup } from '../../charts/public'; import { DataPublicPluginStart } from '../../data/public'; import { KibanaLegacyStart } from '../../kibana_legacy/public'; -import { LEGACY_CHARTS_LIBRARY } from '../../visualizations/common/constants'; +import { LEGACY_CHARTS_LIBRARY } from '../../vis_type_xy/common/index'; +import { LEGACY_PIE_CHARTS_LIBRARY } from '../../vis_type_pie/common/index'; import { createVisTypeVislibVisFn } from './vis_type_vislib_vis_fn'; import { createPieVisFn } from './pie_fn'; @@ -50,17 +51,18 @@ export class VisTypeVislibPlugin core: VisTypeVislibCoreSetup, { expressions, visualizations, charts }: VisTypeVislibPluginSetupDependencies ) { - if (!core.uiSettings.get(LEGACY_CHARTS_LIBRARY, false)) { - // Register only non-replaced vis types - convertedTypeDefinitions.forEach(visualizations.createBaseVisualization); - expressions.registerRenderer(getVislibVisRenderer(core, charts)); - expressions.registerFunction(createVisTypeVislibVisFn()); - } else { - // Register all vis types - visLibVisTypeDefinitions.forEach(visualizations.createBaseVisualization); + const typeDefinitions = !core.uiSettings.get(LEGACY_CHARTS_LIBRARY, false) + ? convertedTypeDefinitions + : visLibVisTypeDefinitions; + // register vislib XY axis charts + typeDefinitions.forEach(visualizations.createBaseVisualization); + expressions.registerRenderer(getVislibVisRenderer(core, charts)); + expressions.registerFunction(createVisTypeVislibVisFn()); + + if (core.uiSettings.get(LEGACY_PIE_CHARTS_LIBRARY, false)) { + // register vislib pie chart visualizations.createBaseVisualization(pieVisTypeDefinition); - expressions.registerRenderer(getVislibVisRenderer(core, charts)); - [createVisTypeVislibVisFn(), createPieVisFn()].forEach(expressions.registerFunction); + expressions.registerFunction(createPieVisFn()); } } diff --git a/src/plugins/vis_type_xy/common/index.ts b/src/plugins/vis_type_xy/common/index.ts index f17bc8476d9a6..a80946f7c62fa 100644 --- a/src/plugins/vis_type_xy/common/index.ts +++ b/src/plugins/vis_type_xy/common/index.ts @@ -19,3 +19,5 @@ export enum ChartType { * Type of xy visualizations */ export type XyVisType = ChartType | 'horizontal_bar'; + +export const LEGACY_CHARTS_LIBRARY = 'visualization:visualize:legacyChartsLibrary'; diff --git a/src/plugins/vis_type_xy/kibana.json b/src/plugins/vis_type_xy/kibana.json index 1d7fd6a0813b4..c25f035fb6d4b 100644 --- a/src/plugins/vis_type_xy/kibana.json +++ b/src/plugins/vis_type_xy/kibana.json @@ -2,8 +2,10 @@ "id": "visTypeXy", "version": "kibana", "ui": true, + "server": true, "requiredPlugins": ["charts", "data", "expressions", "visualizations", "usageCollection"], "requiredBundles": ["kibanaUtils", "visDefaultEditor"], + "extraPublicDirs": ["common/index"], "owner": { "name": "Kibana App", "githubTeam": "kibana-app" diff --git a/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx b/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx index c9ed82fcf58e5..fb6b4bb41d9ba 100644 --- a/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx +++ b/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx @@ -19,7 +19,6 @@ import { import { Aspects } from '../types'; import './_detailed_tooltip.scss'; -import { fillEmptyValue } from '../utils/get_series_name_fn'; import { COMPLEX_SPLIT_ACCESSOR, isRangeAggType } from '../utils/accessors'; interface TooltipData { @@ -100,8 +99,7 @@ export const getTooltipData = ( return data; }; -const renderData = ({ label, value: rawValue }: TooltipData, index: number) => { - const value = fillEmptyValue(rawValue); +const renderData = ({ label, value }: TooltipData, index: number) => { return label && value ? ( <tr key={label + value + index}> <td className="detailedTooltip__label"> diff --git a/src/plugins/vis_type_xy/public/components/xy_settings.tsx b/src/plugins/vis_type_xy/public/components/xy_settings.tsx index 8922f512522a0..8d6a7eecdfe52 100644 --- a/src/plugins/vis_type_xy/public/components/xy_settings.tsx +++ b/src/plugins/vis_type_xy/public/components/xy_settings.tsx @@ -29,7 +29,6 @@ import { renderEndzoneTooltip } from '../../../charts/public'; import { getThemeService, getUISettings } from '../services'; import { VisConfig } from '../types'; -import { fillEmptyValue } from '../utils/get_series_name_fn'; declare global { interface Window { @@ -134,7 +133,7 @@ export const XYSettings: FC<XYSettingsProps> = ({ }; const headerValueFormatter: TickFormatter<any> | undefined = xAxis.ticks?.formatter - ? (value) => fillEmptyValue(xAxis.ticks?.formatter?.(value)) ?? '' + ? (value) => xAxis.ticks?.formatter?.(value) ?? '' : undefined; const headerFormatter = isTimeChart && xDomain && adjustedXDomain diff --git a/src/plugins/vis_type_xy/public/config/get_axis.ts b/src/plugins/vis_type_xy/public/config/get_axis.ts index 08b17c882eea6..71d33cc20d057 100644 --- a/src/plugins/vis_type_xy/public/config/get_axis.ts +++ b/src/plugins/vis_type_xy/public/config/get_axis.ts @@ -27,7 +27,6 @@ import { YScaleType, SeriesParam, } from '../types'; -import { fillEmptyValue } from '../utils/get_series_name_fn'; export function getAxis<S extends XScaleType | YScaleType>( { type, title: axisTitle, labels, scale: axisScale, ...axis }: CategoryAxis, @@ -90,8 +89,7 @@ function getLabelFormatter( } return (value: any) => { - const formattedStringValue = `${formatter ? formatter(value) : value}`; - const finalValue = fillEmptyValue(formattedStringValue); + const finalValue = `${formatter ? formatter(value) : value}`; if (finalValue.length > truncate) { return `${finalValue.slice(0, truncate)}...`; diff --git a/src/plugins/vis_type_xy/public/plugin.ts b/src/plugins/vis_type_xy/public/plugin.ts index e8d53127765b4..b595d3172f143 100644 --- a/src/plugins/vis_type_xy/public/plugin.ts +++ b/src/plugins/vis_type_xy/public/plugin.ts @@ -23,7 +23,7 @@ import { } from './services'; import { visTypesDefinitions } from './vis_types'; -import { LEGACY_CHARTS_LIBRARY } from '../../visualizations/common/constants'; +import { LEGACY_CHARTS_LIBRARY } from '../common/'; import { xyVisRenderer } from './vis_renderer'; import * as expressionFunctions from './expression_functions'; diff --git a/src/plugins/vis_type_xy/public/utils/get_series_name_fn.ts b/src/plugins/vis_type_xy/public/utils/get_series_name_fn.ts index 0e54650e22f75..137f8a5558010 100644 --- a/src/plugins/vis_type_xy/public/utils/get_series_name_fn.ts +++ b/src/plugins/vis_type_xy/public/utils/get_series_name_fn.ts @@ -8,21 +8,10 @@ import { memoize } from 'lodash'; -import { i18n } from '@kbn/i18n'; import { XYChartSeriesIdentifier, SeriesName } from '@elastic/charts'; import { VisConfig } from '../types'; -const emptyTextLabel = i18n.translate('visTypeXy.emptyTextColumnValue', { - defaultMessage: '(empty)', -}); - -/** - * Returns empty values - */ -export const fillEmptyValue = <T extends string | number | undefined>(value: T) => - value === '' ? emptyTextLabel : value; - function getSplitValues( splitAccessors: XYChartSeriesIdentifier['splitAccessors'], seriesAspects?: VisConfig['aspects']['series'] @@ -36,7 +25,7 @@ function getSplitValues( const split = (seriesAspects ?? []).find(({ accessor }) => accessor === key); splitValues.push(split?.formatter ? split?.formatter(value) : value); }); - return splitValues.map(fillEmptyValue); + return splitValues; } export const getSeriesNameFn = (aspects: VisConfig['aspects'], multipleY = false) => diff --git a/src/plugins/vis_type_xy/server/index.ts b/src/plugins/vis_type_xy/server/index.ts new file mode 100644 index 0000000000000..a27ac49c0ea49 --- /dev/null +++ b/src/plugins/vis_type_xy/server/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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { VisTypeXyServerPlugin } from './plugin'; + +export const plugin = () => new VisTypeXyServerPlugin(); diff --git a/src/plugins/vis_type_xy/server/plugin.ts b/src/plugins/vis_type_xy/server/plugin.ts new file mode 100644 index 0000000000000..46d6531204c24 --- /dev/null +++ b/src/plugins/vis_type_xy/server/plugin.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; + +import { CoreSetup, Plugin, UiSettingsParams } from 'kibana/server'; + +import { LEGACY_CHARTS_LIBRARY } from '../common'; + +export const getUiSettingsConfig: () => Record<string, UiSettingsParams<boolean>> = () => ({ + // TODO: Remove this when vis_type_vislib is removed + // https://github.com/elastic/kibana/issues/56143 + [LEGACY_CHARTS_LIBRARY]: { + name: i18n.translate('visTypeXy.advancedSettings.visualization.legacyChartsLibrary.name', { + defaultMessage: 'XY axis legacy charts library', + }), + requiresPageReload: true, + value: false, + description: i18n.translate( + 'visTypeXy.advancedSettings.visualization.legacyChartsLibrary.description', + { + defaultMessage: 'Enables legacy charts library for area, line and bar charts in visualize.', + } + ), + deprecation: { + message: i18n.translate( + 'visTypeXy.advancedSettings.visualization.legacyChartsLibrary.deprecation', + { + defaultMessage: + 'The legacy charts library for area, line and bar charts in visualize is deprecated and will not be supported as of 7.16.', + } + ), + docLinksKey: 'visualizationSettings', + }, + category: ['visualization'], + schema: schema.boolean(), + }, +}); + +export class VisTypeXyServerPlugin implements Plugin<object, object> { + public setup(core: CoreSetup) { + core.uiSettings.register(getUiSettingsConfig()); + + return {}; + } + + public start() { + return {}; + } +} diff --git a/src/plugins/visualizations/common/constants.ts b/src/plugins/visualizations/common/constants.ts index a33e74b498a2c..a8a0963ac8948 100644 --- a/src/plugins/visualizations/common/constants.ts +++ b/src/plugins/visualizations/common/constants.ts @@ -7,4 +7,3 @@ */ export const VISUALIZE_ENABLE_LABS_SETTING = 'visualize:enableLabs'; -export const LEGACY_CHARTS_LIBRARY = 'visualization:visualize:legacyChartsLibrary'; diff --git a/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap b/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap index 25ec05c83a8c6..56e2cb1b60f3c 100644 --- a/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap +++ b/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap @@ -14,7 +14,7 @@ exports[`VisualizationNoResults should render according to snapshot 1`] = ` data-euiicon-type="visualizeApp" /> <div - class="euiSpacer euiSpacer--s" + class="euiSpacer euiSpacer--m" /> <span class="euiTextColor euiTextColor--subdued" diff --git a/src/plugins/visualizations/server/plugin.ts b/src/plugins/visualizations/server/plugin.ts index 1fec63f2bb45a..5a5a80b2689d6 100644 --- a/src/plugins/visualizations/server/plugin.ts +++ b/src/plugins/visualizations/server/plugin.ts @@ -18,7 +18,7 @@ import { Logger, } from '../../../core/server'; -import { VISUALIZE_ENABLE_LABS_SETTING, LEGACY_CHARTS_LIBRARY } from '../common/constants'; +import { VISUALIZE_ENABLE_LABS_SETTING } from '../common/constants'; import { visualizationSavedObjectType } from './saved_objects'; @@ -58,27 +58,6 @@ export class VisualizationsPlugin category: ['visualization'], schema: schema.boolean(), }, - // TODO: Remove this when vis_type_vislib is removed - // https://github.com/elastic/kibana/issues/56143 - [LEGACY_CHARTS_LIBRARY]: { - name: i18n.translate( - 'visualizations.advancedSettings.visualization.legacyChartsLibrary.name', - { - defaultMessage: 'Legacy charts library', - } - ), - requiresPageReload: true, - value: false, - description: i18n.translate( - 'visualizations.advancedSettings.visualization.legacyChartsLibrary.description', - { - defaultMessage: - 'Enables legacy charts library for area, line, bar, pie charts in visualize.', - } - ), - category: ['visualization'], - schema: schema.boolean(), - }, }); if (plugins.usageCollection) { diff --git a/test/accessibility/apps/management.ts b/test/accessibility/apps/management.ts index e71f6bb3ebfee..7a99e5832448f 100644 --- a/test/accessibility/apps/management.ts +++ b/test/accessibility/apps/management.ts @@ -82,6 +82,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('Edit field type', async () => { await PageObjects.settings.clickEditFieldFormat(); await a11y.testAppSnapshot(); + await PageObjects.settings.clickCloseEditFieldFormatFlyout(); }); it('Advanced settings', async () => { diff --git a/test/api_integration/apis/ui_counters/ui_counters.ts b/test/api_integration/apis/ui_counters/ui_counters.ts index 2be6ea4341fb0..019dcfd621655 100644 --- a/test/api_integration/apis/ui_counters/ui_counters.ts +++ b/test/api_integration/apis/ui_counters/ui_counters.ts @@ -56,7 +56,8 @@ export default function ({ getService }: FtrProviderContext) { return savedObject; }; - describe('UI Counters API', () => { + // FLAKY: https://github.com/elastic/kibana/issues/98240 + describe.skip('UI Counters API', () => { const dayDate = moment().format('DDMMYYYY'); before(async () => await esArchiver.emptyKibanaIndex()); diff --git a/test/functional/apps/context/index.js b/test/functional/apps/context/index.js index 7612dae338d9f..031171a58718b 100644 --- a/test/functional/apps/context/index.js +++ b/test/functional/apps/context/index.js @@ -15,16 +15,18 @@ export default function ({ getService, getPageObjects, loadTestFile }) { describe('context app', function () { this.tags('ciGroup1'); - before(async function () { + before(async () => { await browser.setWindowSize(1200, 800); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); - await esArchiver.load('test/functional/fixtures/es_archiver/visualize'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/visualize.json'); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); await PageObjects.common.navigateToApp('discover'); }); - after(function unloadMakelogs() { - return esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + after(async () => { + await kibanaServer.importExport.unload( + 'test/functional/fixtures/kbn_archiver/visualize.json' + ); }); loadTestFile(require.resolve('./_context_navigation')); diff --git a/test/functional/apps/dashboard/dashboard_state.ts b/test/functional/apps/dashboard/dashboard_state.ts index 047681e1a8ace..6c259f5a71efa 100644 --- a/test/functional/apps/dashboard/dashboard_state.ts +++ b/test/functional/apps/dashboard/dashboard_state.ts @@ -53,6 +53,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { if (isNewChartsLibraryEnabled) { await kibanaServer.uiSettings.update({ 'visualization:visualize:legacyChartsLibrary': false, + 'visualization:visualize:legacyPieChartsLibrary': false, }); await browser.refresh(); } diff --git a/test/functional/apps/dashboard/index.ts b/test/functional/apps/dashboard/index.ts index 4b83b2ac92deb..e4dc04282e4ac 100644 --- a/test/functional/apps/dashboard/index.ts +++ b/test/functional/apps/dashboard/index.ts @@ -123,6 +123,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await loadLogstash(); await kibanaServer.uiSettings.update({ 'visualization:visualize:legacyChartsLibrary': false, + 'visualization:visualize:legacyPieChartsLibrary': false, }); await browser.refresh(); }); @@ -131,6 +132,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await unloadLogstash(); await kibanaServer.uiSettings.update({ 'visualization:visualize:legacyChartsLibrary': true, + 'visualization:visualize:legacyPieChartsLibrary': true, }); await browser.refresh(); }); diff --git a/test/functional/apps/discover/_data_grid_doc_navigation.ts b/test/functional/apps/discover/_data_grid_doc_navigation.ts index e3e8a20b693f8..cf5532aa6d762 100644 --- a/test/functional/apps/discover/_data_grid_doc_navigation.ts +++ b/test/functional/apps/discover/_data_grid_doc_navigation.ts @@ -41,8 +41,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await rowActions[0].click(); }); - const hasDocHit = await testSubjects.exists('doc-hit'); - expect(hasDocHit).to.be(true); + await retry.waitFor('hit loaded', async () => { + const hasDocHit = await testSubjects.exists('doc-hit'); + return !!hasDocHit; + }); }); // no longer relevant as null field won't be returned in the Fields API response diff --git a/test/functional/apps/discover/_discover.ts b/test/functional/apps/discover/_discover.ts index dce6bfba9cd99..c68db8cbd797b 100644 --- a/test/functional/apps/discover/_discover.ts +++ b/test/functional/apps/discover/_discover.ts @@ -181,8 +181,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/89550 - describe.skip('query #2, which has an empty time range', () => { + describe('query #2, which has an empty time range', () => { const fromTime = 'Jun 11, 1999 @ 09:22:11.000'; const toTime = 'Jun 12, 1999 @ 11:21:04.000'; @@ -193,8 +192,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should show "no results"', async () => { - const isVisible = await PageObjects.discover.hasNoResults(); - expect(isVisible).to.be(true); + await retry.waitFor('no results screen is displayed', async function () { + const isVisible = await PageObjects.discover.hasNoResults(); + return isVisible === true; + }); }); it('should suggest a new time range is picked', async () => { diff --git a/test/functional/apps/discover/_doc_navigation.ts b/test/functional/apps/discover/_doc_navigation.ts index 771dac4d40a64..8d156cb305586 100644 --- a/test/functional/apps/discover/_doc_navigation.ts +++ b/test/functional/apps/discover/_doc_navigation.ts @@ -51,8 +51,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await rowActions[1].click(); }); - const hasDocHit = await testSubjects.exists('doc-hit'); - expect(hasDocHit).to.be(true); + await retry.waitFor('hit loaded', async () => { + const hasDocHit = await testSubjects.exists('doc-hit'); + return !!hasDocHit; + }); }); // no longer relevant as null field won't be returned in the Fields API response diff --git a/test/functional/apps/discover/_huge_fields.ts b/test/functional/apps/discover/_huge_fields.ts index c7fe0a94b6019..24b10e1df0495 100644 --- a/test/functional/apps/discover/_huge_fields.ts +++ b/test/functional/apps/discover/_huge_fields.ts @@ -15,21 +15,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); - // FLAKY: https://github.com/elastic/kibana/issues/96113 - describe.skip('test large number of fields in sidebar', function () { + describe('test large number of fields in sidebar', function () { before(async function () { + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/huge_fields'); await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader'], false); - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/large_fields'); - await PageObjects.settings.navigateTo(); await kibanaServer.uiSettings.update({ 'timepicker:timeDefaults': `{ "from": "2016-10-05T00:00:00", "to": "2016-10-06T00:00:00"}`, }); - await PageObjects.settings.createIndexPattern('*huge*', 'date', true); await PageObjects.common.navigateToApp('discover'); }); it('test_huge data should have expected number of fields', async function () { - await PageObjects.discover.selectIndexPattern('*huge*'); + await PageObjects.discover.selectIndexPattern('testhuge*'); // initially this field should not be rendered const fieldExistsBeforeScrolling = await testSubjects.exists('field-myvar1050'); expect(fieldExistsBeforeScrolling).to.be(false); @@ -41,8 +38,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async () => { await security.testUser.restoreDefaults(); - await esArchiver.unload('test/functional/fixtures/es_archiver/large_fields'); - await kibanaServer.uiSettings.replace({}); + await esArchiver.unload('test/functional/fixtures/es_archiver/huge_fields'); + await kibanaServer.uiSettings.unset('timepicker:timeDefaults'); }); }); } diff --git a/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts b/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts index e986429a15d26..264885490cdfc 100644 --- a/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts +++ b/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts @@ -12,26 +12,31 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); - const log = getService('log'); + const security = getService('security'); const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'timePicker', 'discover']); describe('index pattern with unmapped fields', () => { before(async () => { await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/unmapped_fields'); + await security.testUser.setRoles(['kibana_admin', 'test-index-unmapped-fields']); + const fromTime = 'Jan 20, 2021 @ 00:00:00.000'; + const toTime = 'Jan 25, 2021 @ 00:00:00.000'; + await kibanaServer.uiSettings.replace({ defaultIndex: 'test-index-unmapped-fields', 'discover:searchFieldsFromSource': false, + 'timepicker:timeDefaults': `{ "from": "${fromTime}", "to": "${toTime}"}`, }); - log.debug('discover'); - const fromTime = 'Jan 20, 2021 @ 00:00:00.000'; - const toTime = 'Jan 25, 2021 @ 00:00:00.000'; + await PageObjects.common.navigateToApp('discover'); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); }); after(async () => { await esArchiver.unload('test/functional/fixtures/es_archiver/unmapped_fields'); + await kibanaServer.uiSettings.unset('defaultIndex'); + await kibanaServer.uiSettings.unset('discover:searchFieldsFromSource'); + await kibanaServer.uiSettings.unset('timepicker:timeDefaults'); }); it('unmapped fields exist on a new saved search', async () => { diff --git a/test/functional/apps/discover/_sidebar.ts b/test/functional/apps/discover/_sidebar.ts index 8179f4e44e8b8..d8701261126c4 100644 --- a/test/functional/apps/discover/_sidebar.ts +++ b/test/functional/apps/discover/_sidebar.ts @@ -14,8 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); const testSubjects = getService('testSubjects'); - // Failing: See https://github.com/elastic/kibana/issues/101449 - describe.skip('discover sidebar', function describeIndexTests() { + describe('discover sidebar', function describeIndexTests() { before(async function () { await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/discover'); diff --git a/test/functional/apps/discover/_source_filters.ts b/test/functional/apps/discover/_source_filters.ts index f3793dc3e0288..6c6979b39702c 100644 --- a/test/functional/apps/discover/_source_filters.ts +++ b/test/functional/apps/discover/_source_filters.ts @@ -23,8 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { defaultIndex: 'logstash-*', }); - log.debug('load kibana index with default index pattern'); - await esArchiver.load('test/functional/fixtures/es_archiver/visualize_source-filters'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/visualize.json'); // and load a set of makelogs data await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); @@ -43,6 +42,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.sleep(1000); }); + after(async () => { + await kibanaServer.importExport.unload( + 'test/functional/fixtures/kbn_archiver/visualize.json' + ); + }); + it('should not get the field referer', async function () { const fieldNames = await PageObjects.discover.getAllFieldNames(); expect(fieldNames).to.not.contain('referer'); diff --git a/test/functional/apps/getting_started/_shakespeare.ts b/test/functional/apps/getting_started/_shakespeare.ts index 945c1fdcbdcf4..ae6841b85c98d 100644 --- a/test/functional/apps/getting_started/_shakespeare.ts +++ b/test/functional/apps/getting_started/_shakespeare.ts @@ -57,6 +57,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { if (isNewChartsLibraryEnabled) { await kibanaServer.uiSettings.update({ 'visualization:visualize:legacyChartsLibrary': false, + 'visualization:visualize:legacyPieChartsLibrary': false, }); await browser.refresh(); } diff --git a/test/functional/apps/getting_started/index.ts b/test/functional/apps/getting_started/index.ts index b75a30037d065..4c1c052ef15a2 100644 --- a/test/functional/apps/getting_started/index.ts +++ b/test/functional/apps/getting_started/index.ts @@ -24,6 +24,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { before(async () => { await kibanaServer.uiSettings.update({ 'visualization:visualize:legacyChartsLibrary': false, + 'visualization:visualize:legacyPieChartsLibrary': false, }); await browser.refresh(); }); @@ -31,6 +32,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { after(async () => { await kibanaServer.uiSettings.update({ 'visualization:visualize:legacyChartsLibrary': true, + 'visualization:visualize:legacyPieChartsLibrary': true, }); await browser.refresh(); }); diff --git a/test/functional/apps/management/_import_objects.ts b/test/functional/apps/management/_import_objects.ts index 0278955c577a1..6ef0bfd5a09e8 100644 --- a/test/functional/apps/management/_import_objects.ts +++ b/test/functional/apps/management/_import_objects.ts @@ -419,14 +419,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'index-pattern-test-1' ); - await testSubjects.click('pagination-button-next'); + const flyout = await testSubjects.find('importSavedObjectsFlyout'); + + await (await flyout.findByTestSubject('pagination-button-next')).click(); await PageObjects.savedObjects.setOverriddenIndexPatternValue( 'missing-index-pattern-7', 'index-pattern-test-2' ); - await testSubjects.click('pagination-button-previous'); + await (await flyout.findByTestSubject('pagination-button-previous')).click(); const selectedIdForMissingIndexPattern1 = await testSubjects.getAttribute( 'managementChangeIndexSelection-missing-index-pattern-1', @@ -435,7 +437,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(selectedIdForMissingIndexPattern1).to.eql('f1e4c910-a2e6-11e7-bb30-233be9be6a20'); - await testSubjects.click('pagination-button-next'); + await (await flyout.findByTestSubject('pagination-button-next')).click(); const selectedIdForMissingIndexPattern7 = await testSubjects.getAttribute( 'managementChangeIndexSelection-missing-index-pattern-7', diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts index cecd206abd1db..bc6160eba3846 100644 --- a/test/functional/apps/visualize/index.ts +++ b/test/functional/apps/visualize/index.ts @@ -31,6 +31,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { before(async () => { await kibanaServer.uiSettings.update({ 'visualization:visualize:legacyChartsLibrary': false, + 'visualization:visualize:legacyPieChartsLibrary': false, }); await browser.refresh(); }); @@ -38,6 +39,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { after(async () => { await kibanaServer.uiSettings.update({ 'visualization:visualize:legacyChartsLibrary': true, + 'visualization:visualize:legacyPieChartsLibrary': true, }); await browser.refresh(); }); diff --git a/test/functional/config.js b/test/functional/config.js index bab1148cf372a..670488003e56c 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -58,6 +58,7 @@ export default async function ({ readConfigFile }) { 'accessibility:disableAnimations': true, 'dateFormat:tz': 'UTC', 'visualization:visualize:legacyChartsLibrary': true, + 'visualization:visualize:legacyPieChartsLibrary': true, }, }, @@ -292,6 +293,21 @@ export default async function ({ readConfigFile }) { kibana: [], }, + 'test-index-unmapped-fields': { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['test-index-unmapped-fields'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + animals: { elasticsearch: { cluster: [], diff --git a/test/functional/fixtures/es_archiver/huge_fields/data.json.gz b/test/functional/fixtures/es_archiver/huge_fields/data.json.gz new file mode 100644 index 0000000000000..1ce42c64c53a3 Binary files /dev/null and b/test/functional/fixtures/es_archiver/huge_fields/data.json.gz differ diff --git a/test/functional/fixtures/es_archiver/huge_fields/mappings.json b/test/functional/fixtures/es_archiver/huge_fields/mappings.json new file mode 100644 index 0000000000000..49a677a42f2ba --- /dev/null +++ b/test/functional/fixtures/es_archiver/huge_fields/mappings.json @@ -0,0 +1,24 @@ +{ + "type": "index", + "value": { + "index": "testhuge", + "mappings": { + "properties": { + "date": { + "type": "date" + } + } + }, + "settings": { + "index": { + "mapping": { + "total_fields": { + "limit": "50000" + } + }, + "number_of_replicas": "1", + "number_of_shards": "5" + } + } + } +} \ No newline at end of file diff --git a/test/functional/fixtures/es_archiver/visualize/data.json b/test/functional/fixtures/es_archiver/visualize/data.json deleted file mode 100644 index d48aa3e98d18a..0000000000000 --- a/test/functional/fixtures/es_archiver/visualize/data.json +++ /dev/null @@ -1,388 +0,0 @@ -{ - "type": "doc", - "value": { - "id": "index-pattern:logstash-*", - "index": ".kibana", - "source": { - "coreMigrationVersion": "7.14.0", - "index-pattern": { - "fieldAttrs": "{\"utc_time\":{\"customLabel\":\"UTC time\"}}", - "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}", - "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", - "timeFieldName": "@timestamp", - "title": "logstash-*" - }, - "migrationVersion": { - "index-pattern": "7.11.0" - }, - "references": [ - ], - "type": "index-pattern" - }, - "type": "_doc" - } -} - -{ - "type": "doc", - "value": { - "id": "index-pattern:logstash*", - "index": ".kibana", - "source": { - "coreMigrationVersion": "7.14.0", - "index-pattern": { - "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}", - "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", - "title": "logstash*" - }, - "migrationVersion": { - "index-pattern": "7.11.0" - }, - "references": [ - ], - "type": "index-pattern" - }, - "type": "_doc" - } -} - -{ - "type": "doc", - "value": { - "id": "index-pattern:long-window-logstash-*", - "index": ".kibana", - "source": { - "coreMigrationVersion": "7.14.0", - "index-pattern": { - "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}", - "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", - "timeFieldName": "@timestamp", - "title": "long-window-logstash-*" - }, - "migrationVersion": { - "index-pattern": "7.11.0" - }, - "references": [ - ], - "type": "index-pattern" - }, - "type": "_doc" - } -} - -{ - "type": "doc", - "value": { - "id": "visualization:Shared-Item-Visualization-AreaChart", - "index": ".kibana", - "source": { - "coreMigrationVersion": "7.14.0", - "migrationVersion": { - "visualization": "7.14.0" - }, - "references": [ - { - "id": "logstash-*", - "name": "kibanaSavedObjectMeta.searchSourceJSON.index", - "type": "index-pattern" - } - ], - "type": "visualization", - "visualization": { - "description": "AreaChart", - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" - }, - "title": "Shared-Item Visualization AreaChart", - "uiStateJSON": "{}", - "version": 1, - "visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}" - } - }, - "type": "_doc" - } -} - -{ - "type": "doc", - "value": { - "id": "visualization:Visualization-AreaChart", - "index": ".kibana", - "source": { - "coreMigrationVersion": "7.14.0", - "migrationVersion": { - "visualization": "7.14.0" - }, - "references": [ - { - "id": "logstash-*", - "name": "kibanaSavedObjectMeta.searchSourceJSON.index", - "type": "index-pattern" - } - ], - "type": "visualization", - "visualization": { - "description": "AreaChart", - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" - }, - "title": "Visualization AreaChart", - "uiStateJSON": "{}", - "version": 1, - "visState": "{\"title\":\"Visualization AreaChart\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{},\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"now-15m\",\"to\":\"now\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}]}" - } - }, - "type": "_doc" - } -} - -{ - "type": "doc", - "value": { - "id": "visualization:68305470-87bc-11e9-a991-3b492a7c3e09", - "index": ".kibana", - "source": { - "coreMigrationVersion": "7.14.0", - "migrationVersion": { - "visualization": "7.14.0" - }, - "references": [ - { - "id": "logstash-*", - "name": "control_0_index_pattern", - "type": "index-pattern" - }, - { - "id": "logstash-*", - "name": "control_1_index_pattern", - "type": "index-pattern" - } - ], - "type": "visualization", - "updated_at": "2019-06-05T18:04:48.310Z", - "visualization": { - "description": "", - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" - }, - "title": "chained input control", - "uiStateJSON": "{}", - "version": 1, - "visState": "{\"title\":\"chained input control\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559757816862\",\"fieldName\":\"geo.src\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1559757836347\",\"fieldName\":\"clientip\",\"parent\":\"1559757816862\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_1_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}" - } - }, - "type": "_doc" - } -} - -{ - "type": "doc", - "value": { - "id": "visualization:64983230-87bf-11e9-a991-3b492a7c3e09", - "index": ".kibana", - "source": { - "coreMigrationVersion": "7.14.0", - "migrationVersion": { - "visualization": "7.14.0" - }, - "references": [ - { - "id": "logstash-*", - "name": "control_0_index_pattern", - "type": "index-pattern" - } - ], - "type": "visualization", - "updated_at": "2019-06-05T18:26:10.771Z", - "visualization": { - "description": "", - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" - }, - "title": "dynamic options input control", - "uiStateJSON": "{}", - "version": 1, - "visState": "{\"title\":\"dynamic options input control\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559759127876\",\"fieldName\":\"geo.src\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}" - } - }, - "type": "_doc" - } -} - -{ - "type": "doc", - "value": { - "id": "visualization:5d2de430-87c0-11e9-a991-3b492a7c3e09", - "index": ".kibana", - "source": { - "coreMigrationVersion": "7.14.0", - "migrationVersion": { - "visualization": "7.14.0" - }, - "references": [ - { - "id": "logstash-*", - "name": "control_0_index_pattern", - "type": "index-pattern" - }, - { - "id": "logstash-*", - "name": "control_1_index_pattern", - "type": "index-pattern" - } - ], - "type": "visualization", - "updated_at": "2019-06-05T18:33:07.827Z", - "visualization": { - "description": "", - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" - }, - "title": "chained input control with dynamic options", - "uiStateJSON": "{}", - "version": 1, - "visState": "{\"title\":\"chained input control with dynamic options\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559759550755\",\"fieldName\":\"machine.os.raw\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1559759557302\",\"fieldName\":\"geo.src\",\"parent\":\"1559759550755\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_1_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}" - } - }, - "type": "_doc" - } -} - -{ - "type": "doc", - "value": { - "id": "index-pattern:test_index*", - "index": ".kibana", - "source": { - "coreMigrationVersion": "7.14.0", - "index-pattern": { - "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"message.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"message\"}}},{\"name\":\"user\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"user.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"user\"}}}]", - "title": "test_index*" - }, - "migrationVersion": { - "index-pattern": "7.11.0" - }, - "references": [ - ], - "type": "index-pattern" - }, - "type": "_doc" - } -} - -{ - "type": "doc", - "value": { - "id": "visualization:AreaChart-no-date-field", - "index": ".kibana", - "source": { - "coreMigrationVersion": "7.14.0", - "migrationVersion": { - "visualization": "7.14.0" - }, - "references": [ - { - "id": "test_index*", - "name": "kibanaSavedObjectMeta.searchSourceJSON.index", - "type": "index-pattern" - } - ], - "type": "visualization", - "visualization": { - "description": "AreaChart", - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" - }, - "title": "AreaChart [no date field]", - "uiStateJSON": "{}", - "version": 1, - "visState": "{\"title\":\"AreaChart [no date field]\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"addTooltip\":true,\"addLegend\":true,\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}" - } - }, - "type": "_doc" - } -} - -{ - "type": "doc", - "value": { - "id": "index-pattern:log*", - "index": ".kibana", - "source": { - "coreMigrationVersion": "7.14.0", - "index-pattern": { - "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}", - "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", - "title": "log*" - }, - "migrationVersion": { - "index-pattern": "7.11.0" - }, - "references": [ - ], - "type": "index-pattern" - }, - "type": "_doc" - } -} - -{ - "type": "doc", - "value": { - "id": "visualization:AreaChart-no-time-filter", - "index": ".kibana", - "source": { - "coreMigrationVersion": "7.14.0", - "migrationVersion": { - "visualization": "7.14.0" - }, - "references": [ - { - "id": "log*", - "name": "kibanaSavedObjectMeta.searchSourceJSON.index", - "type": "index-pattern" - } - ], - "type": "visualization", - "visualization": { - "description": "AreaChart", - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" - }, - "title": "AreaChart [no time filter]", - "uiStateJSON": "{}", - "version": 1, - "visState": "{\"title\":\"AreaChart [no time filter]\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"addTooltip\":true,\"addLegend\":true,\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}" - } - }, - "type": "_doc" - } -} - -{ - "type": "doc", - "value": { - "id": "visualization:VegaMap", - "index": ".kibana", - "source": { - "coreMigrationVersion": "7.14.0", - "migrationVersion": { - "visualization": "7.14.0" - }, - "references": [ - ], - "type": "visualization", - "visualization": { - "description": "VegaMap", - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" - }, - "title": "VegaMap", - "uiStateJSON": "{}", - "version": 1, - "visState": "{\"aggs\":[],\"params\":{\"spec\":\"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\\"map\\\", latitude: 25, longitude: -70, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: kibana_sample_data_flights\\n %context%: true\\n // Uncomment to enable time filtering\\n // %timefield%: timestamp\\n body: {\\n size: 0\\n aggs: {\\n origins: {\\n terms: {field: \\\"OriginAirportID\\\", size: 10000}\\n aggs: {\\n originLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\\"OriginLocation\\\", \\\"Origin\\\"]\\n }\\n }\\n }\\n distinations: {\\n terms: {field: \\\"DestAirportID\\\", size: 10000}\\n aggs: {\\n destLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\\"DestLocation\\\"]\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\\"aggregations.origins.buckets\\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n originLocation.hits.hits[0]._source.OriginLocation.lon\\n originLocation.hits.hits[0]._source.OriginLocation.lat\\n ]\\n }\\n ]\\n }\\n {\\n name: selectedDatum\\n on: [\\n {trigger: \\\"!selected\\\", remove: true}\\n {trigger: \\\"selected\\\", insert: \\\"selected\\\"}\\n ]\\n }\\n ]\\n signals: [\\n {\\n name: selected\\n value: null\\n on: [\\n {events: \\\"@airport:mouseover\\\", update: \\\"datum\\\"}\\n {events: \\\"@airport:mouseout\\\", update: \\\"null\\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: airportSize\\n type: linear\\n domain: {data: \\\"table\\\", field: \\\"doc_count\\\"}\\n range: [\\n {signal: \\\"zoom*zoom*0.2+1\\\"}\\n {signal: \\\"zoom*zoom*10+1\\\"}\\n ]\\n }\\n ]\\n marks: [\\n {\\n type: group\\n from: {\\n facet: {\\n name: facetedDatum\\n data: selectedDatum\\n field: distinations.buckets\\n }\\n }\\n data: [\\n {\\n name: facetDatumElems\\n source: facetedDatum\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n destLocation.hits.hits[0]._source.DestLocation.lon\\n destLocation.hits.hits[0]._source.DestLocation.lat\\n ]\\n }\\n {type: \\\"formula\\\", expr: \\\"{x:parent.x, y:parent.y}\\\", as: \\\"source\\\"}\\n {type: \\\"formula\\\", expr: \\\"{x:datum.x, y:datum.y}\\\", as: \\\"target\\\"}\\n {type: \\\"linkpath\\\", shape: \\\"diagonal\\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: lineThickness\\n type: log\\n clamp: true\\n range: [1, 8]\\n }\\n {\\n name: lineOpacity\\n type: log\\n clamp: true\\n range: [0.2, 0.8]\\n }\\n ]\\n marks: [\\n {\\n from: {data: \\\"facetDatumElems\\\"}\\n type: path\\n interactive: false\\n encode: {\\n update: {\\n path: {field: \\\"path\\\"}\\n stroke: {value: \\\"black\\\"}\\n strokeWidth: {scale: \\\"lineThickness\\\", field: \\\"doc_count\\\"}\\n strokeOpacity: {scale: \\\"lineOpacity\\\", field: \\\"doc_count\\\"}\\n }\\n }\\n }\\n ]\\n }\\n {\\n name: airport\\n type: symbol\\n from: {data: \\\"table\\\"}\\n encode: {\\n update: {\\n size: {scale: \\\"airportSize\\\", field: \\\"doc_count\\\"}\\n xc: {signal: \\\"datum.x\\\"}\\n yc: {signal: \\\"datum.y\\\"}\\n tooltip: {\\n signal: \\\"{title: datum.originLocation.hits.hits[0]._source.Origin + ' (' + datum.key + ')', connnections: length(datum.distinations.buckets), flights: datum.doc_count}\\\"\\n }\\n }\\n }\\n }\\n ]\\n}\"},\"title\":\"[Flights] Airport Connections (Hover Over Airport)\",\"type\":\"vega\"}" - } - }, - "type": "_doc" - } -} \ No newline at end of file diff --git a/test/functional/fixtures/es_archiver/visualize/mappings.json b/test/functional/fixtures/es_archiver/visualize/mappings.json deleted file mode 100644 index d032352d9a688..0000000000000 --- a/test/functional/fixtures/es_archiver/visualize/mappings.json +++ /dev/null @@ -1,487 +0,0 @@ -{ - "type": "index", - "value": { - "aliases": { - ".kibana_$KIBANA_PACKAGE_VERSION": {}, - ".kibana": {} - }, - "index": ".kibana_$KIBANA_PACKAGE_VERSION_001", - "mappings": { - "_meta": { - "migrationMappingPropertyHashes": { - "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", - "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", - "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", - "config": "c63748b75f39d0c54de12d12c1ccbc20", - "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", - "coreMigrationVersion": "2f4316de49999235636386fe51dc06c1", - "dashboard": "40554caf09725935e2c02e02563a2d07", - "index-pattern": "45915a1ad866812242df474eb0479052", - "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", - "legacy-url-alias": "6155300fd11a00e23d5cbaa39f0fce0a", - "migrationVersion": "4a1746014a75ade3a714e1db5763276f", - "namespace": "2f4316de49999235636386fe51dc06c1", - "namespaces": "2f4316de49999235636386fe51dc06c1", - "originId": "2f4316de49999235636386fe51dc06c1", - "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", - "references": "7997cf5a56cc02bdc9c93361bde732b0", - "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", - "search": "db2c00e39b36f40930a3b9fc71c823e1", - "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "telemetry": "36a616f7026dfa617d6655df850fe16d", - "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", - "type": "2f4316de49999235636386fe51dc06c1", - "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3", - "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", - "updated_at": "00da57df13e94e9d98437d13ace4bfe0", - "url": "c7f66a0df8b1b52f17c28c4adb111105", - "usage-counters": "8cc260bdceffec4ffc3ad165c97dc1b4", - "visualization": "f819cf6636b75c9e76ba733a0c6ef355" - } - }, - "dynamic": "strict", - "properties": { - "application_usage_daily": { - "dynamic": "false", - "properties": { - "timestamp": { - "type": "date" - } - } - }, - "application_usage_totals": { - "dynamic": "false", - "type": "object" - }, - "application_usage_transactional": { - "dynamic": "false", - "type": "object" - }, - "config": { - "dynamic": "false", - "properties": { - "buildNum": { - "type": "keyword" - } - } - }, - "core-usage-stats": { - "dynamic": "false", - "type": "object" - }, - "coreMigrationVersion": { - "type": "keyword" - }, - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "doc_values": false, - "index": false, - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "index": false, - "type": "text" - } - } - }, - "optionsJSON": { - "index": false, - "type": "text" - }, - "panelsJSON": { - "index": false, - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "pause": { - "doc_values": false, - "index": false, - "type": "boolean" - }, - "section": { - "doc_values": false, - "index": false, - "type": "integer" - }, - "value": { - "doc_values": false, - "index": false, - "type": "integer" - } - } - }, - "timeFrom": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "timeRestore": { - "doc_values": false, - "index": false, - "type": "boolean" - }, - "timeTo": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "index-pattern": { - "dynamic": "false", - "properties": { - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } - }, - "legacy-url-alias": { - "dynamic": "false", - "properties": { - "disabled": { - "type": "boolean" - }, - "sourceId": { - "type": "keyword" - }, - "targetType": { - "type": "keyword" - } - } - }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "index-pattern": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "visualization": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "namespace": { - "type": "keyword" - }, - "namespaces": { - "type": "keyword" - }, - "originId": { - "type": "keyword" - }, - "query": { - "properties": { - "description": { - "type": "text" - }, - "filters": { - "enabled": false, - "type": "object" - }, - "query": { - "properties": { - "language": { - "type": "keyword" - }, - "query": { - "index": false, - "type": "keyword" - } - } - }, - "timefilter": { - "enabled": false, - "type": "object" - }, - "title": { - "type": "text" - } - } - }, - "references": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "sample-data-telemetry": { - "properties": { - "installCount": { - "type": "long" - }, - "unInstallCount": { - "type": "long" - } - } - }, - "search": { - "properties": { - "columns": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "description": { - "type": "text" - }, - "grid": { - "enabled": false, - "type": "object" - }, - "hideChart": { - "doc_values": false, - "index": false, - "type": "boolean" - }, - "hits": { - "doc_values": false, - "index": false, - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "index": false, - "type": "text" - } - } - }, - "sort": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "search-telemetry": { - "dynamic": "false", - "type": "object" - }, - "server": { - "dynamic": "false", - "type": "object" - }, - "telemetry": { - "properties": { - "allowChangingOptInStatus": { - "type": "boolean" - }, - "enabled": { - "type": "boolean" - }, - "lastReported": { - "type": "date" - }, - "lastVersionChecked": { - "type": "keyword" - }, - "reportFailureCount": { - "type": "integer" - }, - "reportFailureVersion": { - "type": "keyword" - }, - "sendUsageFrom": { - "type": "keyword" - }, - "userHasSeenNotice": { - "type": "boolean" - } - } - }, - "timelion-sheet": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "timelion_chart_height": { - "type": "integer" - }, - "timelion_columns": { - "type": "integer" - }, - "timelion_interval": { - "type": "keyword" - }, - "timelion_other_interval": { - "type": "keyword" - }, - "timelion_rows": { - "type": "integer" - }, - "timelion_sheet": { - "type": "text" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "type": { - "type": "keyword" - }, - "ui-counter": { - "properties": { - "count": { - "type": "integer" - } - } - }, - "ui-metric": { - "properties": { - "count": { - "type": "integer" - } - } - }, - "updated_at": { - "type": "date" - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "usage-counters": { - "dynamic": "false", - "properties": { - "domainId": { - "type": "keyword" - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "index": false, - "type": "text" - } - } - }, - "savedSearchRefName": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "index": false, - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "index": false, - "type": "text" - } - } - } - } - }, - "settings": { - "index": { - "auto_expand_replicas": "0-1", - "number_of_replicas": "0", - "number_of_shards": "1", - "priority": "10", - "refresh_interval": "1s", - "routing_partition_size": "1" - } - } - } -} \ No newline at end of file diff --git a/test/functional/fixtures/kbn_archiver/visualize.json b/test/functional/fixtures/kbn_archiver/visualize.json index 758841e8d81ef..660da856964b4 100644 --- a/test/functional/fixtures/kbn_archiver/visualize.json +++ b/test/functional/fixtures/kbn_archiver/visualize.json @@ -6,14 +6,14 @@ "timeFieldName": "@timestamp", "title": "logstash-*" }, - "coreMigrationVersion": "8.0.0", + "coreMigrationVersion": "7.14.0", "id": "logstash-*", "migrationVersion": { "index-pattern": "7.11.0" }, "references": [], "type": "index-pattern", - "version": "WzI2LDJd" + "version": "WzEzLDFd" } { @@ -27,10 +27,10 @@ "version": 1, "visState": "{\"title\":\"chained input control with dynamic options\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559759550755\",\"fieldName\":\"machine.os.raw\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1559759557302\",\"fieldName\":\"geo.src\",\"parent\":\"1559759550755\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_1_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}" }, - "coreMigrationVersion": "8.0.0", + "coreMigrationVersion": "7.14.0", "id": "5d2de430-87c0-11e9-a991-3b492a7c3e09", "migrationVersion": { - "visualization": "7.13.0" + "visualization": "7.14.0" }, "references": [ { @@ -46,7 +46,7 @@ ], "type": "visualization", "updated_at": "2019-06-05T18:33:07.827Z", - "version": "WzMzLDJd" + "version": "WzIwLDFd" } { @@ -60,10 +60,10 @@ "version": 1, "visState": "{\"title\":\"dynamic options input control\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559759127876\",\"fieldName\":\"geo.src\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}" }, - "coreMigrationVersion": "8.0.0", + "coreMigrationVersion": "7.14.0", "id": "64983230-87bf-11e9-a991-3b492a7c3e09", "migrationVersion": { - "visualization": "7.13.0" + "visualization": "7.14.0" }, "references": [ { @@ -74,7 +74,7 @@ ], "type": "visualization", "updated_at": "2019-06-05T18:26:10.771Z", - "version": "WzMyLDJd" + "version": "WzE5LDFd" } { @@ -88,10 +88,10 @@ "version": 1, "visState": "{\"title\":\"chained input control\",\"type\":\"input_control_vis\",\"params\":{\"controls\":[{\"id\":\"1559757816862\",\"fieldName\":\"geo.src\",\"parent\":\"\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_0_index_pattern\"},{\"id\":\"1559757836347\",\"fieldName\":\"clientip\",\"parent\":\"1559757816862\",\"label\":\"\",\"type\":\"list\",\"options\":{\"type\":\"terms\",\"multiselect\":true,\"dynamicOptions\":true,\"size\":5,\"order\":\"desc\"},\"indexPatternRefName\":\"control_1_index_pattern\"}],\"updateFiltersOnChange\":false,\"useTimeFilter\":false,\"pinFilters\":false},\"aggs\":[]}" }, - "coreMigrationVersion": "8.0.0", + "coreMigrationVersion": "7.14.0", "id": "68305470-87bc-11e9-a991-3b492a7c3e09", "migrationVersion": { - "visualization": "7.13.0" + "visualization": "7.14.0" }, "references": [ { @@ -107,7 +107,7 @@ ], "type": "visualization", "updated_at": "2019-06-05T18:04:48.310Z", - "version": "WzMxLDJd" + "version": "WzE4LDFd" } { @@ -115,10 +115,14 @@ "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"message.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"message\"}}},{\"name\":\"user\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"user.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"user\"}}}]", "title": "test_index*" }, + "coreMigrationVersion": "7.14.0", "id": "test_index*", + "migrationVersion": { + "index-pattern": "7.11.0" + }, "references": [], "type": "index-pattern", - "version": "WzI1LDJd" + "version": "WzIxLDFd" } { @@ -132,10 +136,10 @@ "version": 1, "visState": "{\"title\":\"AreaChart [no date field]\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"addTooltip\":true,\"addLegend\":true,\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}" }, - "coreMigrationVersion": "8.0.0", + "coreMigrationVersion": "7.14.0", "id": "AreaChart-no-date-field", "migrationVersion": { - "visualization": "7.13.0" + "visualization": "7.14.0" }, "references": [ { @@ -145,7 +149,7 @@ } ], "type": "visualization", - "version": "WzM0LDJd" + "version": "WzIyLDFd" } { @@ -154,14 +158,14 @@ "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", "title": "log*" }, - "coreMigrationVersion": "8.0.0", + "coreMigrationVersion": "7.14.0", "id": "log*", "migrationVersion": { "index-pattern": "7.11.0" }, "references": [], "type": "index-pattern", - "version": "WzM1LDJd" + "version": "WzIzLDFd" } { @@ -175,10 +179,10 @@ "version": 1, "visState": "{\"title\":\"AreaChart [no time filter]\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"addTooltip\":true,\"addLegend\":true,\"times\":[],\"addTimeMarker\":false,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}" }, - "coreMigrationVersion": "8.0.0", + "coreMigrationVersion": "7.14.0", "id": "AreaChart-no-time-filter", "migrationVersion": { - "visualization": "7.13.0" + "visualization": "7.14.0" }, "references": [ { @@ -188,7 +192,7 @@ } ], "type": "visualization", - "version": "WzM2LDJd" + "version": "WzI0LDFd" } { @@ -202,10 +206,10 @@ "version": 1, "visState": "{\"title\":\"New Visualization\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}" }, - "coreMigrationVersion": "8.0.0", + "coreMigrationVersion": "7.14.0", "id": "Shared-Item-Visualization-AreaChart", "migrationVersion": { - "visualization": "7.13.0" + "visualization": "7.14.0" }, "references": [ { @@ -215,7 +219,7 @@ } ], "type": "visualization", - "version": "WzI5LDJd" + "version": "WzE2LDFd" } { @@ -229,14 +233,14 @@ "version": 1, "visState": "{\"aggs\":[],\"params\":{\"spec\":\"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\\"map\\\", latitude: 25, longitude: -70, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: kibana_sample_data_flights\\n %context%: true\\n // Uncomment to enable time filtering\\n // %timefield%: timestamp\\n body: {\\n size: 0\\n aggs: {\\n origins: {\\n terms: {field: \\\"OriginAirportID\\\", size: 10000}\\n aggs: {\\n originLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\\"OriginLocation\\\", \\\"Origin\\\"]\\n }\\n }\\n }\\n distinations: {\\n terms: {field: \\\"DestAirportID\\\", size: 10000}\\n aggs: {\\n destLocation: {\\n top_hits: {\\n size: 1\\n _source: {\\n includes: [\\\"DestLocation\\\"]\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\\"aggregations.origins.buckets\\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n originLocation.hits.hits[0]._source.OriginLocation.lon\\n originLocation.hits.hits[0]._source.OriginLocation.lat\\n ]\\n }\\n ]\\n }\\n {\\n name: selectedDatum\\n on: [\\n {trigger: \\\"!selected\\\", remove: true}\\n {trigger: \\\"selected\\\", insert: \\\"selected\\\"}\\n ]\\n }\\n ]\\n signals: [\\n {\\n name: selected\\n value: null\\n on: [\\n {events: \\\"@airport:mouseover\\\", update: \\\"datum\\\"}\\n {events: \\\"@airport:mouseout\\\", update: \\\"null\\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: airportSize\\n type: linear\\n domain: {data: \\\"table\\\", field: \\\"doc_count\\\"}\\n range: [\\n {signal: \\\"zoom*zoom*0.2+1\\\"}\\n {signal: \\\"zoom*zoom*10+1\\\"}\\n ]\\n }\\n ]\\n marks: [\\n {\\n type: group\\n from: {\\n facet: {\\n name: facetedDatum\\n data: selectedDatum\\n field: distinations.buckets\\n }\\n }\\n data: [\\n {\\n name: facetDatumElems\\n source: facetedDatum\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n destLocation.hits.hits[0]._source.DestLocation.lon\\n destLocation.hits.hits[0]._source.DestLocation.lat\\n ]\\n }\\n {type: \\\"formula\\\", expr: \\\"{x:parent.x, y:parent.y}\\\", as: \\\"source\\\"}\\n {type: \\\"formula\\\", expr: \\\"{x:datum.x, y:datum.y}\\\", as: \\\"target\\\"}\\n {type: \\\"linkpath\\\", shape: \\\"diagonal\\\"}\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: lineThickness\\n type: log\\n clamp: true\\n range: [1, 8]\\n }\\n {\\n name: lineOpacity\\n type: log\\n clamp: true\\n range: [0.2, 0.8]\\n }\\n ]\\n marks: [\\n {\\n from: {data: \\\"facetDatumElems\\\"}\\n type: path\\n interactive: false\\n encode: {\\n update: {\\n path: {field: \\\"path\\\"}\\n stroke: {value: \\\"black\\\"}\\n strokeWidth: {scale: \\\"lineThickness\\\", field: \\\"doc_count\\\"}\\n strokeOpacity: {scale: \\\"lineOpacity\\\", field: \\\"doc_count\\\"}\\n }\\n }\\n }\\n ]\\n }\\n {\\n name: airport\\n type: symbol\\n from: {data: \\\"table\\\"}\\n encode: {\\n update: {\\n size: {scale: \\\"airportSize\\\", field: \\\"doc_count\\\"}\\n xc: {signal: \\\"datum.x\\\"}\\n yc: {signal: \\\"datum.y\\\"}\\n tooltip: {\\n signal: \\\"{title: datum.originLocation.hits.hits[0]._source.Origin + ' (' + datum.key + ')', connnections: length(datum.distinations.buckets), flights: datum.doc_count}\\\"\\n }\\n }\\n }\\n }\\n ]\\n}\"},\"title\":\"[Flights] Airport Connections (Hover Over Airport)\",\"type\":\"vega\"}" }, - "coreMigrationVersion": "8.0.0", + "coreMigrationVersion": "7.14.0", "id": "VegaMap", "migrationVersion": { - "visualization": "7.13.0" + "visualization": "7.14.0" }, "references": [], "type": "visualization", - "version": "WzM3LDJd" + "version": "WzI1LDFd" } { @@ -250,10 +254,10 @@ "version": 1, "visState": "{\"title\":\"Visualization AreaChart\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{},\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"timeRange\":{\"from\":\"now-15m\",\"to\":\"now\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}]}" }, - "coreMigrationVersion": "8.0.0", + "coreMigrationVersion": "7.14.0", "id": "Visualization-AreaChart", "migrationVersion": { - "visualization": "7.13.0" + "visualization": "7.14.0" }, "references": [ { @@ -263,7 +267,7 @@ } ], "type": "visualization", - "version": "WzMwLDJd" + "version": "WzE3LDFd" } { @@ -272,14 +276,14 @@ "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", "title": "logstash*" }, - "coreMigrationVersion": "8.0.0", + "coreMigrationVersion": "7.14.0", "id": "logstash*", "migrationVersion": { "index-pattern": "7.11.0" }, "references": [], "type": "index-pattern", - "version": "WzI3LDJd" + "version": "WzE0LDFd" } { @@ -289,12 +293,12 @@ "timeFieldName": "@timestamp", "title": "long-window-logstash-*" }, - "coreMigrationVersion": "8.0.0", + "coreMigrationVersion": "7.14.0", "id": "long-window-logstash-*", "migrationVersion": { "index-pattern": "7.11.0" }, "references": [], "type": "index-pattern", - "version": "WzI4LDJd" + "version": "WzE1LDFd" } \ No newline at end of file diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 65b899d2e2fb0..dc3a04568316e 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -448,7 +448,10 @@ export class DiscoverPageObject extends FtrService { public async closeSidebarFieldFilter() { await this.testSubjects.click('toggleFieldFilterButton'); - await this.testSubjects.missingOrFail('filterSelectionPanel'); + + await this.retry.waitFor('sidebar filter closed', async () => { + return !(await this.testSubjects.exists('filterSelectionPanel')); + }); } public async waitForChartLoadingComplete(renderCount: number) { diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 88951bb04c956..cb8f198177017 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -739,6 +739,10 @@ export class SettingsPageObject extends FtrService { await this.testSubjects.click('editFieldFormat'); } + async clickCloseEditFieldFormatFlyout() { + await this.testSubjects.click('euiFlyoutCloseButton'); + } + async associateIndexPattern(oldIndexPatternId: string, newIndexPatternTitle: string) { await this.find.clickByCssSelector( `select[data-test-subj="managementChangeIndexSelection-${oldIndexPatternId}"] > diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index 6e263dd1cdbbf..7f1ea64bcd979 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -563,7 +563,7 @@ export class VisualBuilderPageObject extends FtrService { public async checkColorPickerPopUpIsPresent(): Promise<void> { this.log.debug(`Check color picker popup is present`); - await this.testSubjects.existOrFail('colorPickerPopover', { timeout: 5000 }); + await this.testSubjects.existOrFail('euiColorPickerPopover', { timeout: 5000 }); } public async changePanelPreview(nth: number = 0): Promise<void> { diff --git a/test/functional/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts index c8587f4ffd346..64b8c363fa6c2 100644 --- a/test/functional/page_objects/visualize_chart_page.ts +++ b/test/functional/page_objects/visualize_chart_page.ts @@ -37,7 +37,8 @@ export class VisualizeChartPageObject extends FtrService { public async isNewChartsLibraryEnabled(): Promise<boolean> { const legacyChartsLibrary = Boolean( - await this.kibanaServer.uiSettings.get('visualization:visualize:legacyChartsLibrary') + (await this.kibanaServer.uiSettings.get('visualization:visualize:legacyChartsLibrary')) && + (await this.kibanaServer.uiSettings.get('visualization:visualize:legacyPieChartsLibrary')) ) ?? true; const enabled = !legacyChartsLibrary; this.log.debug(`-- isNewChartsLibraryEnabled = ${enabled}`); diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index a11a254509e7a..e930406cdcce8 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -57,6 +57,7 @@ export class VisualizePageObject extends FtrService { defaultIndex: 'logstash-*', [UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0,0.[000]b', 'visualization:visualize:legacyChartsLibrary': !isNewLibrary, + 'visualization:visualize:legacyPieChartsLibrary': !isNewLibrary, }); } diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts index 9aca790b0b437..4340f16492a7c 100644 --- a/test/functional/services/dashboard/panel_actions.ts +++ b/test/functional/services/dashboard/panel_actions.ts @@ -211,36 +211,29 @@ export class DashboardPanelActionsService extends FtrService { await this.testSubjects.click('confirmSaveSavedObjectButton'); } - async expectExistsRemovePanelAction() { - this.log.debug('expectExistsRemovePanelAction'); - await this.expectExistsPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ); - } - - async expectExistsPanelAction(testSubject: string) { + async expectExistsPanelAction(testSubject: string, title?: string) { this.log.debug('expectExistsPanelAction', testSubject); - await this.openContextMenu(); - if (await this.testSubjects.exists(CLONE_PANEL_DATA_TEST_SUBJ)) return; - if (await this.hasContextMenuMoreItem()) { - await this.clickContextMenuMoreItem(); + + const panelWrapper = title ? await this.getPanelHeading(title) : undefined; + await this.openContextMenu(panelWrapper); + + if (!(await this.testSubjects.exists(testSubject))) { + if (await this.hasContextMenuMoreItem()) { + await this.clickContextMenuMoreItem(); + } + await this.testSubjects.existOrFail(testSubject); } - await this.testSubjects.existOrFail(CLONE_PANEL_DATA_TEST_SUBJ); - await this.toggleContextMenu(); + await this.toggleContextMenu(panelWrapper); } - async expectMissingPanelAction(testSubject: string) { - this.log.debug('expectMissingPanelAction', testSubject); - await this.openContextMenu(); - await this.testSubjects.missingOrFail(testSubject); - if (await this.hasContextMenuMoreItem()) { - await this.clickContextMenuMoreItem(); - await this.testSubjects.missingOrFail(testSubject); - } - await this.toggleContextMenu(); + async expectExistsRemovePanelAction() { + this.log.debug('expectExistsRemovePanelAction'); + await this.expectExistsPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ); } - async expectExistsEditPanelAction() { + async expectExistsEditPanelAction(title?: string) { this.log.debug('expectExistsEditPanelAction'); - await this.expectExistsPanelAction(EDIT_PANEL_DATA_TEST_SUBJ); + await this.expectExistsPanelAction(EDIT_PANEL_DATA_TEST_SUBJ, title); } async expectExistsReplacePanelAction() { @@ -253,6 +246,22 @@ export class DashboardPanelActionsService extends FtrService { await this.expectExistsPanelAction(CLONE_PANEL_DATA_TEST_SUBJ); } + async expectExistsToggleExpandAction() { + this.log.debug('expectExistsToggleExpandAction'); + await this.expectExistsPanelAction(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ); + } + + async expectMissingPanelAction(testSubject: string) { + this.log.debug('expectMissingPanelAction', testSubject); + await this.openContextMenu(); + await this.testSubjects.missingOrFail(testSubject); + if (await this.hasContextMenuMoreItem()) { + await this.clickContextMenuMoreItem(); + await this.testSubjects.missingOrFail(testSubject); + } + await this.toggleContextMenu(); + } + async expectMissingEditPanelAction() { this.log.debug('expectMissingEditPanelAction'); await this.expectMissingPanelAction(EDIT_PANEL_DATA_TEST_SUBJ); @@ -273,11 +282,6 @@ export class DashboardPanelActionsService extends FtrService { await this.expectMissingPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ); } - async expectExistsToggleExpandAction() { - this.log.debug('expectExistsToggleExpandAction'); - await this.expectExistsPanelAction(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ); - } - async getPanelHeading(title: string) { return await this.testSubjects.find(`embeddablePanelHeading-${title.replace(/\s/g, '')}`); } diff --git a/test/interpreter_functional/test_suites/run_pipeline/index.ts b/test/interpreter_functional/test_suites/run_pipeline/index.ts index 9cf7e0deba2fa..f8c37bab02b86 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/index.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/index.ts @@ -21,7 +21,7 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid before(async () => { await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); - await esArchiver.load('test/functional/fixtures/es_archiver/visualize_embedding'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/visualize.json'); await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'Australia/North', defaultIndex: 'logstash-*', @@ -32,6 +32,12 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid await testSubjects.find('pluginContent'); }); + after(async () => { + await kibanaServer.importExport.unload( + 'test/functional/fixtures/kbn_archiver/visualize.json' + ); + }); + loadTestFile(require.resolve('./basic')); loadTestFile(require.resolve('./tag_cloud')); loadTestFile(require.resolve('./metric')); diff --git a/test/plugin_functional/test_suites/core_plugins/status.ts b/test/plugin_functional/test_suites/core_plugins/status.ts new file mode 100644 index 0000000000000..2b0f15cb39273 --- /dev/null +++ b/test/plugin_functional/test_suites/core_plugins/status.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { ServiceStatusLevels } from '../../../../src/core/server'; +import { PluginFunctionalProviderContext } from '../../services'; + +export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) { + const supertest = getService('supertest'); + const log = getService('log'); + + const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); + const getStatus = async (pluginName?: string) => { + const resp = await supertest.get('/api/status?v8format=true'); + + if (pluginName) { + return resp.body.status.plugins[pluginName]; + } else { + return resp.body.status.overall; + } + }; + + const setStatus = async <T extends keyof typeof ServiceStatusLevels>(level: T) => + supertest + .post(`/internal/core_plugin_a/status/set?level=${level}`) + .set('kbn-xsrf', 'xxx') + .expect(200); + + describe('status service', () => { + // This test must comes first because the timeout only applies to the initial emission + it("returns a timeout for status check that doesn't emit after 30s", async () => { + let aStatus = await getStatus('corePluginA'); + expect(aStatus.level).to.eql('unavailable'); + + // Status will remain in unavailable due to core services until custom status timesout + // Keep polling until that condition ends, up to a timeout + const start = Date.now(); + while ('elasticsearch' in (aStatus.meta?.affectedServices ?? {})) { + aStatus = await getStatus('corePluginA'); + expect(aStatus.level).to.eql('unavailable'); + + // If it's been more than 40s, break out of this loop + if (Date.now() - start >= 40_000) { + throw new Error(`Timed out waiting for status timeout after 40s`); + } + + log.info('Waiting for status check to timeout...'); + await delay(2000); + } + + expect(aStatus.summary).to.eql('Status check timed out after 30s'); + }); + + it('propagates status issues to dependencies', async () => { + await setStatus('degraded'); + await delay(1000); + expect((await getStatus('corePluginA')).level).to.eql('degraded'); + expect((await getStatus('corePluginB')).level).to.eql('degraded'); + + await setStatus('available'); + await delay(1000); + expect((await getStatus('corePluginA')).level).to.eql('available'); + expect((await getStatus('corePluginB')).level).to.eql('available'); + }); + }); +} diff --git a/test/plugin_functional/test_suites/custom_visualizations/index.js b/test/plugin_functional/test_suites/custom_visualizations/index.js index 0998b97da67ff..22b0f21fb983a 100644 --- a/test/plugin_functional/test_suites/custom_visualizations/index.js +++ b/test/plugin_functional/test_suites/custom_visualizations/index.js @@ -14,7 +14,7 @@ export default function ({ getService, loadTestFile }) { describe('custom visualizations', function () { before(async () => { await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/visualize'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/visualize.json'); await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'Australia/North', defaultIndex: 'logstash-*', @@ -22,6 +22,12 @@ export default function ({ getService, loadTestFile }) { await browser.setWindowSize(1300, 900); }); + after(async () => { + await kibanaServer.importExport.unload( + 'test/functional/fixtures/kbn_archiver/visualize.json' + ); + }); + loadTestFile(require.resolve('./self_changing_vis')); }); } diff --git a/test/scripts/test/server_integration.sh b/test/scripts/test/server_integration.sh index 1ff4a772bb6e0..6ec08c7727e20 100755 --- a/test/scripts/test/server_integration.sh +++ b/test/scripts/test/server_integration.sh @@ -12,3 +12,10 @@ checks-reporter-with-killswitch "Server Integration Tests" \ --bail \ --debug \ --kibana-install-dir $KIBANA_INSTALL_DIR + +# Tests that must be run against source in order to build test plugins +checks-reporter-with-killswitch "Status Integration Tests" \ + node scripts/functional_tests \ + --config test/server_integration/http/platform/config.status.ts \ + --bail \ + --debug \ diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_a/kibana.json b/test/server_integration/__fixtures__/plugins/status_plugin_a/kibana.json new file mode 100644 index 0000000000000..36981d446c9f9 --- /dev/null +++ b/test/server_integration/__fixtures__/plugins/status_plugin_a/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "statusPluginA", + "version": "0.0.1", + "kibanaVersion": "kibana", + "server": true, + "ui": false +} diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_a/package.json b/test/server_integration/__fixtures__/plugins/status_plugin_a/package.json new file mode 100644 index 0000000000000..5c73bca024f4e --- /dev/null +++ b/test/server_integration/__fixtures__/plugins/status_plugin_a/package.json @@ -0,0 +1,14 @@ +{ + "name": "status_plugin_a", + "version": "1.0.0", + "main": "target/test/server_integration/__fixtures__/plugins/status_plugin_a", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "SSPL-1.0 OR Elastic License 2.0", + "scripts": { + "kbn": "node ../../../../../../scripts/kbn.js", + "build": "rm -rf './target' && ../../../../../../node_modules/.bin/tsc" + } +} \ No newline at end of file diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_a/server/index.ts b/test/server_integration/__fixtures__/plugins/status_plugin_a/server/index.ts new file mode 100644 index 0000000000000..cf221c00e32b0 --- /dev/null +++ b/test/server_integration/__fixtures__/plugins/status_plugin_a/server/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { StatusPluginAPlugin } from './plugin'; + +export const plugin = () => new StatusPluginAPlugin(); diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_a/server/plugin.ts b/test/server_integration/__fixtures__/plugins/status_plugin_a/server/plugin.ts new file mode 100644 index 0000000000000..b2e4f0dd322c4 --- /dev/null +++ b/test/server_integration/__fixtures__/plugins/status_plugin_a/server/plugin.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { schema } from '@kbn/config-schema'; +import { Subject } from 'rxjs'; +import { + Plugin, + CoreSetup, + ServiceStatus, + ServiceStatusLevels, +} from '../../../../../../src/core/server'; + +export class StatusPluginAPlugin implements Plugin { + private status$ = new Subject<ServiceStatus>(); + + public setup(core: CoreSetup, deps: {}) { + // Set a custom status that will not emit immediately to force a timeout + core.status.set(this.status$); + + const router = core.http.createRouter(); + + router.post( + { + path: '/internal/status_plugin_a/status/set', + validate: { + query: schema.object({ + level: schema.oneOf([ + schema.literal('available'), + schema.literal('degraded'), + schema.literal('unavailable'), + schema.literal('critical'), + ]), + }), + }, + }, + (context, req, res) => { + const { level } = req.query; + + this.status$.next({ + level: ServiceStatusLevels[level], + summary: `statusPluginA is ${level}`, + }); + + return res.ok(); + } + ); + } + + public start() {} + public stop() {} +} diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_a/tsconfig.json b/test/server_integration/__fixtures__/plugins/status_plugin_a/tsconfig.json new file mode 100644 index 0000000000000..5069db62589c7 --- /dev/null +++ b/test/server_integration/__fixtures__/plugins/status_plugin_a/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true, + "composite": true + }, + "include": [ + "index.ts", + "server/**/*.ts", + "../../../../../../typings/**/*", + ], + "exclude": [], + "references": [ + { "path": "../../../../../src/core/tsconfig.json" } + ] +} diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_b/kibana.json b/test/server_integration/__fixtures__/plugins/status_plugin_b/kibana.json new file mode 100644 index 0000000000000..fa02f42d500af --- /dev/null +++ b/test/server_integration/__fixtures__/plugins/status_plugin_b/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "statusPluginB", + "version": "0.0.1", + "kibanaVersion": "kibana", + "server": true, + "ui": false, + "requiredPlugins": ["statusPluginA"] +} diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_b/package.json b/test/server_integration/__fixtures__/plugins/status_plugin_b/package.json new file mode 100644 index 0000000000000..3799d5d470754 --- /dev/null +++ b/test/server_integration/__fixtures__/plugins/status_plugin_b/package.json @@ -0,0 +1,14 @@ +{ + "name": "status_plugin_b", + "version": "1.0.0", + "main": "target/test/server_integration/__fixtures__/plugins/status_plugin_b", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "SSPL-1.0 OR Elastic License 2.0", + "scripts": { + "kbn": "node ../../../../../../scripts/kbn.js", + "build": "rm -rf './target' && ../../../../../../node_modules/.bin/tsc" + } +} \ No newline at end of file diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_b/server/index.ts b/test/server_integration/__fixtures__/plugins/status_plugin_b/server/index.ts new file mode 100644 index 0000000000000..2002d234827b9 --- /dev/null +++ b/test/server_integration/__fixtures__/plugins/status_plugin_b/server/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { StatusPluginBPlugin } from './plugin'; + +export const plugin = () => new StatusPluginBPlugin(); diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_b/server/plugin.ts b/test/server_integration/__fixtures__/plugins/status_plugin_b/server/plugin.ts new file mode 100644 index 0000000000000..191e8135f69a9 --- /dev/null +++ b/test/server_integration/__fixtures__/plugins/status_plugin_b/server/plugin.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Plugin } from 'kibana/server'; + +export class StatusPluginBPlugin implements Plugin { + public setup() {} + public start() {} + public stop() {} +} diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_b/tsconfig.json b/test/server_integration/__fixtures__/plugins/status_plugin_b/tsconfig.json new file mode 100644 index 0000000000000..224aa42ef68d2 --- /dev/null +++ b/test/server_integration/__fixtures__/plugins/status_plugin_b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true, + "composite": true + }, + "include": [ + "index.ts", + "server/**/*.ts", + "../../../../../typings/**/*", + ], + "exclude": [], + "references": [ + { "path": "../../../../../src/core/tsconfig.json" } + ] +} diff --git a/test/server_integration/http/platform/config.status.ts b/test/server_integration/http/platform/config.status.ts new file mode 100644 index 0000000000000..8cc76c901f47c --- /dev/null +++ b/test/server_integration/http/platform/config.status.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import fs from 'fs'; +import path from 'path'; +import { FtrConfigProviderContext } from '@kbn/test'; + +/* + * These tests exist in a separate configuration because: + * 1) It must run as the first test after Kibana launches to clear the unavailable status. A separate config makes this + * easier to manage and prevent from breaking. + * 2) The other server_integration tests run against a built distributable, however the FTR does not support building + * and installing plugins against built Kibana. This test must be run against source only in order to build the + * fixture plugins + */ +// eslint-disable-next-line import/no-default-export +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const httpConfig = await readConfigFile(require.resolve('../../config')); + + // Find all folders in __fixtures__/plugins since we treat all them as plugin folder + const allFiles = fs.readdirSync(path.resolve(__dirname, '../../__fixtures__/plugins')); + const plugins = allFiles.filter((file) => + fs.statSync(path.resolve(__dirname, '../../__fixtures__/plugins', file)).isDirectory() + ); + + return { + testFiles: [ + // Status test should be first to resolve manually created "unavailable" plugin + require.resolve('./status'), + ], + services: httpConfig.get('services'), + servers: httpConfig.get('servers'), + junit: { + reportName: 'Kibana Platform Status Integration Tests', + }, + esTestCluster: httpConfig.get('esTestCluster'), + kbnTestServer: { + ...httpConfig.get('kbnTestServer'), + serverArgs: [ + ...httpConfig.get('kbnTestServer.serverArgs'), + ...plugins.map( + (pluginDir) => + `--plugin-path=${path.resolve(__dirname, '../../__fixtures__/plugins', pluginDir)}` + ), + ], + runOptions: { + ...httpConfig.get('kbnTestServer.runOptions'), + // Don't wait for Kibana to be completely ready so that we can test the status timeouts + wait: /\[Kibana\]\[http\] http server running/, + }, + }, + }; +} diff --git a/test/server_integration/http/platform/status.ts b/test/server_integration/http/platform/status.ts new file mode 100644 index 0000000000000..0dcf82c9bea9e --- /dev/null +++ b/test/server_integration/http/platform/status.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import type { ServiceStatus, ServiceStatusLevels } from '../../../../src/core/server'; +import { FtrProviderContext } from '../../services/types'; + +type ServiceStatusSerialized = Omit<ServiceStatus, 'level'> & { level: string }; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const retry = getService('retry'); + + const getStatus = async (pluginName: string): Promise<ServiceStatusSerialized> => { + const resp = await supertest.get('/api/status?v8format=true'); + + return resp.body.status.plugins[pluginName]; + }; + + const setStatus = async <T extends keyof typeof ServiceStatusLevels>(level: T) => + supertest + .post(`/internal/status_plugin_a/status/set?level=${level}`) + .set('kbn-xsrf', 'xxx') + .expect(200); + + describe('status service', () => { + // This test must comes first because the timeout only applies to the initial emission + it("returns a timeout for status check that doesn't emit after 30s", async () => { + let aStatus = await getStatus('statusPluginA'); + expect(aStatus.level).to.eql('unavailable'); + + // Status will remain in unavailable until the custom status check times out + // Keep polling until that condition ends, up to a timeout + await retry.waitForWithTimeout(`Status check to timeout`, 40_000, async () => { + aStatus = await getStatus('statusPluginA'); + return aStatus.summary === 'Status check timed out after 30s'; + }); + + expect(aStatus.level).to.eql('unavailable'); + expect(aStatus.summary).to.eql('Status check timed out after 30s'); + }); + + it('propagates status issues to dependencies', async () => { + await setStatus('degraded'); + await retry.waitForWithTimeout( + `statusPluginA status to update`, + 5_000, + async () => (await getStatus('statusPluginA')).level === 'degraded' + ); + expect((await getStatus('statusPluginA')).level).to.eql('degraded'); + expect((await getStatus('statusPluginB')).level).to.eql('degraded'); + + await setStatus('available'); + await retry.waitForWithTimeout( + `statusPluginA status to update`, + 5_000, + async () => (await getStatus('statusPluginA')).level === 'available' + ); + expect((await getStatus('statusPluginA')).level).to.eql('available'); + expect((await getStatus('statusPluginB')).level).to.eql('available'); + }); + }); +} diff --git a/test/tsconfig.json b/test/tsconfig.json index 3e02283946080..8cf33d93a4067 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -17,7 +17,12 @@ "api_integration/apis/telemetry/fixtures/*.json", "api_integration/apis/telemetry/fixtures/*.json", ], - "exclude": ["target/**/*", "plugin_functional/plugins/**/*", "interpreter_functional/plugins/**/*"], + "exclude": [ + "target/**/*", + "interpreter_functional/plugins/**/*", + "plugin_functional/plugins/**/*", + "server_integration/__fixtures__/plugins/**/*", + ], "references": [ { "path": "../src/core/tsconfig.json" }, { "path": "../src/plugins/telemetry_management_section/tsconfig.json" }, @@ -52,5 +57,7 @@ { "path": "../src/plugins/visualize/tsconfig.json" }, { "path": "plugin_functional/plugins/core_app_status/tsconfig.json" }, { "path": "plugin_functional/plugins/core_provider_plugin/tsconfig.json" }, + { "path": "server_integration/__fixtures__/plugins/status_plugin_a/tsconfig.json" }, + { "path": "server_integration/__fixtures__/plugins/status_plugin_b/tsconfig.json" }, ] } diff --git a/test/visual_regression/tests/vega/vega_map_visualization.ts b/test/visual_regression/tests/vega/vega_map_visualization.ts index 96b08467e4a8f..d891e7f2bab6b 100644 --- a/test/visual_regression/tests/vega/vega_map_visualization.ts +++ b/test/visual_regression/tests/vega/vega_map_visualization.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['common', 'visualize', 'visChart', 'visEditor', 'vegaChart']); const visualTesting = getService('visualTesting'); @@ -18,12 +19,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.loadIfNeeded( 'test/functional/fixtures/es_archiver/kibana_sample_data_flights' ); - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/visualize'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/visualize.json'); }); after(async () => { await esArchiver.unload('test/functional/fixtures/es_archiver/kibana_sample_data_flights'); - await esArchiver.unload('test/functional/fixtures/es_archiver/visualize'); + await kibanaServer.importExport.unload( + 'test/functional/fixtures/kbn_archiver/visualize.json' + ); }); it('should show map with vega layer', async function () { diff --git a/tsconfig.json b/tsconfig.json index c91f7b768a5c4..f6df8fcbb6406 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -70,7 +70,6 @@ { "path": "./src/plugins/visualize/tsconfig.json" }, { "path": "./src/plugins/index_pattern_management/tsconfig.json" }, { "path": "./src/plugins/index_pattern_field_editor/tsconfig.json" }, - { "path": "./x-pack/plugins/actions/tsconfig.json" }, { "path": "./x-pack/plugins/alerting/tsconfig.json" }, { "path": "./x-pack/plugins/apm/tsconfig.json" }, diff --git a/tsconfig.refs.json b/tsconfig.refs.json index 3baf5c323ef81..e08b50cc055c1 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -105,6 +105,7 @@ { "path": "./x-pack/plugins/stack_alerts/tsconfig.json" }, { "path": "./x-pack/plugins/task_manager/tsconfig.json" }, { "path": "./x-pack/plugins/telemetry_collection_xpack/tsconfig.json" }, + { "path": "./x-pack/plugins/timelines/tsconfig.json" }, { "path": "./x-pack/plugins/transform/tsconfig.json" }, { "path": "./x-pack/plugins/translations/tsconfig.json" }, { "path": "./x-pack/plugins/triggers_actions_ui/tsconfig.json" }, diff --git a/x-pack/package.json b/x-pack/package.json index 1397a3da81072..1af3d569e41ab 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -26,7 +26,6 @@ "yarn": "^1.21.1" }, "devDependencies": { - "@kbn/plugin-helpers": "link:../packages/kbn-plugin-helpers", "@kbn/test": "link:../packages/kbn-test" } } \ No newline at end of file diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 5b4a197eea462..b19e89a599840 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -19,7 +19,7 @@ Table of Contents - [Usage](#usage) - [Kibana Actions Configuration](#kibana-actions-configuration) - [Configuration Options](#configuration-options) - - [Adding Built-in Action Types to allowedHosts](#adding-built-in-action-types-to-allowedhosts) + - [**allowedHosts** configuration](#allowedhosts-configuration) - [Configuration Utilities](#configuration-utilities) - [Action types](#action-types) - [Methods](#methods) @@ -54,6 +54,9 @@ Table of Contents - [`subActionParams (getFields)`](#subactionparams-getfields-2) - [`subActionParams (incidentTypes)`](#subactionparams-incidenttypes) - [`subActionParams (severity)`](#subactionparams-severity) + - [Swimlane](#swimlane) + - [`params`](#params-3) + - [| severity | The severity of the incident. | string _(optional)_ |](#-severity-----the-severity-of-the-incident-----string-optional-) - [Command Line Utility](#command-line-utility) - [Developing New Action Types](#developing-new-action-types) - [licensing](#licensing) @@ -102,8 +105,8 @@ This module provides utilities for interacting with the configuration. | ensureUriAllowed | _uri_: The URI you wish to validate is allowed | Validates whether the URI is allowed. This checks the configuration and validates that the hostname of the URI is in the list of allowed Hosts and throws an error if it is not allowed. If the configuration says that all URI's are allowed (using an "\*") then it will never throw. | No return value, throws if URI isn't allowed | | ensureHostnameAllowed | _hostname_: The Hostname you wish to validate is allowed | Validates whether the Hostname is allowed. This checks the configuration and validates that the hostname is in the list of allowed Hosts and throws an error if it is not allowed. If the configuration says that all Hostnames are allowed (using an "\*") then it will never throw | No return value, throws if Hostname isn't allowed . | | ensureActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Throws an error if the actionType is not enabled | No return value, throws if actionType isn't enabled | -| isRejectUnauthorizedCertificatesEnabled | _none_ | Returns value of `rejectUnauthorized` from configuration. | Boolean | -| getProxySettings | _none_ | If `proxyUrl` is set in the configuration, returns the proxy settings `proxyUrl`, `proxyHeaders` and `proxyRejectUnauthorizedCertificates`. Otherwise returns _undefined_. | Undefined or ProxySettings | +| isRejectUnauthorizedCertificatesEnabled | _none_ | Returns value of `rejectUnauthorized` from configuration. | Boolean | +| getProxySettings | _none_ | If `proxyUrl` is set in the configuration, returns the proxy settings `proxyUrl`, `proxyHeaders` and `proxyRejectUnauthorizedCertificates`. Otherwise returns _undefined_. | Undefined or ProxySettings | ## Action types @@ -113,17 +116,17 @@ This module provides utilities for interacting with the configuration. The following table describes the properties of the `options` object. -| Property | Description | Type | -| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | -| id | Unique identifier for the action type. For convention, ids starting with `.` are reserved for built in action types. We recommend using a convention like `<plugin_id>.mySpecialAction` for your action types. | string | -| name | A user-friendly name for the action type. These will be displayed in dropdowns when chosing action types. | string | -| maxAttempts | The maximum number of times this action will attempt to execute when scheduled. | number | -| minimumLicenseRequired | The license required to use the action type. | string | +| Property | Description | Type | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | +| id | Unique identifier for the action type. For convention, ids starting with `.` are reserved for built in action types. We recommend using a convention like `<plugin_id>.mySpecialAction` for your action types. | string | +| name | A user-friendly name for the action type. These will be displayed in dropdowns when chosing action types. | string | +| maxAttempts | The maximum number of times this action will attempt to execute when scheduled. | number | +| minimumLicenseRequired | The license required to use the action type. | string | | validate.params | When developing an action type, it needs to accept parameters to know what to do with the action. (Example `to`, `from`, `subject`, `body` of an email). See the current built-in email action type for an example of the state-of-the-art validation. <p>Technically, the value of this property should have a property named `validate()` which is a function that takes a params object to validate and returns a sanitized version of that object to pass to the execution function. Validation errors should be thrown from the `validate()` function and will be available as an error message | schema / validation function | -| validate.config | Similar to params, a config may be required when creating an action (for example `host` and `port` for an email server). | schema / validation function | -| validate.secrets | Similar to params, a secrets object may be required when creating an action (for example `user` and `password` for an email server). | schema / validation function | -| executor | This is where the code of an action type lives. This is a function gets called for executing an action from either alerting or manually by using the exposed function (see firing actions). For full details, see executor section below. | Function | -| renderParameterTemplates | Optionally define a function to provide custom rendering for this action type. | Function | +| validate.config | Similar to params, a config may be required when creating an action (for example `host` and `port` for an email server). | schema / validation function | +| validate.secrets | Similar to params, a secrets object may be required when creating an action (for example `user` and `password` for an email server). | schema / validation function | +| executor | This is where the code of an action type lives. This is a function gets called for executing an action from either alerting or manually by using the exposed function (see firing actions). For full details, see executor section below. | Function | +| renderParameterTemplates | Optionally define a function to provide custom rendering for this action type. | Function | **Important** - The config object is persisted in ElasticSearch and updated via the ElasticSearch update document API. This API allows "partial updates" - and this can cause issues with the encryption used on specified properties. So, a `validate()` function should return values for all configuration properties, so that partial updates do not occur. Setting property values to `null` rather than `undefined`, or not including a property in the config object, is all you need to do to ensure partial updates won't occur. @@ -133,15 +136,15 @@ This is the primary function for an action type. Whenever the action needs to ex **executor(options)** -| Property | Description | -| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| actionId | The action saved object id that the action type is executing for. | -| config | The action configuration. If you would like to validate the config before being passed to the executor, define `validate.config` within the action type. | -| secrets | The decrypted secrets object given to an action. This comes from the action saved object that is partially or fully encrypted within the data store. If you would like to validate the secrets object before being passed to the executor, define `validate.secrets` within the action type. | -| params | Parameters for the execution. These will be given at execution time by either an alert or manually provided when calling the plugin provided execute function. | -| services.scopedClusterClient | Use this to do Elasticsearch queries on the cluster Kibana connects to. Serves the same purpose as the normal IClusterClient, but exposes an additional `asCurrentUser` method that doesn't use credentials of the Kibana internal user (as `asInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead.| -| services.savedObjectsClient | This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.<br><br>The scope of the saved objects client is tied to the user in context calling the execute API or the API key provided to the execute plugin function (only when security isenabled). | -| services.log(tags, [data], [timestamp]) | Use this to create server logs. (This is the same function as server.log) +| Property | Description | +| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| actionId | The action saved object id that the action type is executing for. | +| config | The action configuration. If you would like to validate the config before being passed to the executor, define `validate.config` within the action type. | +| secrets | The decrypted secrets object given to an action. This comes from the action saved object that is partially or fully encrypted within the data store. If you would like to validate the secrets object before being passed to the executor, define `validate.secrets` within the action type. | +| params | Parameters for the execution. These will be given at execution time by either an alert or manually provided when calling the plugin provided execute function. | +| services.scopedClusterClient | Use this to do Elasticsearch queries on the cluster Kibana connects to. Serves the same purpose as the normal IClusterClient, but exposes an additional `asCurrentUser` method that doesn't use credentials of the Kibana internal user (as `asInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead. | +| services.savedObjectsClient | This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.<br><br>The scope of the saved objects client is tied to the user in context calling the execute API or the API key provided to the execute plugin function (only when security isenabled). | +| services.log(tags, [data], [timestamp]) | Use this to create server logs. (This is the same function as server.log) | ### Example @@ -262,16 +265,16 @@ The [ServiceNow user documentation `params`](https://www.elastic.co/guide/en/kib The following table describes the properties of the `incident` object. -| Property | Description | Type | -| ----------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------- | -| short_description | The title of the incident. | string | -| description | The description of the incident. | string _(optional)_ | +| Property | Description | Type | +| ----------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------- | +| short_description | The title of the incident. | string | +| description | The description of the incident. | string _(optional)_ | | externalId | The ID of the incident in ServiceNow. If present, the incident is updated. Otherwise, a new incident is created. | string _(optional)_ | -| severity | The severity in ServiceNow. | string _(optional)_ | -| urgency | The urgency in ServiceNow. | string _(optional)_ | -| impact | The impact in ServiceNow. | string _(optional)_ | -| category | The category in ServiceNow. | string _(optional)_ | -| subcategory | The subcategory in ServiceNow. | string _(optional)_ | +| severity | The severity in ServiceNow. | string _(optional)_ | +| urgency | The urgency in ServiceNow. | string _(optional)_ | +| impact | The impact in ServiceNow. | string _(optional)_ | +| category | The category in ServiceNow. | string _(optional)_ | +| subcategory | The subcategory in ServiceNow. | string _(optional)_ | #### `subActionParams (getFields)` @@ -311,20 +314,20 @@ The [Jira user documentation `params`](https://www.elastic.co/guide/en/kibana/ma The following table describes the properties of the `incident` object. -| Property | Description | Type | -| ----------- | ---------------------------------------------------------------------------------------------------------------- | --------------------- | -| summary | The title of the issue. | string | -| description | The description of the issue. | string _(optional)_ | +| Property | Description | Type | +| ----------- | ------------------------------------------------------------------------------------------------------- | --------------------- | +| summary | The title of the issue. | string | +| description | The description of the issue. | string _(optional)_ | | externalId | The ID of the issue in Jira. If present, the incident is updated. Otherwise, a new incident is created. | string _(optional)_ | -| issueType | The ID of the issue type in Jira. | string _(optional)_ | -| priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ | -| labels | An array of labels. Labels cannot contain spaces. | string[] _(optional)_ | -| parent | The ID or key of the parent issue. Only for `Sub-task` issue types. | string _(optional)_ | +| issueType | The ID of the issue type in Jira. | string _(optional)_ | +| priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ | +| labels | An array of labels. Labels cannot contain spaces. | string[] _(optional)_ | +| parent | The ID or key of the parent issue. Only for `Sub-task` issue types. | string _(optional)_ | #### `subActionParams (getIncident)` -| Property | Description | Type | -| ---------- | --------------------------- | ------ | +| Property | Description | Type | +| ---------- | ---------------------------- | ------ | | externalId | The ID of the issue in Jira. | string | #### `subActionParams (issueTypes)` @@ -333,20 +336,20 @@ No parameters for the `issueTypes` subaction. Provide an empty object `{}`. #### `subActionParams (fieldsByIssueType)` -| Property | Description | Type | -| -------- | -------------------------------- | ------ | +| Property | Description | Type | +| -------- | --------------------------------- | ------ | | id | The ID of the issue type in Jira. | string | #### `subActionParams (issues)` -| Property | Description | Type | -| -------- | ----------------------- | ------ | +| Property | Description | Type | +| -------- | ------------------------ | ------ | | title | The title to search for. | string | #### `subActionParams (issue)` -| Property | Description | Type | -| -------- | --------------------------- | ------ | +| Property | Description | Type | +| -------- | ---------------------------- | ------ | | id | The ID of the issue in Jira. | string | #### `subActionParams (getFields)` @@ -360,10 +363,10 @@ The [IBM Resilient user documentation `params`](https://www.elastic.co/guide/en/ ### `params` -| Property | Description | Type | -| --------------- | -------------------------------------------------------------------------------------------------- | ------ | +| Property | Description | Type | +| --------------- | ------------------------------------------------------------------------------------------------- | ------ | | subAction | The subaction to perform. It can be `pushToService`, `getFields`, `incidentTypes`, and `severity. | string | -| subActionParams | The parameters of the subaction. | object | +| subActionParams | The parameters of the subaction. | object | #### `subActionParams (pushToService)` @@ -374,13 +377,13 @@ The [IBM Resilient user documentation `params`](https://www.elastic.co/guide/en/ The following table describes the properties of the `incident` object. -| Property | Description | Type | -| ------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------- | -| name | The title of the incident. | string _(optional)_ | -| description | The description of the incident. | string _(optional)_ | +| Property | Description | Type | +| ------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------- | +| name | The title of the incident. | string _(optional)_ | +| description | The description of the incident. | string _(optional)_ | | externalId | The ID of the incident in IBM Resilient. If present, the incident is updated. Otherwise, a new incident is created. | string _(optional)_ | -| incidentTypes | An array with the IDs of IBM Resilient incident types. | number[] _(optional)_ | -| severityCode | IBM Resilient ID of the severity code. | number _(optional)_ | +| incidentTypes | An array with the IDs of IBM Resilient incident types. | number[] _(optional)_ | +| severityCode | IBM Resilient ID of the severity code. | number _(optional)_ | #### `subActionParams (getFields)` @@ -394,6 +397,36 @@ No parameters for the `incidentTypes` subaction. Provide an empty object `{}`. No parameters for the `severity` subaction. Provide an empty object `{}`. +--- +## Swimlane + + +### `params` + +| Property | Description | Type | +| --------------- | ---------------------------------------------------- | ------ | +| subAction | The subaction to perform. It can be `pushToService`. | string | +| subActionParams | The parameters of the subaction. | object | + + +`subActionParams (pushToService)` + +| Property | Description | Type | +| -------- | ------------------------------------------------------------------------------------------------------------- | --------------------- | +| incident | The Swimlane incident. | object | +| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ | + + +The following table describes the properties of the `incident` object. + +| Property | Description | Type | +| ----------- | -------------------------------- | ------------------- | +| alertId | The alert id. | string _(optional)_ | +| caseId | The case id of the incident. | string _(optional)_ | +| caseName | The case name of the incident. | string _(optional)_ | +| description | The description of the incident. | string _(optional)_ | +| ruleName | The rule name. | string _(optional)_ | +| severity | The severity of the incident. | string _(optional)_ | --- # Command Line Utility diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 16388b2faf52e..012cd1a58de7e 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -429,7 +429,7 @@ describe('create()', () => { idleInterval: schema.duration().validate('1h'), pageSize: 100, }, - tls: { + ssl: { verificationMode: 'full', proxyVerificationMode: 'full', }, diff --git a/x-pack/plugins/actions/server/actions_config.mock.ts b/x-pack/plugins/actions/server/actions_config.mock.ts index 19a43951377b6..36298d84acabc 100644 --- a/x-pack/plugins/actions/server/actions_config.mock.ts +++ b/x-pack/plugins/actions/server/actions_config.mock.ts @@ -15,7 +15,7 @@ const createActionsConfigMock = () => { ensureHostnameAllowed: jest.fn().mockReturnValue({}), ensureUriAllowed: jest.fn().mockReturnValue({}), ensureActionTypeEnabled: jest.fn().mockReturnValue({}), - getTLSSettings: jest.fn().mockReturnValue({ + getSSLSettings: jest.fn().mockReturnValue({ verificationMode: 'full', }), getProxySettings: jest.fn().mockReturnValue(undefined), diff --git a/x-pack/plugins/actions/server/actions_config.test.ts b/x-pack/plugins/actions/server/actions_config.test.ts index 93dad226e0c99..51cd9e5599472 100644 --- a/x-pack/plugins/actions/server/actions_config.test.ts +++ b/x-pack/plugins/actions/server/actions_config.test.ts @@ -37,7 +37,7 @@ const defaultActionsConfig: ActionsConfig = { idleInterval: schema.duration().validate('1h'), pageSize: 100, }, - tls: { + ssl: { proxyVerificationMode: 'full', verificationMode: 'full', }, @@ -316,38 +316,38 @@ describe('getProxySettings', () => { proxyRejectUnauthorizedCertificates: true, }; let proxySettings = getActionsConfigurationUtilities(configTrue).getProxySettings(); - expect(proxySettings?.proxyTLSSettings.verificationMode).toBe('full'); + expect(proxySettings?.proxySSLSettings.verificationMode).toBe('full'); const configFalse: ActionsConfig = { ...defaultActionsConfig, proxyUrl: 'https://proxy.elastic.co', proxyRejectUnauthorizedCertificates: false, - tls: {}, + ssl: {}, }; proxySettings = getActionsConfigurationUtilities(configFalse).getProxySettings(); - expect(proxySettings?.proxyTLSSettings.verificationMode).toBe('none'); + expect(proxySettings?.proxySSLSettings.verificationMode).toBe('none'); }); - test('returns proper verificationMode value, based on the TLS proxy configuration', () => { + test('returns proper verificationMode value, based on the SSL proxy configuration', () => { const configTrue: ActionsConfig = { ...defaultActionsConfig, proxyUrl: 'https://proxy.elastic.co', - tls: { + ssl: { proxyVerificationMode: 'full', }, }; let proxySettings = getActionsConfigurationUtilities(configTrue).getProxySettings(); - expect(proxySettings?.proxyTLSSettings.verificationMode).toBe('full'); + expect(proxySettings?.proxySSLSettings.verificationMode).toBe('full'); const configFalse: ActionsConfig = { ...defaultActionsConfig, proxyUrl: 'https://proxy.elastic.co', - tls: { + ssl: { proxyVerificationMode: 'none', }, }; proxySettings = getActionsConfigurationUtilities(configFalse).getProxySettings(); - expect(proxySettings?.proxyTLSSettings.verificationMode).toBe('none'); + expect(proxySettings?.proxySSLSettings.verificationMode).toBe('none'); }); test('returns proxy headers', () => { @@ -432,13 +432,13 @@ describe('getProxySettings', () => { customHostSettings: [ { url: 'https://elastic.co', - tls: { + ssl: { verificationMode: 'full', }, }, { url: 'smtp://elastic.co:123', - tls: { + ssl: { verificationMode: 'none', }, smtp: { @@ -465,24 +465,24 @@ describe('getProxySettings', () => { }); }); -describe('getTLSSettings', () => { - test('returns proper verificationMode value, based on the TLS proxy configuration', () => { +describe('getSSLSettings', () => { + test('returns proper verificationMode value, based on the SSL proxy configuration', () => { const configTrue: ActionsConfig = { ...defaultActionsConfig, - tls: { + ssl: { verificationMode: 'full', }, }; - let tlsSettings = getActionsConfigurationUtilities(configTrue).getTLSSettings(); - expect(tlsSettings.verificationMode).toBe('full'); + let sslSettings = getActionsConfigurationUtilities(configTrue).getSSLSettings(); + expect(sslSettings.verificationMode).toBe('full'); const configFalse: ActionsConfig = { ...defaultActionsConfig, - tls: { + ssl: { verificationMode: 'none', }, }; - tlsSettings = getActionsConfigurationUtilities(configFalse).getTLSSettings(); - expect(tlsSettings.verificationMode).toBe('none'); + sslSettings = getActionsConfigurationUtilities(configFalse).getSSLSettings(); + expect(sslSettings.verificationMode).toBe('none'); }); }); diff --git a/x-pack/plugins/actions/server/actions_config.ts b/x-pack/plugins/actions/server/actions_config.ts index d25101f8279f8..9ce9439b726d4 100644 --- a/x-pack/plugins/actions/server/actions_config.ts +++ b/x-pack/plugins/actions/server/actions_config.ts @@ -14,8 +14,8 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { ActionsConfig, AllowedHosts, EnabledActionTypes, CustomHostSettings } from './config'; import { getCanonicalCustomHostUrl } from './lib/custom_host_settings'; import { ActionTypeDisabledError } from './lib'; -import { ProxySettings, ResponseSettings, TLSSettings } from './types'; -import { getTLSSettingsFromConfig } from './builtin_action_types/lib/get_node_tls_options'; +import { ProxySettings, ResponseSettings, SSLSettings } from './types'; +import { getSSLSettingsFromConfig } from './builtin_action_types/lib/get_node_ssl_options'; export { AllowedHosts, EnabledActionTypes } from './config'; @@ -31,7 +31,7 @@ export interface ActionsConfigurationUtilities { ensureHostnameAllowed: (hostname: string) => void; ensureUriAllowed: (uri: string) => void; ensureActionTypeEnabled: (actionType: string) => void; - getTLSSettings: () => TLSSettings; + getSSLSettings: () => SSLSettings; getProxySettings: () => undefined | ProxySettings; getResponseSettings: () => ResponseSettings; getCustomHostSettings: (targetUrl: string) => CustomHostSettings | undefined; @@ -94,8 +94,8 @@ function getProxySettingsFromConfig(config: ActionsConfig): undefined | ProxySet proxyBypassHosts: arrayAsSet(config.proxyBypassHosts), proxyOnlyHosts: arrayAsSet(config.proxyOnlyHosts), proxyHeaders: config.proxyHeaders, - proxyTLSSettings: getTLSSettingsFromConfig( - config.tls?.proxyVerificationMode, + proxySSLSettings: getSSLSettingsFromConfig( + config.ssl?.proxyVerificationMode, config.proxyRejectUnauthorizedCertificates ), }; @@ -146,8 +146,8 @@ export function getActionsConfigurationUtilities( isActionTypeEnabled, getProxySettings: () => getProxySettingsFromConfig(config), getResponseSettings: () => getResponseSettingsFromConfig(config), - getTLSSettings: () => - getTLSSettingsFromConfig(config.tls?.verificationMode, config.rejectUnauthorized), + getSSLSettings: () => + getSSLSettingsFromConfig(config.ssl?.verificationMode, config.rejectUnauthorized), ensureUriAllowed(uri: string) { if (!isUriAllowed(uri)) { throw new Error(allowListErrorMessage(AllowListingField.URL, uri)); diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts index 98ea436b17f3e..8e9ea1c5e4aa9 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts @@ -285,7 +285,7 @@ describe('execute()', () => { "getCustomHostSettings": [MockFunction], "getProxySettings": [MockFunction], "getResponseSettings": [MockFunction], - "getTLSSettings": [MockFunction], + "getSSLSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], "isUriAllowed": [MockFunction], @@ -346,7 +346,7 @@ describe('execute()', () => { "getCustomHostSettings": [MockFunction], "getProxySettings": [MockFunction], "getResponseSettings": [MockFunction], - "getTLSSettings": [MockFunction], + "getSSLSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], "isUriAllowed": [MockFunction], diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts index 10955af2f3b13..5feb47ea6c962 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts @@ -21,6 +21,7 @@ const ACTION_TYPE_IDS = [ '.pagerduty', '.server-log', '.slack', + '.swimlane', '.teams', '.webhook', ]; diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.ts b/x-pack/plugins/actions/server/builtin_action_types/index.ts index 551d3d02ff05d..07859cba4c371 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.ts @@ -12,6 +12,7 @@ import { Logger } from '../../../../../src/core/server'; import { getActionType as getEmailActionType } from './email'; import { getActionType as getIndexActionType } from './es_index'; import { getActionType as getPagerDutyActionType } from './pagerduty'; +import { getActionType as getSwimlaneActionType } from './swimlane'; import { getActionType as getServerLogActionType } from './server_log'; import { getActionType as getSlackActionType } from './slack'; import { getActionType as getWebhookActionType } from './webhook'; @@ -65,6 +66,7 @@ export function registerBuiltInActionTypes({ ); actionTypeRegistry.register(getIndexActionType({ logger })); actionTypeRegistry.register(getPagerDutyActionType({ logger, configurationUtilities })); + actionTypeRegistry.register(getSwimlaneActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getServerLogActionType({ logger })); actionTypeRegistry.register(getSlackActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getWebhookActionType({ logger, configurationUtilities })); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts index 3161e97583b72..aa439787ad96f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts @@ -25,7 +25,7 @@ import { JiraSecretConfigurationType, JiraExecutorResultData, ExecutorSubActionGetFieldsByIssueTypeParams, - ExecutorSubActionGetIssueTypesParams, + ExecutorSubActionCommonFieldsParams, ExecutorSubActionGetIssuesParams, ExecutorSubActionGetIssueParams, ExecutorSubActionGetIncidentParams, @@ -137,7 +137,7 @@ async function executor( } if (subAction === 'issueTypes') { - const getIssueTypesParams = subActionParams as ExecutorSubActionGetIssueTypesParams; + const getIssueTypesParams = subActionParams as ExecutorSubActionCommonFieldsParams; data = await api.issueTypes({ externalService, params: getIssueTypesParams, diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts index a81dfaeef8175..eb2f540deaa9a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts @@ -25,14 +25,6 @@ export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( ExternalIncidentServiceSecretConfiguration ); -export const ExecutorSubActionSchema = schema.oneOf([ - schema.literal('getIncident'), - schema.literal('pushToService'), - schema.literal('handshake'), - schema.literal('issueTypes'), - schema.literal('fieldsByIssueType'), -]); - export const ExecutorSubActionPushParamsSchema = schema.object({ incident: schema.object({ summary: schema.string(), diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts index f6462bac9d83e..9430d734287d3 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts @@ -155,12 +155,12 @@ describe('Jira service', () => { ).toThrow(); }); - test('throws without username', () => { + test('throws without email/username', () => { expect(() => createExternalService( { - config: { apiUrl: 'test.com' }, - secrets: { apiToken: '', email: 'elastic@elastic.com' }, + config: { apiUrl: 'test.com', projectKey: 'CK' }, + secrets: { apiToken: 'token' }, }, logger, configurationUtilities @@ -168,12 +168,12 @@ describe('Jira service', () => { ).toThrow(); }); - test('throws without password', () => { + test('throws without apiToken/password', () => { expect(() => createExternalService( { - config: { apiUrl: 'test.com' }, - secrets: { apiToken: '', email: undefined }, + config: { apiUrl: 'test.com', projectKey: 'CK' }, + secrets: { email: 'elastic@elastic.com' }, }, logger, configurationUtilities diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts index 89a5551554c4a..74d53901d55d9 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts @@ -16,10 +16,10 @@ import { ExecutorSubActionGetIncidentParamsSchema, ExecutorSubActionHandshakeParamsSchema, ExecutorSubActionGetCapabilitiesParamsSchema, - ExecutorSubActionGetIssueTypesParamsSchema, ExecutorSubActionGetFieldsByIssueTypeParamsSchema, ExecutorSubActionGetIssuesParamsSchema, ExecutorSubActionGetIssueParamsSchema, + ExecutorSubActionCommonFieldsParamsSchema, } from './schema'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { Logger } from '../../../../../../src/core/server'; @@ -124,8 +124,8 @@ export type ExecutorSubActionGetCapabilitiesParams = TypeOf< typeof ExecutorSubActionGetCapabilitiesParamsSchema >; -export type ExecutorSubActionGetIssueTypesParams = TypeOf< - typeof ExecutorSubActionGetIssueTypesParamsSchema +export type ExecutorSubActionCommonFieldsParams = TypeOf< + typeof ExecutorSubActionCommonFieldsParamsSchema >; export type ExecutorSubActionGetFieldsByIssueTypeParams = TypeOf< @@ -157,12 +157,12 @@ export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs { export interface GetIssueTypesHandlerArgs { externalService: ExternalService; - params: ExecutorSubActionGetIssueTypesParams; + params: ExecutorSubActionCommonFieldsParams; } export interface GetCommonFieldsHandlerArgs { externalService: ExternalService; - params: ExecutorSubActionGetIssueTypesParams; + params: ExecutorSubActionCommonFieldsParams; } export interface GetFieldsByIssueTypeHandlerArgs { diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts index ccd5a044971df..292471aaf9b6d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts @@ -75,7 +75,7 @@ describe('request', () => { test('it have been called with proper proxy agent for a valid url', async () => { configurationUtilities.getProxySettings.mockReturnValue({ - proxyTLSSettings: { + proxySSLSettings: { verificationMode: 'full', }, proxyUrl: 'https://localhost:1212', @@ -110,7 +110,7 @@ describe('request', () => { test('it have been called with proper proxy agent for an invalid url', async () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: ':nope:', - proxyTLSSettings: { + proxySSLSettings: { verificationMode: 'none', }, proxyBypassHosts: undefined, @@ -141,7 +141,7 @@ describe('request', () => { test('it bypasses with proxyBypassHosts when expected', async () => { configurationUtilities.getProxySettings.mockReturnValue({ - proxyTLSSettings: { + proxySSLSettings: { verificationMode: 'full', }, proxyUrl: 'https://elastic.proxy.co', @@ -164,7 +164,7 @@ describe('request', () => { test('it does not bypass with proxyBypassHosts when expected', async () => { configurationUtilities.getProxySettings.mockReturnValue({ - proxyTLSSettings: { + proxySSLSettings: { verificationMode: 'full', }, proxyUrl: 'https://elastic.proxy.co', @@ -187,7 +187,7 @@ describe('request', () => { test('it proxies with proxyOnlyHosts when expected', async () => { configurationUtilities.getProxySettings.mockReturnValue({ - proxyTLSSettings: { + proxySSLSettings: { verificationMode: 'full', }, proxyUrl: 'https://elastic.proxy.co', @@ -210,7 +210,7 @@ describe('request', () => { test('it does not proxy with proxyOnlyHosts when expected', async () => { configurationUtilities.getProxySettings.mockReturnValue({ - proxyTLSSettings: { + proxySSLSettings: { verificationMode: 'full', }, proxyUrl: 'https://elastic.proxy.co', diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils_connection.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils_connection.test.ts index 235fca005e225..4ed9485e923a7 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils_connection.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils_connection.test.ts @@ -86,7 +86,7 @@ describe('axios connections', () => { testServer = server; const configurationUtilities = getACUfromConfig({ - tls: { + ssl: { verificationMode: 'none', }, }); @@ -99,7 +99,7 @@ describe('axios connections', () => { testServer = server; const configurationUtilities = getACUfromConfig({ - customHostSettings: [{ url, tls: { verificationMode: 'none' } }], + customHostSettings: [{ url, ssl: { verificationMode: 'none' } }], }); const res = await request({ axios, url, logger, configurationUtilities }); expect(res.status).toBe(200); @@ -110,7 +110,7 @@ describe('axios connections', () => { testServer = server; const configurationUtilities = getACUfromConfig({ - customHostSettings: [{ url, tls: { certificateAuthoritiesData: CA } }], + customHostSettings: [{ url, ssl: { certificateAuthoritiesData: CA } }], }); const res = await request({ axios, url, logger, configurationUtilities }); expect(res.status).toBe(200); @@ -121,7 +121,7 @@ describe('axios connections', () => { testServer = server; const configurationUtilities = getACUfromConfig({ - customHostSettings: [{ url, tls: { certificateAuthoritiesData: KIBANA_CRT } }], + customHostSettings: [{ url, ssl: { certificateAuthoritiesData: KIBANA_CRT } }], }); const fn = async () => await request({ axios, url, logger, configurationUtilities }); await expect(fn()).rejects.toThrow('certificate'); @@ -135,7 +135,7 @@ describe('axios connections', () => { customHostSettings: [ { url, - tls: { + ssl: { certificateAuthoritiesData: CA, verificationMode: 'none', }, @@ -151,13 +151,13 @@ describe('axios connections', () => { testServer = server; const configurationUtilities = getACUfromConfig({ - tls: { + ssl: { verificationMode: 'none', }, customHostSettings: [ { url, - tls: { + ssl: { certificateAuthoritiesData: CA, }, }, @@ -173,7 +173,7 @@ describe('axios connections', () => { testServer = server; const configurationUtilities = getACUfromConfig({ - customHostSettings: [{ url: otherUrl, tls: { verificationMode: 'none' } }], + customHostSettings: [{ url: otherUrl, ssl: { verificationMode: 'none' } }], }); const fn = async () => await request({ axios, url, logger, configurationUtilities }); await expect(fn()).rejects.toThrow('certificate'); @@ -184,7 +184,7 @@ describe('axios connections', () => { testServer = server; const configurationUtilities = getACUfromConfig({ - customHostSettings: [{ url, tls: { certificateAuthoritiesData: 'garbage' } }], + customHostSettings: [{ url, ssl: { certificateAuthoritiesData: 'garbage' } }], }); const fn = async () => await request({ axios, url, logger, configurationUtilities }); await expect(fn()).rejects.toThrow('certificate'); @@ -196,7 +196,7 @@ describe('axios connections', () => { const ca = '-----BEGIN CERTIFICATE-----\ngarbage\n-----END CERTIFICATE-----\n'; const configurationUtilities = getACUfromConfig({ - customHostSettings: [{ url, tls: { certificateAuthoritiesData: ca } }], + customHostSettings: [{ url, ssl: { certificateAuthoritiesData: ca } }], }); const fn = async () => await request({ axios, url, logger, configurationUtilities }); await expect(fn()).rejects.toThrow('certificate'); @@ -255,7 +255,7 @@ const BaseActionsConfig: ActionsConfig = { proxyUrl: undefined, proxyHeaders: undefined, proxyRejectUnauthorizedCertificates: true, - tls: { + ssl: { proxyVerificationMode: 'full', verificationMode: 'full', }, diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts index 8b4abe86e271a..0c1112da5909f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts @@ -30,7 +30,7 @@ describe('getCustomAgents', () => { test('get agents for valid proxy URL', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyTLSSettings: { + proxySSLSettings: { verificationMode: 'none', }, proxyBypassHosts: undefined, @@ -44,7 +44,7 @@ describe('getCustomAgents', () => { test('return default agents for invalid proxy URL', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: ':nope: not a valid URL', - proxyTLSSettings: { + proxySSLSettings: { verificationMode: 'none', }, proxyBypassHosts: undefined, @@ -64,7 +64,7 @@ describe('getCustomAgents', () => { test('returns non-proxy agents for matching proxyBypassHosts', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyTLSSettings: { + proxySSLSettings: { verificationMode: 'none', }, proxyBypassHosts: new Set([targetHost]), @@ -78,7 +78,7 @@ describe('getCustomAgents', () => { test('returns proxy agents for non-matching proxyBypassHosts', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyTLSSettings: { + proxySSLSettings: { verificationMode: 'none', }, proxyBypassHosts: new Set([targetHost]), @@ -96,7 +96,7 @@ describe('getCustomAgents', () => { test('returns proxy agents for matching proxyOnlyHosts', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyTLSSettings: { + proxySSLSettings: { verificationMode: 'none', }, proxyBypassHosts: undefined, @@ -110,7 +110,7 @@ describe('getCustomAgents', () => { test('returns non-proxy agents for non-matching proxyOnlyHosts', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyTLSSettings: { + proxySSLSettings: { verificationMode: 'none', }, proxyBypassHosts: undefined, @@ -128,7 +128,7 @@ describe('getCustomAgents', () => { test('handles custom host settings', () => { configurationUtilities.getCustomHostSettings.mockReturnValue({ url: targetUrlCanonical, - tls: { + ssl: { verificationMode: 'none', certificateAuthoritiesData: 'ca data here', }, @@ -141,7 +141,7 @@ describe('getCustomAgents', () => { test('handles custom host settings with proxy', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyTLSSettings: { + proxySSLSettings: { verificationMode: 'none', }, proxyBypassHosts: undefined, @@ -149,7 +149,7 @@ describe('getCustomAgents', () => { }); configurationUtilities.getCustomHostSettings.mockReturnValue({ url: targetUrlCanonical, - tls: { + ssl: { verificationMode: 'none', certificateAuthoritiesData: 'ca data here', }, @@ -163,12 +163,12 @@ describe('getCustomAgents', () => { }); test('handles overriding global verificationMode "none"', () => { - configurationUtilities.getTLSSettings.mockReturnValue({ + configurationUtilities.getSSLSettings.mockReturnValue({ verificationMode: 'none', }); configurationUtilities.getCustomHostSettings.mockReturnValue({ url: targetUrlCanonical, - tls: { + ssl: { verificationMode: 'certificate', }, }); @@ -181,12 +181,12 @@ describe('getCustomAgents', () => { }); test('handles overriding global verificationMode "full"', () => { - configurationUtilities.getTLSSettings.mockReturnValue({ + configurationUtilities.getSSLSettings.mockReturnValue({ verificationMode: 'full', }); configurationUtilities.getCustomHostSettings.mockReturnValue({ url: targetUrlCanonical, - tls: { + ssl: { verificationMode: 'none', }, }); @@ -199,12 +199,12 @@ describe('getCustomAgents', () => { }); test('handles overriding global verificationMode "none" with a proxy', () => { - configurationUtilities.getTLSSettings.mockReturnValue({ + configurationUtilities.getSSLSettings.mockReturnValue({ verificationMode: 'none', }); configurationUtilities.getCustomHostSettings.mockReturnValue({ url: targetUrlCanonical, - tls: { + ssl: { verificationMode: 'full', }, }); @@ -212,7 +212,7 @@ describe('getCustomAgents', () => { proxyUrl: 'https://someproxyhost', // note: this setting doesn't come into play, it's for the connection to // the proxy, not the target url - proxyTLSSettings: { + proxySSLSettings: { verificationMode: 'none', }, proxyBypassHosts: undefined, @@ -226,12 +226,12 @@ describe('getCustomAgents', () => { }); test('handles overriding global verificationMode "full" with a proxy', () => { - configurationUtilities.getTLSSettings.mockReturnValue({ + configurationUtilities.getSSLSettings.mockReturnValue({ verificationMode: 'full', }); configurationUtilities.getCustomHostSettings.mockReturnValue({ url: targetUrlCanonical, - tls: { + ssl: { verificationMode: 'none', }, }); @@ -239,7 +239,7 @@ describe('getCustomAgents', () => { proxyUrl: 'https://someproxyhost', // note: this setting doesn't come into play, it's for the connection to // the proxy, not the target url - proxyTLSSettings: { + proxySSLSettings: { verificationMode: 'none', }, proxyBypassHosts: undefined, diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts index a327ee3ffe931..83d31ae1355d3 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts @@ -11,7 +11,7 @@ import HttpProxyAgent from 'http-proxy-agent'; import { HttpsProxyAgent } from 'https-proxy-agent'; import { Logger } from '../../../../../../src/core/server'; import { ActionsConfigurationUtilities } from '../../actions_config'; -import { getNodeTLSOptions, getTLSSettingsFromConfig } from './get_node_tls_options'; +import { getNodeSSLOptions, getSSLSettingsFromConfig } from './get_node_ssl_options'; interface GetCustomAgentsResponse { httpAgent: HttpAgent | undefined; @@ -23,14 +23,14 @@ export function getCustomAgents( logger: Logger, url: string ): GetCustomAgentsResponse { - const generalTLSSettings = configurationUtilities.getTLSSettings(); - const agentTLSOptions = getNodeTLSOptions(logger, generalTLSSettings.verificationMode); + const generalSSLSettings = configurationUtilities.getSSLSettings(); + const agentSSLOptions = getNodeSSLOptions(logger, generalSSLSettings.verificationMode); // the default for rejectUnauthorized is the global setting, which can // be overridden (below) with a custom host setting const defaultAgents = { httpAgent: undefined, httpsAgent: new HttpsAgent({ - ...agentTLSOptions, + ...agentSSLOptions, }), }; @@ -43,28 +43,28 @@ export function getCustomAgents( } // update the defaultAgents.httpsAgent if configured - const tlsSettings = customHostSettings?.tls; + const sslSettings = customHostSettings?.ssl; let agentOptions: AgentOptions | undefined; - if (tlsSettings) { + if (sslSettings) { logger.debug(`Creating customized connection settings for: ${url}`); agentOptions = defaultAgents.httpsAgent.options; - if (tlsSettings.certificateAuthoritiesData) { - agentOptions.ca = tlsSettings.certificateAuthoritiesData; + if (sslSettings.certificateAuthoritiesData) { + agentOptions.ca = sslSettings.certificateAuthoritiesData; } - const tlsSettingsFromConfig = getTLSSettingsFromConfig( - tlsSettings.verificationMode, - tlsSettings.rejectUnauthorized + const sslSettingsFromConfig = getSSLSettingsFromConfig( + sslSettings.verificationMode, + sslSettings.rejectUnauthorized ); // see: src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts // This is where the global rejectUnauthorized is overridden by a custom host - const customHostNodeTLSOptions = getNodeTLSOptions( + const customHostNodeSSLOptions = getNodeSSLOptions( logger, - tlsSettingsFromConfig.verificationMode + sslSettingsFromConfig.verificationMode ); - if (customHostNodeTLSOptions.rejectUnauthorized !== undefined) { - agentOptions.rejectUnauthorized = customHostNodeTLSOptions.rejectUnauthorized; + if (customHostNodeSSLOptions.rejectUnauthorized !== undefined) { + agentOptions.rejectUnauthorized = customHostNodeSSLOptions.rejectUnauthorized; } } @@ -107,12 +107,12 @@ export function getCustomAgents( return defaultAgents; } - const proxyNodeTLSOptions = getNodeTLSOptions( + const proxyNodeSSLOptions = getNodeSSLOptions( logger, - proxySettings.proxyTLSSettings.verificationMode + proxySettings.proxySSLSettings.verificationMode ); // At this point, we are going to use a proxy, so we need new agents. - // We will though, copy over the calculated tls options from above, into + // We will though, copy over the calculated ssl options from above, into // the https agent. const httpAgent = new HttpProxyAgent(proxySettings.proxyUrl); const httpsAgent = (new HttpsProxyAgent({ @@ -121,7 +121,7 @@ export function getCustomAgents( protocol: proxyUrl.protocol, headers: proxySettings.proxyHeaders, // do not fail on invalid certs if value is false - ...proxyNodeTLSOptions, + ...proxyNodeSSLOptions, }) as unknown) as HttpsAgent; // vsCode wasn't convinced HttpsProxyAgent is an https.Agent, so we convinced it diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_ssl_options.test.ts similarity index 67% rename from x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.test.ts rename to x-pack/plugins/actions/server/builtin_action_types/lib/get_node_ssl_options.test.ts index 7d131985053f1..893191b2ca2b4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_ssl_options.test.ts @@ -4,35 +4,35 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { getNodeTLSOptions, getTLSSettingsFromConfig } from './get_node_tls_options'; +import { getNodeSSLOptions, getSSLSettingsFromConfig } from './get_node_ssl_options'; import { Logger } from '../../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>; -describe('getNodeTLSOptions', () => { - test('get node.js TLS options: rejectUnauthorized eql true for the verification mode "full"', () => { - const nodeOption = getNodeTLSOptions(logger, 'full'); +describe('getNodeSSLOptions', () => { + test('get node.js SSL options: rejectUnauthorized eql true for the verification mode "full"', () => { + const nodeOption = getNodeSSLOptions(logger, 'full'); expect(nodeOption).toMatchObject({ rejectUnauthorized: true, }); }); - test('get node.js TLS options: rejectUnauthorized eql true for the verification mode "certificate"', () => { - const nodeOption = getNodeTLSOptions(logger, 'certificate'); + test('get node.js SSL options: rejectUnauthorized eql true for the verification mode "certificate"', () => { + const nodeOption = getNodeSSLOptions(logger, 'certificate'); expect(nodeOption.checkServerIdentity).not.toBeNull(); expect(nodeOption.rejectUnauthorized).toBeTruthy(); }); - test('get node.js TLS options: rejectUnauthorized eql false for the verification mode "none"', () => { - const nodeOption = getNodeTLSOptions(logger, 'none'); + test('get node.js SSL options: rejectUnauthorized eql false for the verification mode "none"', () => { + const nodeOption = getNodeSSLOptions(logger, 'none'); expect(nodeOption).toMatchObject({ rejectUnauthorized: false, }); }); - test('get node.js TLS options: rejectUnauthorized eql true for the verification mode value which does not exist, the logger called with the proper warning message', () => { - const nodeOption = getNodeTLSOptions(logger, 'notexist'); + test('get node.js SSL options: rejectUnauthorized eql true for the verification mode value which does not exist, the logger called with the proper warning message', () => { + const nodeOption = getNodeSSLOptions(logger, 'notexist'); expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(` Array [ Array [ @@ -46,23 +46,23 @@ describe('getNodeTLSOptions', () => { }); }); -describe('getTLSSettingsFromConfig', () => { +describe('getSSLSettingsFromConfig', () => { test('get verificationMode eql "none" if legacy rejectUnauthorized eql false', () => { - const nodeOption = getTLSSettingsFromConfig(undefined, false); + const nodeOption = getSSLSettingsFromConfig(undefined, false); expect(nodeOption).toMatchObject({ verificationMode: 'none', }); }); test('get verificationMode eql "none" if legacy rejectUnauthorized eql true', () => { - const nodeOption = getTLSSettingsFromConfig(undefined, true); + const nodeOption = getSSLSettingsFromConfig(undefined, true); expect(nodeOption).toMatchObject({ verificationMode: 'full', }); }); test('get verificationMode eql "certificate", ignore rejectUnauthorized', () => { - const nodeOption = getTLSSettingsFromConfig('certificate', false); + const nodeOption = getSSLSettingsFromConfig('certificate', false); expect(nodeOption).toMatchObject({ verificationMode: 'certificate', }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_ssl_options.ts similarity index 92% rename from x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.ts rename to x-pack/plugins/actions/server/builtin_action_types/lib/get_node_ssl_options.ts index 423e9756b13f8..46e90ec3be697 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_ssl_options.ts @@ -6,10 +6,10 @@ */ import { PeerCertificate } from 'tls'; -import { TLSSettings } from '../../types'; +import { SSLSettings } from '../../types'; import { Logger } from '../../../../../../src/core/server'; -export function getNodeTLSOptions( +export function getNodeSSLOptions( logger: Logger, verificationMode?: string ): { @@ -44,10 +44,10 @@ export function getNodeTLSOptions( return agentOptions; } -export function getTLSSettingsFromConfig( +export function getSSLSettingsFromConfig( verificationMode?: 'none' | 'certificate' | 'full', rejectUnauthorized?: boolean -): TLSSettings { +): SSLSettings { if (verificationMode) { return { verificationMode }; } else if (rejectUnauthorized !== undefined) { diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts index 9bdb2d9481142..3719dd8cd737c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts @@ -76,7 +76,7 @@ describe('send_email module', () => { }, { proxyUrl: 'https://example.com', - proxyTLSSettings: { + proxySSLSettings: { verificationMode: 'none', }, proxyBypassHosts: undefined, @@ -238,7 +238,7 @@ describe('send_email module', () => { }, { proxyUrl: 'https://proxy.com', - proxyTLSSettings: { + proxySSLSettings: { verificationMode: 'none', }, proxyBypassHosts: new Set(['example.com']), @@ -272,7 +272,7 @@ describe('send_email module', () => { }, { proxyUrl: 'https://proxy.com', - proxyTLSSettings: { + proxySSLSettings: { verificationMode: 'none', }, proxyBypassHosts: new Set(['not-example.com']), @@ -308,7 +308,7 @@ describe('send_email module', () => { }, { proxyUrl: 'https://proxy.com', - proxyTLSSettings: { + proxySSLSettings: { verificationMode: 'none', }, proxyBypassHosts: undefined, @@ -344,7 +344,7 @@ describe('send_email module', () => { }, { proxyUrl: 'https://proxy.com', - proxyTLSSettings: {}, + proxySSLSettings: {}, proxyBypassHosts: undefined, proxyOnlyHosts: new Set(['not-example.com']), } @@ -377,7 +377,7 @@ describe('send_email module', () => { undefined, { url: 'smtp://example.com:1025', - tls: { + ssl: { certificateAuthoritiesData: 'ca cert data goes here', }, smtp: { @@ -419,7 +419,7 @@ describe('send_email module', () => { undefined, { url: 'smtp://example.com:1025', - tls: { + ssl: { certificateAuthoritiesData: 'ca cert data goes here', rejectUnauthorized: true, }, @@ -461,13 +461,13 @@ describe('send_email module', () => { }, { proxyUrl: 'https://proxy.com', - proxyTLSSettings: {}, + proxySSLSettings: {}, proxyBypassHosts: undefined, proxyOnlyHosts: undefined, }, { url: 'smtp://example.com:1025', - tls: { + ssl: { certificateAuthoritiesData: 'ca cert data goes here', rejectUnauthorized: true, }, diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts index 9f601840bc982..b32ea7d74f025 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts @@ -12,7 +12,7 @@ import { default as MarkdownIt } from 'markdown-it'; import { Logger } from '../../../../../../src/core/server'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { CustomHostSettings } from '../../config'; -import { getNodeTLSOptions, getTLSSettingsFromConfig } from './get_node_tls_options'; +import { getNodeSSLOptions, getSSLSettingsFromConfig } from './get_node_ssl_options'; // an email "service" which doesn't actually send, just returns what it would send export const JSON_TRANSPORT_SERVICE = '__json'; @@ -59,7 +59,7 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom // eslint-disable-next-line @typescript-eslint/no-explicit-any const transportConfig: Record<string, any> = {}; const proxySettings = configurationUtilities.getProxySettings(); - const generalTLSSettings = configurationUtilities.getTLSSettings(); + const generalSSLSettings = configurationUtilities.getSSLSettings(); if (hasAuth && user != null && password != null) { transportConfig.auth = { @@ -92,9 +92,9 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom customHostSettings = configurationUtilities.getCustomHostSettings(`smtp://${host}:${port}`); if (proxySettings && useProxy) { - transportConfig.tls = getNodeTLSOptions( + transportConfig.tls = getNodeSSLOptions( logger, - proxySettings?.proxyTLSSettings.verificationMode + proxySettings?.proxySSLSettings.verificationMode ); transportConfig.proxy = proxySettings.proxyUrl; transportConfig.headers = proxySettings.proxyHeaders; @@ -104,25 +104,25 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom // authenticate rarely have valid certs; eg cloud proxy, and npm maildev transportConfig.tls = { rejectUnauthorized: false }; } else { - transportConfig.tls = getNodeTLSOptions(logger, generalTLSSettings.verificationMode); + transportConfig.tls = getNodeSSLOptions(logger, generalSSLSettings.verificationMode); } // finally, allow customHostSettings to override some of the settings // see: https://nodemailer.com/smtp/ if (customHostSettings) { const tlsConfig: Record<string, unknown> = {}; - const tlsSettings = customHostSettings.tls; + const sslSettings = customHostSettings.ssl; const smtpSettings = customHostSettings.smtp; - if (tlsSettings?.certificateAuthoritiesData) { - tlsConfig.ca = tlsSettings?.certificateAuthoritiesData; + if (sslSettings?.certificateAuthoritiesData) { + tlsConfig.ca = sslSettings?.certificateAuthoritiesData; } - const tlsSettingsFromConfig = getTLSSettingsFromConfig( - tlsSettings?.verificationMode, - tlsSettings?.rejectUnauthorized + const sslSettingsFromConfig = getSSLSettingsFromConfig( + sslSettings?.verificationMode, + sslSettings?.rejectUnauthorized ); - const nodeTLSOptions = getNodeTLSOptions(logger, tlsSettingsFromConfig.verificationMode); + const nodeTLSOptions = getNodeSSLOptions(logger, sslSettingsFromConfig.verificationMode); if (!transportConfig.tls) { transportConfig.tls = { ...tlsConfig, ...nodeTLSOptions }; } else { diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts index 9095780fea17c..9f76a236cacd5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts @@ -25,14 +25,6 @@ export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( ExternalIncidentServiceSecretConfiguration ); -export const ExecutorSubActionSchema = schema.oneOf([ - schema.literal('getIncident'), - schema.literal('pushToService'), - schema.literal('handshake'), - schema.literal('incidentTypes'), - schema.literal('severity'), -]); - export const ExecutorSubActionPushParamsSchema = schema.object({ incident: schema.object({ name: schema.string(), diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts index 59b0803d189cd..6fec30803d6d7 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts @@ -24,14 +24,6 @@ export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( ExternalIncidentServiceSecretConfiguration ); -export const ExecutorSubActionSchema = schema.oneOf([ - schema.literal('getFields'), - schema.literal('getIncident'), - schema.literal('pushToService'), - schema.literal('handshake'), - schema.literal('getChoices'), -]); - const CommentsSchema = schema.nullable( schema.arrayOf( schema.object({ diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts index 4108424e26ac4..7953f0ab365e8 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts @@ -194,7 +194,7 @@ describe('execute()', () => { const configurationUtilities = actionsConfigMock.create(); configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyTLSSettings: { + proxySSLSettings: { verificationMode: 'none', }, proxyBypassHosts: undefined, @@ -221,7 +221,7 @@ describe('execute()', () => { const configurationUtilities = actionsConfigMock.create(); configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyTLSSettings: { + proxySSLSettings: { verificationMode: 'none', }, proxyBypassHosts: new Set(['example.com']), @@ -248,7 +248,7 @@ describe('execute()', () => { const configurationUtilities = actionsConfigMock.create(); configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyTLSSettings: { + proxySSLSettings: { verificationMode: 'none', }, proxyBypassHosts: new Set(['not-example.com']), @@ -275,7 +275,7 @@ describe('execute()', () => { const configurationUtilities = actionsConfigMock.create(); configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyTLSSettings: { + proxySSLSettings: { verificationMode: 'none', }, proxyBypassHosts: undefined, @@ -302,7 +302,7 @@ describe('execute()', () => { const configurationUtilities = actionsConfigMock.create(); configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyTLSSettings: { + proxySSLSettings: { verificationMode: 'none', }, proxyBypassHosts: undefined, diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts new file mode 100644 index 0000000000000..1e633e2175808 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { api } from './api'; +import { ExternalService } from './types'; +import { + apiParams, + externalServiceMock, + recordResponseCreate, + recordResponseUpdate, +} from './mocks'; +import { Logger } from '@kbn/logging'; + +let mockedLogger: jest.Mocked<Logger>; + +describe('api', () => { + let externalService: jest.Mocked<ExternalService>; + + beforeEach(() => { + externalService = externalServiceMock.create(); + }); + + describe('pushToService', () => { + test('it pushes a new record', async () => { + const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } }; + const res = await api.pushToService({ + externalService, + logger: mockedLogger, + params, + }); + + expect(externalService.createComment).toHaveBeenCalled(); + expect(externalService.createRecord).toHaveBeenCalled(); + expect(externalService.updateRecord).not.toHaveBeenCalled(); + + expect(res).toEqual({ + ...recordResponseCreate, + comments: [ + { + commentId: '123456', + pushedDate: '2021-06-01T17:29:51.092Z', + }, + { + commentId: '123456', + pushedDate: '2021-06-01T17:29:51.092Z', + }, + ], + }); + }); + + test('it pushes a new record without comment', async () => { + const params = { + ...apiParams, + incident: { ...apiParams.incident, externalId: null }, + comments: [], + }; + const res = await api.pushToService({ + externalService, + logger: mockedLogger, + params, + }); + + expect(externalService.createComment).not.toHaveBeenCalled(); + expect(externalService.createRecord).toHaveBeenCalled(); + expect(res).toEqual(recordResponseCreate); + }); + + test('updates existing record', async () => { + const res = await api.pushToService({ + externalService, + logger: mockedLogger, + params: apiParams, + }); + + expect(externalService.createComment).toHaveBeenCalled(); + expect(externalService.createRecord).not.toHaveBeenCalled(); + expect(externalService.updateRecord).toHaveBeenCalled(); + expect(res).toEqual({ + ...recordResponseUpdate, + comments: [ + { + commentId: '123456', + pushedDate: '2021-06-01T17:29:51.092Z', + }, + { + commentId: '123456', + pushedDate: '2021-06-01T17:29:51.092Z', + }, + ], + }); + }); + + test('it calls createRecord correctly', async () => { + const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } }; + await api.pushToService({ externalService, params, logger: mockedLogger }); + + expect(externalService.createRecord).toHaveBeenCalledWith({ + incident: { + alertId: '123456', + caseId: '123456', + caseName: 'case name', + description: 'case desc', + ruleName: 'rule name', + severity: 'critical', + }, + }); + }); + + test('it calls createComment correctly', async () => { + const mockedToISOString = jest + .spyOn(Date.prototype, 'toISOString') + .mockReturnValue('2021-06-15T18:02:29.404Z'); + + const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } }; + await api.pushToService({ externalService, params, logger: mockedLogger }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + createdDate: '2021-06-15T18:02:29.404Z', + incidentId: '123456', + comment: { + commentId: 'case-comment-1', + comment: 'A comment', + }, + }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + createdDate: '2021-06-15T18:02:29.404Z', + incidentId: '123456', + comment: { + commentId: 'case-comment-2', + comment: 'Another comment', + }, + }); + + mockedToISOString.mockRestore(); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts new file mode 100644 index 0000000000000..343a94e52711f --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts @@ -0,0 +1,60 @@ +/* + * 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 { + ExternalServiceIncidentResponse, + ExternalServiceApi, + Incident, + PushToServiceApiHandlerArgs, + PushToServiceResponse, +} from './types'; + +const pushToServiceHandler = async ({ + externalService, + params, +}: PushToServiceApiHandlerArgs): Promise<ExternalServiceIncidentResponse> => { + const { comments } = params; + let res: PushToServiceResponse; + const { externalId, ...rest } = params.incident; + const incident: Incident = rest; + + if (externalId != null) { + res = await externalService.updateRecord({ + incidentId: externalId, + incident, + }); + } else { + res = await externalService.createRecord({ incident }); + } + + const createdDate = new Date().toISOString(); + + if (comments && Array.isArray(comments) && comments.length > 0) { + res.comments = []; + for (const currentComment of comments) { + const comment = await externalService.createComment({ + incidentId: res.id, + comment: currentComment, + createdDate, + }); + + res.comments = [ + ...(res.comments ?? []), + { + commentId: comment.commentId, + pushedDate: comment.pushedDate, + }, + ]; + } + } + + return res; +}; + +export const api: ExternalServiceApi = { + pushToService: pushToServiceHandler, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts new file mode 100644 index 0000000000000..c2974ec28486c --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts @@ -0,0 +1,90 @@ +/* + * 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 { getBodyForEventAction } from './helpers'; +import { mappings } from './mocks'; + +describe('Create Record Mapping', () => { + const appId = '45678'; + + test('it maps successfully', () => { + const params = { + alertId: 'al123', + ruleName: 'Rule Name', + severity: 'Critical', + caseName: 'Case Name', + caseId: 'es3456789', + description: 'case desc', + externalId: null, + }; + + const data = getBodyForEventAction(appId, mappings, params); + expect(data.applicationId).toEqual(appId); + expect(data.id).not.toBeDefined(); + expect(data.values?.[mappings.alertIdConfig?.id ?? 0]).toEqual(params.alertId); + expect(data.values?.[mappings.ruleNameConfig.id]).toEqual(params.ruleName); + expect(data.values?.[mappings.caseNameConfig?.id ?? 0]).toEqual(params.caseName); + expect(data.values?.[mappings.caseIdConfig?.id ?? 0]).toEqual(params.caseId); + expect(data.values?.[mappings?.severityConfig?.id ?? 0]).toEqual(params.severity); + expect(data.values?.[mappings?.descriptionConfig?.id ?? 0]).toEqual(params.description); + }); + + test('it contains the id if defined', () => { + const params = { + alertId: 'al123', + ruleName: 'Rule Name', + severity: 'Critical', + caseName: 'Case Name', + caseId: 'es3456789', + description: 'case desc', + externalId: null, + }; + const data = getBodyForEventAction(appId, mappings, params, '123'); + expect(data.id).toEqual('123'); + }); + + test('it does not includes null mappings', () => { + const params = { + alertId: 'al123', + ruleName: 'Rule Name', + severity: 'Critical', + caseName: 'Case Name', + caseId: 'es3456789', + description: 'case desc', + externalId: null, + }; + + // @ts-expect-error + const data = getBodyForEventAction(appId, { ...mappings, test: null }, params); + expect(data.values?.test).not.toBeDefined(); + }); + + test('it converts a numeric values correctly', () => { + const params = { + alertId: 'thisIsNotANumber', + ruleName: 'Rule Name', + severity: 'Critical', + caseName: 'Case Name', + caseId: '123', + description: 'case desc', + externalId: null, + }; + + const data = getBodyForEventAction( + appId, + { + ...mappings, + caseIdConfig: { ...mappings.caseIdConfig, fieldType: 'numeric' }, + alertIdConfig: { ...mappings.alertIdConfig, fieldType: 'numeric' }, + }, + params + ); + + expect(data.values?.[mappings.alertIdConfig?.id ?? 0]).toBe(0); + expect(data.values?.[mappings.caseIdConfig?.id ?? 0]).toBe(123); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts new file mode 100644 index 0000000000000..13b2df1c97f16 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts @@ -0,0 +1,58 @@ +/* + * 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 { CreateRecordParams, Incident, SwimlaneRecordPayload, MappingConfigType } from './types'; + +type ConfigMapping = Omit<MappingConfigType, 'commentsConfig'>; + +const mappingKeysToIncidentKeys: Record<keyof ConfigMapping, keyof Incident> = { + ruleNameConfig: 'ruleName', + alertIdConfig: 'alertId', + caseIdConfig: 'caseId', + caseNameConfig: 'caseName', + severityConfig: 'severity', + descriptionConfig: 'description', +}; + +export const getBodyForEventAction = ( + applicationId: string, + mappingConfig: MappingConfigType, + params: CreateRecordParams['incident'], + incidentId?: string +): SwimlaneRecordPayload => { + const data: SwimlaneRecordPayload = { + applicationId, + ...(incidentId ? { id: incidentId } : {}), + values: {}, + }; + + return (Object.keys(mappingConfig) as Array<keyof ConfigMapping>).reduce((acc, key) => { + const fieldMap = mappingConfig[key]; + + if (!fieldMap) { + return acc; + } + + const { id, fieldType } = fieldMap; + const paramName = mappingKeysToIncidentKeys[key]; + const value = params[paramName]; + + if (value) { + switch (fieldType) { + case 'numeric': { + const number = Number(value); + return { ...acc, values: { ...acc.values, [id]: isNaN(number) ? 0 : number } }; + } + default: { + return { ...acc, values: { ...acc.values, [id]: value } }; + } + } + } + + return acc; + }, data); +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts new file mode 100644 index 0000000000000..de5010436b6b3 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts @@ -0,0 +1,116 @@ +/* + * 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 { curry } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import { Logger } from '@kbn/logging'; +import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../../types'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { + SwimlaneExecutorResultData, + SwimlanePublicConfigurationType, + SwimlaneSecretConfigurationType, + ExecutorParams, + ExecutorSubActionPushParams, +} from './types'; +import { validate } from './validators'; +import { + ExecutorParamsSchema, + SwimlaneSecretsConfiguration, + SwimlaneServiceConfiguration, +} from './schema'; +import { createExternalService } from './service'; +import { api } from './api'; + +interface GetActionTypeParams { + logger: Logger; + configurationUtilities: ActionsConfigurationUtilities; +} + +const supportedSubActions: string[] = ['pushToService']; + +// action type definition +export function getActionType( + params: GetActionTypeParams +): ActionType< + SwimlanePublicConfigurationType, + SwimlaneSecretConfigurationType, + ExecutorParams, + SwimlaneExecutorResultData | {} +> { + const { logger, configurationUtilities } = params; + + return { + id: '.swimlane', + minimumLicenseRequired: 'gold', + name: i18n.translate('xpack.actions.builtin.swimlaneTitle', { + defaultMessage: 'Swimlane', + }), + validate: { + config: schema.object(SwimlaneServiceConfiguration, { + validate: curry(validate.config)(configurationUtilities), + }), + secrets: schema.object(SwimlaneSecretsConfiguration, { + validate: curry(validate.secrets)(configurationUtilities), + }), + params: ExecutorParamsSchema, + }, + executor: curry(executor)({ logger, configurationUtilities }), + }; +} + +async function executor( + { + logger, + configurationUtilities, + }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities }, + execOptions: ActionTypeExecutorOptions< + SwimlanePublicConfigurationType, + SwimlaneSecretConfigurationType, + ExecutorParams + > +): Promise<ActionTypeExecutorResult<SwimlaneExecutorResultData | {}>> { + const { actionId, config, params, secrets } = execOptions; + const { subAction, subActionParams } = params as ExecutorParams; + let data: SwimlaneExecutorResultData | null = null; + + const externalService = createExternalService( + { + config, + secrets, + }, + logger, + configurationUtilities + ); + + if (!api[subAction]) { + const errorMessage = `[Action][ExternalService] -> [Swimlane] Unsupported subAction type ${subAction}.`; + logger.error(errorMessage); + throw new Error(errorMessage); + } + + if (!supportedSubActions.includes(subAction)) { + const errorMessage = `[Action][ExternalService] -> [Swimlane] subAction ${subAction} not implemented.`; + logger.error(errorMessage); + throw new Error(errorMessage); + } + + if (subAction === 'pushToService') { + const pushToServiceParams = subActionParams as ExecutorSubActionPushParams; + + data = await api.pushToService({ + externalService, + params: pushToServiceParams, + logger, + }); + + logger.debug(`response push to service for incident id: ${data.id}`); + } + + return { status: 'ok', data: data ?? {}, actionId }; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts new file mode 100644 index 0000000000000..f9931049d81c2 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts @@ -0,0 +1,124 @@ +/* + * 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 { ExecutorSubActionPushParams, ExternalService, PushToServiceApiParams } from './types'; + +export const applicationFields = [ + { + id: 'adnlas', + name: 'Severity', + key: 'severity', + fieldType: 'text', + }, + { + id: 'adnfls', + name: 'Rule Name', + key: 'rule-name', + fieldType: 'text', + }, + { + id: 'a6sst', + name: 'Case Id', + key: 'case-id-name', + fieldType: 'text', + }, + { + id: 'a6fst', + name: 'Case Name', + key: 'case-name', + fieldType: 'text', + }, + { + id: 'a6fdf', + name: 'Comments', + key: 'comments', + fieldType: 'notes', + }, + { + id: 'a6fde', + name: 'Description', + key: 'description', + fieldType: 'text', + }, + { + id: 'dfnkls', + name: 'Alert ID', + key: 'alert-id', + fieldType: 'text', + }, +]; + +export const mappings = { + severityConfig: applicationFields[0], + ruleNameConfig: applicationFields[1], + caseIdConfig: applicationFields[2], + caseNameConfig: applicationFields[3], + commentsConfig: applicationFields[4], + descriptionConfig: applicationFields[5], + alertIdConfig: applicationFields[6], +}; + +export const getApplicationResponse = { fields: applicationFields }; + +export const recordResponseCreate = { + id: '123456', + title: 'neato', + url: 'swimlane.com', + pushedDate: '2021-06-01T17:29:51.092Z', +}; + +export const recordResponseUpdate = { + id: '98765', + title: 'not neato', + url: 'laneswim.com', + pushedDate: '2021-06-01T17:29:51.092Z', +}; + +export const commentResponse = { + commentId: '123456', + pushedDate: '2021-06-01T17:29:51.092Z', +}; + +const createMock = (): jest.Mocked<ExternalService> => { + return { + createComment: jest.fn().mockImplementation(() => Promise.resolve(commentResponse)), + createRecord: jest.fn().mockImplementation(() => Promise.resolve(recordResponseCreate)), + updateRecord: jest.fn().mockImplementation(() => Promise.resolve(recordResponseUpdate)), + }; +}; + +const externalServiceMock = { + create: createMock, +}; + +const executorParams: ExecutorSubActionPushParams = { + incident: { + ruleName: 'rule name', + alertId: '123456', + caseName: 'case name', + severity: 'critical', + caseId: '123456', + description: 'case desc', + externalId: 'incident-3', + }, + comments: [ + { + commentId: 'case-comment-1', + comment: 'A comment', + }, + { + commentId: 'case-comment-2', + comment: 'Another comment', + }, + ], +}; + +const apiParams: PushToServiceApiParams = { + ...executorParams, +}; + +export { externalServiceMock, executorParams, apiParams }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts new file mode 100644 index 0000000000000..7f4bdc8ca6c0d --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +export const ConfigMap = { + id: schema.string(), + key: schema.string(), + name: schema.string(), + fieldType: schema.string(), +}; + +export const ConfigMapSchema = schema.object(ConfigMap); + +export const ConfigMapping = { + ruleNameConfig: schema.nullable(ConfigMapSchema), + alertIdConfig: schema.nullable(ConfigMapSchema), + caseIdConfig: schema.nullable(ConfigMapSchema), + caseNameConfig: schema.nullable(ConfigMapSchema), + commentsConfig: schema.nullable(ConfigMapSchema), + severityConfig: schema.nullable(ConfigMapSchema), + descriptionConfig: schema.nullable(ConfigMapSchema), +}; + +export const ConfigMappingSchema = schema.object(ConfigMapping); + +export const SwimlaneServiceConfiguration = { + apiUrl: schema.string(), + appId: schema.string(), + connectorType: schema.string(), + mappings: ConfigMappingSchema, +}; + +export const SwimlaneServiceConfigurationSchema = schema.object(SwimlaneServiceConfiguration); + +export const SwimlaneSecretsConfiguration = { + apiToken: schema.string(), +}; + +export const SwimlaneSecretsConfigurationSchema = schema.object(SwimlaneSecretsConfiguration); + +const SwimlaneFields = { + alertId: schema.nullable(schema.string()), + ruleName: schema.nullable(schema.string()), + caseId: schema.nullable(schema.string()), + caseName: schema.nullable(schema.string()), + severity: schema.nullable(schema.string()), + description: schema.nullable(schema.string()), +}; + +export const ExecutorSubActionPushParamsSchema = schema.object({ + incident: schema.object({ + ...SwimlaneFields, + externalId: schema.nullable(schema.string()), + }), + comments: schema.nullable( + schema.arrayOf( + schema.object({ + comment: schema.string(), + commentId: schema.string(), + }) + ) + ), +}); + +export const ExecutorParamsSchema = schema.oneOf([ + schema.object({ + subAction: schema.literal('pushToService'), + subActionParams: ExecutorSubActionPushParamsSchema, + }), +]); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts new file mode 100644 index 0000000000000..77f4686f8acd0 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts @@ -0,0 +1,434 @@ +/* + * 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 axios from 'axios'; + +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { Logger } from '../../../../../../src/core/server'; +import { actionsConfigMock } from '../../actions_config.mock'; +import * as utils from '../lib/axios_utils'; +import { createExternalService } from './service'; +import { mappings } from './mocks'; +import { ExternalService } from './types'; + +const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>; + +jest.mock('axios'); +jest.mock('../lib/axios_utils', () => { + const originalUtils = jest.requireActual('../lib/axios_utils'); + return { + ...originalUtils, + request: jest.fn(), + }; +}); + +axios.create = jest.fn(() => axios); +const requestMock = utils.request as jest.Mock; +const configurationUtilities = actionsConfigMock.create(); + +describe('Swimlane Service', () => { + let service: ExternalService; + const config = { + apiUrl: 'https://test.swimlane.com/', + appId: 'bcq16kdTbz5jlwM6h', + connectorType: 'all', + mappings, + }; + const apiToken = 'token'; + + const headers = { + 'Content-Type': 'application/json', + 'Private-Token': apiToken, + }; + + const incident = { + ruleName: 'Rule Name', + caseId: 'Case Id', + caseName: 'Case Name', + severity: 'Severity', + externalId: null, + description: 'Description', + alertId: 'Alert Id', + }; + + const url = config.apiUrl.slice(0, -1); + + beforeAll(() => { + service = createExternalService( + { + // The trailing slash at the end of the url is intended. + // All API calls need to have the trailing slash removed. + config, + secrets: { apiToken }, + }, + logger, + configurationUtilities + ); + }); + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('createExternalService', () => { + test('throws without url', () => { + expect(() => + createExternalService( + { + config: { + // @ts-ignore + apiUrl: null, + appId: '99999', + mappings, + }, + secrets: { apiToken }, + }, + logger, + configurationUtilities + ) + ).toThrow(); + }); + + test('throws without app id', () => { + expect(() => + createExternalService( + { + config: { + apiUrl: 'test.com', + // @ts-ignore + appId: null, + }, + secrets: { apiToken }, + }, + logger, + configurationUtilities + ) + ).toThrow(); + }); + + test('throws without mappings', () => { + expect(() => + createExternalService( + { + config: { + apiUrl: 'test.com', + appId: '987987', + // @ts-ignore + mappings: null, + }, + secrets: { apiToken }, + }, + logger, + configurationUtilities + ) + ).toThrow(); + }); + + test('throws without api token', () => { + expect(() => { + return createExternalService( + { + config: { apiUrl: 'test.com', appId: '78978', mappings, connectorType: 'all' }, + secrets: { + // @ts-ignore + apiToken: null, + }, + }, + logger, + configurationUtilities + ); + }).toThrow(); + }); + }); + + describe('createRecord', () => { + const data = { + id: '123', + name: 'title', + createdDate: '2021-06-01T17:29:51.092Z', + }; + + test('it creates a record correctly', async () => { + requestMock.mockImplementation(() => ({ + data, + })); + + const res = await service.createRecord({ + incident, + }); + + expect(res).toEqual({ + id: '123', + title: 'title', + pushedDate: '2021-06-01T17:29:51.092Z', + url: `${url}/record/${config.appId}/123`, + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data, + })); + + await service.createRecord({ + incident, + }); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + logger, + headers, + data: { + applicationId: config.appId, + values: { + [mappings.ruleNameConfig.id]: 'Rule Name', + [mappings.caseNameConfig.id]: 'Case Name', + [mappings.caseIdConfig.id]: 'Case Id', + [mappings.severityConfig.id]: 'Severity', + [mappings.descriptionConfig.id]: 'Description', + [mappings.alertIdConfig.id]: 'Alert Id', + }, + }, + url: `${url}/api/app/${config.appId}/record`, + method: 'post', + configurationUtilities, + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + await expect(service.createRecord({ incident })).rejects.toThrow( + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown` + ); + }); + }); + + describe('updateRecord', () => { + const data = { + id: '123', + name: 'title', + modifiedDate: '2021-06-01T17:29:51.092Z', + }; + const incidentId = '123'; + + test('it updates a record correctly', async () => { + requestMock.mockImplementation(() => ({ + data, + })); + + const res = await service.updateRecord({ + incident, + incidentId, + }); + + expect(res).toEqual({ + id: '123', + title: 'title', + pushedDate: '2021-06-01T17:29:51.092Z', + url: `${url}/record/${config.appId}/123`, + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data, + })); + + await service.updateRecord({ + incident, + incidentId, + }); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + logger, + headers, + data: { + applicationId: config.appId, + id: incidentId, + values: { + [mappings.ruleNameConfig.id]: 'Rule Name', + [mappings.caseNameConfig.id]: 'Case Name', + [mappings.caseIdConfig.id]: 'Case Id', + [mappings.severityConfig.id]: 'Severity', + [mappings.descriptionConfig.id]: 'Description', + [mappings.alertIdConfig.id]: 'Alert Id', + }, + }, + url: `${url}/api/app/${config.appId}/record/${incidentId}`, + method: 'patch', + configurationUtilities, + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + await expect(service.updateRecord({ incident, incidentId })).rejects.toThrow( + `[Action][Swimlane]: Unable to update record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown` + ); + }); + }); + + describe('createComment', () => { + const data = { + id: '123', + name: 'title', + modifiedDate: '2021-06-01T17:29:51.092Z', + }; + const incidentId = '123'; + const comment = { commentId: '456', comment: 'A comment' }; + const createdDate = '2021-06-01T17:29:51.092Z'; + + test('it updates a record correctly', async () => { + requestMock.mockImplementation(() => ({ + data, + })); + + const res = await service.createComment({ + comment, + incidentId, + createdDate, + }); + + expect(res).toEqual({ + commentId: '456', + pushedDate: '2021-06-01T17:29:51.092Z', + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data, + })); + + await service.createComment({ + comment, + incidentId, + createdDate, + }); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + logger, + headers, + data: { + createdDate, + fieldId: mappings.commentsConfig.id, + isRichText: true, + message: comment.comment, + }, + url: `${url}/api/app/${config.appId}/record/${incidentId}/${mappings.commentsConfig.id}/comment`, + method: 'post', + configurationUtilities, + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + await expect(service.createComment({ comment, incidentId, createdDate })).rejects.toThrow( + `[Action][Swimlane]: Unable to create comment in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown` + ); + }); + }); + + describe('error messages', () => { + const errorResponse = { ErrorCode: '1', Argument: 'Invalid field' }; + + test('it contains the response error', async () => { + requestMock.mockImplementation(() => { + const error = new Error('An error has occurred'); + // @ts-ignore + error.response = { data: errorResponse }; + throw error; + }); + + await expect( + service.createRecord({ + incident, + }) + ).rejects.toThrow( + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: Invalid field (1)` + ); + }); + + test('it shows an empty string for reason if the ErrorCode is undefined', async () => { + requestMock.mockImplementation(() => { + const error = new Error('An error has occurred'); + // @ts-ignore + error.response = { data: { ErrorCode: '1' } }; + throw error; + }); + + await expect( + service.createRecord({ + incident, + }) + ).rejects.toThrow( + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown` + ); + }); + + test('it shows an empty string for reason if the Argument is undefined', async () => { + requestMock.mockImplementation(() => { + const error = new Error('An error has occurred'); + // @ts-ignore + error.response = { data: { Argument: 'Invalid field' } }; + throw error; + }); + + await expect( + service.createRecord({ + incident, + }) + ).rejects.toThrow( + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown` + ); + }); + + test('it shows an empty string for reason if data is undefined', async () => { + requestMock.mockImplementation(() => { + const error = new Error('An error has occurred'); + // @ts-ignore + error.response = {}; + throw error; + }); + + await expect( + service.createRecord({ + incident, + }) + ).rejects.toThrow( + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown` + ); + }); + + test('it shows the status code', async () => { + requestMock.mockImplementation(() => { + const error = new Error('An error has occurred'); + // @ts-ignore + error.response = { data: errorResponse, status: 400 }; + throw error; + }); + + await expect( + service.createRecord({ + incident, + }) + ).rejects.toThrow( + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 400. Error: An error has occurred. Reason: Invalid field (1)` + ); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts new file mode 100644 index 0000000000000..f68d22121dbcc --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts @@ -0,0 +1,196 @@ +/* + * 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 { Logger } from '@kbn/logging'; +import axios from 'axios'; + +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { getErrorMessage, request } from '../lib/axios_utils'; +import { getBodyForEventAction } from './helpers'; +import { + CreateCommentParams, + CreateRecordParams, + ExternalService, + ExternalServiceCredentials, + ExternalServiceIncidentResponse, + MappingConfigType, + ResponseError, + SwimlanePublicConfigurationType, + SwimlaneRecordPayload, + SwimlaneSecretConfigurationType, + UpdateRecordParams, +} from './types'; +import * as i18n from './translations'; + +const createErrorMessage = (errorResponse: ResponseError | null | undefined): string => { + if (errorResponse == null) { + return 'unknown'; + } + + const { ErrorCode, Argument } = errorResponse; + return Argument != null && ErrorCode != null ? `${Argument} (${ErrorCode})` : 'unknown'; +}; + +export const createExternalService = ( + { config, secrets }: ExternalServiceCredentials, + logger: Logger, + configurationUtilities: ActionsConfigurationUtilities +): ExternalService => { + const { apiUrl: url, appId, mappings } = config as SwimlanePublicConfigurationType; + const { apiToken } = secrets as SwimlaneSecretConfigurationType; + + const axiosInstance = axios.create(); + + if (!url || !appId || !apiToken || !mappings) { + throw Error(`[Action]${i18n.NAME}: Wrong configuration.`); + } + + const headers: Record<string, string> = { + 'Content-Type': 'application/json', + 'Private-Token': `${secrets.apiToken}`, + }; + + const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url; + const apiUrl = urlWithoutTrailingSlash.endsWith('api') + ? urlWithoutTrailingSlash + : urlWithoutTrailingSlash + '/api'; + + const getPostRecordUrl = (id: string) => `${apiUrl}/app/${id}/record`; + + const getPostRecordIdUrl = (id: string, recordId: string) => + `${getPostRecordUrl(id)}/${recordId}`; + + const getRecordIdUrl = (id: string, recordId: string) => + `${urlWithoutTrailingSlash}/record/${id}/${recordId}`; + + const getPostCommentUrl = (id: string, recordId: string, commentFieldId: string) => + `${getPostRecordIdUrl(id, recordId)}/${commentFieldId}/comment`; + + const getCommentFieldId = (fieldMappings: MappingConfigType): string | null => + fieldMappings.commentsConfig?.id || null; + + const createRecord = async ( + params: CreateRecordParams + ): Promise<ExternalServiceIncidentResponse> => { + try { + const mappingConfig = mappings as MappingConfigType; + const data = getBodyForEventAction(appId, mappingConfig, params.incident); + + const res = await request({ + axios: axiosInstance, + configurationUtilities, + data, + headers, + logger, + method: 'post', + url: getPostRecordUrl(appId), + }); + return { + id: res.data.id, + title: res.data.name, + url: getRecordIdUrl(appId, res.data.id), + pushedDate: new Date(res.data.createdDate).toISOString(), + }; + } catch (error) { + throw new Error( + getErrorMessage( + i18n.NAME, + `Unable to create record in application with id ${appId}. Status: ${ + error.response?.status ?? 500 + }. Error: ${error.message}. Reason: ${createErrorMessage(error.response?.data)}` + ) + ); + } + }; + + const updateRecord = async ( + params: UpdateRecordParams + ): Promise<ExternalServiceIncidentResponse> => { + try { + const mappingConfig = mappings as MappingConfigType; + const data = getBodyForEventAction(appId, mappingConfig, params.incident, params.incidentId); + + const res = await request<SwimlaneRecordPayload>({ + axios: axiosInstance, + configurationUtilities, + data, + headers, + logger, + method: 'patch', + url: getPostRecordIdUrl(appId, params.incidentId), + }); + + return { + id: res.data.id, + title: res.data.name, + url: getRecordIdUrl(appId, params.incidentId), + pushedDate: new Date(res.data.modifiedDate).toISOString(), + }; + } catch (error) { + throw new Error( + getErrorMessage( + i18n.NAME, + `Unable to update record in application with id ${appId}. Status: ${ + error.response?.status ?? 500 + }. Error: ${error.message}. Reason: ${createErrorMessage(error.response?.data)}` + ) + ); + } + }; + + const createComment = async ({ incidentId, comment, createdDate }: CreateCommentParams) => { + try { + const mappingConfig = mappings as MappingConfigType; + const fieldId = getCommentFieldId(mappingConfig); + + if (fieldId == null) { + throw new Error(`No comment field mapped in ${i18n.NAME} connector`); + } + + const data = { + createdDate, + fieldId, + isRichText: true, + message: comment.comment, + }; + + await request({ + axios: axiosInstance, + configurationUtilities, + data, + headers, + logger, + method: 'post', + url: getPostCommentUrl(appId, incidentId, fieldId), + }); + + /** + * Swimlane response does not contain any data. + * We cannot get an externalCommentId + */ + return { + commentId: comment.commentId, + pushedDate: createdDate, + }; + } catch (error) { + throw new Error( + getErrorMessage( + i18n.NAME, + `Unable to create comment in application with id ${appId}. Status: ${ + error.response?.status ?? 500 + }. Error: ${error.message}. Reason: ${createErrorMessage(error.response?.data)}` + ) + ); + } + }; + + return { + createComment, + createRecord, + updateRecord, + }; +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/translations.ts new file mode 100644 index 0000000000000..671cf224448f6 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/translations.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 { i18n } from '@kbn/i18n'; + +export const NAME = i18n.translate('xpack.actions.builtin.case.swimlaneTitle', { + defaultMessage: 'Swimlane', +}); + +export const ALLOWED_HOSTS_ERROR = (message: string) => + i18n.translate('xpack.actions.builtin.swimlane.configuration.apiAllowedHostsError', { + defaultMessage: 'error configuring connector action: {message}', + values: { + message, + }, + }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts new file mode 100644 index 0000000000000..5cb3b10989621 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { TypeOf } from '@kbn/config-schema'; +import { Logger } from '@kbn/logging'; +import { + ConfigMappingSchema, + ExecutorParamsSchema, + ExecutorSubActionPushParamsSchema, + SwimlaneSecretsConfigurationSchema, + SwimlaneServiceConfigurationSchema, +} from './schema'; +import { ActionsConfigurationUtilities } from '../../actions_config'; + +export type SwimlanePublicConfigurationType = TypeOf<typeof SwimlaneServiceConfigurationSchema>; +export type SwimlaneSecretConfigurationType = TypeOf<typeof SwimlaneSecretsConfigurationSchema>; + +export type MappingConfigType = TypeOf<typeof ConfigMappingSchema>; +export type ExecutorParams = TypeOf<typeof ExecutorParamsSchema>; +export type ExecutorSubActionPushParams = TypeOf<typeof ExecutorSubActionPushParamsSchema>; + +export interface ExternalServiceCredentials { + config: SwimlanePublicConfigurationType; + secrets: SwimlaneSecretConfigurationType; +} + +export interface ExternalServiceValidation { + config: (configurationUtilities: ActionsConfigurationUtilities, configObject: any) => void; + secrets: (configurationUtilities: ActionsConfigurationUtilities, secrets: any) => void; +} + +export interface CreateRecordParams { + incident: Incident; +} +export interface UpdateRecordParams extends CreateRecordParams { + incidentId: string; +} + +export type PushToServiceApiParams = ExecutorSubActionPushParams; +export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: PushToServiceApiParams; + logger: Logger; +} + +export interface ExternalServiceIncidentResponse { + id: string; + title: string; + url: string; + pushedDate: string; +} +export interface ExternalServiceCommentResponse { + commentId: string; + pushedDate: string; + externalCommentId?: string; +} + +export interface FieldConfig { + id: string; + name: string; + key: string; + fieldType: string; +} + +export interface SwimlaneRecordPayload { + applicationId: string; + values: SwimlaneDataValues; + id?: string; +} + +export interface ExternalService { + createComment: (params: CreateCommentParams) => Promise<ExternalServiceCommentResponse>; + createRecord: (params: CreateRecordParams) => Promise<ExternalServiceIncidentResponse>; + updateRecord: (params: UpdateRecordParams) => Promise<ExternalServiceIncidentResponse>; +} + +export type Incident = Omit<ExecutorSubActionPushParams['incident'], 'externalId'>; + +export interface ExternalServiceApiHandlerArgs { + externalService: ExternalService; +} + +export interface GetApplicationHandlerArgs { + externalService: ExternalService; +} + +export interface PushToServiceResponse extends ExternalServiceIncidentResponse { + comments?: ExternalServiceCommentResponse[]; +} + +export interface ExternalServiceApi { + pushToService: (args: PushToServiceApiHandlerArgs) => Promise<ExternalServiceIncidentResponse>; +} + +export type SwimlaneExecutorResultData = ExternalServiceIncidentResponse; +export type SwimlaneDataValues = Record<string, string | number>; +export interface SwimlaneComment { + fieldId: string; + message: string | number; + createdDate: string; + isRichText: boolean; +} +export type SwimlaneDataComments = Record<string, SwimlaneComment[]>; + +export interface SimpleComment { + comment: SwimlaneComment['message']; + commentId: string; +} + +export interface CreateCommentParams { + incidentId: string; + comment: SimpleComment; + createdDate: string; +} + +export interface ResponseError { + ErrorCode: number; + Argument: string; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/validators.ts new file mode 100644 index 0000000000000..1972cd7e6af0b --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/validators.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { ExternalServiceValidation, SwimlanePublicConfigurationType } from './types'; +import * as i18n from './translations'; + +export const validateCommonConfig = ( + configurationUtilities: ActionsConfigurationUtilities, + configObject: SwimlanePublicConfigurationType +) => { + try { + configurationUtilities.ensureUriAllowed(configObject.apiUrl); + } catch (allowedListError) { + return i18n.ALLOWED_HOSTS_ERROR(allowedListError.message); + } +}; + +export const validateCommonSecrets = () => {}; + +export const validate: ExternalServiceValidation = { + config: validateCommonConfig, + secrets: validateCommonSecrets, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts b/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts index bf34789e03fae..497300b86bdea 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts @@ -170,7 +170,7 @@ describe('execute()', () => { "getCustomHostSettings": [MockFunction], "getProxySettings": [MockFunction], "getResponseSettings": [MockFunction], - "getTLSSettings": [MockFunction], + "getSSLSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], "isUriAllowed": [MockFunction], @@ -234,7 +234,7 @@ describe('execute()', () => { "getCustomHostSettings": [MockFunction], "getProxySettings": [MockFunction], "getResponseSettings": [MockFunction], - "getTLSSettings": [MockFunction], + "getSSLSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], "isUriAllowed": [MockFunction], diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts index b2c865c2f5374..c04c79075abdc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts @@ -293,7 +293,7 @@ describe('execute()', () => { "getCustomHostSettings": [MockFunction], "getProxySettings": [MockFunction], "getResponseSettings": [MockFunction], - "getTLSSettings": [MockFunction], + "getSSLSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], "isUriAllowed": [MockFunction], @@ -386,7 +386,7 @@ describe('execute()', () => { "getCustomHostSettings": [MockFunction], "getProxySettings": [MockFunction], "getResponseSettings": [MockFunction], - "getTLSSettings": [MockFunction], + "getSSLSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], "isUriAllowed": [MockFunction], diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts index 9774bfb05d4ff..d99b9349e977b 100644 --- a/x-pack/plugins/actions/server/config.test.ts +++ b/x-pack/plugins/actions/server/config.test.ts @@ -178,9 +178,9 @@ describe('config validation', () => { ); }); - test('action with tls configuration', () => { + test('action with ssl configuration', () => { const config: Record<string, unknown> = { - tls: { + ssl: { verificationMode: 'none', proxyVerificationMode: 'none', }, @@ -208,7 +208,7 @@ describe('config validation', () => { "proxyRejectUnauthorizedCertificates": true, "rejectUnauthorized": true, "responseTimeout": "PT1M", - "tls": Object { + "ssl": Object { "proxyVerificationMode": "none", "verificationMode": "none", }, diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts index 8859a2d8881a2..1ae196c25a756 100644 --- a/x-pack/plugins/actions/server/config.ts +++ b/x-pack/plugins/actions/server/config.ts @@ -31,7 +31,7 @@ const customHostSettingsSchema = schema.object({ requireTLS: schema.maybe(schema.boolean()), }) ), - tls: schema.maybe( + ssl: schema.maybe( schema.object({ /** * @deprecated in favor of `verificationMode` @@ -78,16 +78,16 @@ export const configSchema = schema.object({ proxyUrl: schema.maybe(schema.string()), proxyHeaders: schema.maybe(schema.recordOf(schema.string(), schema.string())), /** - * @deprecated in favor of `tls.proxyVerificationMode` + * @deprecated in favor of `ssl.proxyVerificationMode` **/ proxyRejectUnauthorizedCertificates: schema.boolean({ defaultValue: true }), proxyBypassHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))), proxyOnlyHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))), /** - * @deprecated in favor of `tls.verificationMode` + * @deprecated in favor of `ssl.verificationMode` **/ rejectUnauthorized: schema.boolean({ defaultValue: true }), - tls: schema.maybe( + ssl: schema.maybe( schema.object({ verificationMode: schema.maybe( schema.oneOf( diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index 692ff6fa0a508..230ed826cb108 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -47,7 +47,6 @@ export type { TeamsActionTypeId, TeamsActionParams, } from './builtin_action_types'; - export type { PluginSetupContract, PluginStartContract } from './plugin'; export { asSavedObjectExecutionSource, asHttpRequestExecutionSource } from './lib'; @@ -64,19 +63,19 @@ export const config: PluginConfigDescriptor<ActionsConfig> = { if ( customHostSettings.find( (customHostSchema: CustomHostSettings) => - !!customHostSchema.tls && !!customHostSchema.tls.rejectUnauthorized + !!customHostSchema.ssl && !!customHostSchema.ssl.rejectUnauthorized ) ) { addDeprecation({ message: - `"xpack.actions.customHostSettings[<index>].tls.rejectUnauthorized" is deprecated.` + - `Use "xpack.actions.customHostSettings[<index>].tls.verificationMode" instead, ` + + `"xpack.actions.customHostSettings[<index>].ssl.rejectUnauthorized" is deprecated.` + + `Use "xpack.actions.customHostSettings[<index>].ssl.verificationMode" instead, ` + `with the setting "verificationMode:full" eql to "rejectUnauthorized:true", ` + `and "verificationMode:none" eql to "rejectUnauthorized:false".`, correctiveActions: { manualSteps: [ - `Remove "xpack.actions.customHostSettings[<index>].tls.rejectUnauthorized" from your kibana configs.`, - `Use "xpack.actions.customHostSettings[<index>].tls.verificationMode" ` + + `Remove "xpack.actions.customHostSettings[<index>].ssl.rejectUnauthorized" from your kibana configs.`, + `Use "xpack.actions.customHostSettings[<index>].ssl.verificationMode" ` + `with the setting "verificationMode:full" eql to "rejectUnauthorized:true", ` + `and "verificationMode:none" eql to "rejectUnauthorized:false".`, ], diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index 37d461d6b2a50..440de161490aa 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -109,6 +109,96 @@ test('successfully executes', async () => { }); expect(loggerMock.debug).toBeCalledWith('executing action test:1: 1'); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "event": Object { + "action": "execute-start", + }, + "kibana": Object { + "saved_objects": Array [ + Object { + "id": "1", + "namespace": "some-namespace", + "rel": "primary", + "type": "action", + "type_id": "test", + }, + ], + }, + "message": "action started: test:1: 1", + }, + ], + Array [ + Object { + "event": Object { + "action": "execute", + "outcome": "success", + }, + "kibana": Object { + "saved_objects": Array [ + Object { + "id": "1", + "namespace": "some-namespace", + "rel": "primary", + "type": "action", + "type_id": "test", + }, + ], + }, + "message": "action executed: test:1: 1", + }, + ], + ] + `); +}); + +test('successfully executes as a task', async () => { + const actionType: jest.Mocked<ActionType> = { + id: 'test', + name: 'Test', + minimumLicenseRequired: 'basic', + executor: jest.fn(), + }; + const actionSavedObject = { + id: '1', + type: 'action', + attributes: { + actionTypeId: 'test', + config: { + bar: true, + }, + secrets: { + baz: true, + }, + }, + references: [], + }; + const actionResult = { + id: actionSavedObject.id, + name: actionSavedObject.id, + ...pick(actionSavedObject.attributes, 'actionTypeId', 'config'), + isPreconfigured: false, + }; + actionsClient.get.mockResolvedValueOnce(actionResult); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); + actionTypeRegistry.get.mockReturnValueOnce(actionType); + + const scheduleDelay = 10000; // milliseconds + const scheduled = new Date(Date.now() - scheduleDelay); + await actionExecutor.execute({ + ...executeParams, + taskInfo: { + scheduled, + }, + }); + + const eventTask = eventLogger.logEvent.mock.calls[0][0]?.kibana?.task; + expect(eventTask).toBeDefined(); + expect(eventTask?.scheduled).toBe(scheduled.toISOString()); + expect(eventTask?.schedule_delay).toBeGreaterThanOrEqual(scheduleDelay * 1000 * 1000); + expect(eventTask?.schedule_delay).toBeLessThanOrEqual(2 * scheduleDelay * 1000 * 1000); }); test('provides empty config when config and / or secrets is empty', async () => { diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index e9e7b17288611..9e62b123951df 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -25,6 +25,9 @@ import { ActionsClient } from '../actions_client'; import { ActionExecutionSource } from './action_execution_source'; import { RelatedSavedObjects } from './related_saved_objects'; +// 1,000,000 nanoseconds in 1 millisecond +const Millis2Nanos = 1000 * 1000; + export interface ActionExecutorContext { logger: Logger; spaces?: SpacesServiceStart; @@ -39,11 +42,16 @@ export interface ActionExecutorContext { preconfiguredActions: PreConfiguredAction[]; } +export interface TaskInfo { + scheduled: Date; +} + export interface ExecuteOptions<Source = unknown> { actionId: string; request: KibanaRequest; params: Record<string, unknown>; source?: ActionExecutionSource<Source>; + taskInfo?: TaskInfo; relatedSavedObjects?: RelatedSavedObjects; } @@ -71,6 +79,7 @@ export class ActionExecutor { params, request, source, + taskInfo, relatedSavedObjects, }: ExecuteOptions): Promise<ActionTypeExecutorResult<unknown>> { if (!this.isInitialized) { @@ -143,9 +152,19 @@ export class ActionExecutor { const actionLabel = `${actionTypeId}:${actionId}: ${name}`; logger.debug(`executing action ${actionLabel}`); + const task = taskInfo + ? { + task: { + scheduled: taskInfo.scheduled.toISOString(), + schedule_delay: Millis2Nanos * (Date.now() - taskInfo.scheduled.getTime()), + }, + } + : {}; + const event: IEvent = { event: { action: EVENT_LOG_ACTIONS.execute }, kibana: { + ...task, saved_objects: [ { rel: SAVED_OBJECT_REL_PRIMARY, diff --git a/x-pack/plugins/actions/server/lib/custom_host_settings.test.ts b/x-pack/plugins/actions/server/lib/custom_host_settings.test.ts index ad07ea21d7917..ec7b46e545112 100644 --- a/x-pack/plugins/actions/server/lib/custom_host_settings.test.ts +++ b/x-pack/plugins/actions/server/lib/custom_host_settings.test.ts @@ -112,14 +112,14 @@ describe('custom_host_settings', () => { customHostSettings: [ { url: 'https://elastic.co:443', - tls: { + ssl: { certificateAuthoritiesData: 'xyz', rejectUnauthorized: false, }, }, { url: 'smtp://mail.elastic.com:25', - tls: { + ssl: { certificateAuthoritiesData: 'abc', rejectUnauthorized: true, }, @@ -338,7 +338,7 @@ describe('custom_host_settings', () => { customHostSettings: [ { url: 'https://almost.purrfect.com/', - tls: { + ssl: { certificateAuthoritiesFiles: 'this-file-does-not-exist', }, }, @@ -350,7 +350,7 @@ describe('custom_host_settings', () => { customHostSettings: [ { url: 'https://almost.purrfect.com:443', - tls: { + ssl: { certificateAuthoritiesFiles: 'this-file-does-not-exist', }, }, @@ -371,7 +371,7 @@ describe('custom_host_settings', () => { customHostSettings: [ { url: 'https://almost.purrfect.com/', - tls: { + ssl: { certificateAuthoritiesFiles: CA_FILE1, }, }, @@ -380,7 +380,7 @@ describe('custom_host_settings', () => { const resConfig = resolveCustomHosts(mockLogger, config); // not checking the full structure anymore, just ca bits - expect(resConfig?.customHostSettings?.[0].tls?.certificateAuthoritiesData).toBe(CA_CONTENTS1); + expect(resConfig?.customHostSettings?.[0].ssl?.certificateAuthoritiesData).toBe(CA_CONTENTS1); expect(warningLogs()).toEqual([]); }); @@ -390,7 +390,7 @@ describe('custom_host_settings', () => { customHostSettings: [ { url: 'https://almost.purrfect.com/', - tls: { + ssl: { certificateAuthoritiesFiles: [CA_FILE1, CA_FILE2], }, }, @@ -399,7 +399,7 @@ describe('custom_host_settings', () => { const resConfig = resolveCustomHosts(mockLogger, config); // not checking the full structure anymore, just ca bits - expect(resConfig?.customHostSettings?.[0].tls?.certificateAuthoritiesData).toBe( + expect(resConfig?.customHostSettings?.[0].ssl?.certificateAuthoritiesData).toBe( `${CA_CONTENTS1}\n${CA_CONTENTS2}` ); expect(warningLogs()).toEqual([]); @@ -411,7 +411,7 @@ describe('custom_host_settings', () => { customHostSettings: [ { url: 'https://almost.purrfect.com/', - tls: { + ssl: { certificateAuthoritiesFiles: [CA_FILE2], certificateAuthoritiesData: CA_CONTENTS1, }, @@ -421,7 +421,7 @@ describe('custom_host_settings', () => { const resConfig = resolveCustomHosts(mockLogger, config); // not checking the full structure anymore, just ca bits - expect(resConfig?.customHostSettings?.[0].tls?.certificateAuthoritiesData).toBe( + expect(resConfig?.customHostSettings?.[0].ssl?.certificateAuthoritiesData).toBe( `${CA_CONTENTS1}\n${CA_CONTENTS2}` ); expect(warningLogs()).toEqual([]); @@ -468,13 +468,13 @@ describe('custom_host_settings', () => { customHostSettings: [ { url: 'https://almost.purrfect.com/', - tls: { + ssl: { rejectUnauthorized: true, }, }, { url: 'https://almost.purrfect.com:443', - tls: { + ssl: { rejectUnauthorized: false, }, }, @@ -486,7 +486,7 @@ describe('custom_host_settings', () => { customHostSettings: [ { url: 'https://almost.purrfect.com:443', - tls: { + ssl: { rejectUnauthorized: true, }, }, diff --git a/x-pack/plugins/actions/server/lib/custom_host_settings.ts b/x-pack/plugins/actions/server/lib/custom_host_settings.ts index bfc8dad48aab6..0ff8624d42cfe 100644 --- a/x-pack/plugins/actions/server/lib/custom_host_settings.ts +++ b/x-pack/plugins/actions/server/lib/custom_host_settings.ts @@ -86,8 +86,8 @@ export function resolveCustomHosts(logger: Logger, config: ActionsConfig): Actio } // read the specified ca files, add their content to certificateAuthoritiesData - if (customHostSetting.tls) { - let files = customHostSetting.tls?.certificateAuthoritiesFiles || []; + if (customHostSetting.ssl) { + let files = customHostSetting.ssl?.certificateAuthoritiesFiles || []; if (typeof files === 'string') { files = [files]; } @@ -134,12 +134,12 @@ export function resolveCustomHosts(logger: Logger, config: ActionsConfig): Actio } function appendToCertificateAuthoritiesData(customHost: CustomHostSettingsWriteable, cert: string) { - const tls = customHost.tls; - if (tls) { - if (!tls.certificateAuthoritiesData) { - tls.certificateAuthoritiesData = cert; + const ssl = customHost.ssl; + if (ssl) { + if (!ssl.certificateAuthoritiesData) { + ssl.certificateAuthoritiesData = cert; } else { - tls.certificateAuthoritiesData += '\n' + cert; + ssl.certificateAuthoritiesData += '\n' + cert; } } } diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts index 2292994e3ccfd..495d638951b56 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts @@ -133,6 +133,9 @@ test('executes the task by calling the executor with proper parameters', async ( authorization: 'ApiKey MTIzOmFiYw==', }, }), + taskInfo: { + scheduled: new Date(), + }, }); const [executeParams] = mockedActionExecutor.execute.mock.calls[0]; @@ -255,6 +258,9 @@ test('uses API key when provided', async () => { authorization: 'ApiKey MTIzOmFiYw==', }, }), + taskInfo: { + scheduled: new Date(), + }, }); const [executeParams] = mockedActionExecutor.execute.mock.calls[0]; @@ -300,6 +306,9 @@ test('uses relatedSavedObjects when provided', async () => { authorization: 'ApiKey MTIzOmFiYw==', }, }), + taskInfo: { + scheduled: new Date(), + }, }); }); @@ -323,7 +332,6 @@ test('sanitizes invalid relatedSavedObjects when provided', async () => { }); await taskRunner.run(); - expect(mockedActionExecutor.execute).toHaveBeenCalledWith({ actionId: '2', params: { baz: true }, @@ -334,6 +342,9 @@ test('sanitizes invalid relatedSavedObjects when provided', async () => { authorization: 'ApiKey MTIzOmFiYw==', }, }), + taskInfo: { + scheduled: new Date(), + }, }); }); @@ -363,6 +374,9 @@ test(`doesn't use API key when not provided`, async () => { request: expect.objectContaining({ headers: {}, }), + taskInfo: { + scheduled: new Date(), + }, }); const [executeParams] = mockedActionExecutor.execute.mock.calls[0]; diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts index 0515963ab82f4..64169de728f75 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts @@ -72,6 +72,10 @@ export class TaskRunnerFactory { getUnsecuredSavedObjectsClient, } = this.taskRunnerContext!; + const taskInfo = { + scheduled: taskInstance.runAt, + }; + return { async run() { const { spaceId, actionTaskParamsId } = taskInstance.params as Record<string, string>; @@ -118,6 +122,7 @@ export class TaskRunnerFactory { actionId, request: fakeRequest, ...getSourceFromReferences(references), + taskInfo, relatedSavedObjects: validatedRelatedSavedObjects(logger, relatedSavedObjects), }); } catch (e) { diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index c8c9967afca1a..7c05d16923b9d 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -22,7 +22,7 @@ export { ActionTypeExecutorResult } from '../common'; export { GetFieldsByIssueTypeResponse as JiraGetFieldsResponse } from './builtin_action_types/jira/types'; export { GetCommonFieldsResponse as ServiceNowGetFieldsResponse } from './builtin_action_types/servicenow/types'; export { GetCommonFieldsResponse as ResilientGetFieldsResponse } from './builtin_action_types/resilient/types'; - +export { SwimlanePublicConfigurationType } from './builtin_action_types/swimlane/types'; export type WithoutQueryAndParams<T> = Pick<T, Exclude<keyof T, 'query' | 'params'>>; export type GetServicesFunction = (request: KibanaRequest) => Services; export type ActionTypeRegistryContract = PublicMethodsOf<ActionTypeRegistry>; @@ -142,7 +142,7 @@ export interface ProxySettings { proxyBypassHosts: Set<string> | undefined; proxyOnlyHosts: Set<string> | undefined; proxyHeaders?: Record<string, string>; - proxyTLSSettings: TLSSettings; + proxySSLSettings: SSLSettings; } export interface ResponseSettings { @@ -150,6 +150,6 @@ export interface ResponseSettings { timeout: number; } -export interface TLSSettings { +export interface SSLSettings { verificationMode?: 'none' | 'certificate' | 'full'; } diff --git a/x-pack/plugins/actions/server/usage/actions_usage_collector.ts b/x-pack/plugins/actions/server/usage/actions_usage_collector.ts index 06248e1fa95a8..80e0c19092c78 100644 --- a/x-pack/plugins/actions/server/usage/actions_usage_collector.ts +++ b/x-pack/plugins/actions/server/usage/actions_usage_collector.ts @@ -18,6 +18,7 @@ const byTypeSchema: MakeSchemaFrom<ActionsUsage>['count_by_type'] = { __email: { type: 'long' }, __index: { type: 'long' }, __pagerduty: { type: 'long' }, + __swimlane: { type: 'long' }, '__server-log': { type: 'long' }, __slack: { type: 'long' }, __webhook: { type: 'long' }, diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts index 033ffcceb6a0a..1dcd19119b6fd 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts @@ -195,7 +195,6 @@ test('enqueues execution per selected action', async () => { "id": "1", "license": "basic", "name": "name-of-alert", - "namespace": "test1", "ruleset": "alerts", }, }, diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts index 968fff540dc03..3004ed599128e 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts @@ -209,7 +209,6 @@ export function createExecutionHandler< license: alertType.minimumLicenseRequired, category: alertType.id, ruleset: alertType.producer, - ...namespace, name: alertName, }, }; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 8ab267a5610d3..88d1b1b24a4ec 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -282,13 +282,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, } @@ -394,6 +397,10 @@ describe('Task Runner', () => { kind: 'alert', }, kibana: { + task: { + schedule_delay: 0, + scheduled: '1970-01-01T00:00:00.000Z', + }, saved_objects: [ { id: '1', @@ -409,7 +416,6 @@ describe('Task Runner', () => { category: 'test', id: '1', license: 'basic', - namespace: undefined, ruleset: 'alerts', }, }); @@ -518,6 +524,10 @@ describe('Task Runner', () => { alerting: { status: 'active', }, + task: { + schedule_delay: 0, + scheduled: '1970-01-01T00:00:00.000Z', + }, saved_objects: [ { id: '1', @@ -534,7 +544,6 @@ describe('Task Runner', () => { id: '1', license: 'basic', name: 'alert-name', - namespace: undefined, ruleset: 'alerts', }, }); @@ -603,6 +612,10 @@ describe('Task Runner', () => { kind: 'alert', }, kibana: { + task: { + schedule_delay: 0, + scheduled: '1970-01-01T00:00:00.000Z', + }, saved_objects: [ { id: '1', @@ -618,7 +631,6 @@ describe('Task Runner', () => { category: 'test', id: '1', license: 'basic', - namespace: undefined, ruleset: 'alerts', }, }); @@ -700,6 +712,10 @@ describe('Task Runner', () => { alerting: { status: 'active', }, + task: { + schedule_delay: 0, + scheduled: '1970-01-01T00:00:00.000Z', + }, saved_objects: [ { id: '1', @@ -716,7 +732,6 @@ describe('Task Runner', () => { id: '1', license: 'basic', name: 'alert-name', - namespace: undefined, ruleset: 'alerts', }, }); @@ -854,13 +869,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -897,7 +915,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -926,6 +943,10 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert executed: test:1: 'alert-name'", "rule": Object { @@ -933,7 +954,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -1151,13 +1171,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -1194,7 +1217,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -1231,7 +1253,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -1273,7 +1294,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -1302,6 +1322,10 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert executed: test:1: 'alert-name'", "rule": Object { @@ -1309,7 +1333,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -1433,13 +1456,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -1476,7 +1502,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -1513,7 +1538,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -1555,7 +1579,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -1597,7 +1620,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -1626,6 +1648,10 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert executed: test:1: 'alert-name'", "rule": Object { @@ -1633,7 +1659,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -1968,13 +1993,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -2012,7 +2040,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -2049,7 +2076,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -2078,6 +2104,10 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert executed: test:1: 'alert-name'", "rule": Object { @@ -2085,7 +2115,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -2294,13 +2323,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -2333,13 +2365,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution failure: test:1: 'alert-name'", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -2397,13 +2432,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -2436,13 +2474,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "test:1: execution failed", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -2508,13 +2549,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -2547,13 +2591,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "test:1: execution failed", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -2619,13 +2666,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -2658,13 +2708,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "test:1: execution failed", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -2729,13 +2782,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -2768,13 +2824,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "test:1: execution failed", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3007,13 +3066,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3050,7 +3112,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3087,7 +3148,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3124,7 +3184,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3161,7 +3220,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3190,6 +3248,10 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert executed: test:1: 'alert-name'", "rule": Object { @@ -3197,7 +3259,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3291,13 +3352,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3334,7 +3398,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3371,7 +3434,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3400,6 +3462,10 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert executed: test:1: 'alert-name'", "rule": Object { @@ -3407,7 +3473,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3493,13 +3558,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3534,7 +3602,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3569,7 +3636,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3598,6 +3664,10 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert executed: test:1: 'alert-name'", "rule": Object { @@ -3605,7 +3675,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3686,13 +3755,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3729,7 +3801,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3766,7 +3837,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3795,6 +3865,10 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert executed: test:1: 'alert-name'", "rule": Object { @@ -3802,7 +3876,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3885,13 +3958,16 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert execution start: \\"1\\"", "rule": Object { "category": "test", "id": "1", "license": "basic", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3925,7 +4001,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3959,7 +4034,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, @@ -3988,6 +4062,10 @@ describe('Task Runner', () => { "type_id": "test", }, ], + "task": Object { + "schedule_delay": 0, + "scheduled": "1970-01-01T00:00:00.000Z", + }, }, "message": "alert executed: test:1: 'alert-name'", "rule": Object { @@ -3995,7 +4073,6 @@ describe('Task Runner', () => { "id": "1", "license": "basic", "name": "alert-name", - "namespace": undefined, "ruleset": "alerts", }, }, diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index b712b6237c8a7..c66c054bc8ac3 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -54,6 +54,9 @@ import { getEsErrorMessage } from '../lib/errors'; const FALLBACK_RETRY_INTERVAL = '5m'; +// 1,000,000 nanoseconds in 1 millisecond +const Millis2Nanos = 1000 * 1000; + type Event = Exclude<IEvent, undefined>; interface AlertTaskRunResult { @@ -489,15 +492,17 @@ export class TaskRunner< schedule: taskSchedule, } = this.taskInstance; - const runDate = new Date().toISOString(); - this.logger.debug(`executing alert ${this.alertType.id}:${alertId} at ${runDate}`); + const runDate = new Date(); + const runDateString = runDate.toISOString(); + this.logger.debug(`executing alert ${this.alertType.id}:${alertId} at ${runDateString}`); const namespace = this.context.spaceIdToNamespace(spaceId); const eventLogger = this.context.eventLogger; + const scheduleDelay = runDate.getTime() - this.taskInstance.runAt.getTime(); const event: IEvent = { // explicitly set execute timestamp so it will be before other events // generated here (new-instance, schedule-action, etc) - '@timestamp': runDate, + '@timestamp': runDateString, event: { action: EVENT_LOG_ACTIONS.execute, kind: 'alert', @@ -513,13 +518,16 @@ export class TaskRunner< namespace, }, ], + task: { + scheduled: this.taskInstance.runAt.toISOString(), + schedule_delay: Millis2Nanos * scheduleDelay, + }, }, rule: { id: alertId, license: this.alertType.minimumLicenseRequired, category: this.alertType.id, ruleset: this.alertType.producer, - namespace, }, }; @@ -814,7 +822,6 @@ function generateNewAndRecoveredInstanceEvents< license: ruleType.minimumLicenseRequired, category: ruleType.id, ruleset: ruleType.producer, - namespace, name: rule.name, }, }; diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/select_anomaly_severity.test.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/select_anomaly_severity.test.tsx index 7b56eaa4721de..8c8f0aa8b9b24 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/select_anomaly_severity.test.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/select_anomaly_severity.test.tsx @@ -7,7 +7,7 @@ import { render } from '@testing-library/react'; import React, { ReactNode } from 'react'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { ANOMALY_SEVERITY } from '../../../../common/ml_constants'; import { SelectAnomalySeverity } from './select_anomaly_severity'; diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.stories.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.stories.tsx new file mode 100644 index 0000000000000..8cc16dd801c25 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.stories.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, { ComponentType } from 'react'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; +import { + ApmPluginContext, + ApmPluginContextValue, +} from '../../../../context/apm_plugin/apm_plugin_context'; +import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; +import { ErrorDistribution } from './'; + +export default { + title: 'app/ErrorGroupDetails/Distribution', + component: ErrorDistribution, + decorators: [ + (Story: ComponentType) => { + const apmPluginContextMock = ({ + observabilityRuleTypeRegistry: { getFormatter: () => undefined }, + } as unknown) as ApmPluginContextValue; + + const kibanaContextServices = { + uiSettings: { get: () => {} }, + }; + + return ( + <EuiThemeProvider> + <ApmPluginContext.Provider value={apmPluginContextMock}> + <KibanaContextProvider services={kibanaContextServices}> + <Story /> + </KibanaContextProvider> + </ApmPluginContext.Provider> + </EuiThemeProvider> + ); + }, + ], +}; + +export function Example() { + const distribution = { + noHits: false, + bucketSize: 62350, + buckets: [ + { key: 1624279912350, count: 6 }, + { key: 1624279974700, count: 1 }, + { key: 1624280037050, count: 2 }, + { key: 1624280099400, count: 3 }, + { key: 1624280161750, count: 13 }, + { key: 1624280224100, count: 1 }, + { key: 1624280286450, count: 2 }, + { key: 1624280348800, count: 0 }, + { key: 1624280411150, count: 4 }, + { key: 1624280473500, count: 4 }, + { key: 1624280535850, count: 1 }, + { key: 1624280598200, count: 4 }, + { key: 1624280660550, count: 0 }, + { key: 1624280722900, count: 2 }, + { key: 1624280785250, count: 3 }, + { key: 1624280847600, count: 0 }, + ], + }; + + return <ErrorDistribution distribution={distribution} title="Foo title" />; +} + +export function EmptyState() { + return ( + <ErrorDistribution + distribution={{ + bucketSize: 10, + buckets: [], + noHits: true, + }} + title="Foo title" + /> + ); +} diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx index 643653c24aeb3..e53aaf97cdf75 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx @@ -67,6 +67,7 @@ export function ErrorDistribution({ distribution, title }: Props) { const xFormatter = niceTimeFormatter([xMin, xMax]); const { observabilityRuleTypeRegistry } = useApmPluginContext(); + const { alerts } = useApmServiceContext(); const { getFormatter } = observabilityRuleTypeRegistry; const [selectedAlertId, setSelectedAlertId] = useState<string | undefined>( @@ -84,7 +85,7 @@ export function ErrorDistribution({ distribution, title }: Props) { }; return ( - <div> + <> <EuiTitle size="xs"> <span>{title}</span> </EuiTitle> @@ -124,7 +125,7 @@ export function ErrorDistribution({ distribution, title }: Props) { alerts: alerts?.filter( (alert) => alert[RULE_ID]?.[0] === AlertType.ErrorCount ), - chartStartTime: buckets[0].x0, + chartStartTime: buckets[0]?.x0, getFormatter, selectedAlertId, setSelectedAlertId, @@ -143,6 +144,6 @@ export function ErrorDistribution({ distribution, title }: Props) { </Suspense> </Chart> </div> - </div> + </> ); } 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 index 20d930d28599f..63ba7047696ca 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx @@ -47,10 +47,11 @@ export function UXActionMenu({ const uxExploratoryViewLink = createExploratoryViewUrl( { - 'ux-series': { + 'ux-series': ({ dataType: 'ux', + isNew: true, time: { from: rangeFrom, to: rangeTo }, - } as SeriesUrl, + } as unknown) as SeriesUrl, }, http?.basePath.get() ); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx index e0486af6cd6ef..5c63cc24b6fdf 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx @@ -89,7 +89,7 @@ export function PageLoadDistribution() { { [`${serviceName}-page-views`]: { dataType: 'ux', - reportType: 'dist', + reportType: 'data-distribution', time: { from: rangeFrom!, to: rangeTo! }, reportDefinitions: { 'service.name': serviceName as string[], diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx index c45637e5d3c82..667d0b5e4b4db 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx @@ -64,7 +64,7 @@ export function PageViewsTrend() { { [`${serviceName}-page-views`]: { dataType: 'ux', - reportType: 'kpi', + reportType: 'kpi-over-time', time: { from: rangeFrom!, to: rangeTo! }, reportDefinitions: { 'service.name': serviceName as string[], diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx index 8549f09bba248..09fbf07b8ecbd 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx @@ -5,10 +5,21 @@ * 2.0. */ +import { ReactNode } from 'react'; +import { StyledComponent } from 'styled-components'; import { EuiFlyout } from '@elastic/eui'; -import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; +import { + euiStyled, + EuiTheme, +} from '../../../../../../../../../../src/plugins/kibana_react/common'; -export const ResponsiveFlyout = euiStyled(EuiFlyout)` +// TODO: EUI team follow up on complex types and styled-components `styled` +// https://github.com/elastic/eui/issues/4855 +export const ResponsiveFlyout: StyledComponent< + typeof EuiFlyout, + EuiTheme, + { children?: ReactNode } +> = euiStyled(EuiFlyout)` width: 100%; @media (min-width: 800px) { diff --git a/x-pack/plugins/apm/public/components/routing/app_root.tsx b/x-pack/plugins/apm/public/components/routing/app_root.tsx index 2bb387ae315ff..8fc59a01eeca0 100644 --- a/x-pack/plugins/apm/public/components/routing/app_root.tsx +++ b/x-pack/plugins/apm/public/components/routing/app_root.tsx @@ -24,7 +24,7 @@ import { } from '../../context/apm_plugin/apm_plugin_context'; import { LicenseProvider } from '../../context/license/license_context'; import { UrlParamsProvider } from '../../context/url_params_context/url_params_context'; -import { useBreadcrumbs } from '../../hooks/use_breadcrumbs'; +import { useApmBreadcrumbs } from '../../hooks/use_apm_breadcrumbs'; import { ApmPluginStartDeps } from '../../plugin'; import { HeaderMenuPortal } from '../../../../observability/public'; import { ApmHeaderActionMenu } from '../shared/apm_header_action_menu'; @@ -79,7 +79,7 @@ export function ApmAppRoot({ } function MountApmHeaderActionMenu() { - useBreadcrumbs(apmRouteConfig); + useApmBreadcrumbs(apmRouteConfig); const { setHeaderActionMenu } = useApmPluginContext().appMountParameters; return ( diff --git a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx index a16f81826636b..bcc1932dde7cb 100644 --- a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx @@ -44,6 +44,7 @@ const mockCore = { ml: {}, }, currentAppId$: new Observable(), + getUrlForApp: (appId: string) => '', navigateToUrl: (url: string) => {}, }, chrome: { diff --git a/x-pack/plugins/apm/public/hooks/use_breadcrumbs.test.tsx b/x-pack/plugins/apm/public/hooks/use_apm_breadcrumbs.test.tsx similarity index 79% rename from x-pack/plugins/apm/public/hooks/use_breadcrumbs.test.tsx rename to x-pack/plugins/apm/public/hooks/use_apm_breadcrumbs.test.tsx index 64990651b52bb..1cdb84c324750 100644 --- a/x-pack/plugins/apm/public/hooks/use_breadcrumbs.test.tsx +++ b/x-pack/plugins/apm/public/hooks/use_apm_breadcrumbs.test.tsx @@ -15,14 +15,15 @@ import { mockApmPluginContextValue, MockApmPluginContextWrapper, } from '../context/apm_plugin/mock_apm_plugin_context'; -import { useBreadcrumbs } from './use_breadcrumbs'; +import { useApmBreadcrumbs } from './use_apm_breadcrumbs'; +import { useBreadcrumbs } from '../../../observability/public'; + +jest.mock('../../../observability/public'); function createWrapper(path: string) { return ({ children }: { children?: ReactNode }) => { const value = (produce(mockApmPluginContextValue, (draft) => { draft.core.application.navigateToUrl = (url: string) => Promise.resolve(); - draft.core.chrome.docTitle.change = changeTitle; - draft.core.chrome.setBreadcrumbs = setBreadcrumbs; }) as unknown) as ApmPluginContextValue; return ( @@ -36,27 +37,18 @@ function createWrapper(path: string) { } function mountBreadcrumb(path: string) { - renderHook(() => useBreadcrumbs(apmRouteConfig), { + renderHook(() => useApmBreadcrumbs(apmRouteConfig), { wrapper: createWrapper(path), }); } -const changeTitle = jest.fn(); -const setBreadcrumbs = jest.fn(); - -describe('useBreadcrumbs', () => { - it('changes the page title', () => { - mountBreadcrumb('/'); - - expect(changeTitle).toHaveBeenCalledWith(['APM']); - }); - +describe('useApmBreadcrumbs', () => { test('/services/:serviceName/errors/:groupId', () => { mountBreadcrumb( '/services/opbeans-node/errors/myGroupId?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0' ); - expect(setBreadcrumbs).toHaveBeenCalledWith( + expect(useBreadcrumbs).toHaveBeenCalledWith( expect.arrayContaining([ expect.objectContaining({ text: 'APM', @@ -81,20 +73,12 @@ describe('useBreadcrumbs', () => { expect.objectContaining({ text: 'myGroupId', href: undefined }), ]) ); - - expect(changeTitle).toHaveBeenCalledWith([ - 'myGroupId', - 'Errors', - 'opbeans-node', - 'Services', - 'APM', - ]); }); test('/services/:serviceName/errors', () => { mountBreadcrumb('/services/opbeans-node/errors?kuery=myKuery'); - expect(setBreadcrumbs).toHaveBeenCalledWith( + expect(useBreadcrumbs).toHaveBeenCalledWith( expect.arrayContaining([ expect.objectContaining({ text: 'APM', @@ -111,19 +95,12 @@ describe('useBreadcrumbs', () => { expect.objectContaining({ text: 'Errors', href: undefined }), ]) ); - - expect(changeTitle).toHaveBeenCalledWith([ - 'Errors', - 'opbeans-node', - 'Services', - 'APM', - ]); }); test('/services/:serviceName/transactions', () => { mountBreadcrumb('/services/opbeans-node/transactions?kuery=myKuery'); - expect(setBreadcrumbs).toHaveBeenCalledWith( + expect(useBreadcrumbs).toHaveBeenCalledWith( expect.arrayContaining([ expect.objectContaining({ text: 'APM', @@ -140,13 +117,6 @@ describe('useBreadcrumbs', () => { expect.objectContaining({ text: 'Transactions', href: undefined }), ]) ); - - expect(changeTitle).toHaveBeenCalledWith([ - 'Transactions', - 'opbeans-node', - 'Services', - 'APM', - ]); }); test('/services/:serviceName/transactions/view?transactionName=my-transaction-name', () => { @@ -154,7 +124,7 @@ describe('useBreadcrumbs', () => { '/services/opbeans-node/transactions/view?kuery=myKuery&transactionName=my-transaction-name' ); - expect(setBreadcrumbs).toHaveBeenCalledWith( + expect(useBreadcrumbs).toHaveBeenCalledWith( expect.arrayContaining([ expect.objectContaining({ text: 'APM', @@ -179,13 +149,5 @@ describe('useBreadcrumbs', () => { }), ]) ); - - expect(changeTitle).toHaveBeenCalledWith([ - 'my-transaction-name', - 'Transactions', - 'opbeans-node', - 'Services', - 'APM', - ]); }); }); diff --git a/x-pack/plugins/apm/public/hooks/use_breadcrumbs.ts b/x-pack/plugins/apm/public/hooks/use_apm_breadcrumbs.ts similarity index 85% rename from x-pack/plugins/apm/public/hooks/use_breadcrumbs.ts rename to x-pack/plugins/apm/public/hooks/use_apm_breadcrumbs.ts index d907c27319d26..d64bcadf79577 100644 --- a/x-pack/plugins/apm/public/hooks/use_breadcrumbs.ts +++ b/x-pack/plugins/apm/public/hooks/use_apm_breadcrumbs.ts @@ -7,14 +7,15 @@ import { History, Location } from 'history'; import { ChromeBreadcrumb } from 'kibana/public'; -import { MouseEvent, ReactNode, useEffect } from 'react'; +import { MouseEvent } from 'react'; import { + match as Match, matchPath, RouteComponentProps, useHistory, - match as Match, useLocation, } from 'react-router-dom'; +import { useBreadcrumbs } from '../../../observability/public'; import { APMRouteDefinition, BreadcrumbTitle } from '../application/routes'; import { getAPMHref } from '../components/shared/Links/apm/APMLink'; import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context'; @@ -164,33 +165,17 @@ function routeDefinitionsToBreadcrumbs({ return breadcrumbs; } -/** - * Get an array for a page title from a list of breadcrumbs - */ -function getTitleFromBreadcrumbs(breadcrumbs: ChromeBreadcrumb[]): string[] { - function removeNonStrings(item: ReactNode): item is string { - return typeof item === 'string'; - } - - return breadcrumbs - .map(({ text }) => text) - .reverse() - .filter(removeNonStrings); -} - /** * Determine the breadcrumbs from the routes, set them, and update the page * title when the route changes. */ -export function useBreadcrumbs(routes: APMRouteDefinition[]) { +export function useApmBreadcrumbs(routes: APMRouteDefinition[]) { const history = useHistory(); const location = useLocation(); const { search } = location; const { core } = useApmPluginContext(); const { basePath } = core.http; const { navigateToUrl } = core.application; - const { docTitle, setBreadcrumbs } = core.chrome; - const changeTitle = docTitle.change; function wrappedGetAPMHref(path: string) { return getAPMHref({ basePath, path, search }); @@ -206,10 +191,6 @@ export function useBreadcrumbs(routes: APMRouteDefinition[]) { wrappedGetAPMHref, navigateToUrl, }); - const title = getTitleFromBreadcrumbs(breadcrumbs); - useEffect(() => { - changeTitle(title); - setBreadcrumbs(breadcrumbs); - }, [breadcrumbs, changeTitle, location, title, setBreadcrumbs]); + useBreadcrumbs(breadcrumbs); } diff --git a/x-pack/plugins/apm/server/lib/fleet/source_maps.test.ts b/x-pack/plugins/apm/server/lib/fleet/source_maps.test.ts index 61a4fa4436e69..d6a1770a91591 100644 --- a/x-pack/plugins/apm/server/lib/fleet/source_maps.test.ts +++ b/x-pack/plugins/apm/server/lib/fleet/source_maps.test.ts @@ -103,12 +103,51 @@ const artifacts = [ describe('Source maps', () => { describe('getPackagePolicyWithSourceMap', () => { - it('returns unchanged package policy when artifacts is empty', () => { + it('removes source map from package policy', () => { + const packagePolicyWithSourceMaps = { + ...packagePolicy, + inputs: [ + { + ...packagePolicy.inputs[0], + compiled_input: { + 'apm-server': { + ...packagePolicy.inputs[0].compiled_input['apm-server'], + value: { + rum: { + source_mapping: { + metadata: [ + { + 'service.name': 'service_name', + 'service.version': '1.0.0', + 'bundle.filepath': + 'http://localhost:3000/static/js/main.chunk.js', + 'sourcemap.url': + '/api/fleet/artifacts/service_name-1.0.0/my-id-1', + }, + { + 'service.name': 'service_name', + 'service.version': '2.0.0', + 'bundle.filepath': + 'http://localhost:3000/static/js/main.chunk.js', + 'sourcemap.url': + '/api/fleet/artifacts/service_name-2.0.0/my-id-2', + }, + ], + }, + }, + }, + }, + }, + }, + ], + }; const updatedPackagePolicy = getPackagePolicyWithSourceMap({ - packagePolicy, + packagePolicy: packagePolicyWithSourceMaps, artifacts: [], }); - expect(updatedPackagePolicy).toEqual(packagePolicy); + expect(updatedPackagePolicy.inputs[0].config).toEqual({ + 'apm-server': { value: { rum: { source_mapping: { metadata: [] } } } }, + }); }); it('adds source maps into the package policy', () => { const updatedPackagePolicy = getPackagePolicyWithSourceMap({ diff --git a/x-pack/plugins/apm/server/lib/fleet/source_maps.ts b/x-pack/plugins/apm/server/lib/fleet/source_maps.ts index b313fbad2806f..6d608f7751f3b 100644 --- a/x-pack/plugins/apm/server/lib/fleet/source_maps.ts +++ b/x-pack/plugins/apm/server/lib/fleet/source_maps.ts @@ -97,9 +97,6 @@ export function getPackagePolicyWithSourceMap({ packagePolicy: PackagePolicy; artifacts: ArtifactSourceMap[]; }) { - if (!artifacts.length) { - return packagePolicy; - } const [firstInput, ...restInputs] = packagePolicy.inputs; return { ...packagePolicy, diff --git a/x-pack/plugins/apm/server/routes/source_maps.ts b/x-pack/plugins/apm/server/routes/source_maps.ts index 24ea825774b0a..f6d160e68a76a 100644 --- a/x-pack/plugins/apm/server/routes/source_maps.ts +++ b/x-pack/plugins/apm/server/routes/source_maps.ts @@ -5,6 +5,7 @@ * 2.0. */ import Boom from '@hapi/boom'; +import { jsonRt } from '@kbn/io-ts-utils'; import * as t from 'io-ts'; import { SavedObjectsClientContract } from 'kibana/server'; import { @@ -34,7 +35,7 @@ export const sourceMapRt = t.intersection([ const listSourceMapRoute = createApmServerRoute({ endpoint: 'GET /api/apm/sourcemaps', options: { tags: ['access:apm'] }, - handler: async ({ plugins, logger }) => { + handler: async ({ plugins }) => { try { const fleetPluginStart = await plugins.fleet?.start(); if (fleetPluginStart) { @@ -51,21 +52,26 @@ const listSourceMapRoute = createApmServerRoute({ }); const uploadSourceMapRoute = createApmServerRoute({ - endpoint: 'POST /api/apm/sourcemaps/{serviceName}/{serviceVersion}', - options: { tags: ['access:apm', 'access:apm_write'] }, + endpoint: 'POST /api/apm/sourcemaps', + options: { + tags: ['access:apm', 'access:apm_write'], + body: { accepts: ['multipart/form-data'] }, + }, params: t.type({ - path: t.type({ - serviceName: t.string, - serviceVersion: t.string, - }), body: t.type({ - bundleFilepath: t.string, - sourceMap: sourceMapRt, + service_name: t.string, + service_version: t.string, + bundle_filepath: t.string, + sourcemap: jsonRt.pipe(sourceMapRt), }), }), handler: async ({ params, plugins, core }) => { - const { serviceName, serviceVersion } = params.path; - const { bundleFilepath, sourceMap } = params.body; + const { + service_name: serviceName, + service_version: serviceVersion, + bundle_filepath: bundleFilepath, + sourcemap: sourceMap, + } = params.body; const fleetPluginStart = await plugins.fleet?.start(); const coreStart = await core.start(); const esClient = coreStart.elasticsearch.client.asInternalUser; @@ -107,7 +113,7 @@ const deleteSourceMapRoute = createApmServerRoute({ id: t.string, }), }), - handler: async ({ context, params, plugins, core }) => { + handler: async ({ params, plugins, core }) => { const fleetPluginStart = await plugins.fleet?.start(); const { id } = params.path; const coreStart = await core.start(); diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts index 13bd631085aac..474464dec1f99 100644 --- a/x-pack/plugins/apm/server/routes/typings.ts +++ b/x-pack/plugins/apm/server/routes/typings.ts @@ -39,6 +39,7 @@ export interface APMRouteCreateOptions { | 'access:ml:canGetJobs' | 'access:ml:canCreateJob' >; + body?: { accepts: Array<'application/json' | 'multipart/form-data'> }; }; } diff --git a/x-pack/plugins/canvas/CONTRIBUTING.md b/x-pack/plugins/canvas/CONTRIBUTING.md index d3bff67771244..d8a657ea73c40 100644 --- a/x-pack/plugins/canvas/CONTRIBUTING.md +++ b/x-pack/plugins/canvas/CONTRIBUTING.md @@ -36,8 +36,8 @@ To keep the code terse, Canvas uses i18n "dictionaries": abstracted, static sing ```js -// i18n/components.ts -export const ComponentStrings = { +// asset_manager.tsx +const strings = { // ... AssetManager: { getCopyAssetMessage: (id: string) => @@ -52,10 +52,6 @@ export const ComponentStrings = { // ... }; -// asset_manager.tsx -import { ComponentStrings } from '../../../i18n'; -const { AssetManager: strings } = ComponentStrings; - const text = ( <EuiText> {strings.getSpaceUsedText(percentageUsed)} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/advanced_filter.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/advanced_filter.tsx index 4dfb4c3f09273..b5c009abc2768 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/advanced_filter.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/component/advanced_filter.tsx @@ -5,12 +5,22 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import PropTypes from 'prop-types'; import React, { FunctionComponent } from 'react'; -import { ComponentStrings } from '../../../../../i18n'; +import PropTypes from 'prop-types'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; -const { AdvancedFilter: strings } = ComponentStrings; +const strings = { + getApplyButtonLabel: () => + i18n.translate('xpack.canvas.renderer.advancedFilter.applyButtonLabel', { + defaultMessage: 'Apply', + description: 'This refers to applying the filter to the Canvas workpad', + }), + getInputPlaceholder: () => + i18n.translate('xpack.canvas.renderer.advancedFilter.inputPlaceholder', { + defaultMessage: 'Enter filter expression', + }), +}; export interface Props { /** Optional value for the component */ diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.tsx index 86517c897f02d..43f2e1ecc84f3 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/component/dropdown_filter.tsx @@ -5,12 +5,18 @@ * 2.0. */ -import { EuiIcon } from '@elastic/eui'; -import PropTypes from 'prop-types'; import React, { ChangeEvent, FocusEvent, FunctionComponent } from 'react'; -import { ComponentStrings } from '../../../../../i18n'; +import PropTypes from 'prop-types'; +import { EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; -const { DropdownFilter: strings } = ComponentStrings; +const strings = { + getMatchAllOptionLabel: () => + i18n.translate('xpack.canvas.renderer.dropdownFilter.matchAllOptionLabel', { + defaultMessage: 'ANY', + description: 'The dropdown filter option to match any value in the field.', + }), +}; export interface Props { /** diff --git a/x-pack/plugins/canvas/i18n/components.ts b/x-pack/plugins/canvas/i18n/components.ts deleted file mode 100644 index 6f011bb73e3b0..0000000000000 --- a/x-pack/plugins/canvas/i18n/components.ts +++ /dev/null @@ -1,1543 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import { BOLD_MD_TOKEN, CANVAS, HTML, JSON, PDF, URL, ZIP } from './constants'; - -export const ComponentStrings = { - AddEmbeddableFlyout: { - getNoItemsText: () => - i18n.translate('xpack.canvas.embedObject.noMatchingObjectsMessage', { - defaultMessage: 'No matching objects found.', - }), - getTitleText: () => - i18n.translate('xpack.canvas.embedObject.titleText', { - defaultMessage: 'Add from Kibana', - }), - }, - AdvancedFilter: { - getApplyButtonLabel: () => - i18n.translate('xpack.canvas.renderer.advancedFilter.applyButtonLabel', { - defaultMessage: 'Apply', - description: 'This refers to applying the filter to the Canvas workpad', - }), - getInputPlaceholder: () => - i18n.translate('xpack.canvas.renderer.advancedFilter.inputPlaceholder', { - defaultMessage: 'Enter filter expression', - }), - }, - App: { - getLoadErrorMessage: (error: string) => - i18n.translate('xpack.canvas.app.loadErrorMessage', { - defaultMessage: 'Message: {error}', - values: { - error, - }, - }), - getLoadErrorTitle: () => - i18n.translate('xpack.canvas.app.loadErrorTitle', { - defaultMessage: 'Canvas failed to load', - }), - getLoadingMessage: () => - i18n.translate('xpack.canvas.app.loadingMessage', { - defaultMessage: 'Canvas is loading', - }), - }, - ArgAddPopover: { - getAddAriaLabel: () => - i18n.translate('xpack.canvas.argAddPopover.addAriaLabel', { - defaultMessage: 'Add argument', - }), - }, - ArgFormAdvancedFailure: { - getApplyButtonLabel: () => - i18n.translate('xpack.canvas.argFormAdvancedFailure.applyButtonLabel', { - defaultMessage: 'Apply', - }), - getResetButtonLabel: () => - i18n.translate('xpack.canvas.argFormAdvancedFailure.resetButtonLabel', { - defaultMessage: 'Reset', - }), - getRowErrorMessage: () => - i18n.translate('xpack.canvas.argFormAdvancedFailure.rowErrorMessage', { - defaultMessage: 'Invalid Expression', - }), - }, - ArgFormArgSimpleForm: { - getRemoveAriaLabel: () => - i18n.translate('xpack.canvas.argFormArgSimpleForm.removeAriaLabel', { - defaultMessage: 'Remove', - }), - getRequiredTooltip: () => - i18n.translate('xpack.canvas.argFormArgSimpleForm.requiredTooltip', { - defaultMessage: 'This argument is required, you should specify a value.', - }), - }, - ArgFormPendingArgValue: { - getLoadingMessage: () => - i18n.translate('xpack.canvas.argFormPendingArgValue.loadingMessage', { - defaultMessage: 'Loading', - }), - }, - ArgFormSimpleFailure: { - getFailureTooltip: () => - i18n.translate('xpack.canvas.argFormSimpleFailure.failureTooltip', { - defaultMessage: - 'The interface for this argument could not parse the value, so a fallback input is being used', - }), - }, - Asset: { - getCopyAssetTooltip: () => - i18n.translate('xpack.canvas.asset.copyAssetTooltip', { - defaultMessage: 'Copy id to clipboard', - }), - getCreateImageTooltip: () => - i18n.translate('xpack.canvas.asset.createImageTooltip', { - defaultMessage: 'Create image element', - }), - getDeleteAssetTooltip: () => - i18n.translate('xpack.canvas.asset.deleteAssetTooltip', { - defaultMessage: 'Delete', - }), - getDownloadAssetTooltip: () => - i18n.translate('xpack.canvas.asset.downloadAssetTooltip', { - defaultMessage: 'Download', - }), - getThumbnailAltText: () => - i18n.translate('xpack.canvas.asset.thumbnailAltText', { - defaultMessage: 'Asset thumbnail', - }), - getConfirmModalButtonLabel: () => - i18n.translate('xpack.canvas.asset.confirmModalButtonLabel', { - defaultMessage: 'Remove', - }), - getConfirmModalMessageText: () => - i18n.translate('xpack.canvas.asset.confirmModalDetail', { - defaultMessage: 'Are you sure you want to remove this asset?', - }), - getConfirmModalTitle: () => - i18n.translate('xpack.canvas.asset.confirmModalTitle', { - defaultMessage: 'Remove Asset', - }), - }, - AssetManager: { - getButtonLabel: () => - i18n.translate('xpack.canvas.assetManager.manageButtonLabel', { - defaultMessage: 'Manage assets', - }), - getDescription: () => - i18n.translate('xpack.canvas.assetModal.modalDescription', { - defaultMessage: - 'Below are the image assets in this workpad. Any assets that are currently in use cannot be determined at this time. To reclaim space, delete assets.', - }), - getEmptyAssetsDescription: () => - i18n.translate('xpack.canvas.assetModal.emptyAssetsDescription', { - defaultMessage: 'Import your assets to get started', - }), - getFilePickerPromptText: () => - i18n.translate('xpack.canvas.assetModal.filePickerPromptText', { - defaultMessage: 'Select or drag and drop images', - }), - getLoadingText: () => - i18n.translate('xpack.canvas.assetModal.loadingText', { - defaultMessage: 'Uploading images', - }), - getModalCloseButtonLabel: () => - i18n.translate('xpack.canvas.assetModal.modalCloseButtonLabel', { - defaultMessage: 'Close', - }), - getModalTitle: () => - i18n.translate('xpack.canvas.assetModal.modalTitle', { - defaultMessage: 'Manage workpad assets', - }), - getSpaceUsedText: (percentageUsed: number) => - i18n.translate('xpack.canvas.assetModal.spacedUsedText', { - defaultMessage: '{percentageUsed}% space used', - values: { - percentageUsed, - }, - }), - getCopyAssetMessage: (id: string) => - i18n.translate('xpack.canvas.assetModal.copyAssetMessage', { - defaultMessage: `Copied '{id}' to clipboard`, - values: { - id, - }, - }), - }, - AssetPicker: { - getAssetAltText: () => - i18n.translate('xpack.canvas.assetpicker.assetAltText', { - defaultMessage: 'Asset thumbnail', - }), - }, - CanvasLoading: { - getLoadingLabel: () => - i18n.translate('xpack.canvas.canvasLoading.loadingMessage', { - defaultMessage: 'Loading', - }), - }, - ColorManager: { - getAddAriaLabel: () => - i18n.translate('xpack.canvas.colorManager.addAriaLabel', { - defaultMessage: 'Add Color', - }), - getCodePlaceholder: () => - i18n.translate('xpack.canvas.colorManager.codePlaceholder', { - defaultMessage: 'Color code', - }), - getRemoveAriaLabel: () => - i18n.translate('xpack.canvas.colorManager.removeAriaLabel', { - defaultMessage: 'Remove Color', - }), - }, - CustomElementModal: { - getCancelButtonLabel: () => - i18n.translate('xpack.canvas.customElementModal.cancelButtonLabel', { - defaultMessage: 'Cancel', - }), - getCharactersRemainingDescription: (numberOfRemainingCharacter: number) => - i18n.translate('xpack.canvas.customElementModal.remainingCharactersDescription', { - defaultMessage: '{numberOfRemainingCharacter} characters remaining', - values: { - numberOfRemainingCharacter, - }, - }), - getDescriptionInputLabel: () => - i18n.translate('xpack.canvas.customElementModal.descriptionInputLabel', { - defaultMessage: 'Description', - }), - getElementPreviewTitle: () => - i18n.translate('xpack.canvas.customElementModal.elementPreviewTitle', { - defaultMessage: 'Element preview', - }), - getImageFilePickerPlaceholder: () => - i18n.translate('xpack.canvas.customElementModal.imageFilePickerPlaceholder', { - defaultMessage: 'Select or drag and drop an image', - }), - getImageInputDescription: () => - i18n.translate('xpack.canvas.customElementModal.imageInputDescription', { - defaultMessage: - 'Take a screenshot of your element and upload it here. This can also be done after saving.', - }), - getImageInputLabel: () => - i18n.translate('xpack.canvas.customElementModal.imageInputLabel', { - defaultMessage: 'Thumbnail image', - }), - getNameInputLabel: () => - i18n.translate('xpack.canvas.customElementModal.nameInputLabel', { - defaultMessage: 'Name', - }), - getSaveButtonLabel: () => - i18n.translate('xpack.canvas.customElementModal.saveButtonLabel', { - defaultMessage: 'Save', - }), - }, - DatasourceDatasourceComponent: { - getChangeButtonLabel: () => - i18n.translate('xpack.canvas.datasourceDatasourceComponent.changeButtonLabel', { - defaultMessage: 'Change element data source', - }), - getExpressionArgDescription: () => - i18n.translate('xpack.canvas.datasourceDatasourceComponent.expressionArgDescription', { - defaultMessage: - 'The datasource has an argument controlled by an expression. Use the expression editor to modify the datasource.', - }), - getPreviewButtonLabel: () => - i18n.translate('xpack.canvas.datasourceDatasourceComponent.previewButtonLabel', { - defaultMessage: 'Preview data', - }), - getSaveButtonLabel: () => - i18n.translate('xpack.canvas.datasourceDatasourceComponent.saveButtonLabel', { - defaultMessage: 'Save', - }), - }, - DatasourceDatasourcePreview: { - getEmptyFirstLineDescription: () => - i18n.translate('xpack.canvas.datasourceDatasourcePreview.emptyFirstLineDescription', { - defaultMessage: "We couldn't find any documents matching your search criteria.", - }), - getEmptySecondLineDescription: () => - i18n.translate('xpack.canvas.datasourceDatasourcePreview.emptySecondLineDescription', { - defaultMessage: 'Check your datasource settings and try again.', - }), - getEmptyTitle: () => - i18n.translate('xpack.canvas.datasourceDatasourcePreview.emptyTitle', { - defaultMessage: 'No documents found', - }), - getModalTitle: () => - i18n.translate('xpack.canvas.datasourceDatasourcePreview.modalTitle', { - defaultMessage: 'Datasource preview', - }), - }, - DatasourceNoDatasource: { - getPanelDescription: () => - i18n.translate('xpack.canvas.datasourceNoDatasource.panelDescription', { - defaultMessage: - "This element does not have an attached data source. This is usually because the element is an image or other static asset. If that's not the case you might want to check your expression to make sure it is not malformed.", - }), - getPanelTitle: () => - i18n.translate('xpack.canvas.datasourceNoDatasource.panelTitle', { - defaultMessage: 'No data source present', - }), - }, - DropdownFilter: { - getMatchAllOptionLabel: () => - i18n.translate('xpack.canvas.renderer.dropdownFilter.matchAllOptionLabel', { - defaultMessage: 'ANY', - description: 'The dropdown filter option to match any value in the field.', - }), - }, - ElementConfig: { - getFailedLabel: () => - i18n.translate('xpack.canvas.elementConfig.failedLabel', { - defaultMessage: 'Failed', - description: - 'The label for the total number of elements in a workpad that have thrown an error or failed to load', - }), - getLoadedLabel: () => - i18n.translate('xpack.canvas.elementConfig.loadedLabel', { - defaultMessage: 'Loaded', - description: 'The label for the number of elements in a workpad that have loaded', - }), - getProgressLabel: () => - i18n.translate('xpack.canvas.elementConfig.progressLabel', { - defaultMessage: 'Progress', - description: 'The label for the percentage of elements that have finished loading', - }), - getTitle: () => - i18n.translate('xpack.canvas.elementConfig.title', { - defaultMessage: 'Element status', - description: - '"Elements" refers to the individual text, images, or visualizations that you can add to a Canvas workpad', - }), - getTotalLabel: () => - i18n.translate('xpack.canvas.elementConfig.totalLabel', { - defaultMessage: 'Total', - description: 'The label for the total number of elements in a workpad', - }), - }, - ElementControls: { - getDeleteAriaLabel: () => - i18n.translate('xpack.canvas.elementControls.deleteAriaLabel', { - defaultMessage: 'Delete element', - }), - getDeleteTooltip: () => - i18n.translate('xpack.canvas.elementControls.deleteToolTip', { - defaultMessage: 'Delete', - }), - getEditAriaLabel: () => - i18n.translate('xpack.canvas.elementControls.editAriaLabel', { - defaultMessage: 'Edit element', - }), - getEditTooltip: () => - i18n.translate('xpack.canvas.elementControls.editToolTip', { - defaultMessage: 'Edit', - }), - }, - ElementSettings: { - getDataTabLabel: () => - i18n.translate('xpack.canvas.elementSettings.dataTabLabel', { - defaultMessage: 'Data', - description: - 'This tab contains the settings for the data (i.e. Elasticsearch query) used as ' + - 'the source for a Canvas element', - }), - getDisplayTabLabel: () => - i18n.translate('xpack.canvas.elementSettings.displayTabLabel', { - defaultMessage: 'Display', - description: 'This tab contains the settings for how data is displayed in a Canvas element', - }), - }, - Error: { - getDescription: () => - i18n.translate('xpack.canvas.errorComponent.description', { - defaultMessage: 'Expression failed with the message:', - }), - getTitle: () => - i18n.translate('xpack.canvas.errorComponent.title', { - defaultMessage: 'Whoops! Expression failed', - }), - }, - Expression: { - getCancelButtonLabel: () => - i18n.translate('xpack.canvas.expression.cancelButtonLabel', { - defaultMessage: 'Cancel', - }), - getCloseButtonLabel: () => - i18n.translate('xpack.canvas.expression.closeButtonLabel', { - defaultMessage: 'Close', - }), - getLearnLinkText: () => - i18n.translate('xpack.canvas.expression.learnLinkText', { - defaultMessage: 'Learn expression syntax', - }), - getMaximizeButtonLabel: () => - i18n.translate('xpack.canvas.expression.maximizeButtonLabel', { - defaultMessage: 'Maximize editor', - }), - getMinimizeButtonLabel: () => - i18n.translate('xpack.canvas.expression.minimizeButtonLabel', { - defaultMessage: 'Minimize Editor', - }), - getRunButtonLabel: () => - i18n.translate('xpack.canvas.expression.runButtonLabel', { - defaultMessage: 'Run', - }), - getRunTooltip: () => - i18n.translate('xpack.canvas.expression.runTooltip', { - defaultMessage: 'Run the expression', - }), - }, - ExpressionElementNotSelected: { - getCloseButtonLabel: () => - i18n.translate('xpack.canvas.expressionElementNotSelected.closeButtonLabel', { - defaultMessage: 'Close', - }), - getSelectDescription: () => - i18n.translate('xpack.canvas.expressionElementNotSelected.selectDescription', { - defaultMessage: 'Select an element to show expression input', - }), - }, - ExpressionInput: { - getArgReferenceAliasesDetail: (aliases: string) => - i18n.translate('xpack.canvas.expressionInput.argReferenceAliasesDetail', { - defaultMessage: '{BOLD_MD_TOKEN}Aliases{BOLD_MD_TOKEN}: {aliases}', - values: { - BOLD_MD_TOKEN, - aliases, - }, - }), - getArgReferenceDefaultDetail: (defaultVal: string) => - i18n.translate('xpack.canvas.expressionInput.argReferenceDefaultDetail', { - defaultMessage: '{BOLD_MD_TOKEN}Default{BOLD_MD_TOKEN}: {defaultVal}', - values: { - BOLD_MD_TOKEN, - defaultVal, - }, - }), - getArgReferenceRequiredDetail: (required: string) => - i18n.translate('xpack.canvas.expressionInput.argReferenceRequiredDetail', { - defaultMessage: '{BOLD_MD_TOKEN}Required{BOLD_MD_TOKEN}: {required}', - values: { - BOLD_MD_TOKEN, - required, - }, - }), - getArgReferenceTypesDetail: (types: string) => - i18n.translate('xpack.canvas.expressionInput.argReferenceTypesDetail', { - defaultMessage: '{BOLD_MD_TOKEN}Types{BOLD_MD_TOKEN}: {types}', - values: { - BOLD_MD_TOKEN, - types, - }, - }), - getFunctionReferenceAcceptsDetail: (acceptTypes: string) => - i18n.translate('xpack.canvas.expressionInput.functionReferenceAccepts', { - defaultMessage: '{BOLD_MD_TOKEN}Accepts{BOLD_MD_TOKEN}: {acceptTypes}', - values: { - BOLD_MD_TOKEN, - acceptTypes, - }, - }), - getFunctionReferenceReturnsDetail: (returnType: string) => - i18n.translate('xpack.canvas.expressionInput.functionReferenceReturns', { - defaultMessage: '{BOLD_MD_TOKEN}Returns{BOLD_MD_TOKEN}: {returnType}', - values: { - BOLD_MD_TOKEN, - returnType, - }, - }), - }, - FunctionFormContextError: { - getContextErrorMessage: (errorMessage: string) => - i18n.translate('xpack.canvas.functionForm.contextError', { - defaultMessage: 'ERROR: {errorMessage}', - values: { - errorMessage, - }, - }), - }, - FunctionFormFunctionUnknown: { - getUnknownArgumentTypeErrorMessage: (expressionType: string) => - i18n.translate('xpack.canvas.functionForm.functionUnknown.unknownArgumentTypeError', { - defaultMessage: 'Unknown expression type "{expressionType}"', - values: { - expressionType, - }, - }), - }, - GroupSettings: { - getSaveGroupDescription: () => - i18n.translate('xpack.canvas.groupSettings.saveGroupDescription', { - defaultMessage: 'Save this group as a new element to re-use it throughout your workpad.', - }), - getUngroupDescription: () => - i18n.translate('xpack.canvas.groupSettings.ungroupDescription', { - defaultMessage: 'Ungroup ({uKey}) to edit individual element settings.', - values: { - uKey: 'U', - }, - }), - }, - HelpMenu: { - getDocumentationLinkLabel: () => - i18n.translate('xpack.canvas.helpMenu.documentationLinkLabel', { - defaultMessage: '{CANVAS} documentation', - values: { - CANVAS, - }, - }), - getHelpMenuDescription: () => - i18n.translate('xpack.canvas.helpMenu.description', { - defaultMessage: 'For {CANVAS} specific information', - values: { - CANVAS, - }, - }), - getKeyboardShortcutsLinkLabel: () => - i18n.translate('xpack.canvas.helpMenu.keyboardShortcutsLinkLabel', { - defaultMessage: 'Keyboard shortcuts', - }), - }, - KeyboardShortcutsDoc: { - getFlyoutCloseButtonAriaLabel: () => - i18n.translate('xpack.canvas.keyboardShortcutsDoc.flyout.closeButtonAriaLabel', { - defaultMessage: 'Closes keyboard shortcuts reference', - }), - getShortcutSeparator: () => - i18n.translate('xpack.canvas.keyboardShortcutsDoc.shortcutListSeparator', { - defaultMessage: 'or', - description: - 'Separates which keyboard shortcuts can be used for a single action. Example: "{shortcut1} or {shortcut2} or {shortcut3}"', - }), - getTitle: () => - i18n.translate('xpack.canvas.keyboardShortcutsDoc.flyoutHeaderTitle', { - defaultMessage: 'Keyboard shortcuts', - }), - }, - LabsControl: { - getLabsButtonLabel: () => - i18n.translate('xpack.canvas.workpadHeaderLabsControlSettings.labsButtonLabel', { - defaultMessage: 'Labs', - }), - getAriaLabel: () => - i18n.translate('xpack.canvas.workpadHeaderLabsControlSettings.labsAriaLabel', { - defaultMessage: 'View labs projects', - }), - getTooltip: () => - i18n.translate('xpack.canvas.workpadHeaderLabsControlSettings.labsTooltip', { - defaultMessage: 'View labs projects', - }), - }, - Link: { - getErrorMessage: (message: string) => - i18n.translate('xpack.canvas.link.errorMessage', { - defaultMessage: 'LINK ERROR: {message}', - values: { - message, - }, - }), - }, - MultiElementSettings: { - getMultipleElementsActionsDescription: () => - i18n.translate('xpack.canvas.groupSettings.multipleElementsActionsDescription', { - defaultMessage: - 'Deselect these elements to edit their individual settings, press ({gKey}) to group them, or save this selection as a new ' + - 'element to re-use it throughout your workpad.', - values: { - gKey: 'G', - }, - }), - getMultipleElementsDescription: () => - i18n.translate('xpack.canvas.groupSettings.multipleElementsDescription', { - defaultMessage: 'Multiple elements are currently selected.', - }), - }, - PageConfig: { - getBackgroundColorDescription: () => - i18n.translate('xpack.canvas.pageConfig.backgroundColorDescription', { - defaultMessage: 'Accepts HEX, RGB or HTML color names', - }), - getBackgroundColorLabel: () => - i18n.translate('xpack.canvas.pageConfig.backgroundColorLabel', { - defaultMessage: 'Background', - }), - getNoTransitionDropDownOptionLabel: () => - i18n.translate('xpack.canvas.pageConfig.transitions.noneDropDownOptionLabel', { - defaultMessage: 'None', - description: - 'This is the option the user should choose if they do not want any page transition (i.e. fade in, fade out, etc) to ' + - 'be applied to the current page.', - }), - getTitle: () => - i18n.translate('xpack.canvas.pageConfig.title', { - defaultMessage: 'Page settings', - }), - getTransitionLabel: () => - i18n.translate('xpack.canvas.pageConfig.transitionLabel', { - defaultMessage: 'Transition', - description: - 'This refers to the transition effect, such as fade in or rotate, applied to a page in presentation mode.', - }), - getTransitionPreviewLabel: () => - i18n.translate('xpack.canvas.pageConfig.transitionPreviewLabel', { - defaultMessage: 'Preview', - description: 'This is the label for a preview of the transition effect selected.', - }), - }, - PageManager: { - getPageNumberAriaLabel: (pageNumber: number) => - i18n.translate('xpack.canvas.pageManager.pageNumberAriaLabel', { - defaultMessage: 'Load page number {pageNumber}', - values: { - pageNumber, - }, - }), - getAddPageTooltip: () => - i18n.translate('xpack.canvas.pageManager.addPageTooltip', { - defaultMessage: 'Add a new page to this workpad', - }), - getConfirmRemoveTitle: () => - i18n.translate('xpack.canvas.pageManager.confirmRemoveTitle', { - defaultMessage: 'Remove Page', - }), - getConfirmRemoveDescription: () => - i18n.translate('xpack.canvas.pageManager.confirmRemoveDescription', { - defaultMessage: 'Are you sure you want to remove this page?', - }), - getConfirmRemoveButtonLabel: () => - i18n.translate('xpack.canvas.pageManager.removeButtonLabel', { - defaultMessage: 'Remove', - }), - }, - PagePreviewPageControls: { - getClonePageAriaLabel: () => - i18n.translate('xpack.canvas.pagePreviewPageControls.clonePageAriaLabel', { - defaultMessage: 'Clone page', - }), - getClonePageTooltip: () => - i18n.translate('xpack.canvas.pagePreviewPageControls.clonePageTooltip', { - defaultMessage: 'Clone', - }), - getDeletePageAriaLabel: () => - i18n.translate('xpack.canvas.pagePreviewPageControls.deletePageAriaLabel', { - defaultMessage: 'Delete page', - }), - getDeletePageTooltip: () => - i18n.translate('xpack.canvas.pagePreviewPageControls.deletePageTooltip', { - defaultMessage: 'Delete', - }), - }, - PalettePicker: { - getEmptyPaletteLabel: () => - i18n.translate('xpack.canvas.palettePicker.emptyPaletteLabel', { - defaultMessage: 'None', - }), - getNoPaletteFoundErrorTitle: () => - i18n.translate('xpack.canvas.palettePicker.noPaletteFoundErrorTitle', { - defaultMessage: 'Color palette not found', - }), - }, - SavedElementsModal: { - getAddNewElementDescription: () => - i18n.translate('xpack.canvas.savedElementsModal.addNewElementDescription', { - defaultMessage: 'Group and save workpad elements to create new elements', - }), - getAddNewElementTitle: () => - i18n.translate('xpack.canvas.savedElementsModal.addNewElementTitle', { - defaultMessage: 'Add new elements', - }), - getCancelButtonLabel: () => - i18n.translate('xpack.canvas.savedElementsModal.cancelButtonLabel', { - defaultMessage: 'Cancel', - }), - getDeleteButtonLabel: () => - i18n.translate('xpack.canvas.savedElementsModal.deleteButtonLabel', { - defaultMessage: 'Delete', - }), - getDeleteElementDescription: () => - i18n.translate('xpack.canvas.savedElementsModal.deleteElementDescription', { - defaultMessage: 'Are you sure you want to delete this element?', - }), - getDeleteElementTitle: (elementName: string) => - i18n.translate('xpack.canvas.savedElementsModal.deleteElementTitle', { - defaultMessage: `Delete element '{elementName}'?`, - values: { - elementName, - }, - }), - getEditElementTitle: () => - i18n.translate('xpack.canvas.savedElementsModal.editElementTitle', { - defaultMessage: 'Edit element', - }), - getElementsTitle: () => - i18n.translate('xpack.canvas.savedElementsModal.elementsTitle', { - defaultMessage: 'Elements', - description: 'Title for the "Elements" tab when adding a new element', - }), - getFindElementPlaceholder: () => - i18n.translate('xpack.canvas.savedElementsModal.findElementPlaceholder', { - defaultMessage: 'Find element', - }), - getModalTitle: () => - i18n.translate('xpack.canvas.savedElementsModal.modalTitle', { - defaultMessage: 'My elements', - }), - getMyElementsTitle: () => - i18n.translate('xpack.canvas.savedElementsModal.myElementsTitle', { - defaultMessage: 'My elements', - description: 'Title for the "My elements" tab when adding a new element', - }), - getSavedElementsModalCloseButtonLabel: () => - i18n.translate('xpack.canvas.workpadHeader.addElementModalCloseButtonLabel', { - defaultMessage: 'Close', - }), - }, - ShareWebsiteFlyout: { - getRuntimeStepTitle: () => - i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.downloadRuntimeTitle', { - defaultMessage: 'Download runtime', - }), - getSnippentsStepTitle: () => - i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.addSnippetsTitle', { - defaultMessage: 'Add snippets to website', - }), - getStepsDescription: () => - i18n.translate('xpack.canvas.shareWebsiteFlyout.description', { - defaultMessage: - 'Follow these steps to share a static version of this workpad on an external website. It will be a visual snapshot of the current workpad, and will not have access to live data.', - }), - getTitle: () => - i18n.translate('xpack.canvas.shareWebsiteFlyout.flyoutTitle', { - defaultMessage: 'Share on a website', - }), - getUnsupportedRendererWarning: () => - i18n.translate('xpack.canvas.workpadHeaderShareMenu.unsupportedRendererWarning', { - defaultMessage: - 'This workpad contains render functions that are not supported by the {CANVAS} Shareable Workpad Runtime. These elements will not be rendered:', - values: { - CANVAS, - }, - }), - getWorkpadStepTitle: () => - i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.downloadWorkpadTitle', { - defaultMessage: 'Download workpad', - }), - }, - ShareWebsiteRuntimeStep: { - getDownloadLabel: () => - i18n.translate('xpack.canvas.shareWebsiteFlyout.runtimeStep.downloadLabel', { - defaultMessage: 'Download runtime', - }), - getStepDescription: () => - i18n.translate('xpack.canvas.shareWebsiteFlyout.runtimeStep.description', { - defaultMessage: - 'In order to render a Shareable Workpad, you also need to include the {CANVAS} Shareable Workpad Runtime. You can skip this step if the runtime is already included on your website.', - values: { - CANVAS, - }, - }), - }, - ShareWebsiteSnippetsStep: { - getAutoplayParameterDescription: () => - i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.autoplayParameterDescription', { - defaultMessage: 'Should the runtime automatically move through the pages of the workpad?', - }), - getCallRuntimeLabel: () => - i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.callRuntimeLabel', { - defaultMessage: 'Call Runtime', - }), - getHeightParameterDescription: () => - i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.heightParameterDescription', { - defaultMessage: 'The height of the Workpad. Defaults to the Workpad height.', - }), - getIncludeRuntimeLabel: () => - i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.includeRuntimeLabel', { - defaultMessage: 'Include Runtime', - }), - getIntervalParameterDescription: () => - i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.intervalParameterDescription', { - defaultMessage: - 'The interval upon which the pages will advance in time format, (e.g. {twoSeconds}, {oneMinute})', - values: { - twoSeconds: '2s', - oneMinute: '1m', - }, - }), - getPageParameterDescription: () => - i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.pageParameterDescription', { - defaultMessage: 'The page to display. Defaults to the page specified by the Workpad.', - }), - getParametersDescription: () => - i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.parametersDescription', { - defaultMessage: - 'There are a number of inline parameters to configure the Shareable Workpad.', - }), - getParametersTitle: () => - i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.parametersLabel', { - defaultMessage: 'Parameters', - }), - getPlaceholderLabel: () => - i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.placeholderLabel', { - defaultMessage: 'Placeholder', - }), - getRequiredLabel: () => - i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.requiredLabel', { - defaultMessage: 'required', - }), - getShareableParameterDescription: () => - i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.shareableParameterDescription', { - defaultMessage: 'The type of shareable. In this case, a {CANVAS} Workpad.', - values: { - CANVAS, - }, - }), - getSnippetsStepDescription: () => - i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.description', { - defaultMessage: - 'The Workpad is placed within the {HTML} of the site by using an {HTML} placeholder. Parameters for the runtime are included inline. See the full list of parameters below. You can include more than one workpad on the page.', - values: { - HTML, - }, - }), - getToolbarParameterDescription: () => - i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.toolbarParameterDescription', { - defaultMessage: 'Should the toolbar be hidden?', - }), - getUrlParameterDescription: () => - i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.urlParameterDescription', { - defaultMessage: 'The {URL} of the Shareable Workpad {JSON} file.', - values: { - URL, - JSON, - }, - }), - getWidthParameterDescription: () => - i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.widthParameterDescription', { - defaultMessage: 'The width of the Workpad. Defaults to the Workpad width.', - }), - }, - ShareWebsiteWorkpadStep: { - getDownloadLabel: () => - i18n.translate('xpack.canvas.shareWebsiteFlyout.workpadStep.downloadLabel', { - defaultMessage: 'Download workpad', - }), - getStepDescription: () => - i18n.translate('xpack.canvas.shareWebsiteFlyout.workpadStep.description', { - defaultMessage: - 'The workpad will be exported as a single {JSON} file for sharing in another site.', - values: { - JSON, - }, - }), - }, - SidebarContent: { - getGroupedElementSidebarTitle: () => - i18n.translate('xpack.canvas.sidebarContent.groupedElementSidebarTitle', { - defaultMessage: 'Grouped element', - description: - 'The title displayed when a grouped element is selected. "elements" refer to the different visualizations, images, ' + - 'text, etc that can be added in a Canvas workpad. These elements can be grouped into a larger "grouped element" ' + - 'that contains multiple individual elements.', - }), - getMultiElementSidebarTitle: () => - i18n.translate('xpack.canvas.sidebarContent.multiElementSidebarTitle', { - defaultMessage: 'Multiple elements', - description: - 'The title displayed when multiple elements are selected. "elements" refer to the different visualizations, images, ' + - 'text, etc that can be added in a Canvas workpad.', - }), - getSingleElementSidebarTitle: () => - i18n.translate('xpack.canvas.sidebarContent.singleElementSidebarTitle', { - defaultMessage: 'Selected element', - description: - 'The title displayed when a single element are selected. "element" refer to the different visualizations, images, ' + - 'text, etc that can be added in a Canvas workpad.', - }), - }, - SidebarHeader: { - getBringForwardAriaLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.bringForwardArialLabel', { - defaultMessage: 'Move element up one layer', - }), - getBringToFrontAriaLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.bringToFrontArialLabel', { - defaultMessage: 'Move element to top layer', - }), - getSendBackwardAriaLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.sendBackwardArialLabel', { - defaultMessage: 'Move element down one layer', - }), - getSendToBackAriaLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.sendToBackArialLabel', { - defaultMessage: 'Move element to bottom layer', - }), - }, - TextStylePicker: { - getAlignCenterOption: () => - i18n.translate('xpack.canvas.textStylePicker.alignCenterOption', { - defaultMessage: 'Align center', - }), - getAlignLeftOption: () => - i18n.translate('xpack.canvas.textStylePicker.alignLeftOption', { - defaultMessage: 'Align left', - }), - getAlignRightOption: () => - i18n.translate('xpack.canvas.textStylePicker.alignRightOption', { - defaultMessage: 'Align right', - }), - getAlignmentOptionsControlLegend: () => - i18n.translate('xpack.canvas.textStylePicker.alignmentOptionsControl', { - defaultMessage: 'Alignment options', - }), - getFontColorLabel: () => - i18n.translate('xpack.canvas.textStylePicker.fontColorLabel', { - defaultMessage: 'Font Color', - }), - getStyleBoldOption: () => - i18n.translate('xpack.canvas.textStylePicker.styleBoldOption', { - defaultMessage: 'Bold', - }), - getStyleItalicOption: () => - i18n.translate('xpack.canvas.textStylePicker.styleItalicOption', { - defaultMessage: 'Italic', - }), - getStyleUnderlineOption: () => - i18n.translate('xpack.canvas.textStylePicker.styleUnderlineOption', { - defaultMessage: 'Underline', - }), - getStyleOptionsControlLegend: () => - i18n.translate('xpack.canvas.textStylePicker.styleOptionsControl', { - defaultMessage: 'Style options', - }), - }, - TimePicker: { - getApplyButtonLabel: () => - i18n.translate('xpack.canvas.timePicker.applyButtonLabel', { - defaultMessage: 'Apply', - }), - }, - Toolbar: { - getEditorButtonLabel: () => - i18n.translate('xpack.canvas.toolbar.editorButtonLabel', { - defaultMessage: 'Expression editor', - }), - getNextPageAriaLabel: () => - i18n.translate('xpack.canvas.toolbar.nextPageAriaLabel', { - defaultMessage: 'Next Page', - }), - getPageButtonLabel: (pageNum: number, totalPages: number) => - i18n.translate('xpack.canvas.toolbar.pageButtonLabel', { - defaultMessage: 'Page {pageNum}{rest}', - values: { - pageNum, - rest: totalPages > 1 ? ` of ${totalPages}` : '', - }, - }), - getPreviousPageAriaLabel: () => - i18n.translate('xpack.canvas.toolbar.previousPageAriaLabel', { - defaultMessage: 'Previous Page', - }), - getWorkpadManagerCloseButtonLabel: () => - i18n.translate('xpack.canvas.toolbar.workpadManagerCloseButtonLabel', { - defaultMessage: 'Close', - }), - getErrorMessage: (message: string) => - i18n.translate('xpack.canvas.toolbar.errorMessage', { - defaultMessage: 'TOOLBAR ERROR: {message}', - values: { - message, - }, - }), - }, - ToolbarTray: { - getCloseTrayAriaLabel: () => - i18n.translate('xpack.canvas.toolbarTray.closeTrayAriaLabel', { - defaultMessage: 'Close tray', - }), - }, - VarConfig: { - getAddButtonLabel: () => - i18n.translate('xpack.canvas.varConfig.addButtonLabel', { - defaultMessage: 'Add a variable', - }), - getAddTooltipLabel: () => - i18n.translate('xpack.canvas.varConfig.addTooltipLabel', { - defaultMessage: 'Add a variable', - }), - getCopyActionButtonLabel: () => - i18n.translate('xpack.canvas.varConfig.copyActionButtonLabel', { - defaultMessage: 'Copy snippet', - }), - getCopyActionTooltipLabel: () => - i18n.translate('xpack.canvas.varConfig.copyActionTooltipLabel', { - defaultMessage: 'Copy variable syntax to clipboard', - }), - getCopyNotificationDescription: () => - i18n.translate('xpack.canvas.varConfig.copyNotificationDescription', { - defaultMessage: 'Variable syntax copied to clipboard', - }), - getDeleteActionButtonLabel: () => - i18n.translate('xpack.canvas.varConfig.deleteActionButtonLabel', { - defaultMessage: 'Delete variable', - }), - getDeleteNotificationDescription: () => - i18n.translate('xpack.canvas.varConfig.deleteNotificationDescription', { - defaultMessage: 'Variable successfully deleted', - }), - getEditActionButtonLabel: () => - i18n.translate('xpack.canvas.varConfig.editActionButtonLabel', { - defaultMessage: 'Edit variable', - }), - getEmptyDescription: () => - i18n.translate('xpack.canvas.varConfig.emptyDescription', { - defaultMessage: - 'This workpad has no variables currently. You may add variables to store and edit common values. These variables can then be used in elements or within the expression editor.', - }), - getTableNameLabel: () => - i18n.translate('xpack.canvas.varConfig.tableNameLabel', { - defaultMessage: 'Name', - }), - getTableTypeLabel: () => - i18n.translate('xpack.canvas.varConfig.tableTypeLabel', { - defaultMessage: 'Type', - }), - getTableValueLabel: () => - i18n.translate('xpack.canvas.varConfig.tableValueLabel', { - defaultMessage: 'Value', - }), - getTitle: () => - i18n.translate('xpack.canvas.varConfig.titleLabel', { - defaultMessage: 'Variables', - }), - getTitleTooltip: () => - i18n.translate('xpack.canvas.varConfig.titleTooltip', { - defaultMessage: 'Add variables to store and edit common values', - }), - }, - VarConfigDeleteVar: { - getCancelButtonLabel: () => - i18n.translate('xpack.canvas.varConfigDeleteVar.cancelButtonLabel', { - defaultMessage: 'Cancel', - }), - getDeleteButtonLabel: () => - i18n.translate('xpack.canvas.varConfigDeleteVar.deleteButtonLabel', { - defaultMessage: 'Delete variable', - }), - getTitle: () => - i18n.translate('xpack.canvas.varConfigDeleteVar.titleLabel', { - defaultMessage: 'Delete variable?', - }), - getWarningDescription: () => - i18n.translate('xpack.canvas.varConfigDeleteVar.warningDescription', { - defaultMessage: - 'Deleting this variable may adversely affect the workpad. Are you sure you wish to continue?', - }), - }, - VarConfigEditVar: { - getAddTitle: () => - i18n.translate('xpack.canvas.varConfigEditVar.addTitleLabel', { - defaultMessage: 'Add variable', - }), - getCancelButtonLabel: () => - i18n.translate('xpack.canvas.varConfigEditVar.cancelButtonLabel', { - defaultMessage: 'Cancel', - }), - getDuplicateNameError: () => - i18n.translate('xpack.canvas.varConfigEditVar.duplicateNameError', { - defaultMessage: 'Variable name already in use', - }), - getEditTitle: () => - i18n.translate('xpack.canvas.varConfigEditVar.editTitleLabel', { - defaultMessage: 'Edit variable', - }), - getEditWarning: () => - i18n.translate('xpack.canvas.varConfigEditVar.editWarning', { - defaultMessage: 'Editing a variable in use may adversely affect your workpad', - }), - getNameFieldLabel: () => - i18n.translate('xpack.canvas.varConfigEditVar.nameFieldLabel', { - defaultMessage: 'Name', - }), - getSaveButtonLabel: () => - i18n.translate('xpack.canvas.varConfigEditVar.saveButtonLabel', { - defaultMessage: 'Save changes', - }), - getTypeBooleanLabel: () => - i18n.translate('xpack.canvas.varConfigEditVar.typeBooleanLabel', { - defaultMessage: 'Boolean', - }), - getTypeFieldLabel: () => - i18n.translate('xpack.canvas.varConfigEditVar.typeFieldLabel', { - defaultMessage: 'Type', - }), - getTypeNumberLabel: () => - i18n.translate('xpack.canvas.varConfigEditVar.typeNumberLabel', { - defaultMessage: 'Number', - }), - getTypeStringLabel: () => - i18n.translate('xpack.canvas.varConfigEditVar.typeStringLabel', { - defaultMessage: 'String', - }), - getValueFieldLabel: () => - i18n.translate('xpack.canvas.varConfigEditVar.valueFieldLabel', { - defaultMessage: 'Value', - }), - }, - VarConfigVarValueField: { - getBooleanOptionsLegend: () => - i18n.translate('xpack.canvas.varConfigVarValueField.booleanOptionsLegend', { - defaultMessage: 'Boolean value', - }), - getFalseOption: () => - i18n.translate('xpack.canvas.varConfigVarValueField.falseOption', { - defaultMessage: 'False', - }), - getTrueOption: () => - i18n.translate('xpack.canvas.varConfigVarValueField.trueOption', { - defaultMessage: 'True', - }), - }, - WorkpadConfig: { - getApplyStylesheetButtonLabel: () => - i18n.translate('xpack.canvas.workpadConfig.applyStylesheetButtonLabel', { - defaultMessage: `Apply stylesheet`, - description: - '"stylesheet" refers to the collection of CSS style rules entered by the user.', - }), - getBackgroundColorLabel: () => - i18n.translate('xpack.canvas.workpadConfig.backgroundColorLabel', { - defaultMessage: 'Background color', - }), - getFlipDimensionAriaLabel: () => - i18n.translate('xpack.canvas.workpadConfig.swapDimensionsAriaLabel', { - defaultMessage: `Swap the page's width and height`, - }), - getFlipDimensionTooltip: () => - i18n.translate('xpack.canvas.workpadConfig.swapDimensionsTooltip', { - defaultMessage: 'Swap the width and height', - }), - getGlobalCSSLabel: () => - i18n.translate('xpack.canvas.workpadConfig.globalCSSLabel', { - defaultMessage: `Global CSS overrides`, - }), - getGlobalCSSTooltip: () => - i18n.translate('xpack.canvas.workpadConfig.globalCSSTooltip', { - defaultMessage: `Apply styles to all pages in this workpad`, - }), - getNameLabel: () => - i18n.translate('xpack.canvas.workpadConfig.nameLabel', { - defaultMessage: 'Name', - }), - getPageHeightLabel: () => - i18n.translate('xpack.canvas.workpadConfig.heightLabel', { - defaultMessage: 'Height', - }), - getPageSizeBadgeAriaLabel: (sizeName: string) => - i18n.translate('xpack.canvas.workpadConfig.pageSizeBadgeAriaLabel', { - defaultMessage: `Preset page size: {sizeName}`, - values: { - sizeName, - }, - }), - getPageSizeBadgeOnClickAriaLabel: (sizeName: string) => - i18n.translate('xpack.canvas.workpadConfig.pageSizeBadgeOnClickAriaLabel', { - defaultMessage: `Set page size to {sizeName}`, - values: { - sizeName, - }, - }), - getPageWidthLabel: () => - i18n.translate('xpack.canvas.workpadConfig.widthLabel', { - defaultMessage: 'Width', - }), - getTitle: () => - i18n.translate('xpack.canvas.workpadConfig.title', { - defaultMessage: 'Workpad settings', - }), - getUSLetterButtonLabel: () => - i18n.translate('xpack.canvas.workpadConfig.USLetterButtonLabel', { - defaultMessage: 'US Letter', - description: 'This is referring to the dimensions of U.S. standard letter paper.', - }), - }, - WorkpadHeader: { - getAddElementButtonLabel: () => - i18n.translate('xpack.canvas.workpadHeader.addElementButtonLabel', { - defaultMessage: 'Add element', - }), - getFullScreenButtonAriaLabel: () => - i18n.translate('xpack.canvas.workpadHeader.fullscreenButtonAriaLabel', { - defaultMessage: 'View fullscreen', - }), - getFullScreenTooltip: () => - i18n.translate('xpack.canvas.workpadHeader.fullscreenTooltip', { - defaultMessage: 'Enter fullscreen mode', - }), - getHideEditControlTooltip: () => - i18n.translate('xpack.canvas.workpadHeader.hideEditControlTooltip', { - defaultMessage: 'Hide editing controls', - }), - getNoWritePermissionTooltipText: () => - i18n.translate('xpack.canvas.workpadHeader.noWritePermissionTooltip', { - defaultMessage: "You don't have permission to edit this workpad", - }), - getShowEditControlTooltip: () => - i18n.translate('xpack.canvas.workpadHeader.showEditControlTooltip', { - defaultMessage: 'Show editing controls', - }), - }, - WorkpadHeaderAutoRefreshControls: { - getDisableTooltip: () => - i18n.translate('xpack.canvas.workpadHeaderAutoRefreshControls.disableTooltip', { - defaultMessage: 'Disable auto-refresh', - }), - getIntervalFormLabelText: () => - i18n.translate('xpack.canvas.workpadHeaderAutoRefreshControls.intervalFormLabel', { - defaultMessage: 'Change auto-refresh interval', - }), - getRefreshListDurationManualText: () => - i18n.translate( - 'xpack.canvas.workpadHeaderAutoRefreshControls.refreshListDurationManualText', - { - defaultMessage: 'Manually', - } - ), - getRefreshListTitle: () => - i18n.translate('xpack.canvas.workpadHeaderAutoRefreshControls.refreshListTitle', { - defaultMessage: 'Refresh elements', - }), - }, - WorkpadHeaderCustomInterval: { - getButtonLabel: () => - i18n.translate('xpack.canvas.workpadHeaderCustomInterval.confirmButtonLabel', { - defaultMessage: 'Set', - }), - getFormDescription: () => - i18n.translate('xpack.canvas.workpadHeaderCustomInterval.formDescription', { - defaultMessage: - 'Use shorthand notation, like {secondsExample}, {minutesExample}, or {hoursExample}', - values: { - secondsExample: '30s', - minutesExample: '10m', - hoursExample: '1h', - }, - }), - getFormLabel: () => - i18n.translate('xpack.canvas.workpadHeaderCustomInterval.formLabel', { - defaultMessage: 'Set a custom interval', - }), - }, - WorkpadHeaderEditMenu: { - getAlignmentMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderEditMenu.alignmentMenuItemLabel', { - defaultMessage: 'Alignment', - description: - 'This refers to the vertical (i.e. left, center, right) and horizontal (i.e. top, middle, bottom) ' + - 'alignment options of the selected elements', - }), - getBottomAlignMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderEditMenu.bottomAlignMenuItemLabel', { - defaultMessage: 'Bottom', - }), - getCenterAlignMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderEditMenu.centerAlignMenuItemLabel', { - defaultMessage: 'Center', - description: 'This refers to alignment centered horizontally.', - }), - getCreateElementModalTitle: () => - i18n.translate('xpack.canvas.workpadHeaderEditMenu.createElementModalTitle', { - defaultMessage: 'Create new element', - }), - getDistributionMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderEditMenu.distributionMenutItemLabel', { - defaultMessage: 'Distribution', - description: - 'This refers to the options to evenly spacing the selected elements horizontall or vertically.', - }), - getEditMenuButtonLabel: () => - i18n.translate('xpack.canvas.workpadHeaderEditMenu.editMenuButtonLabel', { - defaultMessage: 'Edit', - }), - getEditMenuLabel: () => - i18n.translate('xpack.canvas.workpadHeaderEditMenu.editMenuLabel', { - defaultMessage: 'Edit options', - }), - getGroupMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderEditMenu.groupMenuItemLabel', { - defaultMessage: 'Group', - description: 'This refers to grouping multiple selected elements.', - }), - getHorizontalDistributionMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderEditMenu.horizontalDistributionMenutItemLabel', { - defaultMessage: 'Horizontal', - }), - getLeftAlignMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderEditMenu.leftAlignMenuItemLabel', { - defaultMessage: 'Left', - }), - getMiddleAlignMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderEditMenu.middleAlignMenuItemLabel', { - defaultMessage: 'Middle', - description: 'This refers to alignment centered vertically.', - }), - getOrderMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderEditMenu.orderMenuItemLabel', { - defaultMessage: 'Order', - description: 'Refers to the order of the elements displayed on the page from front to back', - }), - getRedoMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderEditMenu.redoMenuItemLabel', { - defaultMessage: 'Redo', - }), - getRightAlignMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderEditMenu.rightAlignMenuItemLabel', { - defaultMessage: 'Right', - }), - getSaveElementMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderEditMenu.savedElementMenuItemLabel', { - defaultMessage: 'Save as new element', - }), - getTopAlignMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderEditMenu.topAlignMenuItemLabel', { - defaultMessage: 'Top', - }), - getUndoMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderEditMenu.undoMenuItemLabel', { - defaultMessage: 'Undo', - }), - getUngroupMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderEditMenu.ungroupMenuItemLabel', { - defaultMessage: 'Ungroup', - description: 'This refers to ungrouping a grouped element', - }), - getVerticalDistributionMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderEditMenu.verticalDistributionMenutItemLabel', { - defaultMessage: 'Vertical', - }), - }, - WorkpadHeaderElementMenu: { - getAssetsMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderElementMenu.manageAssetsMenuItemLabel', { - defaultMessage: 'Manage assets', - }), - getChartMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderElementMenu.chartMenuItemLabel', { - defaultMessage: 'Chart', - }), - getElementMenuButtonLabel: () => - i18n.translate('xpack.canvas.workpadHeaderElementMenu.elementMenuButtonLabel', { - defaultMessage: 'Add element', - }), - getElementMenuLabel: () => - i18n.translate('xpack.canvas.workpadHeaderElementMenu.elementMenuLabel', { - defaultMessage: 'Add an element', - }), - getEmbedObjectMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderElementMenu.embedObjectMenuItemLabel', { - defaultMessage: 'Add from Kibana', - }), - getFilterMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderElementMenu.filterMenuItemLabel', { - defaultMessage: 'Filter', - }), - getImageMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderElementMenu.imageMenuItemLabel', { - defaultMessage: 'Image', - }), - getMyElementsMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderElementMenu.myElementsMenuItemLabel', { - defaultMessage: 'My elements', - }), - getOtherMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderElementMenu.otherMenuItemLabel', { - defaultMessage: 'Other', - }), - getProgressMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderElementMenu.progressMenuItemLabel', { - defaultMessage: 'Progress', - }), - getShapeMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderElementMenu.shapeMenuItemLabel', { - defaultMessage: 'Shape', - }), - getTextMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderElementMenu.textMenuItemLabel', { - defaultMessage: 'Text', - }), - }, - WorkpadHeaderKioskControls: { - getCycleFormLabel: () => - i18n.translate('xpack.canvas.workpadHeaderKioskControl.cycleFormLabel', { - defaultMessage: 'Change cycling interval', - }), - getCycleToggleSwitch: () => - i18n.translate('xpack.canvas.workpadHeaderKioskControl.cycleToggleSwitch', { - defaultMessage: 'Cycle slides automatically', - }), - getTitle: () => - i18n.translate('xpack.canvas.workpadHeaderKioskControl.controlTitle', { - defaultMessage: 'Cycle fullscreen pages', - }), - getAutoplayListDurationManualText: () => - i18n.translate('xpack.canvas.workpadHeaderKioskControl.autoplayListDurationManual', { - defaultMessage: 'Manually', - }), - getDisableTooltip: () => - i18n.translate('xpack.canvas.workpadHeaderKioskControl.disableTooltip', { - defaultMessage: 'Disable auto-play', - }), - }, - WorkpadHeaderRefreshControlSettings: { - getRefreshAriaLabel: () => - i18n.translate('xpack.canvas.workpadHeaderRefreshControlSettings.refreshAriaLabel', { - defaultMessage: 'Refresh Elements', - }), - getRefreshTooltip: () => - i18n.translate('xpack.canvas.workpadHeaderRefreshControlSettings.refreshTooltip', { - defaultMessage: 'Refresh data', - }), - }, - WorkpadHeaderShareMenu: { - getCopyPDFMessage: () => - i18n.translate('xpack.canvas.workpadHeaderShareMenu.copyPDFMessage', { - defaultMessage: 'The {PDF} generation {URL} was copied to your clipboard.', - values: { - PDF, - URL, - }, - }), - getCopyShareConfigMessage: () => - i18n.translate('xpack.canvas.workpadHeaderShareMenu.copyShareConfigMessage', { - defaultMessage: 'Copied share markup to clipboard', - }), - getShareableZipErrorTitle: (workpadName: string) => - i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareWebsiteErrorTitle', { - defaultMessage: - "Failed to create {ZIP} file for '{workpadName}'. The workpad may be too large. You'll need to download the files separately.", - values: { - ZIP, - workpadName, - }, - }), - getShareDownloadJSONTitle: () => - i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareDownloadJSONTitle', { - defaultMessage: 'Download as {JSON}', - values: { - JSON, - }, - }), - getShareDownloadPDFTitle: () => - i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareDownloadPDFTitle', { - defaultMessage: '{PDF} reports', - values: { - PDF, - }, - }), - getShareMenuButtonLabel: () => - i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareMenuButtonLabel', { - defaultMessage: 'Share', - }), - getShareWebsiteTitle: () => - i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareWebsiteTitle', { - defaultMessage: 'Share on a website', - }), - getShareWorkpadMessage: () => - i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareWorkpadMessage', { - defaultMessage: 'Share this workpad', - }), - getUnknownExportErrorMessage: (type: string) => - i18n.translate('xpack.canvas.workpadHeaderShareMenu.unknownExportErrorMessage', { - defaultMessage: 'Unknown export type: {type}', - values: { - type, - }, - }), - }, - WorkpadHeaderViewMenu: { - getAutoplayOffMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderViewMenu.autoplayOffMenuItemLabel', { - defaultMessage: 'Turn autoplay off', - }), - getAutoplayOnMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderViewMenu.autoplayOnMenuItemLabel', { - defaultMessage: 'Turn autoplay on', - }), - getAutoplaySettingsMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderViewMenu.autoplaySettingsMenuItemLabel', { - defaultMessage: 'Autoplay settings', - }), - getFullscreenMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderViewMenu.fullscreenMenuLabel', { - defaultMessage: 'Enter fullscreen mode', - }), - getHideEditModeLabel: () => - i18n.translate('xpack.canvas.workpadHeaderViewMenu.hideEditModeLabel', { - defaultMessage: 'Hide editing controls', - }), - getRefreshMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderViewMenu.refreshMenuItemLabel', { - defaultMessage: 'Refresh data', - }), - getRefreshSettingsMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderViewMenu.refreshSettingsMenuItemLabel', { - defaultMessage: 'Auto refresh settings', - }), - getShowEditModeLabel: () => - i18n.translate('xpack.canvas.workpadHeaderViewMenu.showEditModeLabel', { - defaultMessage: 'Show editing controls', - }), - getViewMenuButtonLabel: () => - i18n.translate('xpack.canvas.workpadHeaderViewMenu.viewMenuButtonLabel', { - defaultMessage: 'View', - }), - getViewMenuLabel: () => - i18n.translate('xpack.canvas.workpadHeaderViewMenu.viewMenuLabel', { - defaultMessage: 'View options', - }), - getZoomControlsAriaLabel: () => - i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomControlsAriaLabel', { - defaultMessage: 'Zoom controls', - }), - getZoomControlsTooltip: () => - i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomControlsTooltip', { - defaultMessage: 'Zoom controls', - }), - getZoomFitToWindowText: () => - i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomFitToWindowText', { - defaultMessage: 'Fit to window', - }), - getZoomInText: () => - i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomInText', { - defaultMessage: 'Zoom in', - }), - getZoomMenuItemLabel: () => - i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomMenuItemLabel', { - defaultMessage: 'Zoom', - }), - getZoomOutText: () => - i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomOutText', { - defaultMessage: 'Zoom out', - }), - getZoomPanelTitle: () => - i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomPanelTitle', { - defaultMessage: 'Zoom', - }), - getZoomPercentage: (scale: number) => - i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomResetText', { - defaultMessage: '{scalePercentage}%', - values: { - scalePercentage: scale * 100, - }, - }), - getZoomResetText: () => - i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomPrecentageValue', { - defaultMessage: 'Reset', - }), - }, -}; diff --git a/x-pack/plugins/canvas/i18n/index.ts b/x-pack/plugins/canvas/i18n/index.ts index 14c9e5d221b79..d35b915ea7fb6 100644 --- a/x-pack/plugins/canvas/i18n/index.ts +++ b/x-pack/plugins/canvas/i18n/index.ts @@ -6,7 +6,6 @@ */ export * from './capabilities'; -export * from './components'; export * from './constants'; export * from './errors'; export * from './expression_types'; diff --git a/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx b/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx index 194d2d8b3ddf5..d9df1e4661fbf 100644 --- a/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx +++ b/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx @@ -8,15 +8,20 @@ import React, { MouseEventHandler, FC } from 'react'; import PropTypes from 'prop-types'; import { EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + // @ts-expect-error untyped local import { Popover, PopoverChildrenProps } from '../popover'; import { ArgAdd } from '../arg_add'; // @ts-expect-error untyped local import { Arg } from '../../expression_types/arg'; -import { ComponentStrings } from '../../../i18n'; - -const { ArgAddPopover: strings } = ComponentStrings; +const strings = { + getAddAriaLabel: () => + i18n.translate('xpack.canvas.argAddPopover.addAriaLabel', { + defaultMessage: 'Add argument', + }), +}; interface ArgOptions { arg: Arg; diff --git a/x-pack/plugins/canvas/public/components/arg_form/advanced_failure.js b/x-pack/plugins/canvas/public/components/arg_form/advanced_failure.js index c40e74186e87e..14f47553002ac 100644 --- a/x-pack/plugins/canvas/public/components/arg_form/advanced_failure.js +++ b/x-pack/plugins/canvas/public/components/arg_form/advanced_failure.js @@ -9,12 +9,25 @@ import React from 'react'; import PropTypes from 'prop-types'; import { compose, withProps, withPropsOnChange } from 'recompose'; import { EuiTextArea, EuiButton, EuiButtonEmpty, EuiFormRow, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { fromExpression, toExpression } from '@kbn/interpreter/common'; -import { createStatefulPropHoc } from '../../components/enhance/stateful_prop'; -import { ComponentStrings } from '../../../i18n'; +import { createStatefulPropHoc } from '../../components/enhance/stateful_prop'; -const { ArgFormAdvancedFailure: strings } = ComponentStrings; +const strings = { + getApplyButtonLabel: () => + i18n.translate('xpack.canvas.argFormAdvancedFailure.applyButtonLabel', { + defaultMessage: 'Apply', + }), + getResetButtonLabel: () => + i18n.translate('xpack.canvas.argFormAdvancedFailure.resetButtonLabel', { + defaultMessage: 'Reset', + }), + getRowErrorMessage: () => + i18n.translate('xpack.canvas.argFormAdvancedFailure.rowErrorMessage', { + defaultMessage: 'Invalid Expression', + }), +}; export const AdvancedFailureComponent = (props) => { const { diff --git a/x-pack/plugins/canvas/public/components/arg_form/arg_simple_form.tsx b/x-pack/plugins/canvas/public/components/arg_form/arg_simple_form.tsx index 2ae772cdc197a..84b87373c1c5a 100644 --- a/x-pack/plugins/canvas/public/components/arg_form/arg_simple_form.tsx +++ b/x-pack/plugins/canvas/public/components/arg_form/arg_simple_form.tsx @@ -8,12 +8,20 @@ import React, { ReactNode, MouseEventHandler } from 'react'; import PropTypes from 'prop-types'; import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; -import { TooltipIcon, IconType } from '../tooltip_icon'; - -import { ComponentStrings } from '../../../i18n'; +import { i18n } from '@kbn/i18n'; -const { ArgFormArgSimpleForm: strings } = ComponentStrings; +import { TooltipIcon, IconType } from '../tooltip_icon'; +const strings = { + getRemoveAriaLabel: () => + i18n.translate('xpack.canvas.argFormArgSimpleForm.removeAriaLabel', { + defaultMessage: 'Remove', + }), + getRequiredTooltip: () => + i18n.translate('xpack.canvas.argFormArgSimpleForm.requiredTooltip', { + defaultMessage: 'This argument is required, you should specify a value.', + }), +}; interface Props { children?: ReactNode; required?: boolean; diff --git a/x-pack/plugins/canvas/public/components/arg_form/pending_arg_value.js b/x-pack/plugins/canvas/public/components/arg_form/pending_arg_value.js index ff390a770f80e..f933230f39928 100644 --- a/x-pack/plugins/canvas/public/components/arg_form/pending_arg_value.js +++ b/x-pack/plugins/canvas/public/components/arg_form/pending_arg_value.js @@ -7,11 +7,17 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { ComponentStrings } from '../../../i18n'; +import { i18n } from '@kbn/i18n'; + import { Loading } from '../loading'; import { ArgLabel } from './arg_label'; -const { ArgFormPendingArgValue: strings } = ComponentStrings; +const strings = { + getLoadingMessage: () => + i18n.translate('xpack.canvas.argFormPendingArgValue.loadingMessage', { + defaultMessage: 'Loading', + }), +}; export class PendingArgValue extends React.PureComponent { static propTypes = { diff --git a/x-pack/plugins/canvas/public/components/arg_form/simple_failure.tsx b/x-pack/plugins/canvas/public/components/arg_form/simple_failure.tsx index cc4e92679a870..57173fa413e8f 100644 --- a/x-pack/plugins/canvas/public/components/arg_form/simple_failure.tsx +++ b/x-pack/plugins/canvas/public/components/arg_form/simple_failure.tsx @@ -6,11 +6,17 @@ */ import React from 'react'; -import { TooltipIcon, IconType } from '../tooltip_icon'; +import { i18n } from '@kbn/i18n'; -import { ComponentStrings } from '../../../i18n'; +import { TooltipIcon, IconType } from '../tooltip_icon'; -const { ArgFormSimpleFailure: strings } = ComponentStrings; +const strings = { + getFailureTooltip: () => + i18n.translate('xpack.canvas.argFormSimpleFailure.failureTooltip', { + defaultMessage: + 'The interface for this argument could not parse the value, so a fallback input is being used', + }), +}; // This is what is being generated by render() from the Arg class. It is called in FunctionForm export const SimpleFailure = () => ( diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot index 34b6b333f3ef5..d567d3cf85f13 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot @@ -116,20 +116,13 @@ exports[`Storyshots components/Assets/AssetManager no assets 1`] = ` size="xxl" /> <div - className="euiSpacer euiSpacer--s" + className="euiSpacer euiSpacer--m" /> - <span - className="euiTextColor euiTextColor--subdued" + <h2 + className="euiTitle euiTitle--xsmall" > - <h2 - className="euiTitle euiTitle--xsmall" - > - Import your assets to get started - </h2> - <div - className="euiSpacer euiSpacer--m" - /> - </span> + Import your assets to get started + </h2> </div> </div> </div> diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx b/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx index 8f9d90ccbe1d8..024137f640636 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx +++ b/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx @@ -17,6 +17,7 @@ import { EuiTextColor, EuiToolTip, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { useNotifyService } from '../../services'; @@ -25,9 +26,40 @@ import { Clipboard } from '../clipboard'; import { Download } from '../download'; import { AssetType } from '../../../types'; -import { ComponentStrings } from '../../../i18n'; - -const { Asset: strings } = ComponentStrings; +const strings = { + getCopyAssetTooltip: () => + i18n.translate('xpack.canvas.asset.copyAssetTooltip', { + defaultMessage: 'Copy id to clipboard', + }), + getCreateImageTooltip: () => + i18n.translate('xpack.canvas.asset.createImageTooltip', { + defaultMessage: 'Create image element', + }), + getDeleteAssetTooltip: () => + i18n.translate('xpack.canvas.asset.deleteAssetTooltip', { + defaultMessage: 'Delete', + }), + getDownloadAssetTooltip: () => + i18n.translate('xpack.canvas.asset.downloadAssetTooltip', { + defaultMessage: 'Download', + }), + getThumbnailAltText: () => + i18n.translate('xpack.canvas.asset.thumbnailAltText', { + defaultMessage: 'Asset thumbnail', + }), + getConfirmModalButtonLabel: () => + i18n.translate('xpack.canvas.asset.confirmModalButtonLabel', { + defaultMessage: 'Remove', + }), + getConfirmModalMessageText: () => + i18n.translate('xpack.canvas.asset.confirmModalDetail', { + defaultMessage: 'Are you sure you want to remove this asset?', + }), + getConfirmModalTitle: () => + i18n.translate('xpack.canvas.asset.confirmModalTitle', { + defaultMessage: 'Remove Asset', + }), +}; export interface Props { /** The asset to be rendered */ diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx b/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx index 7795aa9671b83..7b004d5ab5099 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx +++ b/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx @@ -24,14 +24,47 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { ASSET_MAX_SIZE } from '../../../common/lib/constants'; import { Loading } from '../loading'; import { Asset } from './asset'; import { AssetType } from '../../../types'; -import { ComponentStrings } from '../../../i18n'; -const { AssetManager: strings } = ComponentStrings; +const strings = { + getDescription: () => + i18n.translate('xpack.canvas.assetModal.modalDescription', { + defaultMessage: + 'Below are the image assets in this workpad. Any assets that are currently in use cannot be determined at this time. To reclaim space, delete assets.', + }), + getEmptyAssetsDescription: () => + i18n.translate('xpack.canvas.assetModal.emptyAssetsDescription', { + defaultMessage: 'Import your assets to get started', + }), + getFilePickerPromptText: () => + i18n.translate('xpack.canvas.assetModal.filePickerPromptText', { + defaultMessage: 'Select or drag and drop images', + }), + getLoadingText: () => + i18n.translate('xpack.canvas.assetModal.loadingText', { + defaultMessage: 'Uploading images', + }), + getModalCloseButtonLabel: () => + i18n.translate('xpack.canvas.assetModal.modalCloseButtonLabel', { + defaultMessage: 'Close', + }), + getModalTitle: () => + i18n.translate('xpack.canvas.assetModal.modalTitle', { + defaultMessage: 'Manage workpad assets', + }), + getSpaceUsedText: (percentageUsed: number) => + i18n.translate('xpack.canvas.assetModal.spacedUsedText', { + defaultMessage: '{percentageUsed}% space used', + values: { + percentageUsed, + }, + }), +}; export interface Props { /** The assets to display within the modal */ diff --git a/x-pack/plugins/canvas/public/components/asset_picker/asset_picker.tsx b/x-pack/plugins/canvas/public/components/asset_picker/asset_picker.tsx index c2e2d8a053247..4bf13577aff53 100644 --- a/x-pack/plugins/canvas/public/components/asset_picker/asset_picker.tsx +++ b/x-pack/plugins/canvas/public/components/asset_picker/asset_picker.tsx @@ -8,12 +8,16 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { EuiFlexGrid, EuiFlexItem, EuiLink, EuiImage, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { CanvasAsset } from '../../../types'; -import { ComponentStrings } from '../../../i18n'; - -const { AssetPicker: strings } = ComponentStrings; +const strings = { + getAssetAltText: () => + i18n.translate('xpack.canvas.assetpicker.assetAltText', { + defaultMessage: 'Asset thumbnail', + }), +}; interface Props { assets: CanvasAsset[]; diff --git a/x-pack/plugins/canvas/public/components/canvas_loading/canvas_loading.component.tsx b/x-pack/plugins/canvas/public/components/canvas_loading/canvas_loading.component.tsx index 38e62f46c945a..8f55c31933291 100644 --- a/x-pack/plugins/canvas/public/components/canvas_loading/canvas_loading.component.tsx +++ b/x-pack/plugins/canvas/public/components/canvas_loading/canvas_loading.component.tsx @@ -7,9 +7,14 @@ import React, { FC } from 'react'; import { EuiPanel, EuiLoadingChart, EuiSpacer, EuiText } from '@elastic/eui'; -import { ComponentStrings } from '../../../i18n/components'; +import { i18n } from '@kbn/i18n'; -const { CanvasLoading: strings } = ComponentStrings; +const strings = { + getLoadingLabel: () => + i18n.translate('xpack.canvas.canvasLoading.loadingMessage', { + defaultMessage: 'Loading', + }), +}; export const CanvasLoading: FC<{ msg?: string }> = ({ msg = `${strings.getLoadingLabel()}...`, diff --git a/x-pack/plugins/canvas/public/components/color_manager/color_manager.tsx b/x-pack/plugins/canvas/public/components/color_manager/color_manager.tsx index ae5cfac85bdc9..50c679c2a1e51 100644 --- a/x-pack/plugins/canvas/public/components/color_manager/color_manager.tsx +++ b/x-pack/plugins/canvas/public/components/color_manager/color_manager.tsx @@ -9,11 +9,24 @@ import React, { FC } from 'react'; import PropTypes from 'prop-types'; import { EuiButtonIcon, EuiFieldText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import tinycolor from 'tinycolor2'; -import { ColorDot } from '../color_dot/color_dot'; +import { i18n } from '@kbn/i18n'; -import { ComponentStrings } from '../../../i18n/components'; +import { ColorDot } from '../color_dot/color_dot'; -const { ColorManager: strings } = ComponentStrings; +const strings = { + getAddAriaLabel: () => + i18n.translate('xpack.canvas.colorManager.addAriaLabel', { + defaultMessage: 'Add Color', + }), + getCodePlaceholder: () => + i18n.translate('xpack.canvas.colorManager.codePlaceholder', { + defaultMessage: 'Color code', + }), + getRemoveAriaLabel: () => + i18n.translate('xpack.canvas.colorManager.removeAriaLabel', { + defaultMessage: 'Remove Color', + }), +}; export interface Props { /** diff --git a/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot b/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot index 18f86aca24302..dc66eef809050 100644 --- a/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot @@ -80,7 +80,7 @@ exports[`Storyshots components/Elements/CustomElementModal with description 1`] className="euiFormControlLayout__childrenWrapper" > <input - aria-describedby="generated-id-help" + aria-describedby="generated-id-help-0" className="euiFieldText canvasCustomElementForm__name" data-test-subj="canvasCustomElementForm-name" id="generated-id" @@ -95,7 +95,7 @@ exports[`Storyshots components/Elements/CustomElementModal with description 1`] </div> <div className="euiFormHelpText euiFormRow__text" - id="generated-id-help" + id="generated-id-help-0" > 40 characters remaining </div> @@ -119,7 +119,7 @@ exports[`Storyshots components/Elements/CustomElementModal with description 1`] className="euiFormRow__fieldWrapper" > <textarea - aria-describedby="generated-id-help" + aria-describedby="generated-id-help-0" className="euiTextArea euiTextArea--resizeVertical" data-test-subj="canvasCustomElementForm-description" id="generated-id" @@ -131,7 +131,7 @@ exports[`Storyshots components/Elements/CustomElementModal with description 1`] /> <div className="euiFormHelpText euiFormRow__text" - id="generated-id-help" + id="generated-id-help-0" > 83 characters remaining </div> @@ -389,7 +389,7 @@ exports[`Storyshots components/Elements/CustomElementModal with image 1`] = ` className="euiFormControlLayout__childrenWrapper" > <input - aria-describedby="generated-id-help" + aria-describedby="generated-id-help-0" className="euiFieldText canvasCustomElementForm__name" data-test-subj="canvasCustomElementForm-name" id="generated-id" @@ -404,7 +404,7 @@ exports[`Storyshots components/Elements/CustomElementModal with image 1`] = ` </div> <div className="euiFormHelpText euiFormRow__text" - id="generated-id-help" + id="generated-id-help-0" > 40 characters remaining </div> @@ -428,7 +428,7 @@ exports[`Storyshots components/Elements/CustomElementModal with image 1`] = ` className="euiFormRow__fieldWrapper" > <textarea - aria-describedby="generated-id-help" + aria-describedby="generated-id-help-0" className="euiTextArea euiTextArea--resizeVertical" data-test-subj="canvasCustomElementForm-description" id="generated-id" @@ -440,7 +440,7 @@ exports[`Storyshots components/Elements/CustomElementModal with image 1`] = ` /> <div className="euiFormHelpText euiFormRow__text" - id="generated-id-help" + id="generated-id-help-0" > 100 characters remaining </div> @@ -695,7 +695,7 @@ exports[`Storyshots components/Elements/CustomElementModal with name 1`] = ` className="euiFormControlLayout__childrenWrapper" > <input - aria-describedby="generated-id-help" + aria-describedby="generated-id-help-0" className="euiFieldText canvasCustomElementForm__name" data-test-subj="canvasCustomElementForm-name" id="generated-id" @@ -710,7 +710,7 @@ exports[`Storyshots components/Elements/CustomElementModal with name 1`] = ` </div> <div className="euiFormHelpText euiFormRow__text" - id="generated-id-help" + id="generated-id-help-0" > 32 characters remaining </div> @@ -734,7 +734,7 @@ exports[`Storyshots components/Elements/CustomElementModal with name 1`] = ` className="euiFormRow__fieldWrapper" > <textarea - aria-describedby="generated-id-help" + aria-describedby="generated-id-help-0" className="euiTextArea euiTextArea--resizeVertical" data-test-subj="canvasCustomElementForm-description" id="generated-id" @@ -746,7 +746,7 @@ exports[`Storyshots components/Elements/CustomElementModal with name 1`] = ` /> <div className="euiFormHelpText euiFormRow__text" - id="generated-id-help" + id="generated-id-help-0" > 100 characters remaining </div> @@ -996,7 +996,7 @@ exports[`Storyshots components/Elements/CustomElementModal with title 1`] = ` className="euiFormControlLayout__childrenWrapper" > <input - aria-describedby="generated-id-help" + aria-describedby="generated-id-help-0" className="euiFieldText canvasCustomElementForm__name" data-test-subj="canvasCustomElementForm-name" id="generated-id" @@ -1011,7 +1011,7 @@ exports[`Storyshots components/Elements/CustomElementModal with title 1`] = ` </div> <div className="euiFormHelpText euiFormRow__text" - id="generated-id-help" + id="generated-id-help-0" > 40 characters remaining </div> @@ -1035,7 +1035,7 @@ exports[`Storyshots components/Elements/CustomElementModal with title 1`] = ` className="euiFormRow__fieldWrapper" > <textarea - aria-describedby="generated-id-help" + aria-describedby="generated-id-help-0" className="euiTextArea euiTextArea--resizeVertical" data-test-subj="canvasCustomElementForm-description" id="generated-id" @@ -1047,7 +1047,7 @@ exports[`Storyshots components/Elements/CustomElementModal with title 1`] = ` /> <div className="euiFormHelpText euiFormRow__text" - id="generated-id-help" + id="generated-id-help-0" > 100 characters remaining </div> diff --git a/x-pack/plugins/canvas/public/components/custom_element_modal/custom_element_modal.tsx b/x-pack/plugins/canvas/public/components/custom_element_modal/custom_element_modal.tsx index 5d9cccba924a9..86d9cab4eeea1 100644 --- a/x-pack/plugins/canvas/public/components/custom_element_modal/custom_element_modal.tsx +++ b/x-pack/plugins/canvas/public/components/custom_element_modal/custom_element_modal.tsx @@ -26,16 +26,57 @@ import { EuiTextArea, EuiTitle, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import { VALID_IMAGE_TYPES } from '../../../common/lib/constants'; import { encode } from '../../../common/lib/dataurl'; import { ElementCard } from '../element_card'; -import { ComponentStrings } from '../../../i18n/components'; const MAX_NAME_LENGTH = 40; const MAX_DESCRIPTION_LENGTH = 100; -const { CustomElementModal: strings } = ComponentStrings; - +const strings = { + getCancelButtonLabel: () => + i18n.translate('xpack.canvas.customElementModal.cancelButtonLabel', { + defaultMessage: 'Cancel', + }), + getCharactersRemainingDescription: (numberOfRemainingCharacter: number) => + i18n.translate('xpack.canvas.customElementModal.remainingCharactersDescription', { + defaultMessage: '{numberOfRemainingCharacter} characters remaining', + values: { + numberOfRemainingCharacter, + }, + }), + getDescriptionInputLabel: () => + i18n.translate('xpack.canvas.customElementModal.descriptionInputLabel', { + defaultMessage: 'Description', + }), + getElementPreviewTitle: () => + i18n.translate('xpack.canvas.customElementModal.elementPreviewTitle', { + defaultMessage: 'Element preview', + }), + getImageFilePickerPlaceholder: () => + i18n.translate('xpack.canvas.customElementModal.imageFilePickerPlaceholder', { + defaultMessage: 'Select or drag and drop an image', + }), + getImageInputDescription: () => + i18n.translate('xpack.canvas.customElementModal.imageInputDescription', { + defaultMessage: + 'Take a screenshot of your element and upload it here. This can also be done after saving.', + }), + getImageInputLabel: () => + i18n.translate('xpack.canvas.customElementModal.imageInputLabel', { + defaultMessage: 'Thumbnail image', + }), + getNameInputLabel: () => + i18n.translate('xpack.canvas.customElementModal.nameInputLabel', { + defaultMessage: 'Name', + }), + getSaveButtonLabel: () => + i18n.translate('xpack.canvas.customElementModal.saveButtonLabel', { + defaultMessage: 'Save', + }), +}; interface Props { /** * initial value of the name of the custom element diff --git a/x-pack/plugins/canvas/public/components/datasource/__stories__/__snapshots__/datasource_component.stories.storyshot b/x-pack/plugins/canvas/public/components/datasource/__stories__/__snapshots__/datasource_component.stories.storyshot index 6d170d78dd01d..836047959caee 100644 --- a/x-pack/plugins/canvas/public/components/datasource/__stories__/__snapshots__/datasource_component.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/datasource/__stories__/__snapshots__/datasource_component.stories.storyshot @@ -39,9 +39,13 @@ exports[`Storyshots components/datasource/DatasourceComponent datasource with ex <div className="euiText euiText--small" > - <p> - The datasource has an argument controlled by an expression. Use the expression editor to modify the datasource. - </p> + <div + className="euiTextColor euiTextColor--default" + > + <p> + The datasource has an argument controlled by an expression. Use the expression editor to modify the datasource. + </p> + </div> </div> </div> </div> diff --git a/x-pack/plugins/canvas/public/components/datasource/datasource_component.js b/x-pack/plugins/canvas/public/components/datasource/datasource_component.js index faddc3a60b990..f09ce4c925820 100644 --- a/x-pack/plugins/canvas/public/components/datasource/datasource_component.js +++ b/x-pack/plugins/canvas/public/components/datasource/datasource_component.js @@ -18,13 +18,27 @@ import { EuiHorizontalRule, } from '@elastic/eui'; import { isEqual } from 'lodash'; -import { ComponentStrings } from '../../../i18n'; +import { i18n } from '@kbn/i18n'; + import { getDefaultIndex } from '../../lib/es_service'; import { DatasourceSelector } from './datasource_selector'; import { DatasourcePreview } from './datasource_preview'; -const { DatasourceDatasourceComponent: strings } = ComponentStrings; - +const strings = { + getExpressionArgDescription: () => + i18n.translate('xpack.canvas.datasourceDatasourceComponent.expressionArgDescription', { + defaultMessage: + 'The datasource has an argument controlled by an expression. Use the expression editor to modify the datasource.', + }), + getPreviewButtonLabel: () => + i18n.translate('xpack.canvas.datasourceDatasourceComponent.previewButtonLabel', { + defaultMessage: 'Preview data', + }), + getSaveButtonLabel: () => + i18n.translate('xpack.canvas.datasourceDatasourceComponent.saveButtonLabel', { + defaultMessage: 'Save', + }), +}; export class DatasourceComponent extends PureComponent { static propTypes = { args: PropTypes.object.isRequired, diff --git a/x-pack/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js b/x-pack/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js index a55f73a087467..2eb42c5cb98dc 100644 --- a/x-pack/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js +++ b/x-pack/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js @@ -18,12 +18,33 @@ import { EuiSpacer, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + import { Datatable } from '../../datatable'; import { Error } from '../../error'; -import { ComponentStrings } from '../../../../i18n'; -const { DatasourceDatasourcePreview: strings } = ComponentStrings; -const { DatasourceDatasourceComponent: datasourceStrings } = ComponentStrings; +const strings = { + getEmptyFirstLineDescription: () => + i18n.translate('xpack.canvas.datasourceDatasourcePreview.emptyFirstLineDescription', { + defaultMessage: "We couldn't find any documents matching your search criteria.", + }), + getEmptySecondLineDescription: () => + i18n.translate('xpack.canvas.datasourceDatasourcePreview.emptySecondLineDescription', { + defaultMessage: 'Check your datasource settings and try again.', + }), + getEmptyTitle: () => + i18n.translate('xpack.canvas.datasourceDatasourcePreview.emptyTitle', { + defaultMessage: 'No documents found', + }), + getModalTitle: () => + i18n.translate('xpack.canvas.datasourceDatasourcePreview.modalTitle', { + defaultMessage: 'Datasource preview', + }), + getSaveButtonLabel: () => + i18n.translate('xpack.canvas.datasourceDatasourcePreview.saveButtonLabel', { + defaultMessage: 'Save', + }), +}; export const DatasourcePreview = ({ done, datatable }) => ( <EuiModal onClose={done} maxWidth="1000px" className="canvasModal--fixedSize"> @@ -37,7 +58,7 @@ export const DatasourcePreview = ({ done, datatable }) => ( id="xpack.canvas.datasourceDatasourcePreview.modalDescription" defaultMessage="The following data will be available to the selected element upon clicking {saveLabel} in the sidebar." values={{ - saveLabel: <strong>{datasourceStrings.getSaveButtonLabel()}</strong>, + saveLabel: <strong>{strings.getSaveButtonLabel()}</strong>, }} /> </p> diff --git a/x-pack/plugins/canvas/public/components/datasource/no_datasource.js b/x-pack/plugins/canvas/public/components/datasource/no_datasource.js index ef86361a4a3a0..f496d493e9d94 100644 --- a/x-pack/plugins/canvas/public/components/datasource/no_datasource.js +++ b/x-pack/plugins/canvas/public/components/datasource/no_datasource.js @@ -8,9 +8,19 @@ import React from 'react'; import PropTypes from 'prop-types'; import { EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; -import { ComponentStrings } from '../../../i18n'; -const { DatasourceNoDatasource: strings } = ComponentStrings; +const strings = { + getPanelDescription: () => + i18n.translate('xpack.canvas.datasourceNoDatasource.panelDescription', { + defaultMessage: + "This element does not have an attached data source. This is usually because the element is an image or other static asset. If that's not the case you might want to check your expression to make sure it is not malformed.", + }), + getPanelTitle: () => + i18n.translate('xpack.canvas.datasourceNoDatasource.panelTitle', { + defaultMessage: 'No data source present', + }), +}; export const NoDatasource = () => ( <div className="canvasDataSource__section"> diff --git a/x-pack/plugins/canvas/public/components/element_config/element_config.tsx b/x-pack/plugins/canvas/public/components/element_config/element_config.tsx index 683c12f13f0f9..bf09ac3c5ab77 100644 --- a/x-pack/plugins/canvas/public/components/element_config/element_config.tsx +++ b/x-pack/plugins/canvas/public/components/element_config/element_config.tsx @@ -5,13 +5,42 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiAccordion } from '@elastic/eui'; -import PropTypes from 'prop-types'; import React from 'react'; -import { ComponentStrings } from '../../../i18n'; +import PropTypes from 'prop-types'; +import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiAccordion } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import { State } from '../../../types'; -const { ElementConfig: strings } = ComponentStrings; +const strings = { + getFailedLabel: () => + i18n.translate('xpack.canvas.elementConfig.failedLabel', { + defaultMessage: 'Failed', + description: + 'The label for the total number of elements in a workpad that have thrown an error or failed to load', + }), + getLoadedLabel: () => + i18n.translate('xpack.canvas.elementConfig.loadedLabel', { + defaultMessage: 'Loaded', + description: 'The label for the number of elements in a workpad that have loaded', + }), + getProgressLabel: () => + i18n.translate('xpack.canvas.elementConfig.progressLabel', { + defaultMessage: 'Progress', + description: 'The label for the percentage of elements that have finished loading', + }), + getTitle: () => + i18n.translate('xpack.canvas.elementConfig.title', { + defaultMessage: 'Element status', + description: + '"Elements" refers to the individual text, images, or visualizations that you can add to a Canvas workpad', + }), + getTotalLabel: () => + i18n.translate('xpack.canvas.elementConfig.totalLabel', { + defaultMessage: 'Total', + description: 'The label for the total number of elements in a workpad', + }), +}; interface Props { elementStats: State['transient']['elementStats']; diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx index c86b1d6405e24..716f757b7c25e 100644 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx @@ -7,15 +7,24 @@ import React, { FC } from 'react'; import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import { SavedObjectFinderUi, SavedObjectMetaData, } from '../../../../../../src/plugins/saved_objects/public/'; -import { ComponentStrings } from '../../../i18n'; import { useServices } from '../../services'; -const { AddEmbeddableFlyout: strings } = ComponentStrings; - +const strings = { + getNoItemsText: () => + i18n.translate('xpack.canvas.embedObject.noMatchingObjectsMessage', { + defaultMessage: 'No matching objects found.', + }), + getTitleText: () => + i18n.translate('xpack.canvas.embedObject.titleText', { + defaultMessage: 'Add from Kibana', + }), +}; export interface Props { onClose: () => void; onSelect: (id: string, embeddableType: string) => void; diff --git a/x-pack/plugins/canvas/public/components/error/error.tsx b/x-pack/plugins/canvas/public/components/error/error.tsx index b4cc85ba336e9..cb2c2cd5d58c1 100644 --- a/x-pack/plugins/canvas/public/components/error/error.tsx +++ b/x-pack/plugins/canvas/public/components/error/error.tsx @@ -8,18 +8,27 @@ import React, { FC } from 'react'; import PropTypes from 'prop-types'; import { EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; -import { ComponentStrings } from '../../../i18n'; + import { ShowDebugging } from './show_debugging'; +const strings = { + getDescription: () => + i18n.translate('xpack.canvas.errorComponent.description', { + defaultMessage: 'Expression failed with the message:', + }), + getTitle: () => + i18n.translate('xpack.canvas.errorComponent.title', { + defaultMessage: 'Whoops! Expression failed', + }), +}; export interface Props { payload: { error: Error; }; } -const { Error: strings } = ComponentStrings; - export const Error: FC<Props> = ({ payload }) => { const message = get(payload, 'error.message'); diff --git a/x-pack/plugins/canvas/public/components/expression/element_not_selected.js b/x-pack/plugins/canvas/public/components/expression/element_not_selected.js index c7c8c1b063cf1..5f717af6101c1 100644 --- a/x-pack/plugins/canvas/public/components/expression/element_not_selected.js +++ b/x-pack/plugins/canvas/public/components/expression/element_not_selected.js @@ -8,9 +8,18 @@ import React from 'react'; import PropTypes from 'prop-types'; import { EuiButton } from '@elastic/eui'; -import { ComponentStrings } from '../../../i18n'; +import { i18n } from '@kbn/i18n'; -const { ExpressionElementNotSelected: strings } = ComponentStrings; +const strings = { + getCloseButtonLabel: () => + i18n.translate('xpack.canvas.expressionElementNotSelected.closeButtonLabel', { + defaultMessage: 'Close', + }), + getSelectDescription: () => + i18n.translate('xpack.canvas.expressionElementNotSelected.selectDescription', { + defaultMessage: 'Select an element to show expression input', + }), +}; export const ElementNotSelected = ({ done }) => ( <div> diff --git a/x-pack/plugins/canvas/public/components/expression/expression.tsx b/x-pack/plugins/canvas/public/components/expression/expression.tsx index 74fdefc322cc9..ff3fed32c0ac0 100644 --- a/x-pack/plugins/canvas/public/components/expression/expression.tsx +++ b/x-pack/plugins/canvas/public/components/expression/expression.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC, MutableRefObject } from 'react'; +import React, { FC, MutableRefObject, useRef } from 'react'; import PropTypes from 'prop-types'; import { EuiPanel, @@ -17,17 +17,46 @@ import { EuiLink, EuiPortal, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + // @ts-expect-error import { Shortcuts } from 'react-shortcuts'; -import { ComponentStrings } from '../../../i18n'; + import { ExpressionInput } from '../expression_input'; import { ToolTipShortcut } from '../tool_tip_shortcut'; import { ExpressionFunction } from '../../../types'; import { FormState } from './'; -const { Expression: strings } = ComponentStrings; - -const { useRef } = React; +const strings = { + getCancelButtonLabel: () => + i18n.translate('xpack.canvas.expression.cancelButtonLabel', { + defaultMessage: 'Cancel', + }), + getCloseButtonLabel: () => + i18n.translate('xpack.canvas.expression.closeButtonLabel', { + defaultMessage: 'Close', + }), + getLearnLinkText: () => + i18n.translate('xpack.canvas.expression.learnLinkText', { + defaultMessage: 'Learn expression syntax', + }), + getMaximizeButtonLabel: () => + i18n.translate('xpack.canvas.expression.maximizeButtonLabel', { + defaultMessage: 'Maximize editor', + }), + getMinimizeButtonLabel: () => + i18n.translate('xpack.canvas.expression.minimizeButtonLabel', { + defaultMessage: 'Minimize Editor', + }), + getRunButtonLabel: () => + i18n.translate('xpack.canvas.expression.runButtonLabel', { + defaultMessage: 'Run', + }), + getRunTooltip: () => + i18n.translate('xpack.canvas.expression.runTooltip', { + defaultMessage: 'Run the expression', + }), +}; const shortcut = ( ref: MutableRefObject<ExpressionInput | null>, diff --git a/x-pack/plugins/canvas/public/components/expression_input/reference.ts b/x-pack/plugins/canvas/public/components/expression_input/reference.ts index 95d27360aafc9..94a369e6cb8d8 100644 --- a/x-pack/plugins/canvas/public/components/expression_input/reference.ts +++ b/x-pack/plugins/canvas/public/components/expression_input/reference.ts @@ -5,13 +5,64 @@ * 2.0. */ -import { ComponentStrings } from '../../../i18n'; +import { i18n } from '@kbn/i18n'; import { ExpressionFunction, ExpressionFunctionParameter, } from '../../../../../../src/plugins/expressions'; -const { ExpressionInput: strings } = ComponentStrings; +import { BOLD_MD_TOKEN } from '../../../i18n/constants'; + +const strings = { + getArgReferenceAliasesDetail: (aliases: string) => + i18n.translate('xpack.canvas.expressionInput.argReferenceAliasesDetail', { + defaultMessage: '{BOLD_MD_TOKEN}Aliases{BOLD_MD_TOKEN}: {aliases}', + values: { + BOLD_MD_TOKEN, + aliases, + }, + }), + getArgReferenceDefaultDetail: (defaultVal: string) => + i18n.translate('xpack.canvas.expressionInput.argReferenceDefaultDetail', { + defaultMessage: '{BOLD_MD_TOKEN}Default{BOLD_MD_TOKEN}: {defaultVal}', + values: { + BOLD_MD_TOKEN, + defaultVal, + }, + }), + getArgReferenceRequiredDetail: (required: string) => + i18n.translate('xpack.canvas.expressionInput.argReferenceRequiredDetail', { + defaultMessage: '{BOLD_MD_TOKEN}Required{BOLD_MD_TOKEN}: {required}', + values: { + BOLD_MD_TOKEN, + required, + }, + }), + getArgReferenceTypesDetail: (types: string) => + i18n.translate('xpack.canvas.expressionInput.argReferenceTypesDetail', { + defaultMessage: '{BOLD_MD_TOKEN}Types{BOLD_MD_TOKEN}: {types}', + values: { + BOLD_MD_TOKEN, + types, + }, + }), + getFunctionReferenceAcceptsDetail: (acceptTypes: string) => + i18n.translate('xpack.canvas.expressionInput.functionReferenceAccepts', { + defaultMessage: '{BOLD_MD_TOKEN}Accepts{BOLD_MD_TOKEN}: {acceptTypes}', + values: { + BOLD_MD_TOKEN, + acceptTypes, + }, + }), + getFunctionReferenceReturnsDetail: (returnType: string) => + i18n.translate('xpack.canvas.expressionInput.functionReferenceReturns', { + defaultMessage: '{BOLD_MD_TOKEN}Returns{BOLD_MD_TOKEN}: {returnType}', + values: { + BOLD_MD_TOKEN, + returnType, + }, + }), +}; /** * Given an expression function, this function returns a markdown string diff --git a/x-pack/plugins/canvas/public/components/function_form/function_form_context_error.tsx b/x-pack/plugins/canvas/public/components/function_form/function_form_context_error.tsx index a022f98d14e1a..2ee709edbf91c 100644 --- a/x-pack/plugins/canvas/public/components/function_form/function_form_context_error.tsx +++ b/x-pack/plugins/canvas/public/components/function_form/function_form_context_error.tsx @@ -7,16 +7,23 @@ import React, { FunctionComponent } from 'react'; import PropTypes from 'prop-types'; -import { ComponentStrings } from '../../../i18n/components'; +import { i18n } from '@kbn/i18n'; +const strings = { + getContextErrorMessage: (errorMessage: string) => + i18n.translate('xpack.canvas.functionForm.contextError', { + defaultMessage: 'ERROR: {errorMessage}', + values: { + errorMessage, + }, + }), +}; interface Props { context: { error: string; }; } -const { FunctionFormContextError: strings } = ComponentStrings; - export const FunctionFormContextError: FunctionComponent<Props> = ({ context }) => ( <div className="canvasFunctionForm canvasFunctionForm--error"> {strings.getContextErrorMessage(context.error)} diff --git a/x-pack/plugins/canvas/public/components/function_form/function_unknown.tsx b/x-pack/plugins/canvas/public/components/function_form/function_unknown.tsx index b3054e280bbe5..cd7e2f27912a1 100644 --- a/x-pack/plugins/canvas/public/components/function_form/function_unknown.tsx +++ b/x-pack/plugins/canvas/public/components/function_form/function_unknown.tsx @@ -7,13 +7,22 @@ import React, { FunctionComponent } from 'react'; import PropTypes from 'prop-types'; -import { ComponentStrings } from '../../../i18n'; +import { i18n } from '@kbn/i18n'; + +const strings = { + getUnknownArgumentTypeErrorMessage: (expressionType: string) => + i18n.translate('xpack.canvas.functionForm.functionUnknown.unknownArgumentTypeError', { + defaultMessage: 'Unknown expression type "{expressionType}"', + values: { + expressionType, + }, + }), +}; interface Props { /** the type of the argument */ argType: string; } -const { FunctionFormFunctionUnknown: strings } = ComponentStrings; export const FunctionUnknown: FunctionComponent<Props> = ({ argType }) => ( <div className="canvasFunctionForm canvasFunctionForm--unknown-expression"> diff --git a/x-pack/plugins/canvas/public/components/help_menu/help_menu.tsx b/x-pack/plugins/canvas/public/components/help_menu/help_menu.tsx index b10103e1824e5..2877ccf41056d 100644 --- a/x-pack/plugins/canvas/public/components/help_menu/help_menu.tsx +++ b/x-pack/plugins/canvas/public/components/help_menu/help_menu.tsx @@ -7,11 +7,13 @@ import React, { FC, useState, lazy, Suspense } from 'react'; import { EuiButtonEmpty, EuiPortal, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { ExpressionFunction } from 'src/plugins/expressions'; -import { ComponentStrings } from '../../../i18n'; + import { KeyboardShortcutsDoc } from '../keyboard_shortcuts_doc'; let FunctionReferenceGenerator: null | React.LazyExoticComponent<any> = null; + if (process.env.NODE_ENV === 'development') { FunctionReferenceGenerator = lazy(() => import('../function_reference_generator').then((module) => ({ @@ -20,7 +22,12 @@ if (process.env.NODE_ENV === 'development') { ); } -const { HelpMenu: strings } = ComponentStrings; +const strings = { + getKeyboardShortcutsLinkLabel: () => + i18n.translate('xpack.canvas.helpMenu.keyboardShortcutsLinkLabel', { + defaultMessage: 'Keyboard shortcuts', + }), +}; interface Props { functionRegistry: Record<string, ExpressionFunction>; diff --git a/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot b/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot index 5e833944046a4..bc6430c4c0357 100644 --- a/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot @@ -2,1103 +2,1088 @@ exports[`Storyshots components/KeyboardShortcutsDoc default 1`] = ` <div - data-eui="EuiFocusTrap" + data-eui="EuiFlyout" + role="dialog" > + <button + data-test-subj="euiFlyoutCloseButton" + onClick={[Function]} + type="button" + /> <div - className="euiFlyout euiFlyout--small euiFlyout--paddingLarge" - role="dialog" - tabIndex={0} + className="euiFlyoutHeader euiFlyoutHeader--hasBorder" > - <button - aria-label="Closes keyboard shortcuts reference" - className="euiButtonIcon euiButtonIcon--text euiButtonIcon--empty euiButtonIcon--xSmall euiFlyout__closeButton" - data-test-subj="euiFlyoutCloseButton" - disabled={false} - onClick={[Function]} - type="button" + <h2 + className="euiTitle euiTitle--small" > - <span - aria-hidden="true" - className="euiButtonIcon__icon" - color="inherit" - data-euiicon-type="cross" - size="m" - /> - </button> - <div - className="euiFlyoutHeader euiFlyoutHeader--hasBorder" - > - <h2 - className="euiTitle euiTitle--small" - > - Keyboard shortcuts - </h2> - </div> + Keyboard shortcuts + </h2> + </div> + <div + className="euiFlyoutBody" + > <div - className="euiFlyoutBody" + className="euiFlyoutBody__overflow" + tabIndex={0} > <div - className="euiFlyoutBody__overflow" + className="euiFlyoutBody__overflowContent" > <div - className="euiFlyoutBody__overflowContent" + className="canvasKeyboardShortcut" > - <div - className="canvasKeyboardShortcut" + <h4 + className="euiTitle euiTitle--xsmall" + > + Element controls + </h4> + <hr + className="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginSmall" + /> + <dl + className="euiDescriptionList euiDescriptionList--column euiDescriptionList--reverse euiDescriptionList--compressed" > - <h4 - className="euiTitle euiTitle--xsmall" - > - Element controls - </h4> - <hr - className="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginSmall" - /> - <dl - className="euiDescriptionList euiDescriptionList--column euiDescriptionList--reverse euiDescriptionList--compressed" - > - <dt - className="euiDescriptionList__title" - > - Cut - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - CTRL - </code> - </span> - - <span> - <code> - X - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Copy - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - CTRL - </code> - </span> - - <span> - <code> - C - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Paste - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - CTRL - </code> - </span> - - <span> - <code> - V - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Clone - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - CTRL - </code> - </span> - - <span> - <code> - D - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Delete - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - DEL - </code> - </span> - </span> - <span> - - or - - </span> - <span> - <span> - <code> - BACKSPACE - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Bring to front - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - CTRL - </code> - </span> - - <span> - <code> - ↑ - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Bring forward - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - CTRL - </code> - </span> - - <span> - <code> - SHIFT - </code> - </span> - - <span> - <code> - ↑ - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Send backward - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - CTRL - </code> - </span> - - <span> - <code> - ↓ - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Send to back - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - CTRL - </code> - </span> - - <span> - <code> - SHIFT - </code> - </span> - - <span> - <code> - ↓ - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Group - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - G - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Ungroup - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - U - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Shift up by 10px - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - ↑ - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Shift down by 10px - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - ↓ - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Shift left by 10px - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - ← - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Shift right by 10px - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - → - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Shift up by 1px - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - SHIFT - </code> - </span> - - <span> - <code> - ↑ - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Shift down by 1px - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - SHIFT - </code> - </span> - - <span> - <code> - ↓ - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Shift left by 1px - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - SHIFT - </code> - </span> - - <span> - <code> - ← - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Shift right by 1px - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - SHIFT - </code> - </span> - - <span> - <code> - → - </code> - </span> - </span> - </dd> - </dl> - <div - className="euiSpacer euiSpacer--l" - /> - </div> + <dt + className="euiDescriptionList__title" + > + Cut + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + CTRL + </code> + </span> + + <span> + <code> + X + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Copy + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + CTRL + </code> + </span> + + <span> + <code> + C + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Paste + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + CTRL + </code> + </span> + + <span> + <code> + V + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Clone + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + CTRL + </code> + </span> + + <span> + <code> + D + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Delete + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + DEL + </code> + </span> + </span> + <span> + + or + + </span> + <span> + <span> + <code> + BACKSPACE + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Bring to front + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + CTRL + </code> + </span> + + <span> + <code> + ↑ + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Bring forward + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + CTRL + </code> + </span> + + <span> + <code> + SHIFT + </code> + </span> + + <span> + <code> + ↑ + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Send backward + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + CTRL + </code> + </span> + + <span> + <code> + ↓ + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Send to back + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + CTRL + </code> + </span> + + <span> + <code> + SHIFT + </code> + </span> + + <span> + <code> + ↓ + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Group + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + G + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Ungroup + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + U + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Shift up by 10px + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + ↑ + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Shift down by 10px + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + ↓ + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Shift left by 10px + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + ← + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Shift right by 10px + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + → + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Shift up by 1px + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + SHIFT + </code> + </span> + + <span> + <code> + ↑ + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Shift down by 1px + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + SHIFT + </code> + </span> + + <span> + <code> + ↓ + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Shift left by 1px + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + SHIFT + </code> + </span> + + <span> + <code> + ← + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Shift right by 1px + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + SHIFT + </code> + </span> + + <span> + <code> + → + </code> + </span> + </span> + </dd> + </dl> <div - className="canvasKeyboardShortcut" + className="euiSpacer euiSpacer--l" + /> + </div> + <div + className="canvasKeyboardShortcut" + > + <h4 + className="euiTitle euiTitle--xsmall" + > + Expression controls + </h4> + <hr + className="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginSmall" + /> + <dl + className="euiDescriptionList euiDescriptionList--column euiDescriptionList--reverse euiDescriptionList--compressed" > - <h4 - className="euiTitle euiTitle--xsmall" - > - Expression controls - </h4> - <hr - className="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginSmall" - /> - <dl - className="euiDescriptionList euiDescriptionList--column euiDescriptionList--reverse euiDescriptionList--compressed" - > - <dt - className="euiDescriptionList__title" - > - Run whole expression - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - CTRL - </code> - </span> - - <span> - <code> - ENTER - </code> - </span> - </span> - </dd> - </dl> - <div - className="euiSpacer euiSpacer--l" - /> - </div> + <dt + className="euiDescriptionList__title" + > + Run whole expression + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + CTRL + </code> + </span> + + <span> + <code> + ENTER + </code> + </span> + </span> + </dd> + </dl> <div - className="canvasKeyboardShortcut" + className="euiSpacer euiSpacer--l" + /> + </div> + <div + className="canvasKeyboardShortcut" + > + <h4 + className="euiTitle euiTitle--xsmall" > - <h4 - className="euiTitle euiTitle--xsmall" - > - Editor controls - </h4> - <hr - className="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginSmall" - /> - <dl - className="euiDescriptionList euiDescriptionList--column euiDescriptionList--reverse euiDescriptionList--compressed" - > - <dt - className="euiDescriptionList__title" - > - Select multiple elements - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - SHIFT - </code> - </span> - - <span> - <code> - CLICK - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Resize from center - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - ALT - </code> - </span> - - <span> - <code> - DRAG - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Move, resize, and rotate without snapping - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - CTRL - </code> - </span> - - <span> - <code> - DRAG - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Select element below - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - CTRL - </code> - </span> - - <span> - <code> - CLICK - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Undo last action - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - CTRL - </code> - </span> - - <span> - <code> - Z - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Redo last action - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - CTRL - </code> - </span> - - <span> - <code> - SHIFT - </code> - </span> - - <span> - <code> - Z - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Go to previous page - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - ALT - </code> - </span> - - <span> - <code> - [ - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Go to next page - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - ALT - </code> - </span> - - <span> - <code> - ] - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Toggle edit mode - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - ALT - </code> - </span> - - <span> - <code> - E - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Show grid - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - ALT - </code> - </span> - - <span> - <code> - G - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Refresh workpad - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - ALT - </code> - </span> - - <span> - <code> - R - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Zoom in - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - CTRL - </code> - </span> - - <span> - <code> - ALT - </code> - </span> - - <span> - <code> - + - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Zoom out - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - CTRL - </code> - </span> - - <span> - <code> - ALT - </code> - </span> - - <span> - <code> - - - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Reset zoom to 100% - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - CTRL - </code> - </span> - - <span> - <code> - ALT - </code> - </span> - - <span> - <code> - [ - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Enter presentation mode - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - ALT - </code> - </span> - - <span> - <code> - F - </code> - </span> - </span> - <span> - - or - - </span> - <span> - <span> - <code> - ALT - </code> - </span> - - <span> - <code> - P - </code> - </span> - </span> - </dd> - </dl> - <div - className="euiSpacer euiSpacer--l" - /> - </div> + Editor controls + </h4> + <hr + className="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginSmall" + /> + <dl + className="euiDescriptionList euiDescriptionList--column euiDescriptionList--reverse euiDescriptionList--compressed" + > + <dt + className="euiDescriptionList__title" + > + Select multiple elements + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + SHIFT + </code> + </span> + + <span> + <code> + CLICK + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Resize from center + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + ALT + </code> + </span> + + <span> + <code> + DRAG + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Move, resize, and rotate without snapping + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + CTRL + </code> + </span> + + <span> + <code> + DRAG + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Select element below + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + CTRL + </code> + </span> + + <span> + <code> + CLICK + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Undo last action + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + CTRL + </code> + </span> + + <span> + <code> + Z + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Redo last action + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + CTRL + </code> + </span> + + <span> + <code> + SHIFT + </code> + </span> + + <span> + <code> + Z + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Go to previous page + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + ALT + </code> + </span> + + <span> + <code> + [ + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Go to next page + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + ALT + </code> + </span> + + <span> + <code> + ] + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Toggle edit mode + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + ALT + </code> + </span> + + <span> + <code> + E + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Show grid + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + ALT + </code> + </span> + + <span> + <code> + G + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Refresh workpad + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + ALT + </code> + </span> + + <span> + <code> + R + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Zoom in + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + CTRL + </code> + </span> + + <span> + <code> + ALT + </code> + </span> + + <span> + <code> + + + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Zoom out + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + CTRL + </code> + </span> + + <span> + <code> + ALT + </code> + </span> + + <span> + <code> + - + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Reset zoom to 100% + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + CTRL + </code> + </span> + + <span> + <code> + ALT + </code> + </span> + + <span> + <code> + [ + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Enter presentation mode + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + ALT + </code> + </span> + + <span> + <code> + F + </code> + </span> + </span> + <span> + + or + + </span> + <span> + <span> + <code> + ALT + </code> + </span> + + <span> + <code> + P + </code> + </span> + </span> + </dd> + </dl> <div - className="canvasKeyboardShortcut" + className="euiSpacer euiSpacer--l" + /> + </div> + <div + className="canvasKeyboardShortcut" + > + <h4 + className="euiTitle euiTitle--xsmall" + > + Presentation controls + </h4> + <hr + className="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginSmall" + /> + <dl + className="euiDescriptionList euiDescriptionList--column euiDescriptionList--reverse euiDescriptionList--compressed" > - <h4 - className="euiTitle euiTitle--xsmall" - > - Presentation controls - </h4> - <hr - className="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginSmall" - /> - <dl - className="euiDescriptionList euiDescriptionList--column euiDescriptionList--reverse euiDescriptionList--compressed" - > - <dt - className="euiDescriptionList__title" - > - Enter presentation mode - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - ALT - </code> - </span> - - <span> - <code> - F - </code> - </span> - </span> - <span> - - or - - </span> - <span> - <span> - <code> - ALT - </code> - </span> - - <span> - <code> - P - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Exit presentation mode - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - ESC - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Go to previous page - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - ALT - </code> - </span> - - <span> - <code> - [ - </code> - </span> - </span> - <span> - - or - - </span> - <span> - <span> - <code> - BACKSPACE - </code> - </span> - </span> - <span> - - or - - </span> - <span> - <span> - <code> - ← - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Go to next page - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - ALT - </code> - </span> - - <span> - <code> - ] - </code> - </span> - </span> - <span> - - or - - </span> - <span> - <span> - <code> - SPACE - </code> - </span> - </span> - <span> - - or - - </span> - <span> - <span> - <code> - → - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Refresh workpad - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - ALT - </code> - </span> - - <span> - <code> - R - </code> - </span> - </span> - </dd> - <dt - className="euiDescriptionList__title" - > - Toggle page cycling - </dt> - <dd - className="euiDescriptionList__description" - > - <span> - <span> - <code> - P - </code> - </span> - </span> - </dd> - </dl> - <div - className="euiSpacer euiSpacer--l" - /> - </div> + <dt + className="euiDescriptionList__title" + > + Enter presentation mode + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + ALT + </code> + </span> + + <span> + <code> + F + </code> + </span> + </span> + <span> + + or + + </span> + <span> + <span> + <code> + ALT + </code> + </span> + + <span> + <code> + P + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Exit presentation mode + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + ESC + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Go to previous page + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + ALT + </code> + </span> + + <span> + <code> + [ + </code> + </span> + </span> + <span> + + or + + </span> + <span> + <span> + <code> + BACKSPACE + </code> + </span> + </span> + <span> + + or + + </span> + <span> + <span> + <code> + ← + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Go to next page + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + ALT + </code> + </span> + + <span> + <code> + ] + </code> + </span> + </span> + <span> + + or + + </span> + <span> + <span> + <code> + SPACE + </code> + </span> + </span> + <span> + + or + + </span> + <span> + <span> + <code> + → + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Refresh workpad + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + ALT + </code> + </span> + + <span> + <code> + R + </code> + </span> + </span> + </dd> + <dt + className="euiDescriptionList__title" + > + Toggle page cycling + </dt> + <dd + className="euiDescriptionList__description" + > + <span> + <span> + <code> + P + </code> + </span> + </span> + </dd> + </dl> + <div + className="euiSpacer euiSpacer--l" + /> </div> </div> </div> diff --git a/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/keyboard_shortcuts_doc.tsx b/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/keyboard_shortcuts_doc.tsx index 0c98ea70b5b9d..a71976006d51c 100644 --- a/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/keyboard_shortcuts_doc.tsx +++ b/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/keyboard_shortcuts_doc.tsx @@ -17,14 +17,30 @@ import { EuiSpacer, EuiTitle, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import { keymap } from '../../lib/keymap'; import { ShortcutMap, ShortcutNameSpace } from '../../../types/shortcuts'; import { getClientPlatform } from '../../lib/get_client_platform'; import { getId } from '../../lib/get_id'; import { getPrettyShortcut } from '../../lib/get_pretty_shortcut'; -import { ComponentStrings } from '../../../i18n/components'; -const { KeyboardShortcutsDoc: strings } = ComponentStrings; +const strings = { + getFlyoutCloseButtonAriaLabel: () => + i18n.translate('xpack.canvas.keyboardShortcutsDoc.flyout.closeButtonAriaLabel', { + defaultMessage: 'Closes keyboard shortcuts reference', + }), + getShortcutSeparator: () => + i18n.translate('xpack.canvas.keyboardShortcutsDoc.shortcutListSeparator', { + defaultMessage: 'or', + description: + 'Separates which keyboard shortcuts can be used for a single action. Example: "{shortcut1} or {shortcut2} or {shortcut3}"', + }), + getTitle: () => + i18n.translate('xpack.canvas.keyboardShortcutsDoc.flyoutHeaderTitle', { + defaultMessage: 'Keyboard shortcuts', + }), +}; interface DescriptionListItem { title: string; diff --git a/x-pack/plugins/canvas/public/components/page_config/index.js b/x-pack/plugins/canvas/public/components/page_config/index.js index 59f0ac99fd73b..898ac60e68e38 100644 --- a/x-pack/plugins/canvas/public/components/page_config/index.js +++ b/x-pack/plugins/canvas/public/components/page_config/index.js @@ -7,13 +7,22 @@ import { connect } from 'react-redux'; import { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; + import { transitionsRegistry } from '../../lib/transitions_registry'; import { getSelectedPageIndex, getPages } from '../../state/selectors/workpad'; import { stylePage, setPageTransition } from '../../state/actions/pages'; -import { ComponentStrings } from '../../../i18n'; import { PageConfig as Component } from './page_config'; -const { PageConfig: strings } = ComponentStrings; +const strings = { + getNoTransitionDropDownOptionLabel: () => + i18n.translate('xpack.canvas.pageConfig.transitions.noneDropDownOptionLabel', { + defaultMessage: 'None', + description: + 'This is the option the user should choose if they do not want any page transition (i.e. fade in, fade out, etc) to ' + + 'be applied to the current page.', + }), +}; const mapStateToProps = (state) => { const pageIndex = getSelectedPageIndex(state); diff --git a/x-pack/plugins/canvas/public/components/page_config/page_config.js b/x-pack/plugins/canvas/public/components/page_config/page_config.js index bc7d92de2273c..8b0c2fedf3af3 100644 --- a/x-pack/plugins/canvas/public/components/page_config/page_config.js +++ b/x-pack/plugins/canvas/public/components/page_config/page_config.js @@ -16,10 +16,35 @@ import { EuiToolTip, EuiIcon, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import { WorkpadColorPicker } from '../workpad_color_picker'; -import { ComponentStrings } from '../../../i18n'; -const { PageConfig: strings } = ComponentStrings; +const strings = { + getBackgroundColorDescription: () => + i18n.translate('xpack.canvas.pageConfig.backgroundColorDescription', { + defaultMessage: 'Accepts HEX, RGB or HTML color names', + }), + getBackgroundColorLabel: () => + i18n.translate('xpack.canvas.pageConfig.backgroundColorLabel', { + defaultMessage: 'Background', + }), + getTitle: () => + i18n.translate('xpack.canvas.pageConfig.title', { + defaultMessage: 'Page settings', + }), + getTransitionLabel: () => + i18n.translate('xpack.canvas.pageConfig.transitionLabel', { + defaultMessage: 'Transition', + description: + 'This refers to the transition effect, such as fade in or rotate, applied to a page in presentation mode.', + }), + getTransitionPreviewLabel: () => + i18n.translate('xpack.canvas.pageConfig.transitionPreviewLabel', { + defaultMessage: 'Preview', + description: 'This is the label for a preview of the transition effect selected.', + }), +}; export const PageConfig = ({ pageIndex, diff --git a/x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx b/x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx index 06968d2e4be0a..9d1939db43fd5 100644 --- a/x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx +++ b/x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx @@ -8,7 +8,9 @@ import React, { Fragment, Component } from 'react'; import PropTypes from 'prop-types'; import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { DragDropContext, Droppable, Draggable, DragDropContextProps } from 'react-beautiful-dnd'; + // @ts-expect-error untyped dependency import Style from 'style-it'; import { ConfirmModal } from '../confirm_modal'; @@ -16,11 +18,26 @@ import { RoutingLink } from '../routing'; import { WorkpadRoutingContext } from '../../routes/workpad'; import { PagePreview } from '../page_preview'; -import { ComponentStrings } from '../../../i18n'; import { CanvasPage } from '../../../types'; -const { PageManager: strings } = ComponentStrings; - +const strings = { + getAddPageTooltip: () => + i18n.translate('xpack.canvas.pageManager.addPageTooltip', { + defaultMessage: 'Add a new page to this workpad', + }), + getConfirmRemoveTitle: () => + i18n.translate('xpack.canvas.pageManager.confirmRemoveTitle', { + defaultMessage: 'Remove Page', + }), + getConfirmRemoveDescription: () => + i18n.translate('xpack.canvas.pageManager.confirmRemoveDescription', { + defaultMessage: 'Are you sure you want to remove this page?', + }), + getConfirmRemoveButtonLabel: () => + i18n.translate('xpack.canvas.pageManager.removeButtonLabel', { + defaultMessage: 'Remove', + }), +}; export interface Props { isWriteable: boolean; onAddPage: () => void; diff --git a/x-pack/plugins/canvas/public/components/page_preview/page_controls.tsx b/x-pack/plugins/canvas/public/components/page_preview/page_controls.tsx index b29ef1e7fd087..5246fcf822a72 100644 --- a/x-pack/plugins/canvas/public/components/page_preview/page_controls.tsx +++ b/x-pack/plugins/canvas/public/components/page_preview/page_controls.tsx @@ -8,10 +8,26 @@ import React, { FC, ReactEventHandler } from 'react'; import PropTypes from 'prop-types'; import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; -import { ComponentStrings } from '../../../i18n'; - -const { PagePreviewPageControls: strings } = ComponentStrings; +const strings = { + getClonePageAriaLabel: () => + i18n.translate('xpack.canvas.pagePreviewPageControls.clonePageAriaLabel', { + defaultMessage: 'Clone page', + }), + getClonePageTooltip: () => + i18n.translate('xpack.canvas.pagePreviewPageControls.clonePageTooltip', { + defaultMessage: 'Clone', + }), + getDeletePageAriaLabel: () => + i18n.translate('xpack.canvas.pagePreviewPageControls.deletePageAriaLabel', { + defaultMessage: 'Delete page', + }), + getDeletePageTooltip: () => + i18n.translate('xpack.canvas.pagePreviewPageControls.deletePageTooltip', { + defaultMessage: 'Delete', + }), +}; interface Props { pageId: string; diff --git a/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.tsx b/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.tsx index 7ad7bcd8c49c2..dcc77b75f25c3 100644 --- a/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.tsx +++ b/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.tsx @@ -8,10 +8,20 @@ import React, { FC } from 'react'; import PropTypes from 'prop-types'; import { EuiColorPalettePicker, EuiColorPalettePickerPaletteProps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import { palettes, ColorPalette } from '../../../common/lib/palettes'; -import { ComponentStrings } from '../../../i18n'; -const { PalettePicker: strings } = ComponentStrings; +const strings = { + getEmptyPaletteLabel: () => + i18n.translate('xpack.canvas.palettePicker.emptyPaletteLabel', { + defaultMessage: 'None', + }), + getNoPaletteFoundErrorTitle: () => + i18n.translate('xpack.canvas.palettePicker.noPaletteFoundErrorTitle', { + defaultMessage: 'Color palette not found', + }), +}; interface RequiredProps { id?: string; diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot index bbf8b5dcca896..6cd18b83c3351 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/__stories__/__snapshots__/saved_elements_modal.stories.storyshot @@ -94,16 +94,16 @@ exports[`Storyshots components/SavedElementsModal no custom elements 1`] = ` size="xxl" /> <div - className="euiSpacer euiSpacer--s" + className="euiSpacer euiSpacer--m" /> + <h2 + className="euiTitle euiTitle--small" + > + Add new elements + </h2> <span className="euiTextColor euiTextColor--subdued" > - <h2 - className="euiTitle euiTitle--small" - > - Add new elements - </h2> <div className="euiSpacer euiSpacer--m" /> diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/element_controls.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/element_controls.tsx index 220ea193c902e..ad0a0053f55af 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/element_controls.tsx +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/element_controls.tsx @@ -8,9 +8,26 @@ import React, { FunctionComponent, MouseEvent } from 'react'; import PropTypes from 'prop-types'; import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { ComponentStrings } from '../../../i18n/components'; +import { i18n } from '@kbn/i18n'; -const { ElementControls: strings } = ComponentStrings; +const strings = { + getDeleteAriaLabel: () => + i18n.translate('xpack.canvas.elementControls.deleteAriaLabel', { + defaultMessage: 'Delete element', + }), + getDeleteTooltip: () => + i18n.translate('xpack.canvas.elementControls.deleteToolTip', { + defaultMessage: 'Delete', + }), + getEditAriaLabel: () => + i18n.translate('xpack.canvas.elementControls.editAriaLabel', { + defaultMessage: 'Edit element', + }), + getEditTooltip: () => + i18n.translate('xpack.canvas.elementControls.editToolTip', { + defaultMessage: 'Edit', + }), +}; interface Props { /** diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx index bc0039245f432..ee14e89dc4b7d 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx @@ -25,14 +25,59 @@ import { EuiSpacer, EuiButton, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import { sortBy } from 'lodash'; -import { ComponentStrings } from '../../../i18n'; import { CustomElement } from '../../../types'; import { ConfirmModal } from '../confirm_modal/confirm_modal'; import { CustomElementModal } from '../custom_element_modal'; import { ElementGrid } from './element_grid'; -const { SavedElementsModal: strings } = ComponentStrings; +const strings = { + getAddNewElementDescription: () => + i18n.translate('xpack.canvas.savedElementsModal.addNewElementDescription', { + defaultMessage: 'Group and save workpad elements to create new elements', + }), + getAddNewElementTitle: () => + i18n.translate('xpack.canvas.savedElementsModal.addNewElementTitle', { + defaultMessage: 'Add new elements', + }), + getCancelButtonLabel: () => + i18n.translate('xpack.canvas.savedElementsModal.cancelButtonLabel', { + defaultMessage: 'Cancel', + }), + getDeleteButtonLabel: () => + i18n.translate('xpack.canvas.savedElementsModal.deleteButtonLabel', { + defaultMessage: 'Delete', + }), + getDeleteElementDescription: () => + i18n.translate('xpack.canvas.savedElementsModal.deleteElementDescription', { + defaultMessage: 'Are you sure you want to delete this element?', + }), + getDeleteElementTitle: (elementName: string) => + i18n.translate('xpack.canvas.savedElementsModal.deleteElementTitle', { + defaultMessage: `Delete element '{elementName}'?`, + values: { + elementName, + }, + }), + getEditElementTitle: () => + i18n.translate('xpack.canvas.savedElementsModal.editElementTitle', { + defaultMessage: 'Edit element', + }), + getFindElementPlaceholder: () => + i18n.translate('xpack.canvas.savedElementsModal.findElementPlaceholder', { + defaultMessage: 'Find element', + }), + getModalTitle: () => + i18n.translate('xpack.canvas.savedElementsModal.modalTitle', { + defaultMessage: 'My elements', + }), + getSavedElementsModalCloseButtonLabel: () => + i18n.translate('xpack.canvas.workpadHeader.addElementModalCloseButtonLabel', { + defaultMessage: 'Close', + }), +}; export interface Props { /** diff --git a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx index cc0ad5a728b17..e8f2c7a559f58 100644 --- a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx +++ b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.component.tsx @@ -8,12 +8,28 @@ import React, { FunctionComponent } from 'react'; import PropTypes from 'prop-types'; import { EuiTabbedContent } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + // @ts-expect-error unconverted component import { Datasource } from '../../datasource'; // @ts-expect-error unconverted component import { FunctionFormList } from '../../function_form_list'; import { PositionedElement } from '../../../../types'; -import { ComponentStrings } from '../../../../i18n'; + +const strings = { + getDataTabLabel: () => + i18n.translate('xpack.canvas.elementSettings.dataTabLabel', { + defaultMessage: 'Data', + description: + 'This tab contains the settings for the data (i.e. Elasticsearch query) used as ' + + 'the source for a Canvas element', + }), + getDisplayTabLabel: () => + i18n.translate('xpack.canvas.elementSettings.displayTabLabel', { + defaultMessage: 'Display', + description: 'This tab contains the settings for how data is displayed in a Canvas element', + }), +}; interface Props { /** @@ -22,8 +38,6 @@ interface Props { element: PositionedElement; } -const { ElementSettings: strings } = ComponentStrings; - export const ElementSettings: FunctionComponent<Props> = ({ element }) => { const tabs = [ { diff --git a/x-pack/plugins/canvas/public/components/sidebar/group_settings.tsx b/x-pack/plugins/canvas/public/components/sidebar/group_settings.tsx index e13cf338a2bdc..9d95a6978ff50 100644 --- a/x-pack/plugins/canvas/public/components/sidebar/group_settings.tsx +++ b/x-pack/plugins/canvas/public/components/sidebar/group_settings.tsx @@ -7,9 +7,21 @@ import React, { FunctionComponent } from 'react'; import { EuiText } from '@elastic/eui'; -import { ComponentStrings } from '../../../i18n/components'; +import { i18n } from '@kbn/i18n'; -const { GroupSettings: strings } = ComponentStrings; +const strings = { + getSaveGroupDescription: () => + i18n.translate('xpack.canvas.groupSettings.saveGroupDescription', { + defaultMessage: 'Save this group as a new element to re-use it throughout your workpad.', + }), + getUngroupDescription: () => + i18n.translate('xpack.canvas.groupSettings.ungroupDescription', { + defaultMessage: 'Ungroup ({uKey}) to edit individual element settings.', + values: { + uKey: 'U', + }, + }), +}; export const GroupSettings: FunctionComponent = () => ( <div className="canvasSidebar__panel canvasSidebar__panel--isEmpty"> diff --git a/x-pack/plugins/canvas/public/components/sidebar/multi_element_settings.tsx b/x-pack/plugins/canvas/public/components/sidebar/multi_element_settings.tsx index f3bd11f603243..0d73e6397adcc 100644 --- a/x-pack/plugins/canvas/public/components/sidebar/multi_element_settings.tsx +++ b/x-pack/plugins/canvas/public/components/sidebar/multi_element_settings.tsx @@ -7,9 +7,23 @@ import React, { FunctionComponent } from 'react'; import { EuiText } from '@elastic/eui'; -import { ComponentStrings } from '../../../i18n/components'; +import { i18n } from '@kbn/i18n'; -const { MultiElementSettings: strings } = ComponentStrings; +const strings = { + getMultipleElementsActionsDescription: () => + i18n.translate('xpack.canvas.groupSettings.multipleElementsActionsDescription', { + defaultMessage: + 'Deselect these elements to edit their individual settings, press ({gKey}) to group them, or save this selection as a new ' + + 'element to re-use it throughout your workpad.', + values: { + gKey: 'G', + }, + }), + getMultipleElementsDescription: () => + i18n.translate('xpack.canvas.groupSettings.multipleElementsDescription', { + defaultMessage: 'Multiple elements are currently selected.', + }), +}; export const MultiElementSettings: FunctionComponent = () => ( <div className="canvasSidebar__panel canvasSidebar__panel--isEmpty"> diff --git a/x-pack/plugins/canvas/public/components/sidebar/sidebar_content.js b/x-pack/plugins/canvas/public/components/sidebar/sidebar_content.js index a284fc3278436..7292a98fa91ae 100644 --- a/x-pack/plugins/canvas/public/components/sidebar/sidebar_content.js +++ b/x-pack/plugins/canvas/public/components/sidebar/sidebar_content.js @@ -9,15 +9,39 @@ import React, { Fragment } from 'react'; import { connect } from 'react-redux'; import { compose, branch, renderComponent } from 'recompose'; import { EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import { getSelectedToplevelNodes, getSelectedElementId } from '../../state/selectors/workpad'; import { SidebarHeader } from '../sidebar_header'; -import { ComponentStrings } from '../../../i18n'; import { MultiElementSettings } from './multi_element_settings'; import { GroupSettings } from './group_settings'; import { GlobalConfig } from './global_config'; import { ElementSettings } from './element_settings'; -const { SidebarContent: strings } = ComponentStrings; +const strings = { + getGroupedElementSidebarTitle: () => + i18n.translate('xpack.canvas.sidebarContent.groupedElementSidebarTitle', { + defaultMessage: 'Grouped element', + description: + 'The title displayed when a grouped element is selected. "elements" refer to the different visualizations, images, ' + + 'text, etc that can be added in a Canvas workpad. These elements can be grouped into a larger "grouped element" ' + + 'that contains multiple individual elements.', + }), + getMultiElementSidebarTitle: () => + i18n.translate('xpack.canvas.sidebarContent.multiElementSidebarTitle', { + defaultMessage: 'Multiple elements', + description: + 'The title displayed when multiple elements are selected. "elements" refer to the different visualizations, images, ' + + 'text, etc that can be added in a Canvas workpad.', + }), + getSingleElementSidebarTitle: () => + i18n.translate('xpack.canvas.sidebarContent.singleElementSidebarTitle', { + defaultMessage: 'Selected element', + description: + 'The title displayed when a single element are selected. "element" refer to the different visualizations, images, ' + + 'text, etc that can be added in a Canvas workpad.', + }), +}; const mapStateToProps = (state) => ({ selectedToplevelNodes: getSelectedToplevelNodes(state), diff --git a/x-pack/plugins/canvas/public/components/sidebar_header/sidebar_header.tsx b/x-pack/plugins/canvas/public/components/sidebar_header/sidebar_header.tsx index d4f8c7642830d..4ba3a7f90f64b 100644 --- a/x-pack/plugins/canvas/public/components/sidebar_header/sidebar_header.tsx +++ b/x-pack/plugins/canvas/public/components/sidebar_header/sidebar_header.tsx @@ -8,11 +8,30 @@ import React, { FunctionComponent } from 'react'; import PropTypes from 'prop-types'; import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import { ToolTipShortcut } from '../tool_tip_shortcut/'; -import { ComponentStrings } from '../../../i18n/components'; import { ShortcutStrings } from '../../../i18n/shortcuts'; -const { SidebarHeader: strings } = ComponentStrings; +const strings = { + getBringForwardAriaLabel: () => + i18n.translate('xpack.canvas.sidebarHeader.bringForwardArialLabel', { + defaultMessage: 'Move element up one layer', + }), + getBringToFrontAriaLabel: () => + i18n.translate('xpack.canvas.sidebarHeader.bringToFrontArialLabel', { + defaultMessage: 'Move element to top layer', + }), + getSendBackwardAriaLabel: () => + i18n.translate('xpack.canvas.sidebarHeader.sendBackwardArialLabel', { + defaultMessage: 'Move element down one layer', + }), + getSendToBackAriaLabel: () => + i18n.translate('xpack.canvas.sidebarHeader.sendToBackArialLabel', { + defaultMessage: 'Move element to bottom layer', + }), +}; + const shortcutHelp = ShortcutStrings.getShortcutHelp(); interface Props { diff --git a/x-pack/plugins/canvas/public/components/text_style_picker/text_style_picker.tsx b/x-pack/plugins/canvas/public/components/text_style_picker/text_style_picker.tsx index 51b9cf7d60262..8d4a1506ad8a2 100644 --- a/x-pack/plugins/canvas/public/components/text_style_picker/text_style_picker.tsx +++ b/x-pack/plugins/canvas/public/components/text_style_picker/text_style_picker.tsx @@ -8,13 +8,51 @@ import React, { FC, useState } from 'react'; import PropTypes from 'prop-types'; import { EuiFlexGroup, EuiFlexItem, EuiSelect, EuiSpacer, EuiButtonGroup } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { FontValue } from 'src/plugins/expressions'; -import { ComponentStrings } from '../../../i18n'; + import { FontPicker } from '../font_picker'; import { ColorPickerPopover } from '../color_picker_popover'; import { fontSizes } from './font_sizes'; -const { TextStylePicker: strings } = ComponentStrings; +const strings = { + getAlignCenterOption: () => + i18n.translate('xpack.canvas.textStylePicker.alignCenterOption', { + defaultMessage: 'Align center', + }), + getAlignLeftOption: () => + i18n.translate('xpack.canvas.textStylePicker.alignLeftOption', { + defaultMessage: 'Align left', + }), + getAlignRightOption: () => + i18n.translate('xpack.canvas.textStylePicker.alignRightOption', { + defaultMessage: 'Align right', + }), + getAlignmentOptionsControlLegend: () => + i18n.translate('xpack.canvas.textStylePicker.alignmentOptionsControl', { + defaultMessage: 'Alignment options', + }), + getFontColorLabel: () => + i18n.translate('xpack.canvas.textStylePicker.fontColorLabel', { + defaultMessage: 'Font Color', + }), + getStyleBoldOption: () => + i18n.translate('xpack.canvas.textStylePicker.styleBoldOption', { + defaultMessage: 'Bold', + }), + getStyleItalicOption: () => + i18n.translate('xpack.canvas.textStylePicker.styleItalicOption', { + defaultMessage: 'Italic', + }), + getStyleUnderlineOption: () => + i18n.translate('xpack.canvas.textStylePicker.styleUnderlineOption', { + defaultMessage: 'Underline', + }), + getStyleOptionsControlLegend: () => + i18n.translate('xpack.canvas.textStylePicker.styleOptionsControl', { + defaultMessage: 'Style options', + }), +}; export interface StyleProps { family?: FontValue; diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx index 9e89ad4c4f27b..13cc4db7c6217 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx +++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx @@ -8,18 +8,39 @@ import React, { FC, useState, useContext, useEffect } from 'react'; import PropTypes from 'prop-types'; import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { PageManager } from '../page_manager'; import { Expression } from '../expression'; import { Tray } from './tray'; import { CanvasElement } from '../../../types'; -import { ComponentStrings } from '../../../i18n'; import { RoutingButtonIcon } from '../routing'; import { WorkpadRoutingContext } from '../../routes/workpad'; -const { Toolbar: strings } = ComponentStrings; +const strings = { + getEditorButtonLabel: () => + i18n.translate('xpack.canvas.toolbar.editorButtonLabel', { + defaultMessage: 'Expression editor', + }), + getNextPageAriaLabel: () => + i18n.translate('xpack.canvas.toolbar.nextPageAriaLabel', { + defaultMessage: 'Next Page', + }), + getPageButtonLabel: (pageNum: number, totalPages: number) => + i18n.translate('xpack.canvas.toolbar.pageButtonLabel', { + defaultMessage: 'Page {pageNum}{rest}', + values: { + pageNum, + rest: totalPages > 1 ? ` of ${totalPages}` : '', + }, + }), + getPreviousPageAriaLabel: () => + i18n.translate('xpack.canvas.toolbar.previousPageAriaLabel', { + defaultMessage: 'Previous Page', + }), +}; type TrayType = 'pageManager' | 'expression'; diff --git a/x-pack/plugins/canvas/public/components/toolbar/tray/tray.tsx b/x-pack/plugins/canvas/public/components/toolbar/tray/tray.tsx index 0230eb86e121a..bc6eb455bb9b6 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/tray/tray.tsx +++ b/x-pack/plugins/canvas/public/components/toolbar/tray/tray.tsx @@ -8,9 +8,14 @@ import React, { ReactNode, MouseEventHandler } from 'react'; import PropTypes from 'prop-types'; import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; -import { ComponentStrings } from '../../../../i18n'; -const { ToolbarTray: strings } = ComponentStrings; +const strings = { + getCloseTrayAriaLabel: () => + i18n.translate('xpack.canvas.toolbarTray.closeTrayAriaLabel', { + defaultMessage: 'Close tray', + }), +}; interface Props { children: ReactNode; diff --git a/x-pack/plugins/canvas/public/components/var_config/delete_var.tsx b/x-pack/plugins/canvas/public/components/var_config/delete_var.tsx index 69b3306d85ea5..f6ba2d7e28825 100644 --- a/x-pack/plugins/canvas/public/components/var_config/delete_var.tsx +++ b/x-pack/plugins/canvas/public/components/var_config/delete_var.tsx @@ -15,10 +15,29 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import { CanvasVariable } from '../../../types'; -import { ComponentStrings } from '../../../i18n'; -const { VarConfigDeleteVar: strings } = ComponentStrings; +const strings = { + getCancelButtonLabel: () => + i18n.translate('xpack.canvas.varConfigDeleteVar.cancelButtonLabel', { + defaultMessage: 'Cancel', + }), + getDeleteButtonLabel: () => + i18n.translate('xpack.canvas.varConfigDeleteVar.deleteButtonLabel', { + defaultMessage: 'Delete variable', + }), + getTitle: () => + i18n.translate('xpack.canvas.varConfigDeleteVar.titleLabel', { + defaultMessage: 'Delete variable?', + }), + getWarningDescription: () => + i18n.translate('xpack.canvas.varConfigDeleteVar.warningDescription', { + defaultMessage: + 'Deleting this variable may adversely affect the workpad. Are you sure you wish to continue?', + }), +}; import './var_panel.scss'; diff --git a/x-pack/plugins/canvas/public/components/var_config/edit_var.tsx b/x-pack/plugins/canvas/public/components/var_config/edit_var.tsx index 64ec8af291448..35f9e67745aec 100644 --- a/x-pack/plugins/canvas/public/components/var_config/edit_var.tsx +++ b/x-pack/plugins/canvas/public/components/var_config/edit_var.tsx @@ -20,12 +20,61 @@ import { EuiSpacer, EuiCallOut, } from '@elastic/eui'; -import { CanvasVariable } from '../../../types'; +import { i18n } from '@kbn/i18n'; +import { CanvasVariable } from '../../../types'; import { VarValueField } from './var_value_field'; -import { ComponentStrings } from '../../../i18n'; -const { VarConfigEditVar: strings } = ComponentStrings; +const strings = { + getAddTitle: () => + i18n.translate('xpack.canvas.varConfigEditVar.addTitleLabel', { + defaultMessage: 'Add variable', + }), + getCancelButtonLabel: () => + i18n.translate('xpack.canvas.varConfigEditVar.cancelButtonLabel', { + defaultMessage: 'Cancel', + }), + getDuplicateNameError: () => + i18n.translate('xpack.canvas.varConfigEditVar.duplicateNameError', { + defaultMessage: 'Variable name already in use', + }), + getEditTitle: () => + i18n.translate('xpack.canvas.varConfigEditVar.editTitleLabel', { + defaultMessage: 'Edit variable', + }), + getEditWarning: () => + i18n.translate('xpack.canvas.varConfigEditVar.editWarning', { + defaultMessage: 'Editing a variable in use may adversely affect your workpad', + }), + getNameFieldLabel: () => + i18n.translate('xpack.canvas.varConfigEditVar.nameFieldLabel', { + defaultMessage: 'Name', + }), + getSaveButtonLabel: () => + i18n.translate('xpack.canvas.varConfigEditVar.saveButtonLabel', { + defaultMessage: 'Save changes', + }), + getTypeBooleanLabel: () => + i18n.translate('xpack.canvas.varConfigEditVar.typeBooleanLabel', { + defaultMessage: 'Boolean', + }), + getTypeFieldLabel: () => + i18n.translate('xpack.canvas.varConfigEditVar.typeFieldLabel', { + defaultMessage: 'Type', + }), + getTypeNumberLabel: () => + i18n.translate('xpack.canvas.varConfigEditVar.typeNumberLabel', { + defaultMessage: 'Number', + }), + getTypeStringLabel: () => + i18n.translate('xpack.canvas.varConfigEditVar.typeStringLabel', { + defaultMessage: 'String', + }), + getValueFieldLabel: () => + i18n.translate('xpack.canvas.varConfigEditVar.valueFieldLabel', { + defaultMessage: 'Value', + }), +}; import './edit_var.scss'; import './var_panel.scss'; diff --git a/x-pack/plugins/canvas/public/components/var_config/index.tsx b/x-pack/plugins/canvas/public/components/var_config/index.tsx index 3f072e2f95140..db2a84e93a5dc 100644 --- a/x-pack/plugins/canvas/public/components/var_config/index.tsx +++ b/x-pack/plugins/canvas/public/components/var_config/index.tsx @@ -7,12 +7,22 @@ import React, { FC } from 'react'; import copy from 'copy-to-clipboard'; +import { i18n } from '@kbn/i18n'; + import { VarConfig as ChildComponent } from './var_config'; import { useNotifyService } from '../../services'; -import { ComponentStrings } from '../../../i18n'; import { CanvasVariable } from '../../../types'; -const { VarConfig: strings } = ComponentStrings; +const strings = { + getCopyNotificationDescription: () => + i18n.translate('xpack.canvas.varConfig.copyNotificationDescription', { + defaultMessage: 'Variable syntax copied to clipboard', + }), + getDeleteNotificationDescription: () => + i18n.translate('xpack.canvas.varConfig.deleteNotificationDescription', { + defaultMessage: 'Variable successfully deleted', + }), +}; interface Props { variables: CanvasVariable[]; diff --git a/x-pack/plugins/canvas/public/components/var_config/var_config.tsx b/x-pack/plugins/canvas/public/components/var_config/var_config.tsx index 0fe506715d07d..dc8898e2132e7 100644 --- a/x-pack/plugins/canvas/public/components/var_config/var_config.tsx +++ b/x-pack/plugins/canvas/public/components/var_config/var_config.tsx @@ -18,17 +18,15 @@ import { EuiSpacer, EuiButton, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { CanvasVariable } from '../../../types'; -import { ComponentStrings } from '../../../i18n'; import { EditVar } from './edit_var'; import { DeleteVar } from './delete_var'; import './var_config.scss'; -const { VarConfig: strings } = ComponentStrings; - enum PanelMode { List, Edit, @@ -49,6 +47,58 @@ interface Props { onEditVar: (oldVar: CanvasVariable, newVar: CanvasVariable) => void; } +const strings = { + getAddButtonLabel: () => + i18n.translate('xpack.canvas.varConfig.addButtonLabel', { + defaultMessage: 'Add a variable', + }), + getAddTooltipLabel: () => + i18n.translate('xpack.canvas.varConfig.addTooltipLabel', { + defaultMessage: 'Add a variable', + }), + getCopyActionButtonLabel: () => + i18n.translate('xpack.canvas.varConfig.copyActionButtonLabel', { + defaultMessage: 'Copy snippet', + }), + getCopyActionTooltipLabel: () => + i18n.translate('xpack.canvas.varConfig.copyActionTooltipLabel', { + defaultMessage: 'Copy variable syntax to clipboard', + }), + getDeleteActionButtonLabel: () => + i18n.translate('xpack.canvas.varConfig.deleteActionButtonLabel', { + defaultMessage: 'Delete variable', + }), + getEditActionButtonLabel: () => + i18n.translate('xpack.canvas.varConfig.editActionButtonLabel', { + defaultMessage: 'Edit variable', + }), + getEmptyDescription: () => + i18n.translate('xpack.canvas.varConfig.emptyDescription', { + defaultMessage: + 'This workpad has no variables currently. You may add variables to store and edit common values. These variables can then be used in elements or within the expression editor.', + }), + getTableNameLabel: () => + i18n.translate('xpack.canvas.varConfig.tableNameLabel', { + defaultMessage: 'Name', + }), + getTableTypeLabel: () => + i18n.translate('xpack.canvas.varConfig.tableTypeLabel', { + defaultMessage: 'Type', + }), + getTableValueLabel: () => + i18n.translate('xpack.canvas.varConfig.tableValueLabel', { + defaultMessage: 'Value', + }), + getTitle: () => + i18n.translate('xpack.canvas.varConfig.titleLabel', { + defaultMessage: 'Variables', + }), + getTitleTooltip: () => + i18n.translate('xpack.canvas.varConfig.titleTooltip', { + defaultMessage: 'Add variables to store and edit common values', + }), +}; + export const VarConfig: FC<Props> = ({ variables, onCopyVar, diff --git a/x-pack/plugins/canvas/public/components/var_config/var_value_field.tsx b/x-pack/plugins/canvas/public/components/var_config/var_value_field.tsx index c89164dc6efd4..1232ba3977d70 100644 --- a/x-pack/plugins/canvas/public/components/var_config/var_value_field.tsx +++ b/x-pack/plugins/canvas/public/components/var_config/var_value_field.tsx @@ -8,11 +8,24 @@ import React, { FC } from 'react'; import { EuiFieldText, EuiFieldNumber, EuiButtonGroup } from '@elastic/eui'; import { htmlIdGenerator } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { CanvasVariable } from '../../../types'; -import { ComponentStrings } from '../../../i18n'; -const { VarConfigVarValueField: strings } = ComponentStrings; +const strings = { + getBooleanOptionsLegend: () => + i18n.translate('xpack.canvas.varConfigVarValueField.booleanOptionsLegend', { + defaultMessage: 'Boolean value', + }), + getFalseOption: () => + i18n.translate('xpack.canvas.varConfigVarValueField.falseOption', { + defaultMessage: 'False', + }), + getTrueOption: () => + i18n.translate('xpack.canvas.varConfigVarValueField.trueOption', { + defaultMessage: 'True', + }), +}; interface Props { type: CanvasVariable['type']; diff --git a/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.component.tsx b/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.component.tsx index cc6271e376c07..0561ac005519b 100644 --- a/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_color_picker/workpad_color_picker.component.tsx @@ -6,10 +6,15 @@ */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import { ColorPickerPopover, Props } from '../color_picker_popover'; -import { ComponentStrings } from '../../../i18n'; -const { WorkpadConfig: strings } = ComponentStrings; +const strings = { + getBackgroundColorLabel: () => + i18n.translate('xpack.canvas.workpadConfig.backgroundColorLabel', { + defaultMessage: 'Background color', + }), +}; export const WorkpadColorPicker = (props: Props) => { return ( diff --git a/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx b/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx index 2776280d17b32..18e3f2dac9777 100644 --- a/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_config/workpad_config.component.tsx @@ -22,14 +22,70 @@ import { EuiAccordion, EuiButton, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { VarConfig } from '../var_config'; - import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants'; import { CanvasVariable } from '../../../types'; -import { ComponentStrings } from '../../../i18n'; -const { WorkpadConfig: strings } = ComponentStrings; +const strings = { + getApplyStylesheetButtonLabel: () => + i18n.translate('xpack.canvas.workpadConfig.applyStylesheetButtonLabel', { + defaultMessage: `Apply stylesheet`, + description: '"stylesheet" refers to the collection of CSS style rules entered by the user.', + }), + getFlipDimensionAriaLabel: () => + i18n.translate('xpack.canvas.workpadConfig.swapDimensionsAriaLabel', { + defaultMessage: `Swap the page's width and height`, + }), + getFlipDimensionTooltip: () => + i18n.translate('xpack.canvas.workpadConfig.swapDimensionsTooltip', { + defaultMessage: 'Swap the width and height', + }), + getGlobalCSSLabel: () => + i18n.translate('xpack.canvas.workpadConfig.globalCSSLabel', { + defaultMessage: `Global CSS overrides`, + }), + getGlobalCSSTooltip: () => + i18n.translate('xpack.canvas.workpadConfig.globalCSSTooltip', { + defaultMessage: `Apply styles to all pages in this workpad`, + }), + getNameLabel: () => + i18n.translate('xpack.canvas.workpadConfig.nameLabel', { + defaultMessage: 'Name', + }), + getPageHeightLabel: () => + i18n.translate('xpack.canvas.workpadConfig.heightLabel', { + defaultMessage: 'Height', + }), + getPageSizeBadgeAriaLabel: (sizeName: string) => + i18n.translate('xpack.canvas.workpadConfig.pageSizeBadgeAriaLabel', { + defaultMessage: `Preset page size: {sizeName}`, + values: { + sizeName, + }, + }), + getPageSizeBadgeOnClickAriaLabel: (sizeName: string) => + i18n.translate('xpack.canvas.workpadConfig.pageSizeBadgeOnClickAriaLabel', { + defaultMessage: `Set page size to {sizeName}`, + values: { + sizeName, + }, + }), + getPageWidthLabel: () => + i18n.translate('xpack.canvas.workpadConfig.widthLabel', { + defaultMessage: 'Width', + }), + getTitle: () => + i18n.translate('xpack.canvas.workpadConfig.title', { + defaultMessage: 'Workpad settings', + }), + getUSLetterButtonLabel: () => + i18n.translate('xpack.canvas.workpadConfig.USLetterButtonLabel', { + defaultMessage: 'US Letter', + description: 'This is referring to the dimensions of U.S. standard letter paper.', + }), +}; export interface Props { size: { diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.component.tsx index cb66eceac97c3..c78bdb2a78821 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.component.tsx @@ -8,7 +8,8 @@ import React, { Fragment, FunctionComponent, useState } from 'react'; import PropTypes from 'prop-types'; import { EuiButtonEmpty, EuiContextMenu, EuiIcon } from '@elastic/eui'; -import { ComponentStrings } from '../../../../i18n/components'; +import { i18n } from '@kbn/i18n'; + import { ShortcutStrings } from '../../../../i18n/shortcuts'; import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; import { Popover, ClosePopoverFn } from '../../popover'; @@ -16,8 +17,95 @@ import { CustomElementModal } from '../../custom_element_modal'; import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../../common/lib/constants'; import { PositionedElement } from '../../../../types'; -const { WorkpadHeaderEditMenu: strings } = ComponentStrings; const shortcutHelp = ShortcutStrings.getShortcutHelp(); +const strings = { + getAlignmentMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.alignmentMenuItemLabel', { + defaultMessage: 'Alignment', + description: + 'This refers to the vertical (i.e. left, center, right) and horizontal (i.e. top, middle, bottom) ' + + 'alignment options of the selected elements', + }), + getBottomAlignMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.bottomAlignMenuItemLabel', { + defaultMessage: 'Bottom', + }), + getCenterAlignMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.centerAlignMenuItemLabel', { + defaultMessage: 'Center', + description: 'This refers to alignment centered horizontally.', + }), + getCreateElementModalTitle: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.createElementModalTitle', { + defaultMessage: 'Create new element', + }), + getDistributionMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.distributionMenutItemLabel', { + defaultMessage: 'Distribution', + description: + 'This refers to the options to evenly spacing the selected elements horizontall or vertically.', + }), + getEditMenuButtonLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.editMenuButtonLabel', { + defaultMessage: 'Edit', + }), + getEditMenuLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.editMenuLabel', { + defaultMessage: 'Edit options', + }), + getGroupMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.groupMenuItemLabel', { + defaultMessage: 'Group', + description: 'This refers to grouping multiple selected elements.', + }), + getHorizontalDistributionMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.horizontalDistributionMenutItemLabel', { + defaultMessage: 'Horizontal', + }), + getLeftAlignMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.leftAlignMenuItemLabel', { + defaultMessage: 'Left', + }), + getMiddleAlignMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.middleAlignMenuItemLabel', { + defaultMessage: 'Middle', + description: 'This refers to alignment centered vertically.', + }), + getOrderMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.orderMenuItemLabel', { + defaultMessage: 'Order', + description: 'Refers to the order of the elements displayed on the page from front to back', + }), + getRedoMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.redoMenuItemLabel', { + defaultMessage: 'Redo', + }), + getRightAlignMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.rightAlignMenuItemLabel', { + defaultMessage: 'Right', + }), + getSaveElementMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.savedElementMenuItemLabel', { + defaultMessage: 'Save as new element', + }), + getTopAlignMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.topAlignMenuItemLabel', { + defaultMessage: 'Top', + }), + getUndoMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.undoMenuItemLabel', { + defaultMessage: 'Undo', + }), + getUngroupMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.ungroupMenuItemLabel', { + defaultMessage: 'Ungroup', + description: 'This refers to ungrouping a grouped element', + }), + getVerticalDistributionMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.verticalDistributionMenutItemLabel', { + defaultMessage: 'Vertical', + }), +}; export interface Props { /** diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx index 19414f7c8d964..e1d69163e0761 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx @@ -14,8 +14,9 @@ import { EuiIcon, EuiContextMenuPanelItemDescriptor, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../../common/lib'; -import { ComponentStrings } from '../../../../i18n/components'; import { ElementSpec } from '../../../../types'; import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; import { getId } from '../../../lib/get_id'; @@ -31,7 +32,56 @@ interface ElementTypeMeta { [key: string]: { name: string; icon: string }; } -export const { WorkpadHeaderElementMenu: strings } = ComponentStrings; +const strings = { + getAssetsMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.manageAssetsMenuItemLabel', { + defaultMessage: 'Manage assets', + }), + getChartMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.chartMenuItemLabel', { + defaultMessage: 'Chart', + }), + getElementMenuButtonLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.elementMenuButtonLabel', { + defaultMessage: 'Add element', + }), + getElementMenuLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.elementMenuLabel', { + defaultMessage: 'Add an element', + }), + getEmbedObjectMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.embedObjectMenuItemLabel', { + defaultMessage: 'Add from Kibana', + }), + getFilterMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.filterMenuItemLabel', { + defaultMessage: 'Filter', + }), + getImageMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.imageMenuItemLabel', { + defaultMessage: 'Image', + }), + getMyElementsMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.myElementsMenuItemLabel', { + defaultMessage: 'My elements', + }), + getOtherMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.otherMenuItemLabel', { + defaultMessage: 'Other', + }), + getProgressMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.progressMenuItemLabel', { + defaultMessage: 'Progress', + }), + getShapeMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.shapeMenuItemLabel', { + defaultMessage: 'Shape', + }), + getTextMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.textMenuItemLabel', { + defaultMessage: 'Text', + }), +}; // label and icon for the context menu item for each element type const elementTypeMeta: ElementTypeMeta = { diff --git a/x-pack/plugins/canvas/public/components/workpad_header/labs_control/labs_control.tsx b/x-pack/plugins/canvas/public/components/workpad_header/labs_control/labs_control.tsx index eea59e6aa49f3..fde21c7c85c37 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/labs_control/labs_control.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/labs_control/labs_control.tsx @@ -7,15 +7,21 @@ import React, { useState } from 'react'; import { EuiButtonEmpty, EuiNotificationBadge } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { LazyLabsFlyout, withSuspense, } from '../../../../../../../src/plugins/presentation_util/public'; -import { ComponentStrings } from '../../../../i18n'; import { useLabsService } from '../../../services'; -const { LabsControl: strings } = ComponentStrings; + +const strings = { + getLabsButtonLabel: () => + i18n.translate('xpack.canvas.workpadHeaderLabsControlSettings.labsButtonLabel', { + defaultMessage: 'Labs', + }), +}; const Flyout = withSuspense(LazyLabsFlyout, null); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.component.tsx index dd9ddc2707ba6..7b1df158087b4 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.component.tsx @@ -8,10 +8,20 @@ import React, { MouseEventHandler } from 'react'; import PropTypes from 'prop-types'; import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import { ToolTipShortcut } from '../../tool_tip_shortcut'; -import { ComponentStrings } from '../../../../i18n'; -const { WorkpadHeaderRefreshControlSettings: strings } = ComponentStrings; +const strings = { + getRefreshAriaLabel: () => + i18n.translate('xpack.canvas.workpadHeaderRefreshControlSettings.refreshAriaLabel', { + defaultMessage: 'Refresh Elements', + }), + getRefreshTooltip: () => + i18n.translate('xpack.canvas.workpadHeaderRefreshControlSettings.refreshTooltip', { + defaultMessage: 'Refresh data', + }), +}; export interface Props { doRefresh: MouseEventHandler<HTMLButtonElement>; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/pdf_panel.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/pdf_panel.stories.storyshot index 010037bee4a0f..75ee0fcae78f3 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/pdf_panel.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/pdf_panel.stories.storyshot @@ -37,7 +37,7 @@ exports[`Storyshots components/WorkpadHeader/ShareMenu/PDFPanel default 1`] = ` > <button aria-checked={false} - aria-describedby="generated-id-help" + aria-describedby="generated-id-help-0" aria-labelledby="generated-id" className="euiSwitch__button" data-test-subj="reportModeToggle" @@ -80,7 +80,7 @@ exports[`Storyshots components/WorkpadHeader/ShareMenu/PDFPanel default 1`] = ` </div> <div className="euiFormHelpText euiFormRow__text" - id="generated-id-help" + id="generated-id-help-0" > Remove borders and footer logo </div> diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/flyout.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/flyout.component.tsx index 7c90a6fb045b7..5da009e050a27 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/flyout.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/flyout.component.tsx @@ -21,16 +21,46 @@ import { EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ComponentStrings } from '../../../../../i18n/components'; import { ZIP, CANVAS, HTML } from '../../../../../i18n/constants'; import { OnCloseFn } from '../share_menu.component'; import { WorkpadStep } from './workpad_step'; import { RuntimeStep } from './runtime_step'; import { SnippetsStep } from './snippets_step'; -const { ShareWebsiteFlyout: strings } = ComponentStrings; +const strings = { + getRuntimeStepTitle: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.downloadRuntimeTitle', { + defaultMessage: 'Download runtime', + }), + getSnippentsStepTitle: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.addSnippetsTitle', { + defaultMessage: 'Add snippets to website', + }), + getStepsDescription: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.description', { + defaultMessage: + 'Follow these steps to share a static version of this workpad on an external website. It will be a visual snapshot of the current workpad, and will not have access to live data.', + }), + getTitle: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.flyoutTitle', { + defaultMessage: 'Share on a website', + }), + getUnsupportedRendererWarning: () => + i18n.translate('xpack.canvas.workpadHeaderShareMenu.unsupportedRendererWarning', { + defaultMessage: + 'This workpad contains render functions that are not supported by the {CANVAS} Shareable Workpad Runtime. These elements will not be rendered:', + values: { + CANVAS, + }, + }), + getWorkpadStepTitle: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.downloadWorkpadTitle', { + defaultMessage: 'Download workpad', + }), +}; export type OnDownloadFn = (type: 'share' | 'shareRuntime' | 'shareZip') => void; export type OnCopyFn = () => void; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/flyout.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/flyout.ts index 05d0070a5ea69..65c9d6598578d 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/flyout.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/flyout.ts @@ -7,6 +7,8 @@ import { connect } from 'react-redux'; import { compose, withProps } from 'recompose'; +import { i18n } from '@kbn/i18n'; + import { getWorkpad, getRenderedWorkpad, @@ -24,14 +26,35 @@ import { arrayBufferFetch } from '../../../../../common/lib/fetch'; import { API_ROUTE_SHAREABLE_ZIP } from '../../../../../common/lib/constants'; import { renderFunctionNames } from '../../../../../shareable_runtime/supported_renderers'; -import { ComponentStrings } from '../../../../../i18n/components'; import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public/'; import { OnCloseFn } from '../share_menu.component'; +import { ZIP } from '../../../../../i18n/constants'; import { WithKibanaProps } from '../../../../index'; export { OnDownloadFn, OnCopyFn } from './flyout.component'; -const { WorkpadHeaderShareMenu: strings } = ComponentStrings; +const strings = { + getCopyShareConfigMessage: () => + i18n.translate('xpack.canvas.workpadHeaderShareMenu.copyShareConfigMessage', { + defaultMessage: 'Copied share markup to clipboard', + }), + getShareableZipErrorTitle: (workpadName: string) => + i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareWebsiteErrorTitle', { + defaultMessage: + "Failed to create {ZIP} file for '{workpadName}'. The workpad may be too large. You'll need to download the files separately.", + values: { + ZIP, + workpadName, + }, + }), + getUnknownExportErrorMessage: (type: string) => + i18n.translate('xpack.canvas.workpadHeaderShareMenu.unknownExportErrorMessage', { + defaultMessage: 'Unknown export type: {type}', + values: { + type, + }, + }), +}; const getUnsupportedRenderers = (state: State) => { const renderers: string[] = []; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx index c686c403a9a45..8b2fe1a1c0394 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx @@ -7,12 +7,26 @@ import React, { FC } from 'react'; import { EuiText, EuiSpacer, EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; -import { ComponentStrings } from '../../../../../i18n/components'; +import { CANVAS } from '../../../../../i18n/constants'; import { OnDownloadFn } from './flyout'; -const { ShareWebsiteRuntimeStep: strings } = ComponentStrings; +const strings = { + getDownloadLabel: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.runtimeStep.downloadLabel', { + defaultMessage: 'Download runtime', + }), + getStepDescription: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.runtimeStep.description', { + defaultMessage: + 'In order to render a Shareable Workpad, you also need to include the {CANVAS} Shareable Workpad Runtime. You can skip this step if the runtime is already included on your website.', + values: { + CANVAS, + }, + }), +}; export const RuntimeStep: FC<{ onDownload: OnDownloadFn }> = ({ onDownload }) => ( <EuiText size="s"> diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx index bc9f123c623f6..1bac3068e7dbb 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx @@ -16,13 +16,91 @@ import { EuiDescriptionListDescription, EuiHorizontalRule, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; -import { ComponentStrings } from '../../../../../i18n/components'; +import { CANVAS, URL, JSON } from '../../../../../i18n/constants'; import { Clipboard } from '../../../clipboard'; import { OnCopyFn } from './flyout'; -const { ShareWebsiteSnippetsStep: strings } = ComponentStrings; +const strings = { + getAutoplayParameterDescription: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.autoplayParameterDescription', { + defaultMessage: 'Should the runtime automatically move through the pages of the workpad?', + }), + getCallRuntimeLabel: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.callRuntimeLabel', { + defaultMessage: 'Call Runtime', + }), + getHeightParameterDescription: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.heightParameterDescription', { + defaultMessage: 'The height of the Workpad. Defaults to the Workpad height.', + }), + getIncludeRuntimeLabel: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.includeRuntimeLabel', { + defaultMessage: 'Include Runtime', + }), + getIntervalParameterDescription: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.intervalParameterDescription', { + defaultMessage: + 'The interval upon which the pages will advance in time format, (e.g. {twoSeconds}, {oneMinute})', + values: { + twoSeconds: '2s', + oneMinute: '1m', + }, + }), + getPageParameterDescription: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.pageParameterDescription', { + defaultMessage: 'The page to display. Defaults to the page specified by the Workpad.', + }), + getParametersDescription: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.parametersDescription', { + defaultMessage: 'There are a number of inline parameters to configure the Shareable Workpad.', + }), + getParametersTitle: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.parametersLabel', { + defaultMessage: 'Parameters', + }), + getPlaceholderLabel: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.placeholderLabel', { + defaultMessage: 'Placeholder', + }), + getRequiredLabel: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.requiredLabel', { + defaultMessage: 'required', + }), + getShareableParameterDescription: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.shareableParameterDescription', { + defaultMessage: 'The type of shareable. In this case, a {CANVAS} Workpad.', + values: { + CANVAS, + }, + }), + getSnippetsStepDescription: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.description', { + defaultMessage: + 'The Workpad is placed within the {HTML} of the site by using an {HTML} placeholder. Parameters for the runtime are included inline. See the full list of parameters below. You can include more than one workpad on the page.', + values: { + HTML, + }, + }), + getToolbarParameterDescription: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.toolbarParameterDescription', { + defaultMessage: 'Should the toolbar be hidden?', + }), + getUrlParameterDescription: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.urlParameterDescription', { + defaultMessage: 'The {URL} of the Shareable Workpad {JSON} file.', + values: { + URL, + JSON, + }, + }), + getWidthParameterDescription: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.widthParameterDescription', { + defaultMessage: 'The width of the Workpad. Defaults to the Workpad width.', + }), +}; const HTML = `<!-- ${strings.getIncludeRuntimeLabel()} --> <script src="kbn_canvas.js"></script> diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/workpad_step.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/workpad_step.tsx index c5a6a4478c765..3ab358d0fe324 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/workpad_step.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/workpad_step.tsx @@ -7,12 +7,26 @@ import React, { FC } from 'react'; import { EuiText, EuiSpacer, EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; -import { ComponentStrings } from '../../../../../i18n/components'; +import { JSON } from '../../../../../i18n/constants'; import { OnDownloadFn } from './flyout'; -const { ShareWebsiteWorkpadStep: strings } = ComponentStrings; +const strings = { + getDownloadLabel: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.workpadStep.downloadLabel', { + defaultMessage: 'Download workpad', + }), + getStepDescription: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.workpadStep.description', { + defaultMessage: + 'The workpad will be exported as a single {JSON} file for sharing in another site.', + values: { + JSON, + }, + }), +}; export const WorkpadStep: FC<{ onDownload: OnDownloadFn }> = ({ onDownload }) => ( <EuiText size="s"> diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx index d4cb4d0736bb1..5ccc09bf3586b 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx @@ -5,18 +5,47 @@ * 2.0. */ +import React, { FunctionComponent, useState } from 'react'; +import PropTypes from 'prop-types'; import { EuiButtonEmpty, EuiContextMenu, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { IBasePath } from 'kibana/public'; -import PropTypes from 'prop-types'; -import React, { FunctionComponent, useState } from 'react'; + import { ReportingStart } from '../../../../../reporting/public'; -import { ComponentStrings } from '../../../../i18n/components'; +import { PDF, JSON } from '../../../../i18n/constants'; import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; import { ClosePopoverFn, Popover } from '../../popover'; import { ShareWebsiteFlyout } from './flyout'; import { CanvasWorkpadSharingData, getPdfJobParams } from './utils'; -const { WorkpadHeaderShareMenu: strings } = ComponentStrings; +const strings = { + getShareDownloadJSONTitle: () => + i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareDownloadJSONTitle', { + defaultMessage: 'Download as {JSON}', + values: { + JSON, + }, + }), + getShareDownloadPDFTitle: () => + i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareDownloadPDFTitle', { + defaultMessage: '{PDF} reports', + values: { + PDF, + }, + }), + getShareMenuButtonLabel: () => + i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareMenuButtonLabel', { + defaultMessage: 'Share', + }), + getShareWebsiteTitle: () => + i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareWebsiteTitle', { + defaultMessage: 'Share on a website', + }), + getShareWorkpadMessage: () => + i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareWorkpadMessage', { + defaultMessage: 'Share this workpad', + }), +}; type CopyTypes = 'pdf' | 'reportingConfig'; type ExportTypes = 'pdf' | 'json'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts index fc4906817cf6f..ef13655b66aca 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts @@ -7,14 +7,23 @@ import { connect } from 'react-redux'; import { compose, withProps } from 'recompose'; -import { ComponentStrings } from '../../../../i18n'; +import { i18n } from '@kbn/i18n'; + import { CanvasWorkpad, State } from '../../../../types'; import { downloadWorkpad } from '../../../lib/download_workpad'; import { withServices, WithServicesProps } from '../../../services'; import { getPages, getWorkpad } from '../../../state/selectors/workpad'; import { Props as ComponentProps, ShareMenu as Component } from './share_menu.component'; -const { WorkpadHeaderShareMenu: strings } = ComponentStrings; +const strings = { + getUnknownExportErrorMessage: (type: string) => + i18n.translate('xpack.canvas.workpadHeaderShareMenu.unknownExportErrorMessage', { + defaultMessage: 'Unknown export type: {type}', + values: { + type, + }, + }), +}; const mapStateToProps = (state: State) => ({ workpad: getWorkpad(state), diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/auto_refresh_controls.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/auto_refresh_controls.tsx index 1508f8683b8c1..6815ef351e0b8 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/auto_refresh_controls.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/auto_refresh_controls.tsx @@ -22,14 +22,34 @@ import { EuiToolTip, htmlIdGenerator, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import { timeDuration } from '../../../lib/time_duration'; +import { UnitStrings } from '../../../../i18n'; import { CustomInterval } from './custom_interval'; -import { ComponentStrings, UnitStrings } from '../../../../i18n'; -const { WorkpadHeaderAutoRefreshControls: strings } = ComponentStrings; const { time: timeStrings } = UnitStrings; const { getSecondsText, getMinutesText, getHoursText } = timeStrings; +const strings = { + getDisableTooltip: () => + i18n.translate('xpack.canvas.workpadHeaderAutoRefreshControls.disableTooltip', { + defaultMessage: 'Disable auto-refresh', + }), + getIntervalFormLabelText: () => + i18n.translate('xpack.canvas.workpadHeaderAutoRefreshControls.intervalFormLabel', { + defaultMessage: 'Change auto-refresh interval', + }), + getRefreshListDurationManualText: () => + i18n.translate('xpack.canvas.workpadHeaderAutoRefreshControls.refreshListDurationManualText', { + defaultMessage: 'Manually', + }), + getRefreshListTitle: () => + i18n.translate('xpack.canvas.workpadHeaderAutoRefreshControls.refreshListTitle', { + defaultMessage: 'Refresh elements', + }), +}; + interface Props { refreshInterval: number; setRefresh: (interval: number) => void; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/custom_interval.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/custom_interval.tsx index d4d28d19131f0..284749340e440 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/custom_interval.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/custom_interval.tsx @@ -8,12 +8,31 @@ import React, { useState, ChangeEvent } from 'react'; import PropTypes from 'prop-types'; import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiButton, EuiFieldText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { ButtonSize } from '@elastic/eui/src/components/button/button'; import { FlexGroupGutterSize } from '@elastic/eui/src/components/flex/flex_group'; import { getTimeInterval } from '../../../lib/time_interval'; -import { ComponentStrings } from '../../../../i18n'; -const { WorkpadHeaderCustomInterval: strings } = ComponentStrings; +const strings = { + getButtonLabel: () => + i18n.translate('xpack.canvas.workpadHeaderCustomInterval.confirmButtonLabel', { + defaultMessage: 'Set', + }), + getFormDescription: () => + i18n.translate('xpack.canvas.workpadHeaderCustomInterval.formDescription', { + defaultMessage: + 'Use shorthand notation, like {secondsExample}, {minutesExample}, or {hoursExample}', + values: { + secondsExample: '30s', + minutesExample: '10m', + hoursExample: '1h', + }, + }), + getFormLabel: () => + i18n.translate('xpack.canvas.workpadHeaderCustomInterval.formLabel', { + defaultMessage: 'Set a custom interval', + }), +}; interface Props { gutterSize: FlexGroupGutterSize; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/kiosk_controls.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/kiosk_controls.tsx index 55373d7a3515c..b8ed80c870f28 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/kiosk_controls.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/kiosk_controls.tsx @@ -22,14 +22,34 @@ import { EuiFlexGroup, htmlIdGenerator, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import { timeDuration } from '../../../lib/time_duration'; +import { UnitStrings } from '../../../../i18n'; import { CustomInterval } from './custom_interval'; -import { ComponentStrings, UnitStrings } from '../../../../i18n'; -const { WorkpadHeaderKioskControls: strings } = ComponentStrings; const { time: timeStrings } = UnitStrings; const { getSecondsText, getMinutesText } = timeStrings; +const strings = { + getCycleFormLabel: () => + i18n.translate('xpack.canvas.workpadHeaderKioskControl.cycleFormLabel', { + defaultMessage: 'Change cycling interval', + }), + getTitle: () => + i18n.translate('xpack.canvas.workpadHeaderKioskControl.controlTitle', { + defaultMessage: 'Cycle fullscreen pages', + }), + getAutoplayListDurationManualText: () => + i18n.translate('xpack.canvas.workpadHeaderKioskControl.autoplayListDurationManual', { + defaultMessage: 'Manually', + }), + getDisableTooltip: () => + i18n.translate('xpack.canvas.workpadHeaderKioskControl.disableTooltip', { + defaultMessage: 'Disable auto-play', + }), +}; + interface Props { autoplayInterval: number; onSetInterval: (interval: number) => void; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx index 8fb24c1f3c62e..168ddc690c4d4 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx @@ -13,18 +13,80 @@ import { EuiIcon, EuiContextMenuPanelItemDescriptor, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL, CONTEXT_MENU_TOP_BORDER_CLASSNAME, } from '../../../../common/lib/constants'; -import { ComponentStrings } from '../../../../i18n/components'; + import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; import { Popover, ClosePopoverFn } from '../../popover'; import { AutoRefreshControls } from './auto_refresh_controls'; import { KioskControls } from './kiosk_controls'; -const { WorkpadHeaderViewMenu: strings } = ComponentStrings; +const strings = { + getAutoplaySettingsMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.autoplaySettingsMenuItemLabel', { + defaultMessage: 'Autoplay settings', + }), + getFullscreenMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.fullscreenMenuLabel', { + defaultMessage: 'Enter fullscreen mode', + }), + getHideEditModeLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.hideEditModeLabel', { + defaultMessage: 'Hide editing controls', + }), + getRefreshMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.refreshMenuItemLabel', { + defaultMessage: 'Refresh data', + }), + getRefreshSettingsMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.refreshSettingsMenuItemLabel', { + defaultMessage: 'Auto refresh settings', + }), + getShowEditModeLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.showEditModeLabel', { + defaultMessage: 'Show editing controls', + }), + getViewMenuButtonLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.viewMenuButtonLabel', { + defaultMessage: 'View', + }), + getViewMenuLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.viewMenuLabel', { + defaultMessage: 'View options', + }), + getZoomFitToWindowText: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomFitToWindowText', { + defaultMessage: 'Fit to window', + }), + getZoomInText: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomInText', { + defaultMessage: 'Zoom in', + }), + getZoomMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomMenuItemLabel', { + defaultMessage: 'Zoom', + }), + getZoomOutText: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomOutText', { + defaultMessage: 'Zoom out', + }), + getZoomPercentage: (scale: number) => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomResetText', { + defaultMessage: '{scalePercentage}%', + values: { + scalePercentage: scale * 100, + }, + }), + getZoomResetText: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomPrecentageValue', { + defaultMessage: 'Reset', + }), +}; const QUICK_ZOOM_LEVELS = [0.5, 1, 2]; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx index 415d3ddf46709..5320a65a90408 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx @@ -10,7 +10,8 @@ import PropTypes from 'prop-types'; // @ts-expect-error no @types definition import { Shortcuts } from 'react-shortcuts'; import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { ComponentStrings } from '../../../i18n'; +import { i18n } from '@kbn/i18n'; + import { ToolTipShortcut } from '../tool_tip_shortcut/'; import { RefreshControl } from './refresh_control'; // @ts-expect-error untyped local @@ -22,7 +23,28 @@ import { ViewMenu } from './view_menu'; import { LabsControl } from './labs_control'; import { CommitFn } from '../../../types'; -const { WorkpadHeader: strings } = ComponentStrings; +const strings = { + getFullScreenButtonAriaLabel: () => + i18n.translate('xpack.canvas.workpadHeader.fullscreenButtonAriaLabel', { + defaultMessage: 'View fullscreen', + }), + getFullScreenTooltip: () => + i18n.translate('xpack.canvas.workpadHeader.fullscreenTooltip', { + defaultMessage: 'Enter fullscreen mode', + }), + getHideEditControlTooltip: () => + i18n.translate('xpack.canvas.workpadHeader.hideEditControlTooltip', { + defaultMessage: 'Hide editing controls', + }), + getNoWritePermissionTooltipText: () => + i18n.translate('xpack.canvas.workpadHeader.noWritePermissionTooltip', { + defaultMessage: "You don't have permission to edit this workpad", + }), + getShowEditControlTooltip: () => + i18n.translate('xpack.canvas.workpadHeader.showEditControlTooltip', { + defaultMessage: 'Show editing controls', + }), +}; export interface Props { isWriteable: boolean; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__snapshots__/settings.test.tsx.snap b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__snapshots__/settings.test.tsx.snap index 621a6bb211fe9..27f0d3610fb9f 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__snapshots__/settings.test.tsx.snap +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__snapshots__/settings.test.tsx.snap @@ -294,7 +294,7 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` class="euiFormControlLayout__childrenWrapper" > <input - aria-describedby="generated-id-help" + aria-describedby="generated-id-help-0" class="euiFieldText euiFieldText--compressed" id="generated-id" type="text" @@ -304,7 +304,7 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` </div> <div class="euiFormHelpText euiFormRow__text" - id="generated-id-help" + id="generated-id-help-0" > Use shorthand notation, like 30s, 10m, or 1h </div> @@ -585,7 +585,7 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = > <button aria-checked="false" - aria-describedby="generated-id-help" + aria-describedby="generated-id-help-0" aria-labelledby="generated-id" class="euiSwitch__button" data-test-subj="hideToolbarSwitch" @@ -623,7 +623,7 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = </div> <div class="euiFormHelpText euiFormRow__text" - id="generated-id-help" + id="generated-id-help-0" > Hide the toolbar when the mouse is not within the Canvas? </div> @@ -640,4 +640,4 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = </div> `; -exports[`<Settings /> can navigate Toolbar Settings, closes when activated 3`] = `"<div><div data-eui=\\"EuiFocusTrap\\"><div class=\\"euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--noShadow euiPopover__panel euiPopover__panel--top\\" tabindex=\\"0\\" aria-live=\\"off\\" role=\\"dialog\\" aria-modal=\\"true\\" aria-describedby=\\"generated-id\\" style=\\"top: -16px; left: -22px; z-index: 2000;\\"><div class=\\"euiPopover__panelArrow euiPopover__panelArrow--top\\" style=\\"left: 10px; top: 0px;\\"></div><p id=\\"generated-id\\" class=\\"euiScreenReaderOnly\\">You are in a dialog. To close this dialog, hit escape.</p><div><div class=\\"euiContextMenu\\" style=\\"height: 0px;\\"><div class=\\"euiContextMenuPanel euiContextMenu__panel euiContextMenuPanel-txOutLeft\\" tabindex=\\"0\\"><div class=\\"euiContextMenuPanelTitle\\"><span class=\\"euiContextMenu__itemLayout\\">Settings</span></div><div><div><button class=\\"euiContextMenuItem\\" type=\\"button\\"><span class=\\"euiContextMenu__itemLayout\\"><span data-euiicon-type=\\"play\\" class=\\"euiContextMenu__icon\\" color=\\"inherit\\"></span><span class=\\"euiContextMenuItem__text\\">Auto Play</span><span data-euiicon-type=\\"arrowRight\\" class=\\"euiContextMenu__arrow\\"></span></span></button><button class=\\"euiContextMenuItem\\" type=\\"button\\"><span class=\\"euiContextMenu__itemLayout\\"><span data-euiicon-type=\\"boxesHorizontal\\" class=\\"euiContextMenu__icon\\" color=\\"inherit\\"></span><span class=\\"euiContextMenuItem__text\\">Toolbar</span><span data-euiicon-type=\\"arrowRight\\" class=\\"euiContextMenu__arrow\\"></span></span></button></div></div></div><div class=\\"euiContextMenuPanel euiContextMenu__panel euiContextMenuPanel-txInLeft\\" tabindex=\\"0\\"><button class=\\"euiContextMenuPanelTitle\\" type=\\"button\\" data-test-subj=\\"contextMenuPanelTitleButton\\"><span class=\\"euiContextMenu__itemLayout\\"><span data-euiicon-type=\\"arrowLeft\\" class=\\"euiContextMenu__icon\\"></span><span class=\\"euiContextMenu__text\\">Toolbar</span></span></button><div><div><div style=\\"padding: 16px;\\"><div class=\\"euiFormRow\\" id=\\"generated-id-row\\"><div class=\\"euiFormRow__fieldWrapper\\"><div class=\\"euiSwitch\\"><button id=\\"generated-id\\" aria-checked=\\"true\\" class=\\"euiSwitch__button\\" role=\\"switch\\" type=\\"button\\" aria-labelledby=\\"generated-id\\" data-test-subj=\\"hideToolbarSwitch\\" name=\\"toolbarHide\\" aria-describedby=\\"generated-id-help\\"><span class=\\"euiSwitch__body\\"><span class=\\"euiSwitch__thumb\\"></span><span class=\\"euiSwitch__track\\"><span data-euiicon-type=\\"cross\\" class=\\"euiSwitch__icon\\"></span><span data-euiicon-type=\\"check\\" class=\\"euiSwitch__icon euiSwitch__icon--checked\\"></span></span></span></button><span class=\\"euiSwitch__label\\" id=\\"generated-id\\">Hide Toolbar</span></div><div class=\\"euiFormHelpText euiFormRow__text\\" id=\\"generated-id-help\\">Hide the toolbar when the mouse is not within the Canvas?</div></div></div></div></div></div></div></div></div></div></div></div>"`; +exports[`<Settings /> can navigate Toolbar Settings, closes when activated 3`] = `"<div><div data-eui=\\"EuiFocusTrap\\"><div class=\\"euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--noShadow euiPopover__panel euiPopover__panel--top\\" tabindex=\\"0\\" aria-live=\\"off\\" role=\\"dialog\\" aria-modal=\\"true\\" aria-describedby=\\"generated-id\\" style=\\"top: -16px; left: -22px; z-index: 2000;\\"><div class=\\"euiPopover__panelArrow euiPopover__panelArrow--top\\" style=\\"left: 10px; top: 0px;\\"></div><p id=\\"generated-id\\" class=\\"euiScreenReaderOnly\\">You are in a dialog. To close this dialog, hit escape.</p><div><div class=\\"euiContextMenu\\" style=\\"height: 0px;\\"><div class=\\"euiContextMenuPanel euiContextMenu__panel euiContextMenuPanel-txOutLeft\\" tabindex=\\"0\\"><div class=\\"euiContextMenuPanelTitle\\"><span class=\\"euiContextMenu__itemLayout\\">Settings</span></div><div><div><button class=\\"euiContextMenuItem\\" type=\\"button\\"><span class=\\"euiContextMenu__itemLayout\\"><span data-euiicon-type=\\"play\\" class=\\"euiContextMenu__icon\\" color=\\"inherit\\"></span><span class=\\"euiContextMenuItem__text\\">Auto Play</span><span data-euiicon-type=\\"arrowRight\\" class=\\"euiContextMenu__arrow\\"></span></span></button><button class=\\"euiContextMenuItem\\" type=\\"button\\"><span class=\\"euiContextMenu__itemLayout\\"><span data-euiicon-type=\\"boxesHorizontal\\" class=\\"euiContextMenu__icon\\" color=\\"inherit\\"></span><span class=\\"euiContextMenuItem__text\\">Toolbar</span><span data-euiicon-type=\\"arrowRight\\" class=\\"euiContextMenu__arrow\\"></span></span></button></div></div></div><div class=\\"euiContextMenuPanel euiContextMenu__panel euiContextMenuPanel-txInLeft\\" tabindex=\\"0\\"><button class=\\"euiContextMenuPanelTitle\\" type=\\"button\\" data-test-subj=\\"contextMenuPanelTitleButton\\"><span class=\\"euiContextMenu__itemLayout\\"><span data-euiicon-type=\\"arrowLeft\\" class=\\"euiContextMenu__icon\\"></span><span class=\\"euiContextMenu__text\\">Toolbar</span></span></button><div><div><div style=\\"padding: 16px;\\"><div class=\\"euiFormRow\\" id=\\"generated-id-row\\"><div class=\\"euiFormRow__fieldWrapper\\"><div class=\\"euiSwitch\\"><button id=\\"generated-id\\" aria-checked=\\"true\\" class=\\"euiSwitch__button\\" role=\\"switch\\" type=\\"button\\" aria-labelledby=\\"generated-id\\" data-test-subj=\\"hideToolbarSwitch\\" name=\\"toolbarHide\\" aria-describedby=\\"generated-id-help-0\\"><span class=\\"euiSwitch__body\\"><span class=\\"euiSwitch__thumb\\"></span><span class=\\"euiSwitch__track\\"><span data-euiicon-type=\\"cross\\" class=\\"euiSwitch__icon\\"></span><span data-euiicon-type=\\"check\\" class=\\"euiSwitch__icon euiSwitch__icon--checked\\"></span></span></span></button><span class=\\"euiSwitch__label\\" id=\\"generated-id\\">Hide Toolbar</span></div><div class=\\"euiFormHelpText euiFormRow__text\\" id=\\"generated-id-help-0\\">Hide the toolbar when the mouse is not within the Canvas?</div></div></div></div></div></div></div></div></div></div></div></div>"`; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/autoplay_settings.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/autoplay_settings.stories.storyshot index f5823874db73e..b32ae3fc2f49f 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/autoplay_settings.stories.storyshot +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/autoplay_settings.stories.storyshot @@ -101,7 +101,7 @@ exports[`Storyshots shareables/Footer/Settings/AutoplaySettings component: off, className="euiFormControlLayout__childrenWrapper" > <input - aria-describedby="generated-id-help" + aria-describedby="generated-id-help-0" className="euiFieldText euiFieldText--compressed" id="generated-id" onBlur={[Function]} @@ -114,7 +114,7 @@ exports[`Storyshots shareables/Footer/Settings/AutoplaySettings component: off, </div> <div className="euiFormHelpText euiFormRow__text" - id="generated-id-help" + id="generated-id-help-0" > Use shorthand notation, like 30s, 10m, or 1h </div> @@ -264,7 +264,7 @@ exports[`Storyshots shareables/Footer/Settings/AutoplaySettings component: on, 5 className="euiFormControlLayout__childrenWrapper" > <input - aria-describedby="generated-id-help" + aria-describedby="generated-id-help-0" className="euiFieldText euiFieldText--compressed" id="generated-id" onBlur={[Function]} @@ -277,7 +277,7 @@ exports[`Storyshots shareables/Footer/Settings/AutoplaySettings component: on, 5 </div> <div className="euiFormHelpText euiFormRow__text" - id="generated-id-help" + id="generated-id-help-0" > Use shorthand notation, like 30s, 10m, or 1h </div> @@ -427,7 +427,7 @@ exports[`Storyshots shareables/Footer/Settings/AutoplaySettings contextual 1`] = className="euiFormControlLayout__childrenWrapper" > <input - aria-describedby="generated-id-help" + aria-describedby="generated-id-help-0" className="euiFieldText euiFieldText--compressed" id="generated-id" onBlur={[Function]} @@ -440,7 +440,7 @@ exports[`Storyshots shareables/Footer/Settings/AutoplaySettings contextual 1`] = </div> <div className="euiFormHelpText euiFormRow__text" - id="generated-id-help" + id="generated-id-help-0" > Use shorthand notation, like 30s, 10m, or 1h </div> diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/toolbar_settings.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/toolbar_settings.stories.storyshot index a8a3f584bbf51..1aafb9cc6b664 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/toolbar_settings.stories.storyshot +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__stories__/__snapshots__/toolbar_settings.stories.storyshot @@ -34,7 +34,7 @@ exports[`Storyshots shareables/Footer/Settings/ToolbarSettings component: off 1` > <button aria-checked={false} - aria-describedby="generated-id-help" + aria-describedby="generated-id-help-0" aria-labelledby="generated-id" className="euiSwitch__button" data-test-subj="hideToolbarSwitch" @@ -78,7 +78,7 @@ exports[`Storyshots shareables/Footer/Settings/ToolbarSettings component: off 1` </div> <div className="euiFormHelpText euiFormRow__text" - id="generated-id-help" + id="generated-id-help-0" > Hide the toolbar when the mouse is not within the Canvas? </div> @@ -122,7 +122,7 @@ exports[`Storyshots shareables/Footer/Settings/ToolbarSettings component: on 1`] > <button aria-checked={true} - aria-describedby="generated-id-help" + aria-describedby="generated-id-help-0" aria-labelledby="generated-id" className="euiSwitch__button" data-test-subj="hideToolbarSwitch" @@ -166,7 +166,7 @@ exports[`Storyshots shareables/Footer/Settings/ToolbarSettings component: on 1`] </div> <div className="euiFormHelpText euiFormRow__text" - id="generated-id-help" + id="generated-id-help-0" > Hide the toolbar when the mouse is not within the Canvas? </div> @@ -210,7 +210,7 @@ exports[`Storyshots shareables/Footer/Settings/ToolbarSettings contextual 1`] = > <button aria-checked={false} - aria-describedby="generated-id-help" + aria-describedby="generated-id-help-0" aria-labelledby="generated-id" className="euiSwitch__button" data-test-subj="hideToolbarSwitch" @@ -254,7 +254,7 @@ exports[`Storyshots shareables/Footer/Settings/ToolbarSettings contextual 1`] = </div> <div className="euiFormHelpText euiFormRow__text" - id="generated-id-help" + id="generated-id-help-0" > Hide the toolbar when the mouse is not within the Canvas? </div> diff --git a/x-pack/plugins/cases/README.md b/x-pack/plugins/cases/README.md index a1660911567da..cfff8c79ee2d4 100644 --- a/x-pack/plugins/cases/README.md +++ b/x-pack/plugins/cases/README.md @@ -215,7 +215,7 @@ This action type has no `secrets` properties. | -------- | ------------------------------------------------------------------------------------------------- | ----------------- | | id | ID of the connector used for pushing case updates to external systems. | string | | name | The connector name. | string | -| type | The type of the connector. Must be one of these: `.servicenow`, `jira`, `.resilient`, and `.none` | string | +| type | The type of the connector. Must be one of these: `.servicenow`, `.servicenow-sir`, `.swimlane`, `jira`, `.resilient`, and `.none` | string | | fields | Object containing the connector’s fields. | [fields](#fields) | #### `fields` diff --git a/x-pack/plugins/cases/common/api/connectors/index.ts b/x-pack/plugins/cases/common/api/connectors/index.ts index 2a81396025d9a..cee432b17933b 100644 --- a/x-pack/plugins/cases/common/api/connectors/index.ts +++ b/x-pack/plugins/cases/common/api/connectors/index.ts @@ -12,12 +12,14 @@ import { JiraFieldsRT } from './jira'; import { ResilientFieldsRT } from './resilient'; import { ServiceNowITSMFieldsRT } from './servicenow_itsm'; import { ServiceNowSIRFieldsRT } from './servicenow_sir'; +import { SwimlaneFieldsRT } from './swimlane'; export * from './jira'; export * from './servicenow_itsm'; export * from './servicenow_sir'; export * from './resilient'; export * from './mappings'; +export * from './swimlane'; export type ActionConnector = ActionResult; export type ActionTypeConnector = ActionType; @@ -32,10 +34,11 @@ export const ConnectorFieldsRt = rt.union([ export enum ConnectorTypes { jira = '.jira', + none = '.none', resilient = '.resilient', serviceNowITSM = '.servicenow', serviceNowSIR = '.servicenow-sir', - none = '.none', + swimlane = '.swimlane', } export const connectorTypes = Object.values(ConnectorTypes); @@ -55,6 +58,11 @@ const ConnectorServiceNowITSMTypeFieldsRt = rt.type({ fields: rt.union([ServiceNowITSMFieldsRT, rt.null]), }); +const ConnectorSwimlaneTypeFieldsRt = rt.type({ + type: rt.literal(ConnectorTypes.swimlane), + fields: rt.union([SwimlaneFieldsRT, rt.null]), +}); + const ConnectorServiceNowSIRTypeFieldsRt = rt.type({ type: rt.literal(ConnectorTypes.serviceNowSIR), fields: rt.union([ServiceNowSIRFieldsRT, rt.null]), @@ -67,10 +75,11 @@ const ConnectorNoneTypeFieldsRt = rt.type({ export const ConnectorTypeFieldsRt = rt.union([ ConnectorJiraTypeFieldsRt, + ConnectorNoneTypeFieldsRt, ConnectorResillientTypeFieldsRt, ConnectorServiceNowITSMTypeFieldsRt, ConnectorServiceNowSIRTypeFieldsRt, - ConnectorNoneTypeFieldsRt, + ConnectorSwimlaneTypeFieldsRt, ]); export const CaseConnectorRt = rt.intersection([ @@ -85,6 +94,7 @@ export type CaseConnector = rt.TypeOf<typeof CaseConnectorRt>; export type ConnectorTypeFields = rt.TypeOf<typeof ConnectorTypeFieldsRt>; export type ConnectorJiraTypeFields = rt.TypeOf<typeof ConnectorJiraTypeFieldsRt>; export type ConnectorResillientTypeFields = rt.TypeOf<typeof ConnectorResillientTypeFieldsRt>; +export type ConnectorSwimlaneTypeFields = rt.TypeOf<typeof ConnectorSwimlaneTypeFieldsRt>; export type ConnectorServiceNowITSMTypeFields = rt.TypeOf< typeof ConnectorServiceNowITSMTypeFieldsRt >; diff --git a/x-pack/plugins/cases/common/api/connectors/mappings.ts b/x-pack/plugins/cases/common/api/connectors/mappings.ts index e0fdd2d7e62dc..8737a6c5a6462 100644 --- a/x-pack/plugins/cases/common/api/connectors/mappings.ts +++ b/x-pack/plugins/cases/common/api/connectors/mappings.ts @@ -48,9 +48,6 @@ const ConnectorFieldRt = rt.type({ export type ConnectorField = rt.TypeOf<typeof ConnectorFieldRt>; -const GetFieldsResponseRt = rt.type({ - defaultMappings: rt.array(ConnectorMappingsAttributesRT), - fields: rt.array(ConnectorFieldRt), -}); +const GetDefaultMappingsResponseRt = rt.array(ConnectorMappingsAttributesRT); -export type GetFieldsResponse = rt.TypeOf<typeof GetFieldsResponseRt>; +export type GetDefaultMappingsResponse = rt.TypeOf<typeof GetDefaultMappingsResponseRt>; diff --git a/x-pack/plugins/cases/common/api/connectors/swimlane.ts b/x-pack/plugins/cases/common/api/connectors/swimlane.ts new file mode 100644 index 0000000000000..bc4d9df9ae6a0 --- /dev/null +++ b/x-pack/plugins/cases/common/api/connectors/swimlane.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 * as rt from 'io-ts'; + +// New fields should also be added at: x-pack/plugins/cases/server/connectors/case/schema.ts +export const SwimlaneFieldsRT = rt.type({ + caseId: rt.union([rt.string, rt.null]), +}); + +export enum SwimlaneConnectorType { + All = 'all', + Alerts = 'alerts', + Cases = 'cases', +} + +export type SwimlaneFieldsType = rt.TypeOf<typeof SwimlaneFieldsRT>; diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts index 317fe1d8ed144..5d7ee47bb8ea0 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants.ts @@ -4,6 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { ConnectorTypes } from './api'; + export const DEFAULT_DATE_FORMAT = 'dateFormat'; export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz'; @@ -59,16 +61,12 @@ export const CASE_DETAILS_ALERTS_URL = `${CASE_DETAILS_URL}/alerts`; export const ACTION_URL = '/api/actions'; export const ACTION_TYPES_URL = '/api/actions/list_action_types'; -export const SERVICENOW_ITSM_ACTION_TYPE_ID = '.servicenow'; -export const SERVICENOW_SIR_ACTION_TYPE_ID = '.servicenow-sir'; -export const JIRA_ACTION_TYPE_ID = '.jira'; -export const RESILIENT_ACTION_TYPE_ID = '.resilient'; - export const SUPPORTED_CONNECTORS = [ - SERVICENOW_ITSM_ACTION_TYPE_ID, - SERVICENOW_SIR_ACTION_TYPE_ID, - JIRA_ACTION_TYPE_ID, - RESILIENT_ACTION_TYPE_ID, + `${ConnectorTypes.serviceNowITSM}`, + `${ConnectorTypes.serviceNowSIR}`, + `${ConnectorTypes.jira}`, + `${ConnectorTypes.resilient}`, + `${ConnectorTypes.swimlane}`, ]; /** diff --git a/x-pack/plugins/cases/public/common/shared_imports.ts b/x-pack/plugins/cases/public/common/shared_imports.ts index 675204076b02a..4641fcfa2167c 100644 --- a/x-pack/plugins/cases/public/common/shared_imports.ts +++ b/x-pack/plugins/cases/public/common/shared_imports.ts @@ -24,6 +24,8 @@ export { ValidationError, ValidationFunc, VALIDATION_TYPES, + FieldConfig, + ValidationConfig, } from '../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; export { Field, diff --git a/x-pack/plugins/cases/public/components/case_view/index.test.tsx b/x-pack/plugins/cases/public/components/case_view/index.test.tsx index 55de4d07b13b9..1fafbac50c2b9 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.test.tsx @@ -608,6 +608,7 @@ describe('CaseView ', () => { ).toBe(connectorName); }); }); + it('should update connector', async () => { const wrapper = mount( <TestProviders> @@ -628,15 +629,19 @@ describe('CaseView ', () => { wrapper.find('[data-test-subj="connector-edit"] button').simulate('click'); wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click'); - await waitFor(() => wrapper.update()); + await waitFor(() => { + wrapper.update(); + expect(wrapper.find(`[data-test-subj="connector-fields-resilient"]`).exists()).toBeTruthy(); + }); + wrapper.find(`button[data-test-subj="edit-connectors-submit"]`).first().simulate('click'); await waitFor(() => { - const updateObject = updateCaseProperty.mock.calls[0][0]; + wrapper.update(); expect(updateCaseProperty).toHaveBeenCalledTimes(1); + const updateObject = updateCaseProperty.mock.calls[0][0]; expect(updateObject.updateKey).toEqual('connector'); expect(updateObject.updateValue).toEqual({ id: 'resilient-2', diff --git a/x-pack/plugins/cases/public/components/case_view/index.tsx b/x-pack/plugins/cases/public/components/case_view/index.tsx index 05f1c6727b168..9c6e9442c8f56 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.tsx @@ -31,17 +31,14 @@ import { useGetCaseUserActions } from '../../containers/use_get_case_user_action import { usePushToService } from '../use_push_to_service'; import { EditConnector } from '../edit_connector'; import { useConnectors } from '../../containers/configure/use_connectors'; -import { - getConnectorById, - normalizeActionConnector, - getNoneConnector, -} from '../configure_cases/utils'; +import { normalizeActionConnector, getNoneConnector } from '../configure_cases/utils'; import { StatusActionButton } from '../status/button'; import * as i18n from './translations'; import { CasesTimelineIntegration, CasesTimelineIntegrationProvider } from '../timeline_context'; import { useTimelineContext } from '../timeline_context/use_timeline_context'; import { CasesNavigation } from '../links'; import { OwnerProvider } from '../owner_context'; +import { getConnectorById } from '../utils'; import { DoesNotExist } from './does_not_exist'; const gutterTimeline = '70px'; // seems to be a timeline reference from the original file diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.tsx index 3ee4bc77cd237..ac43ec05319a0 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/index.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/index.tsx @@ -24,15 +24,11 @@ import { ActionConnectorTableItem } from '../../../../triggers_actions_ui/public import { SectionWrapper } from '../wrappers'; import { Connectors } from './connectors'; import { ClosureOptions } from './closure_options'; -import { - getConnectorById, - getNoneConnector, - normalizeActionConnector, - normalizeCaseConnector, -} from './utils'; +import { getNoneConnector, normalizeActionConnector, normalizeCaseConnector } from './utils'; import * as i18n from './translations'; import { Owner } from '../../types'; import { OwnerProvider } from '../owner_context'; +import { getConnectorById } from '../utils'; const FormWrapper = styled.div` ${({ theme }) => css` diff --git a/x-pack/plugins/cases/public/components/configure_cases/utils.ts b/x-pack/plugins/cases/public/components/configure_cases/utils.ts index ade1a5e0c2bba..6597417b5068a 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/utils.ts +++ b/x-pack/plugins/cases/public/components/configure_cases/utils.ts @@ -10,10 +10,10 @@ import { CaseField, ActionType, ThirdPartyField, - ActionConnector, CaseConnector, CaseConnectorMapping, } from '../../containers/configure/types'; +import { CaseActionConnector } from '../types'; export const setActionTypeToMapping = ( caseField: CaseField, @@ -54,13 +54,8 @@ export const getNoneConnector = (): CaseConnector => ({ fields: null, }); -export const getConnectorById = ( - id: string, - connectors: ActionConnector[] -): ActionConnector | null => connectors.find((c) => c.id === id) ?? null; - export const normalizeActionConnector = ( - actionConnector: ActionConnector, + actionConnector: CaseActionConnector, fields: CaseConnector['fields'] = null ): CaseConnector => { const caseConnectorFieldsType = { @@ -75,6 +70,6 @@ export const normalizeActionConnector = ( }; export const normalizeCaseConnector = ( - connectors: ActionConnector[], + connectors: CaseActionConnector[], caseConnector: CaseConnector -): ActionConnector | null => connectors.find((c) => c.id === caseConnector.id) ?? null; +): CaseActionConnector | null => connectors.find((c) => c.id === caseConnector.id) ?? null; diff --git a/x-pack/plugins/cases/public/components/connector_selector/form.tsx b/x-pack/plugins/cases/public/components/connector_selector/form.tsx index 210334e93adb8..71a65ae030d9d 100644 --- a/x-pack/plugins/cases/public/components/connector_selector/form.tsx +++ b/x-pack/plugins/cases/public/components/connector_selector/form.tsx @@ -8,6 +8,7 @@ import React, { useCallback } from 'react'; import { isEmpty } from 'lodash/fp'; import { EuiFormRow } from '@elastic/eui'; +import styled from 'styled-components'; import { FieldHook, getFieldValidityAndErrorMessage } from '../../common/shared_imports'; import { ConnectorsDropdown } from '../configure_cases/connectors_dropdown'; @@ -24,6 +25,13 @@ interface ConnectorSelectorProps { handleChange?: (newValue: string) => void; hideConnectorServiceNowSir?: boolean; } + +const EuiFormRowWrapper = styled(EuiFormRow)` + .euiFormErrorText { + display: none; + } +`; + export const ConnectorSelector = ({ connectors, dataTestSubj, @@ -47,7 +55,7 @@ export const ConnectorSelector = ({ ); return isEdit ? ( - <EuiFormRow + <EuiFormRowWrapper data-test-subj={dataTestSubj} describedByIds={idAria ? [idAria] : undefined} error={errorMessage} @@ -65,6 +73,6 @@ export const ConnectorSelector = ({ onChange={onChange} selectedConnector={isEmpty(field.value) ? 'none' : field.value} /> - </EuiFormRow> + </EuiFormRowWrapper> ) : null; }; diff --git a/x-pack/plugins/cases/public/components/connectors/fields_form.tsx b/x-pack/plugins/cases/public/components/connectors/fields_form.tsx index d71da6f87689d..062695fa41cc2 100644 --- a/x-pack/plugins/cases/public/components/connectors/fields_form.tsx +++ b/x-pack/plugins/cases/public/components/connectors/fields_form.tsx @@ -8,7 +8,8 @@ import React, { memo, Suspense } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; -import { CaseActionConnector, ConnectorFieldsProps } from './types'; +import { CaseActionConnector } from '../types'; +import { ConnectorFieldsProps } from './types'; import { getCaseConnectors } from '.'; import { ConnectorTypeFields } from '../../../common'; diff --git a/x-pack/plugins/cases/public/components/connectors/index.ts b/x-pack/plugins/cases/public/components/connectors/index.ts index ad202365ae967..3aa10c56dd8e9 100644 --- a/x-pack/plugins/cases/public/components/connectors/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/index.ts @@ -8,6 +8,7 @@ import { CaseConnectorsRegistry } from './types'; import { createCaseConnectorsRegistry } from './connectors_registry'; import { getCaseConnector as getJiraCaseConnector } from './jira'; +import { getCaseConnector as getSwimlaneCaseConnector } from './swimlane'; import { getCaseConnector as getResilientCaseConnector } from './resilient'; import { getServiceNowITSMCaseConnector, getServiceNowSIRCaseConnector } from './servicenow'; import { @@ -15,6 +16,7 @@ import { ServiceNowITSMFieldsType, ServiceNowSIRFieldsType, ResilientFieldsType, + SwimlaneFieldsType, } from '../../../common'; export { getActionType as getCaseConnectorUi } from './case'; @@ -40,6 +42,7 @@ class CaseConnectors { getServiceNowITSMCaseConnector() ); this.caseConnectorsRegistry.register<ServiceNowSIRFieldsType>(getServiceNowSIRCaseConnector()); + this.caseConnectorsRegistry.register<SwimlaneFieldsType>(getSwimlaneCaseConnector()); } registry(): CaseConnectorsRegistry { diff --git a/x-pack/plugins/cases/public/components/connectors/jira/index.ts b/x-pack/plugins/cases/public/components/connectors/jira/index.ts index f987d9823af8e..d59d20177c14d 100644 --- a/x-pack/plugins/cases/public/components/connectors/jira/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/jira/index.ts @@ -8,13 +8,13 @@ import { lazy } from 'react'; import { CaseConnector } from '../types'; -import { JiraFieldsType } from '../../../../common'; +import { ConnectorTypes, JiraFieldsType } from '../../../../common'; import * as i18n from './translations'; export * from './types'; export const getCaseConnector = (): CaseConnector<JiraFieldsType> => ({ - id: '.jira', + id: ConnectorTypes.jira, fieldsComponent: lazy(() => import('./case_fields')), }); export const fieldLabels = { diff --git a/x-pack/plugins/cases/public/components/connectors/mock.ts b/x-pack/plugins/cases/public/components/connectors/mock.ts index f5429fa2396aa..663b397e6f4fe 100644 --- a/x-pack/plugins/cases/public/components/connectors/mock.ts +++ b/x-pack/plugins/cases/public/components/connectors/mock.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { SwimlaneConnectorType } from '../../../common'; + export const connector = { id: '123', name: 'My connector', @@ -13,6 +15,22 @@ export const connector = { isPreconfigured: false, }; +export const swimlaneConnector = { + id: '123', + name: 'My connector', + actionTypeId: '.swimlane', + config: { + connectorType: SwimlaneConnectorType.Cases, + mappings: { + caseIdConfig: {}, + caseNameConfig: {}, + descriptionConfig: {}, + commentsConfig: {}, + }, + }, + isPreconfigured: false, +}; + export const issues = [ { id: 'personId', title: 'Person Task', key: 'personKey' }, { id: 'womanId', title: 'Woman Task', key: 'womanKey' }, diff --git a/x-pack/plugins/cases/public/components/connectors/resilient/index.ts b/x-pack/plugins/cases/public/components/connectors/resilient/index.ts index 9bf96b16f358c..8a429c0dea091 100644 --- a/x-pack/plugins/cases/public/components/connectors/resilient/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/resilient/index.ts @@ -8,13 +8,13 @@ import { lazy } from 'react'; import { CaseConnector } from '../types'; -import { ResilientFieldsType } from '../../../../common'; +import { ConnectorTypes, ResilientFieldsType } from '../../../../common'; import * as i18n from './translations'; export * from './types'; export const getCaseConnector = (): CaseConnector<ResilientFieldsType> => ({ - id: '.resilient', + id: ConnectorTypes.resilient, fieldsComponent: lazy(() => import('./case_fields')), }); diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/index.ts b/x-pack/plugins/cases/public/components/connectors/servicenow/index.ts index 9df5f87b416e1..88afd902ccf60 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/index.ts @@ -8,16 +8,20 @@ import { lazy } from 'react'; import { CaseConnector } from '../types'; -import { ServiceNowITSMFieldsType, ServiceNowSIRFieldsType } from '../../../../common'; +import { + ConnectorTypes, + ServiceNowITSMFieldsType, + ServiceNowSIRFieldsType, +} from '../../../../common'; import * as i18n from './translations'; export const getServiceNowITSMCaseConnector = (): CaseConnector<ServiceNowITSMFieldsType> => ({ - id: '.servicenow', + id: ConnectorTypes.serviceNowITSM, fieldsComponent: lazy(() => import('./servicenow_itsm_case_fields')), }); export const getServiceNowSIRCaseConnector = (): CaseConnector<ServiceNowSIRFieldsType> => ({ - id: '.servicenow-sir', + id: ConnectorTypes.serviceNowSIR, fieldsComponent: lazy(() => import('./servicenow_sir_case_fields')), }); diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx new file mode 100644 index 0000000000000..1a035d92611bd --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render, screen } from '@testing-library/react'; + +import { SwimlaneConnectorType } from '../../../../common'; +import Fields from './case_fields'; +import * as i18n from './translations'; +import { swimlaneConnector as connector } from '../mock'; + +const fields = { + caseId: '123', +}; + +const onChange = jest.fn(); + +describe('Swimlane Cases Fields', () => { + test('it does not shows the mapping error callout', () => { + render(<Fields connector={connector} fields={fields} onChange={onChange} />); + expect(screen.queryByText(i18n.EMPTY_MAPPING_WARNING_TITLE)).toBeFalsy(); + }); + + test('it shows the mapping error callout when mapping is invalid', () => { + const invalidConnector = { + ...connector, + config: { + ...connector.config, + mappings: {}, + }, + }; + + render(<Fields connector={invalidConnector} fields={fields} onChange={onChange} />); + expect(screen.queryByText(i18n.EMPTY_MAPPING_WARNING_TITLE)).toBeTruthy(); + }); + + test('it shows the mapping error callout when the connector is of type alerts', () => { + const invalidConnector = { + ...connector, + config: { + ...connector.config, + connectorType: SwimlaneConnectorType.Alerts, + }, + }; + + render(<Fields connector={invalidConnector} fields={fields} onChange={onChange} />); + expect(screen.queryByText(i18n.EMPTY_MAPPING_WARNING_TITLE)).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx new file mode 100644 index 0000000000000..b6370504edbb6 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.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 React, { useMemo } from 'react'; +import { EuiCallOut } from '@elastic/eui'; +import * as i18n from './translations'; + +import { ConnectorTypes, SwimlaneFieldsType } from '../../../../common'; +import { ConnectorFieldsProps } from '../types'; +import { ConnectorCard } from '../card'; +import { connectorValidator } from './validator'; + +const SwimlaneComponent: React.FunctionComponent<ConnectorFieldsProps<SwimlaneFieldsType>> = ({ + connector, + isEdit = true, +}) => { + const showMappingWarning = useMemo(() => connectorValidator(connector) != null, [connector]); + + return ( + <> + {!isEdit && ( + <ConnectorCard + connectorType={ConnectorTypes.swimlane} + isLoading={false} + listItems={[]} + title={connector.name} + /> + )} + {showMappingWarning && ( + <EuiCallOut + title={i18n.EMPTY_MAPPING_WARNING_TITLE} + color="danger" + iconType="alert" + data-test-subj="mapping-warning-callout" + > + {i18n.EMPTY_MAPPING_WARNING_DESC} + </EuiCallOut> + )} + </> + ); +}; + +// eslint-disable-next-line import/no-default-export +export { SwimlaneComponent as default }; diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts new file mode 100644 index 0000000000000..bd2eaae9e0174 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { lazy } from 'react'; + +import { CaseConnector } from '../types'; +import { ConnectorTypes, SwimlaneFieldsType } from '../../../../common'; +import * as i18n from './translations'; + +export const getCaseConnector = (): CaseConnector<SwimlaneFieldsType> => { + return { + id: ConnectorTypes.swimlane, + fieldsComponent: lazy(() => import('./case_fields')), + }; +}; + +export const fieldLabels = { + caseId: i18n.CASE_ID_LABEL, + caseName: i18n.CASE_NAME_LABEL, + severity: i18n.SEVERITY_LABEL, +}; diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts new file mode 100644 index 0000000000000..eb6cd168fab99 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ALERT_SOURCE_LABEL = i18n.translate( + 'xpack.cases.connectors.swimlane.alertSourceLabel', + { + defaultMessage: 'Alert Source', + } +); + +export const CASE_ID_LABEL = i18n.translate('xpack.cases.connectors.swimlane.caseIdLabel', { + defaultMessage: 'Case Id', +}); + +export const CASE_NAME_LABEL = i18n.translate('xpack.cases.connectors.swimlane.caseNameLabel', { + defaultMessage: 'Case Name', +}); + +export const SEVERITY_LABEL = i18n.translate('xpack.cases.connectors.swimlane.severityLabel', { + defaultMessage: 'Severity', +}); + +export const EMPTY_MAPPING_WARNING_TITLE = i18n.translate( + 'xpack.cases.connectors.swimlane.emptyMappingWarningTitle', + { + defaultMessage: 'This connector has missing field mappings', + } +); + +export const EMPTY_MAPPING_WARNING_DESC = i18n.translate( + 'xpack.cases.connectors.swimlane.emptyMappingWarningDesc', + { + defaultMessage: + 'This connector cannot be selected because it is missing the required case field mappings. You can edit this connector to add required field mappings or select a connector of type Cases.', + } +); diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.test.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.test.ts new file mode 100644 index 0000000000000..552d988c26330 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.test.ts @@ -0,0 +1,60 @@ +/* + * 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 { SwimlaneConnectorType } from '../../../../common'; +import { swimlaneConnector as connector } from '../mock'; +import { isAnyRequiredFieldNotSet, connectorValidator } from './validator'; + +describe('Swimlane validator', () => { + describe('isAnyRequiredFieldNotSet', () => { + test('it returns true if a required field is not set', () => { + expect(isAnyRequiredFieldNotSet({ notRequired: 'test' })).toBeTruthy(); + }); + + test('it returns false if all required fields are set', () => { + expect(isAnyRequiredFieldNotSet(connector.config.mappings)).toBeFalsy(); + }); + }); + + describe('connectorValidator', () => { + test('it returns an error message if the mapping is not correct', () => { + const invalidConnector = { + ...connector, + config: { + ...connector.config, + mappings: {}, + }, + }; + expect(connectorValidator(invalidConnector)).toEqual({ message: 'Invalid connector' }); + }); + + test('it returns an error message if the connector is of type alerts', () => { + const invalidConnector = { + ...connector, + config: { + ...connector.config, + connectorType: SwimlaneConnectorType.Alerts, + }, + }; + expect(connectorValidator(invalidConnector)).toEqual({ message: 'Invalid connector' }); + }); + + test.each([SwimlaneConnectorType.Cases, SwimlaneConnectorType.All])( + 'it does not return an error message if the connector is of type %s', + (connectorType) => { + const invalidConnector = { + ...connector, + config: { + ...connector.config, + connectorType, + }, + }; + expect(connectorValidator(invalidConnector)).toBe(undefined); + } + ); + }); +}); diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts new file mode 100644 index 0000000000000..4ead75e5854f9 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.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 { SwimlaneConnectorType } from '../../../../common'; +import { ValidationConfig } from '../../../common/shared_imports'; +import { CaseActionConnector } from '../../types'; + +const casesRequiredFields = [ + 'caseIdConfig', + 'caseNameConfig', + 'descriptionConfig', + 'commentsConfig', +]; + +export const isAnyRequiredFieldNotSet = (mapping: Record<string, unknown> | undefined) => + casesRequiredFields.some((field) => mapping?.[field] == null); + +/** + * The user can use either a connector of type cases or all. + * If the connector is of type all we should check if all + * required field have been configured. + */ + +export const connectorValidator = ( + connector: CaseActionConnector +): ReturnType<ValidationConfig['validator']> => { + const { + config: { mappings, connectorType }, + } = connector; + if (connectorType === SwimlaneConnectorType.Alerts || isAnyRequiredFieldNotSet(mappings)) { + return { + message: 'Invalid connector', + }; + } +}; diff --git a/x-pack/plugins/cases/public/components/connectors/types.ts b/x-pack/plugins/cases/public/components/connectors/types.ts index 4eb97513b9f58..5bbd77c790901 100644 --- a/x-pack/plugins/cases/public/components/connectors/types.ts +++ b/x-pack/plugins/cases/public/components/connectors/types.ts @@ -11,12 +11,11 @@ import React from 'react'; import { ActionType as ThirdPartySupportedActions, CaseField, - ActionConnector, ConnectorTypeFields, } from '../../../common'; +import { CaseActionConnector } from '../types'; export { ThirdPartyField as AllThirdPartyFields } from '../../../common'; -export type CaseActionConnector = ActionConnector; export interface ThirdPartyField { label: string; diff --git a/x-pack/plugins/cases/public/components/create/connector.test.tsx b/x-pack/plugins/cases/public/components/create/connector.test.tsx index c453838f6cd7a..bc6d5c8717ece 100644 --- a/x-pack/plugins/cases/public/components/create/connector.test.tsx +++ b/x-pack/plugins/cases/public/components/create/connector.test.tsx @@ -18,6 +18,9 @@ import { useGetSeverity } from '../connectors/resilient/use_get_severity'; import { useGetChoices } from '../connectors/servicenow/use_get_choices'; import { incidentTypes, severity, choices } from '../connectors/mock'; import { schema, FormProps } from './schema'; +import { TestProviders } from '../../common/mock'; +import { useCaseConfigure } from '../../containers/configure/use_configure'; +import { useCaseConfigureResponse } from '../configure_cases/__mock__'; jest.mock('../../common/lib/kibana', () => ({ useKibana: () => ({ @@ -39,10 +42,12 @@ jest.mock('../../common/lib/kibana', () => ({ jest.mock('../connectors/resilient/use_get_incident_types'); jest.mock('../connectors/resilient/use_get_severity'); jest.mock('../connectors/servicenow/use_get_choices'); +jest.mock('../../containers/configure/use_configure'); const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock; const useGetSeverityMock = useGetSeverity as jest.Mock; const useGetChoicesMock = useGetChoices as jest.Mock; +const useCaseConfigureMock = useCaseConfigure as jest.Mock; const useGetIncidentTypesResponse = { isLoading: false, @@ -87,35 +92,30 @@ describe('Connector', () => { useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse); useGetSeverityMock.mockReturnValue(useGetSeverityResponse); useGetChoicesMock.mockReturnValue(useGetChoicesResponse); + useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); }); it('it renders', async () => { const wrapper = mount( - <MockHookWrapperComponent> - <Connector {...defaultProps} /> - </MockHookWrapperComponent> + <TestProviders> + <MockHookWrapperComponent> + <Connector {...defaultProps} /> + </MockHookWrapperComponent> + </TestProviders> ); expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeTruthy(); - expect(wrapper.find(`[data-test-subj="connector-fields"]`).exists()).toBeTruthy(); - - await waitFor(() => { - expect(wrapper.find(`button[data-test-subj="dropdown-connectors"]`).first().text()).toBe( - 'My Connector' - ); - }); - - await waitFor(() => { - wrapper.update(); - expect(wrapper.find(`[data-test-subj="connector-fields-sn-itsm"]`).exists()).toBeTruthy(); - }); + // Selected connector is set to none so no fields should be displayed + expect(wrapper.find(`[data-test-subj="connector-fields"]`).exists()).toBeFalsy(); }); it('it is disabled and loading when isLoadingConnectors=true', async () => { const wrapper = mount( - <MockHookWrapperComponent> - <Connector {...{ ...defaultProps, isLoadingConnectors: true }} /> - </MockHookWrapperComponent> + <TestProviders> + <MockHookWrapperComponent> + <Connector {...{ ...defaultProps, isLoadingConnectors: true }} /> + </MockHookWrapperComponent> + </TestProviders> ); expect( @@ -129,9 +129,11 @@ describe('Connector', () => { it('it is disabled and loading when isLoading=true', async () => { const wrapper = mount( - <MockHookWrapperComponent> - <Connector {...{ ...defaultProps, isLoading: true }} /> - </MockHookWrapperComponent> + <TestProviders> + <MockHookWrapperComponent> + <Connector {...{ ...defaultProps, isLoading: true }} /> + </MockHookWrapperComponent> + </TestProviders> ); expect( @@ -144,9 +146,11 @@ describe('Connector', () => { it(`it should change connector`, async () => { const wrapper = mount( - <MockHookWrapperComponent> - <Connector {...defaultProps} /> - </MockHookWrapperComponent> + <TestProviders> + <MockHookWrapperComponent> + <Connector {...defaultProps} /> + </MockHookWrapperComponent> + </TestProviders> ); expect(wrapper.find(`[data-test-subj="connector-fields-resilient"]`).exists()).toBeFalsy(); diff --git a/x-pack/plugins/cases/public/components/create/connector.tsx b/x-pack/plugins/cases/public/components/create/connector.tsx index 2049f2a083a6f..2ec6d1ffef23d 100644 --- a/x-pack/plugins/cases/public/components/create/connector.tsx +++ b/x-pack/plugins/cases/public/components/create/connector.tsx @@ -5,15 +5,22 @@ * 2.0. */ -import React, { memo, useCallback } from 'react'; +import React, { memo, useCallback, useMemo, useEffect } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { ActionConnector, ConnectorTypes } from '../../../common'; -import { UseField, useFormData, FieldHook, useFormContext } from '../../common/shared_imports'; +import { ConnectorTypes, ActionConnector } from '../../../common'; +import { + UseField, + useFormData, + FieldHook, + useFormContext, + FieldConfig, +} from '../../common/shared_imports'; import { ConnectorSelector } from '../connector_selector/form'; import { ConnectorFieldsForm } from '../connectors/fields_form'; -import { getConnectorById } from '../configure_cases/utils'; -import { FormProps } from './schema'; +import { FormProps, schema } from './schema'; +import { useCaseConfigure } from '../../containers/configure/use_configure'; +import { getConnectorById, getConnectorsFormValidators } from '../utils'; interface Props { connectors: ActionConnector[]; @@ -26,6 +33,7 @@ interface ConnectorsFieldProps { connectors: ActionConnector[]; field: FieldHook<FormProps['fields']>; isEdit: boolean; + setErrors: (errors: boolean) => void; hideConnectorServiceNowSir?: boolean; } @@ -33,11 +41,13 @@ const ConnectorFields = ({ connectors, isEdit, field, + setErrors, hideConnectorServiceNowSir = false, }: ConnectorsFieldProps) => { const [{ connectorId }] = useFormData({ watch: ['connectorId'] }); const { setValue } = field; let connector = getConnectorById(connectorId, connectors) ?? null; + if ( connector && hideConnectorServiceNowSir && @@ -61,18 +71,49 @@ const ConnectorComponent: React.FC<Props> = ({ isLoading, isLoadingConnectors, }) => { - const { getFields } = useFormContext(); + const { getFields, setFieldValue } = useFormContext(); + const { connector: configurationConnector } = useCaseConfigure(); + const handleConnectorChange = useCallback(() => { const { fields } = getFields(); fields.setValue(null); }, [getFields]); + const defaultConnectorId = useMemo(() => { + if ( + hideConnectorServiceNowSir && + configurationConnector.type === ConnectorTypes.serviceNowSIR + ) { + return 'none'; + } + return connectors.some((connector) => connector.id === configurationConnector.id) + ? configurationConnector.id + : 'none'; + }, [ + configurationConnector.id, + configurationConnector.type, + connectors, + hideConnectorServiceNowSir, + ]); + + useEffect(() => setFieldValue('connectorId', defaultConnectorId), [ + defaultConnectorId, + setFieldValue, + ]); + + const connectorIdConfig = getConnectorsFormValidators({ + config: schema.connectorId as FieldConfig, + connectors, + }); + return ( <EuiFlexGroup> <EuiFlexItem> <UseField path="connectorId" + config={connectorIdConfig} component={ConnectorSelector} + defaultValue={defaultConnectorId} componentProps={{ connectors, handleChange: handleConnectorChange, diff --git a/x-pack/plugins/cases/public/components/create/form.test.tsx b/x-pack/plugins/cases/public/components/create/form.test.tsx index 5f3b778a7cafc..783ead9b271fd 100644 --- a/x-pack/plugins/cases/public/components/create/form.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form.test.tsx @@ -17,11 +17,16 @@ import { schema, FormProps } from './schema'; import { CreateCaseForm } from './form'; import { OwnerProvider } from '../owner_context'; import { SECURITY_SOLUTION_OWNER } from '../../../common'; +import { useCaseConfigure } from '../../containers/configure/use_configure'; +import { useCaseConfigureResponse } from '../configure_cases/__mock__'; jest.mock('../../containers/use_get_tags'); jest.mock('../../containers/configure/use_connectors'); +jest.mock('../../containers/configure/use_configure'); + const useGetTagsMock = useGetTags as jest.Mock; const useConnectorsMock = useConnectors as jest.Mock; +const useCaseConfigureMock = useCaseConfigure as jest.Mock; const initialCaseValue: FormProps = { description: '', @@ -54,6 +59,7 @@ describe('CreateCaseForm', () => { jest.resetAllMocks(); useGetTagsMock.mockReturnValue({ tags: ['test'] }); useConnectorsMock.mockReturnValue({ loading: false, connectors: connectorsMock }); + useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); }); it('it renders with steps', async () => { diff --git a/x-pack/plugins/cases/public/components/create/form_context.tsx b/x-pack/plugins/cases/public/components/create/form_context.tsx index 30a60fb5c1e47..65c102583455a 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.tsx @@ -5,23 +5,19 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { schema, FormProps } from './schema'; import { Form, useForm } from '../../common/shared_imports'; -import { - getConnectorById, - getNoneConnector, - normalizeActionConnector, -} from '../configure_cases/utils'; +import { getNoneConnector, normalizeActionConnector } from '../configure_cases/utils'; import { usePostCase } from '../../containers/use_post_case'; import { usePostPushToService } from '../../containers/use_post_push_to_service'; import { useConnectors } from '../../containers/configure/use_connectors'; -import { useCaseConfigure } from '../../containers/configure/use_configure'; import { Case } from '../../containers/types'; -import { CaseType, ConnectorTypes } from '../../../common'; +import { CaseType } from '../../../common'; import { UsePostComment, usePostComment } from '../../containers/use_post_comment'; import { useOwnerContext } from '../owner_context/use_owner_context'; +import { getConnectorById } from '../utils'; const initialCaseValue: FormProps = { description: '', @@ -49,28 +45,10 @@ export const FormContext: React.FC<Props> = ({ }) => { const { connectors, loading: isLoadingConnectors } = useConnectors(); const owner = useOwnerContext(); - const { connector: configurationConnector } = useCaseConfigure(); const { postCase } = usePostCase(); const { postComment } = usePostComment(); const { pushCaseToExternalService } = usePostPushToService(); - const connectorId = useMemo(() => { - if ( - hideConnectorServiceNowSir && - configurationConnector.type === ConnectorTypes.serviceNowSIR - ) { - return 'none'; - } - return connectors.some((connector) => connector.id === configurationConnector.id) - ? configurationConnector.id - : 'none'; - }, [ - configurationConnector.id, - configurationConnector.type, - connectors, - hideConnectorServiceNowSir, - ]); - const submitCase = useCallback( async ( { connectorId: dataConnectorId, fields, syncAlerts = true, ...dataWithoutConnectorId }, @@ -125,9 +103,6 @@ export const FormContext: React.FC<Props> = ({ schema, onSubmit: submitCase, }); - const { setFieldValue } = form; - // Set the selected connector to the configuration connector - useEffect(() => setFieldValue('connectorId', connectorId), [connectorId, setFieldValue]); const childrenWithExtraProp = useMemo( () => diff --git a/x-pack/plugins/cases/public/components/create/schema.tsx b/x-pack/plugins/cases/public/components/create/schema.tsx index 6e6d1a414280e..bea1a46d93760 100644 --- a/x-pack/plugins/cases/public/components/create/schema.tsx +++ b/x-pack/plugins/cases/public/components/create/schema.tsx @@ -49,7 +49,9 @@ export const schema: FormSchema<FormProps> = { label: i18n.CONNECTORS, defaultValue: 'none', }, - fields: {}, + fields: { + defaultValue: null, + }, syncAlerts: { helpText: i18n.SYNC_ALERTS_HELP, type: FIELD_TYPES.TOGGLE, diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.tsx index 570f6e34d2528..8057d188b8c04 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/index.tsx +++ b/x-pack/plugins/cases/public/components/edit_connector/index.tsx @@ -20,15 +20,15 @@ import { import styled from 'styled-components'; import { noop } from 'lodash/fp'; -import { Form, UseField, useForm } from '../../common/shared_imports'; +import { FieldConfig, Form, UseField, useForm } from '../../common/shared_imports'; import { ActionConnector, ConnectorTypeFields } from '../../../common'; import { ConnectorSelector } from '../connector_selector/form'; import { ConnectorFieldsForm } from '../connectors/fields_form'; -import { getConnectorById } from '../configure_cases/utils'; import { CaseUserActions } from '../../containers/types'; import { schema } from './schema'; import { getConnectorFieldsFromUserActions } from './helpers'; import * as i18n from './translations'; +import { getConnectorById, getConnectorsFormValidators } from '../utils'; export interface EditConnectorProps { caseFields: ConnectorTypeFields['fields']; @@ -205,6 +205,11 @@ export const EditConnector = React.memo( }); }, [dispatch]); + const connectorIdConfig = getConnectorsFormValidators({ + config: schema.connectorId as FieldConfig, + connectors, + }); + /** * if this evaluates to true it means that the connector was likely deleted because the case connector was set to something * other than none but we don't find it in the list of connectors returned from the actions plugin @@ -243,6 +248,7 @@ export const EditConnector = React.memo( <EuiFlexItem> <UseField path="connectorId" + config={connectorIdConfig} component={ConnectorSelector} componentProps={{ connectors, diff --git a/x-pack/plugins/cases/public/components/panel/index.tsx b/x-pack/plugins/cases/public/components/panel/index.tsx index 652d22409cb0c..802fd4c7f44a6 100644 --- a/x-pack/plugins/cases/public/components/panel/index.tsx +++ b/x-pack/plugins/cases/public/components/panel/index.tsx @@ -25,7 +25,7 @@ import { EuiPanel } from '@elastic/eui'; * Ref: https://www.styled-components.com/docs/faqs#why-am-i-getting-html-attribute-warnings * Ref: https://reactjs.org/blog/2017/09/08/dom-attributes-in-react-16.html */ -export const Panel = styled(({ loading, ...props }) => <EuiPanel {...props} />)` +export const Panel = styled(({ loading, ...props }) => <EuiPanel {...props} hasBorder />)` position: relative; ${({ loading }) => loading && diff --git a/x-pack/plugins/cases/public/components/types.ts b/x-pack/plugins/cases/public/components/types.ts new file mode 100644 index 0000000000000..014afc371e761 --- /dev/null +++ b/x-pack/plugins/cases/public/components/types.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 { ActionConnector } from '../../common'; + +export type CaseActionConnector = ActionConnector; diff --git a/x-pack/plugins/cases/public/components/utils.ts b/x-pack/plugins/cases/public/components/utils.ts new file mode 100644 index 0000000000000..033529c27a2d4 --- /dev/null +++ b/x-pack/plugins/cases/public/components/utils.ts @@ -0,0 +1,43 @@ +/* + * 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 { ConnectorTypes } from '../../common'; +import { FieldConfig, ValidationConfig } from '../common/shared_imports'; +import { connectorValidator as swimlaneConnectorValidator } from './connectors/swimlane/validator'; +import { CaseActionConnector } from './types'; + +export const getConnectorById = ( + id: string, + connectors: CaseActionConnector[] +): CaseActionConnector | null => connectors.find((c) => c.id === id) ?? null; + +const validators: Record< + string, + (connector: CaseActionConnector) => ReturnType<ValidationConfig['validator']> +> = { + [ConnectorTypes.swimlane]: swimlaneConnectorValidator, +}; + +export const getConnectorsFormValidators = ({ + connectors = [], + config = {}, +}: { + connectors: CaseActionConnector[]; + config: FieldConfig; +}): FieldConfig => ({ + ...config, + validations: [ + { + validator: ({ value: connectorId }) => { + const connector = getConnectorById(connectorId as string, connectors); + if (connector != null) { + return validators[connector.actionTypeId]?.(connector); + } + }, + }, + ], +}); diff --git a/x-pack/plugins/cases/public/containers/api.test.tsx b/x-pack/plugins/cases/public/containers/api.test.tsx index abdee387a2c42..30a76e28e7485 100644 --- a/x-pack/plugins/cases/public/containers/api.test.tsx +++ b/x-pack/plugins/cases/public/containers/api.test.tsx @@ -363,13 +363,14 @@ describe('Case Configuration API', () => { }); test('check url, method, signal', async () => { - await patchComment( - basicCase.id, - basicCase.comments[0].id, - 'updated comment', - basicCase.comments[0].version, - abortCtrl.signal - ); + await patchComment({ + caseId: basicCase.id, + commentId: basicCase.comments[0].id, + commentUpdate: 'updated comment', + version: basicCase.comments[0].version, + signal: abortCtrl.signal, + owner: SECURITY_SOLUTION_OWNER, + }); expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/comments`, { method: 'PATCH', body: JSON.stringify({ @@ -377,19 +378,21 @@ describe('Case Configuration API', () => { type: CommentType.user, id: basicCase.comments[0].id, version: basicCase.comments[0].version, + owner: SECURITY_SOLUTION_OWNER, }), signal: abortCtrl.signal, }); }); test('happy path', async () => { - const resp = await patchComment( - basicCase.id, - basicCase.comments[0].id, - 'updated comment', - basicCase.comments[0].version, - abortCtrl.signal - ); + const resp = await patchComment({ + caseId: basicCase.id, + commentId: basicCase.comments[0].id, + commentUpdate: 'updated comment', + version: basicCase.comments[0].version, + signal: abortCtrl.signal, + owner: SECURITY_SOLUTION_OWNER, + }); expect(resp).toEqual(basicCase); }); }); diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts index 1a2a92850a4ad..b144a874cfc53 100644 --- a/x-pack/plugins/cases/public/containers/api.ts +++ b/x-pack/plugins/cases/public/containers/api.ts @@ -283,14 +283,23 @@ export const postComment = async ( return convertToCamelCase<CaseResponse, Case>(decodeCaseResponse(response)); }; -export const patchComment = async ( - caseId: string, - commentId: string, - commentUpdate: string, - version: string, - signal: AbortSignal, - subCaseId?: string -): Promise<Case> => { +export const patchComment = async ({ + caseId, + commentId, + commentUpdate, + version, + signal, + owner, + subCaseId, +}: { + caseId: string; + commentId: string; + commentUpdate: string; + version: string; + signal: AbortSignal; + owner: string; + subCaseId?: string; +}): Promise<Case> => { const response = await KibanaServices.get().http.fetch<CaseResponse>(getCaseCommentsUrl(caseId), { method: 'PATCH', body: JSON.stringify({ @@ -298,6 +307,7 @@ export const patchComment = async ( type: CommentType.user, id: commentId, version, + owner, }), ...(subCaseId ? { query: { subCaseId } } : {}), signal, diff --git a/x-pack/plugins/cases/public/containers/use_get_action_license.tsx b/x-pack/plugins/cases/public/containers/use_get_action_license.tsx index 4f28d88c14b25..e4ea6d05011a7 100644 --- a/x-pack/plugins/cases/public/containers/use_get_action_license.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_action_license.tsx @@ -11,6 +11,7 @@ import { useToasts } from '../common/lib/kibana'; import { getActionLicense } from './api'; import * as i18n from './translations'; import { ActionLicense } from './types'; +import { ConnectorTypes } from '../../common'; export interface ActionLicenseState { actionLicense: ActionLicense | null; @@ -24,7 +25,7 @@ export const initialData: ActionLicenseState = { isError: false, }; -const MINIMUM_LICENSE_REQUIRED_CONNECTOR = '.jira'; +const MINIMUM_LICENSE_REQUIRED_CONNECTOR = ConnectorTypes.jira; export const useGetActionLicense = (): ActionLicenseState => { const [actionLicenseState, setActionLicensesState] = useState<ActionLicenseState>(initialData); diff --git a/x-pack/plugins/cases/public/containers/use_update_comment.test.tsx b/x-pack/plugins/cases/public/containers/use_update_comment.test.tsx index b936eb126f0d4..14cc4dfab3599 100644 --- a/x-pack/plugins/cases/public/containers/use_update_comment.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_update_comment.test.tsx @@ -5,10 +5,13 @@ * 2.0. */ +import React from 'react'; import { renderHook, act } from '@testing-library/react-hooks'; import { useUpdateComment, UseUpdateComment } from './use_update_comment'; import { basicCase, basicCaseCommentPatch, basicSubCaseId } from './mock'; import * as api from './api'; +import { TestProviders } from '../common/mock'; +import { SECURITY_SOLUTION_OWNER } from '../../common'; jest.mock('./api'); jest.mock('../common/lib/kibana'); @@ -25,6 +28,12 @@ describe('useUpdateComment', () => { updateCase, version: basicCase.comments[0].version, }; + + const renderHookUseUpdateComment = () => + renderHook<string, UseUpdateComment>(() => useUpdateComment(), { + wrapper: ({ children }) => <TestProviders>{children}</TestProviders>, + }); + beforeEach(() => { jest.clearAllMocks(); jest.restoreAllMocks(); @@ -32,9 +41,7 @@ describe('useUpdateComment', () => { it('init', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, UseUpdateComment>(() => - useUpdateComment() - ); + const { result, waitForNextUpdate } = renderHookUseUpdateComment(); await waitForNextUpdate(); expect(result.current).toEqual({ isLoadingIds: [], @@ -48,21 +55,20 @@ describe('useUpdateComment', () => { const spyOnPatchComment = jest.spyOn(api, 'patchComment'); await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, UseUpdateComment>(() => - useUpdateComment() - ); + const { result, waitForNextUpdate } = renderHookUseUpdateComment(); await waitForNextUpdate(); result.current.patchComment(sampleUpdate); await waitForNextUpdate(); - expect(spyOnPatchComment).toBeCalledWith( - basicCase.id, - basicCase.comments[0].id, - 'updated comment', - basicCase.comments[0].version, - abortCtrl.signal, - undefined - ); + expect(spyOnPatchComment).toBeCalledWith({ + caseId: basicCase.id, + commentId: basicCase.comments[0].id, + commentUpdate: 'updated comment', + version: basicCase.comments[0].version, + signal: abortCtrl.signal, + owner: SECURITY_SOLUTION_OWNER, + subCaseId: undefined, + }); }); }); @@ -70,29 +76,26 @@ describe('useUpdateComment', () => { const spyOnPatchComment = jest.spyOn(api, 'patchComment'); await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, UseUpdateComment>(() => - useUpdateComment() - ); + const { result, waitForNextUpdate } = renderHookUseUpdateComment(); await waitForNextUpdate(); result.current.patchComment({ ...sampleUpdate, subCaseId: basicSubCaseId }); await waitForNextUpdate(); - expect(spyOnPatchComment).toBeCalledWith( - basicCase.id, - basicCase.comments[0].id, - 'updated comment', - basicCase.comments[0].version, - abortCtrl.signal, - basicSubCaseId - ); + expect(spyOnPatchComment).toBeCalledWith({ + caseId: basicCase.id, + commentId: basicCase.comments[0].id, + commentUpdate: 'updated comment', + version: basicCase.comments[0].version, + signal: abortCtrl.signal, + owner: SECURITY_SOLUTION_OWNER, + subCaseId: basicSubCaseId, + }); }); }); it('patch comment', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, UseUpdateComment>(() => - useUpdateComment() - ); + const { result, waitForNextUpdate } = renderHookUseUpdateComment(); await waitForNextUpdate(); result.current.patchComment(sampleUpdate); await waitForNextUpdate(); @@ -108,9 +111,7 @@ describe('useUpdateComment', () => { it('set isLoading to true when posting case', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, UseUpdateComment>(() => - useUpdateComment() - ); + const { result, waitForNextUpdate } = renderHookUseUpdateComment(); await waitForNextUpdate(); result.current.patchComment(sampleUpdate); @@ -125,9 +126,7 @@ describe('useUpdateComment', () => { }); await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, UseUpdateComment>(() => - useUpdateComment() - ); + const { result, waitForNextUpdate } = renderHookUseUpdateComment(); await waitForNextUpdate(); result.current.patchComment(sampleUpdate); diff --git a/x-pack/plugins/cases/public/containers/use_update_comment.tsx b/x-pack/plugins/cases/public/containers/use_update_comment.tsx index 478d7ebf1fc32..3c307d86ac7bc 100644 --- a/x-pack/plugins/cases/public/containers/use_update_comment.tsx +++ b/x-pack/plugins/cases/public/containers/use_update_comment.tsx @@ -7,6 +7,7 @@ import { useReducer, useCallback, useRef, useEffect } from 'react'; import { useToasts } from '../common/lib/kibana'; +import { useOwnerContext } from '../components/owner_context/use_owner_context'; import { patchComment } from './api'; import * as i18n from './translations'; import { Case } from './types'; @@ -72,6 +73,9 @@ export const useUpdateComment = (): UseUpdateComment => { const toasts = useToasts(); const isCancelledRef = useRef(false); const abortCtrlRef = useRef(new AbortController()); + // this hook guarantees that there will be at least one value in the owner array, we'll + // just use the first entry just in case there are more than one entry + const owner = useOwnerContext()[0]; const dispatchUpdateComment = useCallback( async ({ @@ -89,14 +93,15 @@ export const useUpdateComment = (): UseUpdateComment => { abortCtrlRef.current = new AbortController(); dispatch({ type: 'FETCH_INIT', payload: commentId }); - const response = await patchComment( + const response = await patchComment({ caseId, commentId, commentUpdate, version, - abortCtrlRef.current.signal, - subCaseId - ); + signal: abortCtrlRef.current.signal, + subCaseId, + owner, + }); if (!isCancelledRef.current) { updateCase(response); diff --git a/x-pack/plugins/cases/server/client/cases/get.ts b/x-pack/plugins/cases/server/client/cases/get.ts index 3df1891391c75..4f8713704361b 100644 --- a/x-pack/plugins/cases/server/client/cases/get.ts +++ b/x-pack/plugins/cases/server/client/cases/get.ts @@ -173,7 +173,6 @@ export const get = async ( let theCase: SavedObject<ESCaseAttributes>; let subCaseIds: string[] = []; - if (ENABLE_CASE_CONNECTOR) { const [caseInfo, subCasesForCaseId] = await Promise.all([ caseService.getCase({ diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 6b4f038871626..9e2066984a9da 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -110,20 +110,24 @@ export const push = async ( alertsInfo, }); - const connectorMappings = await casesClientInternal.configuration.getMappings({ + const getMappingsResponse = await casesClientInternal.configuration.getMappings({ connector: theCase.connector, }); - if (connectorMappings.length === 0) { - throw new Error('Connector mapping has not been created'); - } + const mappings = + getMappingsResponse.length === 0 + ? await casesClientInternal.configuration.createMappings({ + connector: theCase.connector, + owner: theCase.owner, + }) + : getMappingsResponse[0].attributes.mappings; const externalServiceIncident = await createIncident({ actionsClient, theCase, userActions, connector: connector as ActionConnector, - mappings: connectorMappings[0].attributes.mappings, + mappings, alerts, casesConnectors, }); diff --git a/x-pack/plugins/cases/server/client/cases/utils.ts b/x-pack/plugins/cases/server/client/cases/utils.ts index d920c517a0004..f5a10d705e095 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.ts @@ -252,6 +252,7 @@ export const prepareFieldsForTransformation = ({ mappings.reduce( (acc: PipedField[], mapping) => mapping != null && + mapping.target != null && mapping.target !== 'not_mapped' && mapping.action_type !== 'nothing' && mapping.source !== 'comments' diff --git a/x-pack/plugins/cases/server/connectors/case/index.test.ts b/x-pack/plugins/cases/server/connectors/case/index.test.ts index 7b8f57bf0d3bf..51c45bd25444e 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.test.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.test.ts @@ -60,7 +60,7 @@ describe('case connector', () => { connector: { id: 'jira', name: 'Jira', - type: '.jira', + type: ConnectorTypes.jira, fields: { issueType: '10006', priority: 'High', @@ -99,7 +99,7 @@ describe('case connector', () => { connector: { id: 'jira', name: 'Jira', - type: '.jira', + type: ConnectorTypes.jira, fields: { issueType: '10006', priority: 'High', @@ -293,7 +293,7 @@ describe('case connector', () => { connector: { id: 'jira', name: 'Jira', - type: '.jira', + type: ConnectorTypes.jira, fields: { priority: 'High', parent: null, @@ -438,7 +438,7 @@ describe('case connector', () => { connector: { id: 'jira', name: 'Jira', - type: '.jira', + type: ConnectorTypes.jira, fields: { issueType: '10006', priority: 'High', @@ -640,7 +640,7 @@ describe('case connector', () => { connector: { id: 'jira', name: 'Jira', - type: '.jira', + type: ConnectorTypes.jira, fields: { priority: 'High', parent: null, @@ -974,7 +974,7 @@ describe('case connector', () => { connector: { id: 'jira', name: 'Jira', - type: '.jira', + type: ConnectorTypes.jira, fields: { issueType: '10006', priority: 'High', @@ -1003,7 +1003,7 @@ describe('case connector', () => { connector: { id: 'jira', name: 'Jira', - type: '.jira', + type: ConnectorTypes.jira, fields: { issueType: '10006', priority: 'High', diff --git a/x-pack/plugins/cases/server/connectors/case/schema.ts b/x-pack/plugins/cases/server/connectors/case/schema.ts index 596a5a4aae45e..79d3bf62e8a9e 100644 --- a/x-pack/plugins/cases/server/connectors/case/schema.ts +++ b/x-pack/plugins/cases/server/connectors/case/schema.ts @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { CommentType } from '../../../common'; +import { CommentType, ConnectorTypes } from '../../../common'; import { validateConnector } from './validators'; // Reserved for future implementation @@ -77,23 +77,29 @@ const ServiceNowSIRFieldsSchema = schema.object({ subcategory: schema.nullable(schema.string()), }); +const SwimlaneFieldsSchema = schema.object({ + caseId: schema.nullable(schema.string()), +}); + const NoneFieldsSchema = schema.nullable(schema.object({})); const ReducedConnectorFieldsSchema: { [x: string]: any } = { - '.jira': JiraFieldsSchema, - '.resilient': ResilientFieldsSchema, - '.servicenow-sir': ServiceNowSIRFieldsSchema, + [ConnectorTypes.jira]: JiraFieldsSchema, + [ConnectorTypes.resilient]: ResilientFieldsSchema, + [ConnectorTypes.serviceNowSIR]: ServiceNowSIRFieldsSchema, + [ConnectorTypes.swimlane]: SwimlaneFieldsSchema, }; export const ConnectorProps = { id: schema.string(), name: schema.string(), type: schema.oneOf([ - schema.literal('.servicenow'), - schema.literal('.jira'), - schema.literal('.resilient'), - schema.literal('.servicenow-sir'), - schema.literal('.none'), + schema.literal(ConnectorTypes.jira), + schema.literal(ConnectorTypes.none), + schema.literal(ConnectorTypes.resilient), + schema.literal(ConnectorTypes.serviceNowITSM), + schema.literal(ConnectorTypes.serviceNowSIR), + schema.literal(ConnectorTypes.swimlane), ]), // Chain of conditional schemes fields: Object.keys(ReducedConnectorFieldsSchema).reduce( @@ -106,7 +112,7 @@ export const ConnectorProps = { ), schema.conditional( schema.siblingRef('type'), - '.servicenow', + ConnectorTypes.serviceNowITSM, ServiceNowITSMFieldsSchema, NoneFieldsSchema ) diff --git a/x-pack/plugins/cases/server/connectors/case/validators.ts b/x-pack/plugins/cases/server/connectors/case/validators.ts index 03110d15c9d3f..6ab4f3a21a24f 100644 --- a/x-pack/plugins/cases/server/connectors/case/validators.ts +++ b/x-pack/plugins/cases/server/connectors/case/validators.ts @@ -6,9 +6,10 @@ */ import { Connector } from './types'; +import { ConnectorTypes } from '../../../common'; export const validateConnector = (connector: Connector) => { - if (connector.type === '.none' && connector.fields !== null) { + if (connector.type === ConnectorTypes.none && connector.fields !== null) { return 'Fields must be set to null for connectors of type .none'; } }; diff --git a/x-pack/plugins/cases/server/connectors/factory.ts b/x-pack/plugins/cases/server/connectors/factory.ts index 5ed7eb4ade4ca..d0ae7154fe5d9 100644 --- a/x-pack/plugins/cases/server/connectors/factory.ts +++ b/x-pack/plugins/cases/server/connectors/factory.ts @@ -6,16 +6,18 @@ */ import { ConnectorTypes } from '../../common'; +import { ICasesConnector, CasesConnectorsMap } from './types'; import { getCaseConnector as getJiraCaseConnector } from './jira'; import { getCaseConnector as getResilientCaseConnector } from './resilient'; import { getServiceNowITSMCaseConnector, getServiceNowSIRCaseConnector } from './servicenow'; -import { ICasesConnector, CasesConnectorsMap } from './types'; +import { getCaseConnector as getSwimlaneCaseConnector } from './swimlane'; const mapping: Record<ConnectorTypes, ICasesConnector | null> = { [ConnectorTypes.jira]: getJiraCaseConnector(), [ConnectorTypes.serviceNowITSM]: getServiceNowITSMCaseConnector(), [ConnectorTypes.serviceNowSIR]: getServiceNowSIRCaseConnector(), [ConnectorTypes.resilient]: getResilientCaseConnector(), + [ConnectorTypes.swimlane]: getSwimlaneCaseConnector(), [ConnectorTypes.none]: null, }; diff --git a/x-pack/plugins/cases/server/connectors/swimlane/format.test.ts b/x-pack/plugins/cases/server/connectors/swimlane/format.test.ts new file mode 100644 index 0000000000000..55cbbdb68691e --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/swimlane/format.test.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 { CaseResponse } from '../../../common'; +import { format } from './format'; + +describe('Swimlane formatter', () => { + const theCase = { + id: 'case-id', + connector: { fields: null }, + } as CaseResponse; + + it('it formats correctly', async () => { + const res = await format(theCase, []); + expect(res).toEqual({ caseId: theCase.id }); + }); +}); diff --git a/x-pack/plugins/cases/server/connectors/swimlane/format.ts b/x-pack/plugins/cases/server/connectors/swimlane/format.ts new file mode 100644 index 0000000000000..9531e4099a4f4 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/swimlane/format.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. + */ + +import { ConnectorSwimlaneTypeFields } from '../../../common'; +import { Format } from './types'; + +export const format: Format = (theCase) => { + const { caseId = theCase.id } = + (theCase.connector.fields as ConnectorSwimlaneTypeFields['fields']) ?? {}; + return { caseId }; +}; diff --git a/x-pack/plugins/cases/server/connectors/swimlane/index.ts b/x-pack/plugins/cases/server/connectors/swimlane/index.ts new file mode 100644 index 0000000000000..2cad92391bdec --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/swimlane/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. + */ + +import { getMapping } from './mapping'; +import { format } from './format'; +import { SwimlaneCaseConnector } from './types'; + +export const getCaseConnector = (): SwimlaneCaseConnector => ({ + getMapping, + format, +}); diff --git a/x-pack/plugins/cases/server/connectors/swimlane/mapping.ts b/x-pack/plugins/cases/server/connectors/swimlane/mapping.ts new file mode 100644 index 0000000000000..e1e34054463e5 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/swimlane/mapping.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { GetMapping } from './types'; + +export const getMapping: GetMapping = () => { + return [ + { + source: 'title', + target: 'caseName', + action_type: 'overwrite', + }, + { + source: 'description', + target: 'description', + action_type: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + action_type: 'append', + }, + ]; +}; diff --git a/x-pack/plugins/cases/server/connectors/swimlane/types.ts b/x-pack/plugins/cases/server/connectors/swimlane/types.ts new file mode 100644 index 0000000000000..22a1e9f6372d5 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/swimlane/types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SwimlaneFieldsType } from '../../../common/api'; +import { ICasesConnector } from '../types'; + +export type SwimlaneCaseConnector = ICasesConnector<SwimlaneFieldsType>; +export type Format = ICasesConnector<SwimlaneFieldsType>['format']; +export type GetMapping = ICasesConnector<SwimlaneFieldsType>['getMapping']; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/__mocks__/index.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/__mocks__/index.tsx index ee43a3c1a21e2..d52ca5b45613a 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/__mocks__/index.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/__mocks__/index.tsx @@ -6,7 +6,7 @@ */ import React, { ReactNode } from 'react'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { UrlGeneratorsStart } from '../../../../../../../src/plugins/share/public/url_generators'; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx index 86f5564a17d52..59da0f0f4d17e 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx @@ -27,6 +27,7 @@ describe('Background Search Session management status labels', () => { id: 'wtywp9u2802hahgp-gsla', restoreUrl: '/app/great-app-url/#45', reloadUrl: '/app/great-app-url/#45', + numSearches: 1, appId: 'security', status: SearchSessionStatus.IN_PROGRESS, created: '2020-12-02T00:19:32Z', diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx index 42ff270ed44a0..6dfe3a5153670 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx @@ -70,6 +70,7 @@ describe('Background Search Session Management Table', () => { status: SearchSessionStatus.IN_PROGRESS, created: '2020-12-02T00:19:32Z', expires: '2020-12-07T00:19:32Z', + idMapping: {}, }, }, ], @@ -95,10 +96,12 @@ describe('Background Search Session Management Table', () => { ); }); - expect(table.find('thead th').map((node) => node.text())).toMatchInlineSnapshot(` + expect(table.find('thead th .euiTableCellContent__text').map((node) => node.text())) + .toMatchInlineSnapshot(` Array [ "App", "Name", + "# Searches", "Status", "Created", "Expiration", @@ -130,6 +133,7 @@ describe('Background Search Session Management Table', () => { Array [ "App", "Namevery background search ", + "# Searches0", "StatusExpired", "Created2 Dec, 2020, 00:19:32", "Expiration--", diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts index 3857b08ad0a3a..cc79f8002a98c 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts @@ -52,6 +52,7 @@ describe('Search Sessions Management API', () => { status: 'complete', initialState: {}, restoreState: {}, + idMapping: [], }, }, ], @@ -78,6 +79,7 @@ describe('Search Sessions Management API', () => { "id": "hello-pizza-123", "initialState": Object {}, "name": "Veggie", + "numSearches": 0, "reloadUrl": "hello-cool-undefined-url", "restoreState": Object {}, "restoreUrl": "hello-cool-undefined-url", @@ -100,6 +102,7 @@ describe('Search Sessions Management API', () => { expires: moment().subtract(3, 'days'), initialState: {}, restoreState: {}, + idMapping: {}, }, }, ], diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts index 3710dfa16e76b..0369dc4a839b5 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts @@ -90,6 +90,7 @@ const mapToUISession = (urls: UrlGeneratorsStart, config: SessionsConfigSchema) urlGeneratorId, initialState, restoreState, + idMapping, } = savedObject.attributes; const status = getUIStatus(savedObject.attributes); @@ -113,6 +114,7 @@ const mapToUISession = (urls: UrlGeneratorsStart, config: SessionsConfigSchema) reloadUrl, initialState, restoreState, + numSearches: Object.keys(idMapping).length, }; }; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx index 4b68e0c9e2afd..fc4e67360ea4a 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx @@ -70,6 +70,7 @@ describe('Search Sessions Management table column factory', () => { reloadUrl: '/app/great-app-url', restoreUrl: '/app/great-app-url/#42', appId: 'discovery', + numSearches: 3, status: SearchSessionStatus.IN_PROGRESS, created: '2020-12-02T00:19:32Z', expires: '2020-12-07T00:19:32Z', @@ -95,6 +96,12 @@ describe('Search Sessions Management table column factory', () => { "sortable": true, "width": "20%", }, + Object { + "field": "numSearches", + "name": "# Searches", + "render": [Function], + "sortable": true, + }, Object { "field": "status", "name": "Status", @@ -146,10 +153,29 @@ describe('Search Sessions Management table column factory', () => { }); }); + // Num of searches column + describe('num of searches', () => { + test('renders', () => { + const [, , numOfSearches] = getColumns( + mockCoreStart, + mockPluginsSetup, + api, + mockConfig, + tz, + handleAction + ) as Array<EuiTableFieldDataColumnType<UISession>>; + + const numOfSearchesLine = mount( + numOfSearches.render!(mockSession.numSearches, mockSession) as ReactElement + ); + expect(numOfSearchesLine.text()).toMatchInlineSnapshot(`"3"`); + }); + }); + // Status column describe('status', () => { test('render in_progress', () => { - const [, , status] = getColumns( + const [, , , status] = getColumns( mockCoreStart, mockPluginsSetup, api, @@ -165,7 +191,7 @@ describe('Search Sessions Management table column factory', () => { }); test('error handling', () => { - const [, , status] = getColumns( + const [, , , status] = getColumns( mockCoreStart, mockPluginsSetup, api, @@ -188,7 +214,7 @@ describe('Search Sessions Management table column factory', () => { test('render using Browser timezone', () => { tz = 'Browser'; - const [, , , createdDateCol] = getColumns( + const [, , , , createdDateCol] = getColumns( mockCoreStart, mockPluginsSetup, api, @@ -205,7 +231,7 @@ describe('Search Sessions Management table column factory', () => { test('render using AK timezone', () => { tz = 'US/Alaska'; - const [, , , createdDateCol] = getColumns( + const [, , , , createdDateCol] = getColumns( mockCoreStart, mockPluginsSetup, api, @@ -220,7 +246,7 @@ describe('Search Sessions Management table column factory', () => { }); test('error handling', () => { - const [, , , createdDateCol] = getColumns( + const [, , , , createdDateCol] = getColumns( mockCoreStart, mockPluginsSetup, api, diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx index 1805ef52b85f1..7dd4966124e96 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx @@ -16,10 +16,10 @@ import { EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { CoreStart } from 'kibana/public'; import { capitalize } from 'lodash'; import React from 'react'; -import { FormattedMessage } from 'react-intl'; import { RedirectAppLinks } from '../../../../../../../src/plugins/kibana_react/public'; import { IManagementSectionsPluginsSetup, SessionsConfigSchema } from '../'; import { SearchSessionStatus } from '../../../../../../../src/plugins/data/common'; @@ -120,6 +120,20 @@ export const getColumns = ( }, }, + // # Searches + { + field: 'numSearches', + name: i18n.translate('xpack.data.mgmt.searchSessions.table.numSearches', { + defaultMessage: '# Searches', + }), + sortable: true, + render: (numSearches: UISession['numSearches'], session) => ( + <TableText color="subdued" data-test-subj="sessionManagementNumSearchesCol"> + {numSearches} + </TableText> + ), + }, + // Session status { field: 'status', diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts index d0d5ee9fb17dd..6a8ace8dbdc79 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts @@ -34,6 +34,7 @@ export interface UISession { created: string; expires: string | null; status: UISearchSessionState; + numSearches: number; actions?: ACTION[]; reloadUrl: string; restoreUrl: string; diff --git a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx index a16557b50700e..893f352b5d828 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/connected_search_session_indicator/connected_search_session_indicator.test.tsx @@ -22,7 +22,7 @@ import { import { coreMock } from '../../../../../../../src/core/public/mocks'; import { TOUR_RESTORE_STEP_KEY, TOUR_TAKING_TOO_LONG_STEP_KEY } from './search_session_tour'; import userEvent from '@testing-library/user-event'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { createSearchUsageCollectorMock } from '../../../../../../../src/plugins/data/public/search/collectors/mocks'; const coreStart = coreMock.createStart(); diff --git a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.test.tsx b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.test.tsx index ff9e27cad1869..310379f90c789 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.test.tsx @@ -9,7 +9,7 @@ import React, { ReactNode } from 'react'; import { screen, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { SearchSessionIndicator } from './search_session_indicator'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { SearchSessionState } from '../../../../../../../src/plugins/data/public'; function Container({ children }: { children?: ReactNode }) { diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts b/x-pack/plugins/data_enhanced/server/search/session/check_non_persiseted_sessions.test.ts similarity index 65% rename from x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts rename to x-pack/plugins/data_enhanced/server/search/session/check_non_persiseted_sessions.test.ts index c0a48d5d44862..0a80f1c06998f 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/check_non_persiseted_sessions.test.ts @@ -5,10 +5,7 @@ * 2.0. */ -import { - checkRunningSessions as checkRunningSessions$, - CheckRunningSessionsDeps, -} from './check_running_sessions'; +import { checkNonPersistedSessions as checkNonPersistedSessions$ } from './check_non_persiseted_sessions'; import { SearchSessionStatus, SearchSessionSavedObjectAttributes, @@ -16,22 +13,20 @@ import { EQL_SEARCH_STRATEGY, } from '../../../../../../src/plugins/data/common'; import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; -import { SearchSessionsConfig, SearchStatus } from './types'; +import { SearchSessionsConfig, CheckSearchSessionsDeps, SearchStatus } from './types'; import moment from 'moment'; import { SavedObjectsBulkUpdateObject, SavedObjectsDeleteOptions, SavedObjectsClientContract, } from '../../../../../../src/core/server'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; jest.useFakeTimers(); -const checkRunningSessions = (deps: CheckRunningSessionsDeps, config: SearchSessionsConfig) => - checkRunningSessions$(deps, config).toPromise(); +const checkNonPersistedSessions = (deps: CheckSearchSessionsDeps, config: SearchSessionsConfig) => + checkNonPersistedSessions$(deps, config).toPromise(); -describe('getSearchStatus', () => { +describe('checkNonPersistedSessions', () => { let mockClient: any; let savedObjectsClient: jest.Mocked<SavedObjectsClientContract>; const config: SearchSessionsConfig = { @@ -42,7 +37,9 @@ describe('getSearchStatus', () => { maxUpdateRetries: 3, defaultExpiration: moment.duration(7, 'd'), trackingInterval: moment.duration(10, 's'), + expireInterval: moment.duration(10, 'm'), monitoringTaskTimeout: moment.duration(5, 'm'), + cleanupInterval: moment.duration(10, 's'), management: {} as any, }; const mockLogger: any = { @@ -51,16 +48,6 @@ describe('getSearchStatus', () => { error: jest.fn(), }; - const emptySO = { - attributes: { - persisted: false, - status: SearchSessionStatus.IN_PROGRESS, - created: moment().subtract(moment.duration(3, 'm')), - touched: moment().subtract(moment.duration(10, 's')), - idMapping: {}, - }, - }; - beforeEach(() => { savedObjectsClient = savedObjectsClientMock.create(); mockClient = { @@ -81,7 +68,7 @@ describe('getSearchStatus', () => { total: 0, } as any); - await checkRunningSessions( + await checkNonPersistedSessions( { savedObjectsClient, client: mockClient, @@ -94,240 +81,7 @@ describe('getSearchStatus', () => { expect(savedObjectsClient.delete).not.toBeCalled(); }); - describe('pagination', () => { - test('fetches one page if not objects exist', async () => { - savedObjectsClient.find.mockResolvedValueOnce({ - saved_objects: [], - total: 0, - } as any); - - await checkRunningSessions( - { - savedObjectsClient, - client: mockClient, - logger: mockLogger, - }, - config - ); - - expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); - }); - - test('fetches one page if less than page size object are returned', async () => { - savedObjectsClient.find.mockResolvedValueOnce({ - saved_objects: [emptySO, emptySO], - total: 5, - } as any); - - await checkRunningSessions( - { - savedObjectsClient, - client: mockClient, - logger: mockLogger, - }, - config - ); - - expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); - }); - - test('fetches two pages if exactly page size objects are returned', async () => { - let i = 0; - savedObjectsClient.find.mockImplementation(() => { - return new Promise((resolve) => { - resolve({ - saved_objects: i++ === 0 ? [emptySO, emptySO, emptySO, emptySO, emptySO] : [], - total: 5, - page: i, - } as any); - }); - }); - - await checkRunningSessions( - { - savedObjectsClient, - client: mockClient, - logger: mockLogger, - }, - config - ); - - expect(savedObjectsClient.find).toHaveBeenCalledTimes(2); - - // validate that page number increases - const { page: page1 } = savedObjectsClient.find.mock.calls[0][0]; - const { page: page2 } = savedObjectsClient.find.mock.calls[1][0]; - expect(page1).toBe(1); - expect(page2).toBe(2); - }); - - test('fetches two pages if page size +1 objects are returned', async () => { - let i = 0; - savedObjectsClient.find.mockImplementation(() => { - return new Promise((resolve) => { - resolve({ - saved_objects: i++ === 0 ? [emptySO, emptySO, emptySO, emptySO, emptySO] : [emptySO], - total: 5, - page: i, - } as any); - }); - }); - - await checkRunningSessions( - { - savedObjectsClient, - client: mockClient, - logger: mockLogger, - }, - config - ); - - expect(savedObjectsClient.find).toHaveBeenCalledTimes(2); - }); - - test('fetching is abortable', async () => { - let i = 0; - const abort$ = new Subject(); - savedObjectsClient.find.mockImplementation(() => { - return new Promise((resolve) => { - if (++i === 2) { - abort$.next(); - } - resolve({ - saved_objects: i <= 5 ? [emptySO, emptySO, emptySO, emptySO, emptySO] : [], - total: 25, - page: i, - } as any); - }); - }); - - await checkRunningSessions$( - { - savedObjectsClient, - client: mockClient, - logger: mockLogger, - }, - config - ) - .pipe(takeUntil(abort$)) - .toPromise(); - - jest.runAllTimers(); - - // if not for `abort$` then this would be called 6 times! - expect(savedObjectsClient.find).toHaveBeenCalledTimes(2); - }); - - test('sorting is by "touched"', async () => { - savedObjectsClient.find.mockResolvedValueOnce({ - saved_objects: [], - total: 0, - } as any); - - await checkRunningSessions( - { - savedObjectsClient, - client: mockClient, - logger: mockLogger, - }, - config - ); - - expect(savedObjectsClient.find).toHaveBeenCalledWith( - expect.objectContaining({ sortField: 'touched', sortOrder: 'asc' }) - ); - }); - - test('sessions fetched in the beginning are processed even if sessions in the end fail', async () => { - let i = 0; - savedObjectsClient.find.mockImplementation(() => { - return new Promise((resolve, reject) => { - if (++i === 2) { - reject(new Error('Fake find error...')); - } - resolve({ - saved_objects: - i <= 5 - ? [ - i === 1 - ? { - id: '123', - attributes: { - persisted: false, - status: SearchSessionStatus.IN_PROGRESS, - created: moment().subtract(moment.duration(3, 'm')), - touched: moment().subtract(moment.duration(2, 'm')), - idMapping: { - 'map-key': { - strategy: ENHANCED_ES_SEARCH_STRATEGY, - id: 'async-id', - }, - }, - }, - } - : emptySO, - emptySO, - emptySO, - emptySO, - emptySO, - ] - : [], - total: 25, - page: i, - } as any); - }); - }); - - await checkRunningSessions$( - { - savedObjectsClient, - client: mockClient, - logger: mockLogger, - }, - config - ).toPromise(); - - jest.runAllTimers(); - - expect(savedObjectsClient.find).toHaveBeenCalledTimes(2); - - // by checking that delete was called we validate that sessions from session that were successfully fetched were processed - expect(mockClient.asyncSearch.delete).toBeCalled(); - const { id } = mockClient.asyncSearch.delete.mock.calls[0][0]; - expect(id).toBe('async-id'); - }); - }); - describe('delete', () => { - test('doesnt delete a persisted session', async () => { - savedObjectsClient.find.mockResolvedValue({ - saved_objects: [ - { - id: '123', - attributes: { - persisted: true, - status: SearchSessionStatus.IN_PROGRESS, - created: moment().subtract(moment.duration(30, 'm')), - touched: moment().subtract(moment.duration(10, 'm')), - idMapping: {}, - }, - }, - ], - total: 1, - } as any); - await checkRunningSessions( - { - savedObjectsClient, - client: mockClient, - logger: mockLogger, - }, - config - ); - - expect(savedObjectsClient.bulkUpdate).not.toBeCalled(); - expect(savedObjectsClient.delete).not.toBeCalled(); - }); - test('doesnt delete a non persisted, recently touched session', async () => { savedObjectsClient.find.mockResolvedValue({ saved_objects: [ @@ -336,6 +90,7 @@ describe('getSearchStatus', () => { attributes: { persisted: false, status: SearchSessionStatus.IN_PROGRESS, + expires: moment().add(moment.duration(3, 'm')), created: moment().subtract(moment.duration(3, 'm')), touched: moment().subtract(moment.duration(10, 's')), idMapping: {}, @@ -344,7 +99,7 @@ describe('getSearchStatus', () => { ], total: 1, } as any); - await checkRunningSessions( + await checkNonPersistedSessions( { savedObjectsClient, client: mockClient, @@ -367,6 +122,7 @@ describe('getSearchStatus', () => { status: SearchSessionStatus.COMPLETE, created: moment().subtract(moment.duration(3, 'm')), touched: moment().subtract(moment.duration(1, 'm')), + expires: moment().add(moment.duration(3, 'm')), idMapping: { 'search-hash': { id: 'search-id', @@ -379,7 +135,7 @@ describe('getSearchStatus', () => { ], total: 1, } as any); - await checkRunningSessions( + await checkNonPersistedSessions( { savedObjectsClient, client: mockClient, @@ -401,6 +157,7 @@ describe('getSearchStatus', () => { attributes: { persisted: false, status: SearchSessionStatus.IN_PROGRESS, + expires: moment().add(moment.duration(3, 'm')), created: moment().subtract(moment.duration(3, 'm')), touched: moment().subtract(moment.duration(2, 'm')), idMapping: { @@ -415,7 +172,7 @@ describe('getSearchStatus', () => { total: 1, } as any); - await checkRunningSessions( + await checkNonPersistedSessions( { savedObjectsClient, client: mockClient, @@ -441,6 +198,7 @@ describe('getSearchStatus', () => { status: SearchSessionStatus.IN_PROGRESS, created: moment().subtract(moment.duration(3, 'm')), touched: moment().subtract(moment.duration(2, 'm')), + expires: moment().add(moment.duration(3, 'm')), idMapping: { 'map-key': { strategy: ENHANCED_ES_SEARCH_STRATEGY, @@ -453,7 +211,7 @@ describe('getSearchStatus', () => { total: 1, } as any); - await checkRunningSessions( + await checkNonPersistedSessions( { savedObjectsClient, client: mockClient, @@ -481,6 +239,7 @@ describe('getSearchStatus', () => { attributes: { persisted: false, status: SearchSessionStatus.COMPLETE, + expires: moment().add(moment.duration(3, 'm')), created: moment().subtract(moment.duration(30, 'm')), touched: moment().subtract(moment.duration(6, 'm')), idMapping: { @@ -501,7 +260,7 @@ describe('getSearchStatus', () => { total: 1, } as any); - await checkRunningSessions( + await checkNonPersistedSessions( { savedObjectsClient, client: mockClient, @@ -530,6 +289,7 @@ describe('getSearchStatus', () => { attributes: { persisted: false, status: SearchSessionStatus.COMPLETE, + expires: moment().add(moment.duration(3, 'm')), created: moment().subtract(moment.duration(30, 'm')), touched: moment().subtract(moment.duration(6, 'm')), idMapping: { @@ -545,7 +305,7 @@ describe('getSearchStatus', () => { total: 1, } as any); - await checkRunningSessions( + await checkNonPersistedSessions( { savedObjectsClient, client: mockClient, @@ -573,6 +333,7 @@ describe('getSearchStatus', () => { status: SearchSessionStatus.IN_PROGRESS, created: moment().subtract(moment.duration(3, 'm')), touched: moment().subtract(moment.duration(10, 's')), + expires: moment().add(moment.duration(3, 'm')), idMapping: { 'search-hash': { id: 'search-id', @@ -594,7 +355,7 @@ describe('getSearchStatus', () => { }, }); - await checkRunningSessions( + await checkNonPersistedSessions( { savedObjectsClient, client: mockClient, @@ -614,6 +375,7 @@ describe('getSearchStatus', () => { id: '123', attributes: { status: SearchSessionStatus.ERROR, + expires: moment().add(moment.duration(3, 'm')), idMapping: { 'search-hash': { id: 'search-id', @@ -633,7 +395,7 @@ describe('getSearchStatus', () => { total: 1, } as any); - await checkRunningSessions( + await checkNonPersistedSessions( { savedObjectsClient, client: mockClient, @@ -653,6 +415,7 @@ describe('getSearchStatus', () => { namespaces: ['awesome'], attributes: { status: SearchSessionStatus.IN_PROGRESS, + expires: moment().add(moment.duration(3, 'm')), touched: '123', idMapping: { 'search-hash': { @@ -676,7 +439,7 @@ describe('getSearchStatus', () => { }, }); - await checkRunningSessions( + await checkNonPersistedSessions( { savedObjectsClient, client: mockClient, @@ -696,6 +459,7 @@ describe('getSearchStatus', () => { const so = { attributes: { status: SearchSessionStatus.IN_PROGRESS, + expires: moment().add(moment.duration(3, 'm')), touched: '123', idMapping: { 'search-hash': { @@ -719,7 +483,7 @@ describe('getSearchStatus', () => { }, }); - await checkRunningSessions( + await checkNonPersistedSessions( { savedObjectsClient, client: mockClient, @@ -744,6 +508,7 @@ describe('getSearchStatus', () => { savedObjectsClient.bulkUpdate = jest.fn(); const so = { attributes: { + expires: moment().add(moment.duration(3, 'm')), idMapping: { 'search-hash': { id: 'search-id', @@ -766,7 +531,7 @@ describe('getSearchStatus', () => { }, }); - await checkRunningSessions( + await checkNonPersistedSessions( { savedObjectsClient, client: mockClient, diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_non_persiseted_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/check_non_persiseted_sessions.ts new file mode 100644 index 0000000000000..8c75ce91cac6a --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/check_non_persiseted_sessions.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectsFindResult } from 'kibana/server'; +import moment from 'moment'; +import { EMPTY } from 'rxjs'; +import { catchError, concatMap } from 'rxjs/operators'; +import { + nodeBuilder, + ENHANCED_ES_SEARCH_STRATEGY, + SEARCH_SESSION_TYPE, + SearchSessionSavedObjectAttributes, + SearchSessionStatus, + KueryNode, +} from '../../../../../../src/plugins/data/common'; +import { checkSearchSessionsByPage, getSearchSessionsPage$ } from './get_search_session_page'; +import { SearchSessionsConfig, CheckSearchSessionsDeps } from './types'; +import { bulkUpdateSessions, getAllSessionsStatusUpdates } from './update_session_status'; + +export const SEARCH_SESSIONS_CLEANUP_TASK_TYPE = 'search_sessions_cleanup'; +export const SEARCH_SESSIONS_CLEANUP_TASK_ID = `data_enhanced_${SEARCH_SESSIONS_CLEANUP_TASK_TYPE}`; + +function isSessionStale( + session: SavedObjectsFindResult<SearchSessionSavedObjectAttributes>, + config: SearchSessionsConfig +) { + const curTime = moment(); + // Delete cancelled sessions immediately + if (session.attributes.status === SearchSessionStatus.CANCELLED) return true; + // Delete if a running session wasn't polled for in the last notTouchedInProgressTimeout OR + // if a completed \ errored \ canceled session wasn't saved for within notTouchedTimeout + return ( + (session.attributes.status === SearchSessionStatus.IN_PROGRESS && + curTime.diff(moment(session.attributes.touched), 'ms') > + config.notTouchedInProgressTimeout.asMilliseconds()) || + (session.attributes.status !== SearchSessionStatus.IN_PROGRESS && + curTime.diff(moment(session.attributes.touched), 'ms') > + config.notTouchedTimeout.asMilliseconds()) + ); +} + +function checkNonPersistedSessionsPage( + deps: CheckSearchSessionsDeps, + config: SearchSessionsConfig, + filter: KueryNode, + page: number +) { + const { logger, client, savedObjectsClient } = deps; + logger.debug(`${SEARCH_SESSIONS_CLEANUP_TASK_TYPE} Fetching sessions from page ${page}`); + return getSearchSessionsPage$(deps, filter, config.pageSize, page).pipe( + concatMap(async (nonPersistedSearchSessions) => { + if (!nonPersistedSearchSessions.total) return nonPersistedSearchSessions; + + logger.debug( + `${SEARCH_SESSIONS_CLEANUP_TASK_TYPE} Found ${nonPersistedSearchSessions.total} sessions, processing ${nonPersistedSearchSessions.saved_objects.length}` + ); + + const updatedSessions = await getAllSessionsStatusUpdates(deps, nonPersistedSearchSessions); + const deletedSessionIds: string[] = []; + + await Promise.all( + nonPersistedSearchSessions.saved_objects.map(async (session) => { + if (isSessionStale(session, config)) { + // delete saved object to free up memory + // TODO: there's a potential rare edge case of deleting an object and then receiving a new trackId for that same session! + // Maybe we want to change state to deleted and cleanup later? + logger.debug(`Deleting stale session | ${session.id}`); + try { + deletedSessionIds.push(session.id); + await savedObjectsClient.delete(SEARCH_SESSION_TYPE, session.id, { + namespace: session.namespaces?.[0], + }); + } catch (e) { + logger.error( + `${SEARCH_SESSIONS_CLEANUP_TASK_TYPE} Error while deleting session ${session.id}: ${e.message}` + ); + } + + // Send a delete request for each async search to ES + Object.keys(session.attributes.idMapping).map(async (searchKey: string) => { + const searchInfo = session.attributes.idMapping[searchKey]; + if (searchInfo.strategy === ENHANCED_ES_SEARCH_STRATEGY) { + try { + await client.asyncSearch.delete({ id: searchInfo.id }); + } catch (e) { + if (e.message !== 'resource_not_found_exception') { + logger.error( + `${SEARCH_SESSIONS_CLEANUP_TASK_TYPE} Error while deleting async_search ${searchInfo.id}: ${e.message}` + ); + } + } + } + }); + } + }) + ); + + const nonDeletedSessions = updatedSessions.filter((updateSession) => { + return deletedSessionIds.indexOf(updateSession.id) === -1; + }); + + await bulkUpdateSessions(deps, nonDeletedSessions); + + return nonPersistedSearchSessions; + }) + ); +} + +export function checkNonPersistedSessions( + deps: CheckSearchSessionsDeps, + config: SearchSessionsConfig +) { + const { logger } = deps; + + const filters = nodeBuilder.is(`${SEARCH_SESSION_TYPE}.attributes.persisted`, 'false'); + + return checkSearchSessionsByPage(checkNonPersistedSessionsPage, deps, config, filters).pipe( + catchError((e) => { + logger.error( + `${SEARCH_SESSIONS_CLEANUP_TASK_TYPE} Error while processing sessions: ${e?.message}` + ); + return EMPTY; + }) + ); +} diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.test.ts b/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.test.ts new file mode 100644 index 0000000000000..e0b1b74b57d02 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.test.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 { checkPersistedSessionsProgress } from './check_persisted_sessions'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { SearchSessionsConfig } from './types'; +import moment from 'moment'; +import { SavedObjectsClientContract } from '../../../../../../src/core/server'; + +describe('checkPersistedSessionsProgress', () => { + let mockClient: any; + let savedObjectsClient: jest.Mocked<SavedObjectsClientContract>; + const config: SearchSessionsConfig = { + enabled: true, + pageSize: 5, + notTouchedInProgressTimeout: moment.duration(1, 'm'), + notTouchedTimeout: moment.duration(5, 'm'), + maxUpdateRetries: 3, + defaultExpiration: moment.duration(7, 'd'), + trackingInterval: moment.duration(10, 's'), + cleanupInterval: moment.duration(10, 's'), + expireInterval: moment.duration(10, 'm'), + monitoringTaskTimeout: moment.duration(5, 'm'), + management: {} as any, + }; + const mockLogger: any = { + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + + beforeEach(() => { + savedObjectsClient = savedObjectsClientMock.create(); + mockClient = { + asyncSearch: { + status: jest.fn(), + delete: jest.fn(), + }, + eql: { + status: jest.fn(), + delete: jest.fn(), + }, + }; + }); + + test('fetches only running persisted sessions', async () => { + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [], + total: 0, + } as any); + + await checkPersistedSessionsProgress( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + config + ); + + const [findInput] = savedObjectsClient.find.mock.calls[0]; + + expect(findInput.filter.arguments[0].arguments[0].value).toBe( + 'search-session.attributes.persisted' + ); + expect(findInput.filter.arguments[0].arguments[1].value).toBe('true'); + expect(findInput.filter.arguments[1].arguments[0].value).toBe( + 'search-session.attributes.status' + ); + expect(findInput.filter.arguments[1].arguments[1].value).toBe('in_progress'); + }); +}); diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.ts new file mode 100644 index 0000000000000..0d51e97952275 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/check_persisted_sessions.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 { EMPTY, Observable } from 'rxjs'; +import { catchError, concatMap } from 'rxjs/operators'; +import { + nodeBuilder, + SEARCH_SESSION_TYPE, + SearchSessionStatus, + KueryNode, +} from '../../../../../../src/plugins/data/common'; +import { checkSearchSessionsByPage, getSearchSessionsPage$ } from './get_search_session_page'; +import { SearchSessionsConfig, CheckSearchSessionsDeps, SearchSessionsResponse } from './types'; +import { bulkUpdateSessions, getAllSessionsStatusUpdates } from './update_session_status'; + +export const SEARCH_SESSIONS_TASK_TYPE = 'search_sessions_monitor'; +export const SEARCH_SESSIONS_TASK_ID = `data_enhanced_${SEARCH_SESSIONS_TASK_TYPE}`; + +function checkPersistedSessionsPage( + deps: CheckSearchSessionsDeps, + config: SearchSessionsConfig, + filter: KueryNode, + page: number +): Observable<SearchSessionsResponse> { + const { logger } = deps; + logger.debug(`${SEARCH_SESSIONS_TASK_TYPE} Fetching sessions from page ${page}`); + return getSearchSessionsPage$(deps, filter, config.pageSize, page).pipe( + concatMap(async (persistedSearchSessions) => { + if (!persistedSearchSessions.total) return persistedSearchSessions; + + logger.debug( + `${SEARCH_SESSIONS_TASK_TYPE} Found ${persistedSearchSessions.total} sessions, processing ${persistedSearchSessions.saved_objects.length}` + ); + + const updatedSessions = await getAllSessionsStatusUpdates(deps, persistedSearchSessions); + await bulkUpdateSessions(deps, updatedSessions); + + return persistedSearchSessions; + }) + ); +} + +export function checkPersistedSessionsProgress( + deps: CheckSearchSessionsDeps, + config: SearchSessionsConfig +) { + const { logger } = deps; + + const persistedSessionsFilter = nodeBuilder.and([ + nodeBuilder.is(`${SEARCH_SESSION_TYPE}.attributes.persisted`, 'true'), + nodeBuilder.is( + `${SEARCH_SESSION_TYPE}.attributes.status`, + SearchSessionStatus.IN_PROGRESS.toString() + ), + ]); + + return checkSearchSessionsByPage( + checkPersistedSessionsPage, + deps, + config, + persistedSessionsFilter + ).pipe( + catchError((e) => { + logger.error(`${SEARCH_SESSIONS_TASK_TYPE} Error while processing sessions: ${e?.message}`); + return EMPTY; + }) + ); +} diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts deleted file mode 100644 index 6787d31ed2b74..0000000000000 --- a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts +++ /dev/null @@ -1,257 +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 { - ElasticsearchClient, - Logger, - SavedObjectsClientContract, - SavedObjectsFindResult, - SavedObjectsUpdateResponse, -} from 'kibana/server'; -import moment from 'moment'; -import { EMPTY, from, Observable } from 'rxjs'; -import { catchError, concatMap } from 'rxjs/operators'; -import { - nodeBuilder, - ENHANCED_ES_SEARCH_STRATEGY, - SEARCH_SESSION_TYPE, - SearchSessionRequestInfo, - SearchSessionSavedObjectAttributes, - SearchSessionStatus, -} from '../../../../../../src/plugins/data/common'; -import { getSearchStatus } from './get_search_status'; -import { getSessionStatus } from './get_session_status'; -import { SearchSessionsConfig, SearchStatus } from './types'; - -export interface CheckRunningSessionsDeps { - savedObjectsClient: SavedObjectsClientContract; - client: ElasticsearchClient; - logger: Logger; -} - -function isSessionStale( - session: SavedObjectsFindResult<SearchSessionSavedObjectAttributes>, - config: SearchSessionsConfig, - logger: Logger -) { - const curTime = moment(); - // Delete if a running session wasn't polled for in the last notTouchedInProgressTimeout OR - // if a completed \ errored \ canceled session wasn't saved for within notTouchedTimeout - return ( - (session.attributes.status === SearchSessionStatus.IN_PROGRESS && - curTime.diff(moment(session.attributes.touched), 'ms') > - config.notTouchedInProgressTimeout.asMilliseconds()) || - (session.attributes.status !== SearchSessionStatus.IN_PROGRESS && - curTime.diff(moment(session.attributes.touched), 'ms') > - config.notTouchedTimeout.asMilliseconds()) - ); -} - -async function updateSessionStatus( - session: SavedObjectsFindResult<SearchSessionSavedObjectAttributes>, - client: ElasticsearchClient, - logger: Logger -) { - let sessionUpdated = false; - - // Check statuses of all running searches - await Promise.all( - Object.keys(session.attributes.idMapping).map(async (searchKey: string) => { - const updateSearchRequest = ( - currentStatus: Pick<SearchSessionRequestInfo, 'status' | 'error'> - ) => { - sessionUpdated = true; - session.attributes.idMapping[searchKey] = { - ...session.attributes.idMapping[searchKey], - ...currentStatus, - }; - }; - - const searchInfo = session.attributes.idMapping[searchKey]; - if (searchInfo.status === SearchStatus.IN_PROGRESS) { - try { - const currentStatus = await getSearchStatus(client, searchInfo.id); - - if (currentStatus.status !== searchInfo.status) { - logger.debug(`search ${searchInfo.id} | status changed to ${currentStatus.status}`); - updateSearchRequest(currentStatus); - } - } catch (e) { - logger.error(e); - updateSearchRequest({ - status: SearchStatus.ERROR, - error: e.message || e.meta.error?.caused_by?.reason, - }); - } - } - }) - ); - - // And only then derive the session's status - const sessionStatus = getSessionStatus(session.attributes); - if (sessionStatus !== session.attributes.status) { - const now = new Date().toISOString(); - session.attributes.status = sessionStatus; - session.attributes.touched = now; - if (sessionStatus === SearchSessionStatus.COMPLETE) { - session.attributes.completed = now; - } else if (session.attributes.completed) { - session.attributes.completed = null; - } - sessionUpdated = true; - } - - return sessionUpdated; -} - -function getSavedSearchSessionsPage$( - { savedObjectsClient, logger }: CheckRunningSessionsDeps, - config: SearchSessionsConfig, - page: number -) { - logger.debug(`Fetching saved search sessions page ${page}`); - return from( - savedObjectsClient.find<SearchSessionSavedObjectAttributes>({ - page, - perPage: config.pageSize, - type: SEARCH_SESSION_TYPE, - namespaces: ['*'], - // process older sessions first - sortField: 'touched', - sortOrder: 'asc', - filter: nodeBuilder.or([ - nodeBuilder.and([ - nodeBuilder.is( - `${SEARCH_SESSION_TYPE}.attributes.status`, - SearchSessionStatus.IN_PROGRESS.toString() - ), - nodeBuilder.is(`${SEARCH_SESSION_TYPE}.attributes.persisted`, 'true'), - ]), - nodeBuilder.is(`${SEARCH_SESSION_TYPE}.attributes.persisted`, 'false'), - ]), - }) - ); -} - -function checkRunningSessionsPage( - deps: CheckRunningSessionsDeps, - config: SearchSessionsConfig, - page: number -) { - const { logger, client, savedObjectsClient } = deps; - return getSavedSearchSessionsPage$(deps, config, page).pipe( - concatMap(async (runningSearchSessionsResponse) => { - if (!runningSearchSessionsResponse.total) return; - - logger.debug( - `Found ${runningSearchSessionsResponse.total} running sessions, processing ${runningSearchSessionsResponse.saved_objects.length} sessions from page ${page}` - ); - - const updatedSessions = new Array< - SavedObjectsFindResult<SearchSessionSavedObjectAttributes> - >(); - - await Promise.all( - runningSearchSessionsResponse.saved_objects.map(async (session) => { - const updated = await updateSessionStatus(session, client, logger); - let deleted = false; - - if (!session.attributes.persisted) { - if (isSessionStale(session, config, logger)) { - // delete saved object to free up memory - // TODO: there's a potential rare edge case of deleting an object and then receiving a new trackId for that same session! - // Maybe we want to change state to deleted and cleanup later? - logger.debug(`Deleting stale session | ${session.id}`); - try { - await savedObjectsClient.delete(SEARCH_SESSION_TYPE, session.id, { - namespace: session.namespaces?.[0], - }); - deleted = true; - } catch (e) { - logger.error( - `Error while deleting stale search session ${session.id}: ${e.message}` - ); - } - - // Send a delete request for each async search to ES - Object.keys(session.attributes.idMapping).map(async (searchKey: string) => { - const searchInfo = session.attributes.idMapping[searchKey]; - if (searchInfo.strategy === ENHANCED_ES_SEARCH_STRATEGY) { - try { - await client.asyncSearch.delete({ id: searchInfo.id }); - } catch (e) { - logger.error( - `Error while deleting async_search ${searchInfo.id}: ${e.message}` - ); - } - } - }); - } - } - - if (updated && !deleted) { - updatedSessions.push(session); - } - }) - ); - - // Do a bulk update - if (updatedSessions.length) { - // If there's an error, we'll try again in the next iteration, so there's no need to check the output. - const updatedResponse = await savedObjectsClient.bulkUpdate<SearchSessionSavedObjectAttributes>( - updatedSessions.map((session) => ({ - ...session, - namespace: session.namespaces?.[0], - })) - ); - - const success: Array<SavedObjectsUpdateResponse<SearchSessionSavedObjectAttributes>> = []; - const fail: Array<SavedObjectsUpdateResponse<SearchSessionSavedObjectAttributes>> = []; - - updatedResponse.saved_objects.forEach((savedObjectResponse) => { - if ('error' in savedObjectResponse) { - fail.push(savedObjectResponse); - logger.error( - `Error while updating search session ${savedObjectResponse?.id}: ${savedObjectResponse.error?.message}` - ); - } else { - success.push(savedObjectResponse); - } - }); - - logger.debug(`Updating search sessions: success: ${success.length}, fail: ${fail.length}`); - } - - return runningSearchSessionsResponse; - }) - ); -} - -export function checkRunningSessions(deps: CheckRunningSessionsDeps, config: SearchSessionsConfig) { - const { logger } = deps; - - const checkRunningSessionsByPage = (nextPage = 1): Observable<void> => - checkRunningSessionsPage(deps, config, nextPage).pipe( - concatMap((result) => { - if (!result || !result.saved_objects || result.saved_objects.length < config.pageSize) { - return EMPTY; - } else { - // TODO: while processing previous page session list might have been changed and we might skip a session, - // because it would appear now on a different "page". - // This isn't critical, as we would pick it up on a next task iteration, but maybe we could improve this somehow - return checkRunningSessionsByPage(result.page + 1); - } - }) - ); - - return checkRunningSessionsByPage().pipe( - catchError((e) => { - logger.error(`Error while processing search sessions: ${e?.message}`); - return EMPTY; - }) - ); -} diff --git a/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts new file mode 100644 index 0000000000000..e261c324f440f --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/expire_persisted_sessions.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EMPTY, Observable } from 'rxjs'; +import { catchError, concatMap } from 'rxjs/operators'; +import { + nodeBuilder, + SEARCH_SESSION_TYPE, + SearchSessionStatus, + KueryNode, +} from '../../../../../../src/plugins/data/common'; +import { checkSearchSessionsByPage, getSearchSessionsPage$ } from './get_search_session_page'; +import { SearchSessionsConfig, CheckSearchSessionsDeps, SearchSessionsResponse } from './types'; +import { bulkUpdateSessions, getAllSessionsStatusUpdates } from './update_session_status'; + +export const SEARCH_SESSIONS_EXPIRE_TASK_TYPE = 'search_sessions_expire'; +export const SEARCH_SESSIONS_EXPIRE_TASK_ID = `data_enhanced_${SEARCH_SESSIONS_EXPIRE_TASK_TYPE}`; + +function checkSessionExpirationPage( + deps: CheckSearchSessionsDeps, + config: SearchSessionsConfig, + filter: KueryNode, + page: number +): Observable<SearchSessionsResponse> { + const { logger } = deps; + logger.debug(`${SEARCH_SESSIONS_EXPIRE_TASK_TYPE} Fetching sessions from page ${page}`); + return getSearchSessionsPage$(deps, filter, config.pageSize, page).pipe( + concatMap(async (searchSessions) => { + if (!searchSessions.total) return searchSessions; + + logger.debug( + `${SEARCH_SESSIONS_EXPIRE_TASK_TYPE} Found ${searchSessions.total} sessions, processing ${searchSessions.saved_objects.length}` + ); + + const updatedSessions = await getAllSessionsStatusUpdates(deps, searchSessions); + await bulkUpdateSessions(deps, updatedSessions); + + return searchSessions; + }) + ); +} + +export function checkPersistedCompletedSessionExpiration( + deps: CheckSearchSessionsDeps, + config: SearchSessionsConfig +) { + const { logger } = deps; + + const persistedSessionsFilter = nodeBuilder.and([ + nodeBuilder.is(`${SEARCH_SESSION_TYPE}.attributes.persisted`, 'true'), + nodeBuilder.is( + `${SEARCH_SESSION_TYPE}.attributes.status`, + SearchSessionStatus.COMPLETE.toString() + ), + ]); + + return checkSearchSessionsByPage( + checkSessionExpirationPage, + deps, + config, + persistedSessionsFilter + ).pipe( + catchError((e) => { + logger.error( + `${SEARCH_SESSIONS_EXPIRE_TASK_TYPE} Error while processing sessions: ${e?.message}` + ); + return EMPTY; + }) + ); +} diff --git a/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.test.ts b/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.test.ts new file mode 100644 index 0000000000000..df2b7d964642d --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.test.ts @@ -0,0 +1,282 @@ +/* + * 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 { checkSearchSessionsByPage, getSearchSessionsPage$ } from './get_search_session_page'; +import { + SearchSessionStatus, + ENHANCED_ES_SEARCH_STRATEGY, +} from '../../../../../../src/plugins/data/common'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { SearchSessionsConfig, SearchStatus } from './types'; +import moment from 'moment'; +import { SavedObjectsClientContract } from '../../../../../../src/core/server'; +import { of, Subject, throwError } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +jest.useFakeTimers(); + +describe('checkSearchSessionsByPage', () => { + const mockClient = {} as any; + let savedObjectsClient: jest.Mocked<SavedObjectsClientContract>; + const config: SearchSessionsConfig = { + enabled: true, + pageSize: 5, + management: {} as any, + } as any; + const mockLogger: any = { + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + + const emptySO = { + attributes: { + persisted: false, + status: SearchSessionStatus.IN_PROGRESS, + created: moment().subtract(moment.duration(3, 'm')), + touched: moment().subtract(moment.duration(10, 's')), + idMapping: {}, + }, + }; + + beforeEach(() => { + savedObjectsClient = savedObjectsClientMock.create(); + }); + + describe('getSearchSessionsPage$', () => { + test('sorting is by "touched"', async () => { + savedObjectsClient.find.mockResolvedValueOnce({ + saved_objects: [], + total: 0, + } as any); + + await getSearchSessionsPage$( + { + savedObjectsClient, + } as any, + { + type: 'literal', + }, + 1, + 1 + ); + + expect(savedObjectsClient.find).toHaveBeenCalledWith( + expect.objectContaining({ sortField: 'touched', sortOrder: 'asc' }) + ); + }); + }); + + describe('pagination', () => { + test('fetches one page if got empty response', async () => { + const checkFn = jest.fn().mockReturnValue(of(undefined)); + + await checkSearchSessionsByPage( + checkFn, + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + config, + [] + ).toPromise(); + + expect(checkFn).toHaveBeenCalledTimes(1); + }); + + test('fetches one page if got response with no saved objects', async () => { + const checkFn = jest.fn().mockReturnValue( + of({ + total: 0, + }) + ); + + await checkSearchSessionsByPage( + checkFn, + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + config, + [] + ).toPromise(); + + expect(checkFn).toHaveBeenCalledTimes(1); + }); + + test('fetches one page if less than page size object are returned', async () => { + const checkFn = jest.fn().mockReturnValue( + of({ + saved_objects: [emptySO, emptySO], + total: 5, + }) + ); + + await checkSearchSessionsByPage( + checkFn, + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + config, + [] + ).toPromise(); + + expect(checkFn).toHaveBeenCalledTimes(1); + }); + + test('fetches two pages if exactly page size objects are returned', async () => { + let i = 0; + + const checkFn = jest.fn().mockImplementation(() => + of({ + saved_objects: i++ === 0 ? [emptySO, emptySO, emptySO, emptySO, emptySO] : [], + total: 5, + page: i, + }) + ); + + await checkSearchSessionsByPage( + checkFn, + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + config, + [] + ).toPromise(); + + expect(checkFn).toHaveBeenCalledTimes(2); + + // validate that page number increases + const page1 = checkFn.mock.calls[0][3]; + const page2 = checkFn.mock.calls[1][3]; + expect(page1).toBe(1); + expect(page2).toBe(2); + }); + + test('fetches two pages if page size +1 objects are returned', async () => { + let i = 0; + + const checkFn = jest.fn().mockImplementation(() => + of({ + saved_objects: i++ === 0 ? [emptySO, emptySO, emptySO, emptySO, emptySO] : [emptySO], + total: i === 0 ? 5 : 1, + page: i, + }) + ); + + await checkSearchSessionsByPage( + checkFn, + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + config, + [] + ).toPromise(); + + expect(checkFn).toHaveBeenCalledTimes(2); + }); + + test('sessions fetched in the beginning are processed even if sessions in the end fail', async () => { + let i = 0; + + const checkFn = jest.fn().mockImplementation(() => { + if (++i === 2) { + return throwError('Fake find error...'); + } + return of({ + saved_objects: + i <= 5 + ? [ + i === 1 + ? { + id: '123', + attributes: { + persisted: false, + status: SearchSessionStatus.IN_PROGRESS, + created: moment().subtract(moment.duration(3, 'm')), + touched: moment().subtract(moment.duration(2, 'm')), + idMapping: { + 'map-key': { + strategy: ENHANCED_ES_SEARCH_STRATEGY, + id: 'async-id', + status: SearchStatus.IN_PROGRESS, + }, + }, + }, + } + : emptySO, + emptySO, + emptySO, + emptySO, + emptySO, + ] + : [], + total: 25, + page: i, + }); + }); + + await checkSearchSessionsByPage( + checkFn, + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + config, + [] + ) + .toPromise() + .catch(() => {}); + + expect(checkFn).toHaveBeenCalledTimes(2); + }); + + test('fetching is abortable', async () => { + let i = 0; + const abort$ = new Subject(); + + const checkFn = jest.fn().mockImplementation(() => { + if (++i === 2) { + abort$.next(); + } + + return of({ + saved_objects: i <= 5 ? [emptySO, emptySO, emptySO, emptySO, emptySO] : [], + total: 25, + page: i, + }); + }); + + await checkSearchSessionsByPage( + checkFn, + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + config, + [] + ) + .pipe(takeUntil(abort$)) + .toPromise() + .catch(() => {}); + + jest.runAllTimers(); + + // if not for `abort$` then this would be called 6 times! + expect(checkFn).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.ts b/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.ts new file mode 100644 index 0000000000000..74306bac39f7d --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/get_search_session_page.ts @@ -0,0 +1,61 @@ +/* + * 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 { SavedObjectsClientContract, Logger } from 'kibana/server'; +import { from, Observable, EMPTY } from 'rxjs'; +import { concatMap } from 'rxjs/operators'; +import { + SearchSessionSavedObjectAttributes, + SEARCH_SESSION_TYPE, + KueryNode, +} from '../../../../../../src/plugins/data/common'; +import { CheckSearchSessionsDeps, CheckSearchSessionsFn, SearchSessionsConfig } from './types'; + +export interface GetSessionsDeps { + savedObjectsClient: SavedObjectsClientContract; + logger: Logger; +} + +export function getSearchSessionsPage$( + { savedObjectsClient }: GetSessionsDeps, + filter: KueryNode, + pageSize: number, + page: number +) { + return from( + savedObjectsClient.find<SearchSessionSavedObjectAttributes>({ + page, + perPage: pageSize, + type: SEARCH_SESSION_TYPE, + namespaces: ['*'], + // process older sessions first + sortField: 'touched', + sortOrder: 'asc', + filter, + }) + ); +} + +export const checkSearchSessionsByPage = ( + checkFn: CheckSearchSessionsFn, + deps: CheckSearchSessionsDeps, + config: SearchSessionsConfig, + filters: any, + nextPage = 1 +): Observable<void> => + checkFn(deps, config, filters, nextPage).pipe( + concatMap((result) => { + if (!result || !result.saved_objects || result.saved_objects.length < config.pageSize) { + return EMPTY; + } else { + // TODO: while processing previous page session list might have been changed and we might skip a session, + // because it would appear now on a different "page". + // This isn't critical, as we would pick it up on a next task iteration, but maybe we could improve this somehow + return checkSearchSessionsByPage(checkFn, deps, config, filters, result.page + 1); + } + }) + ); diff --git a/x-pack/plugins/data_enhanced/server/search/session/index.ts b/x-pack/plugins/data_enhanced/server/search/session/index.ts index deadeb3f8f07a..1e6841211bb66 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/index.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/index.ts @@ -6,4 +6,3 @@ */ export * from './session_service'; -export { registerSearchSessionsTask, scheduleSearchSessionsTasks } from './monitoring_task'; diff --git a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts b/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts deleted file mode 100644 index 7b7b1412987be..0000000000000 --- a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts +++ /dev/null @@ -1,119 +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 { Duration } from 'moment'; -import { filter, takeUntil } from 'rxjs/operators'; -import { BehaviorSubject } from 'rxjs'; -import { - TaskManagerSetupContract, - TaskManagerStartContract, - RunContext, - TaskRunCreatorFunction, -} from '../../../../task_manager/server'; -import { checkRunningSessions } from './check_running_sessions'; -import { CoreSetup, SavedObjectsClient, Logger } from '../../../../../../src/core/server'; -import { ConfigSchema } from '../../../config'; -import { SEARCH_SESSION_TYPE } from '../../../../../../src/plugins/data/common'; -import { DataEnhancedStartDependencies } from '../../type'; - -export const SEARCH_SESSIONS_TASK_TYPE = 'search_sessions_monitor'; -export const SEARCH_SESSIONS_TASK_ID = `data_enhanced_${SEARCH_SESSIONS_TASK_TYPE}`; - -interface SearchSessionTaskDeps { - taskManager: TaskManagerSetupContract; - logger: Logger; - config: ConfigSchema; -} - -function searchSessionRunner( - core: CoreSetup<DataEnhancedStartDependencies>, - { logger, config }: SearchSessionTaskDeps -): TaskRunCreatorFunction { - return ({ taskInstance }: RunContext) => { - const aborted$ = new BehaviorSubject<boolean>(false); - return { - async run() { - const sessionConfig = config.search.sessions; - const [coreStart] = await core.getStartServices(); - if (!sessionConfig.enabled) { - logger.debug('Search sessions are disabled. Skipping task.'); - return; - } - if (aborted$.getValue()) return; - - const internalRepo = coreStart.savedObjects.createInternalRepository([SEARCH_SESSION_TYPE]); - const internalSavedObjectsClient = new SavedObjectsClient(internalRepo); - await checkRunningSessions( - { - savedObjectsClient: internalSavedObjectsClient, - client: coreStart.elasticsearch.client.asInternalUser, - logger, - }, - sessionConfig - ) - .pipe(takeUntil(aborted$.pipe(filter((aborted) => aborted)))) - .toPromise(); - - return { - state: {}, - }; - }, - cancel: async () => { - aborted$.next(true); - }, - }; - }; -} - -export function registerSearchSessionsTask( - core: CoreSetup<DataEnhancedStartDependencies>, - deps: SearchSessionTaskDeps -) { - deps.taskManager.registerTaskDefinitions({ - [SEARCH_SESSIONS_TASK_TYPE]: { - title: 'Search Sessions Monitor', - createTaskRunner: searchSessionRunner(core, deps), - timeout: `${deps.config.search.sessions.monitoringTaskTimeout.asSeconds()}s`, - }, - }); -} - -export async function unscheduleSearchSessionsTask( - taskManager: TaskManagerStartContract, - logger: Logger -) { - try { - await taskManager.removeIfExists(SEARCH_SESSIONS_TASK_ID); - logger.debug(`Search sessions cleared`); - } catch (e) { - logger.error(`Error clearing task, received ${e.message}`); - } -} - -export async function scheduleSearchSessionsTasks( - taskManager: TaskManagerStartContract, - logger: Logger, - trackingInterval: Duration -) { - await taskManager.removeIfExists(SEARCH_SESSIONS_TASK_ID); - - try { - await taskManager.ensureScheduled({ - id: SEARCH_SESSIONS_TASK_ID, - taskType: SEARCH_SESSIONS_TASK_TYPE, - schedule: { - interval: `${trackingInterval.asSeconds()}s`, - }, - state: {}, - params: {}, - }); - - logger.debug(`Search sessions task, scheduled to run`); - } catch (e) { - logger.error(`Error scheduling task, received ${e.message}`); - } -} diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts index 374dbee2384d5..dd1eafa5d60f8 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts @@ -79,7 +79,9 @@ describe('SearchSessionService', () => { maxUpdateRetries: MAX_UPDATE_RETRIES, defaultExpiration: moment.duration(7, 'd'), monitoringTaskTimeout: moment.duration(5, 'm'), + cleanupInterval: moment.duration(10, 's'), trackingInterval: moment.duration(10, 's'), + expireInterval: moment.duration(10, 'm'), management: {} as any, }, }, @@ -157,7 +159,9 @@ describe('SearchSessionService', () => { maxUpdateRetries: MAX_UPDATE_RETRIES, defaultExpiration: moment.duration(7, 'd'), trackingInterval: moment.duration(10, 's'), + expireInterval: moment.duration(10, 'm'), monitoringTaskTimeout: moment.duration(5, 'm'), + cleanupInterval: moment.duration(10, 's'), management: {} as any, }, }, diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index 138f42549a094..0998c1f42e183 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -24,7 +24,11 @@ import { ENHANCED_ES_SEARCH_STRATEGY, SEARCH_SESSION_TYPE, } from '../../../../../../src/plugins/data/common'; -import { esKuery, ISearchSessionService } from '../../../../../../src/plugins/data/server'; +import { + esKuery, + ISearchSessionService, + NoSearchIdInSessionError, +} from '../../../../../../src/plugins/data/server'; import { AuthenticatedUser, SecurityPluginSetup } from '../../../../security/server'; import { TaskManagerSetupContract, @@ -39,11 +43,26 @@ import { createRequestHash } from './utils'; import { ConfigSchema } from '../../../config'; import { registerSearchSessionsTask, - scheduleSearchSessionsTasks, + scheduleSearchSessionsTask, unscheduleSearchSessionsTask, -} from './monitoring_task'; +} from './setup_task'; import { SearchSessionsConfig, SearchStatus } from './types'; import { DataEnhancedStartDependencies } from '../../type'; +import { + checkPersistedSessionsProgress, + SEARCH_SESSIONS_TASK_ID, + SEARCH_SESSIONS_TASK_TYPE, +} from './check_persisted_sessions'; +import { + SEARCH_SESSIONS_CLEANUP_TASK_TYPE, + checkNonPersistedSessions, + SEARCH_SESSIONS_CLEANUP_TASK_ID, +} from './check_non_persiseted_sessions'; +import { + SEARCH_SESSIONS_EXPIRE_TASK_TYPE, + SEARCH_SESSIONS_EXPIRE_TASK_ID, + checkPersistedCompletedSessionExpiration, +} from './expire_persisted_sessions'; export interface SearchSessionDependencies { savedObjectsClient: SavedObjectsClientContract; @@ -85,11 +104,35 @@ export class SearchSessionService } public setup(core: CoreSetup<DataEnhancedStartDependencies>, deps: SetupDependencies) { - registerSearchSessionsTask(core, { + const taskDeps = { config: this.config, taskManager: deps.taskManager, logger: this.logger, - }); + }; + + registerSearchSessionsTask( + core, + taskDeps, + SEARCH_SESSIONS_TASK_TYPE, + 'persisted session progress', + checkPersistedSessionsProgress + ); + + registerSearchSessionsTask( + core, + taskDeps, + SEARCH_SESSIONS_CLEANUP_TASK_TYPE, + 'non persisted session cleanup', + checkNonPersistedSessions + ); + + registerSearchSessionsTask( + core, + taskDeps, + SEARCH_SESSIONS_EXPIRE_TASK_TYPE, + 'complete session expiration', + checkPersistedCompletedSessionExpiration + ); } public async start(core: CoreStart, deps: StartDependencies) { @@ -99,14 +142,37 @@ export class SearchSessionService public stop() {} private setupMonitoring = async (core: CoreStart, deps: StartDependencies) => { + const taskDeps = { + config: this.config, + taskManager: deps.taskManager, + logger: this.logger, + }; + if (this.sessionConfig.enabled) { - scheduleSearchSessionsTasks( - deps.taskManager, - this.logger, + scheduleSearchSessionsTask( + taskDeps, + SEARCH_SESSIONS_TASK_ID, + SEARCH_SESSIONS_TASK_TYPE, this.sessionConfig.trackingInterval ); + + scheduleSearchSessionsTask( + taskDeps, + SEARCH_SESSIONS_CLEANUP_TASK_ID, + SEARCH_SESSIONS_CLEANUP_TASK_TYPE, + this.sessionConfig.cleanupInterval + ); + + scheduleSearchSessionsTask( + taskDeps, + SEARCH_SESSIONS_EXPIRE_TASK_ID, + SEARCH_SESSIONS_EXPIRE_TASK_TYPE, + this.sessionConfig.expireInterval + ); } else { - unscheduleSearchSessionsTask(deps.taskManager, this.logger); + unscheduleSearchSessionsTask(taskDeps, SEARCH_SESSIONS_TASK_ID); + unscheduleSearchSessionsTask(taskDeps, SEARCH_SESSIONS_CLEANUP_TASK_ID); + unscheduleSearchSessionsTask(taskDeps, SEARCH_SESSIONS_EXPIRE_TASK_ID); } }; @@ -436,7 +502,7 @@ export class SearchSessionService const requestHash = createRequestHash(searchRequest.params); if (!session.attributes.idMapping.hasOwnProperty(requestHash)) { this.logger.error(`getId | ${sessionId} | ${requestHash} not found`); - throw new Error('No search ID in this session matching the given search request'); + throw new NoSearchIdInSessionError(); } this.logger.debug(`getId | ${sessionId} | ${requestHash}`); diff --git a/x-pack/plugins/data_enhanced/server/search/session/setup_task.ts b/x-pack/plugins/data_enhanced/server/search/session/setup_task.ts new file mode 100644 index 0000000000000..a4c9b6039ff64 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/setup_task.ts @@ -0,0 +1,121 @@ +/* + * 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 { Duration } from 'moment'; +import { filter, takeUntil } from 'rxjs/operators'; +import { BehaviorSubject } from 'rxjs'; +import { RunContext, TaskRunCreatorFunction } from '../../../../task_manager/server'; +import { CoreSetup, SavedObjectsClient } from '../../../../../../src/core/server'; +import { SEARCH_SESSION_TYPE } from '../../../../../../src/plugins/data/common'; +import { DataEnhancedStartDependencies } from '../../type'; +import { + SearchSessionTaskSetupDeps, + SearchSessionTaskStartDeps, + SearchSessionTaskFn, +} from './types'; + +export function searchSessionTaskRunner( + core: CoreSetup<DataEnhancedStartDependencies>, + deps: SearchSessionTaskSetupDeps, + title: string, + checkFn: SearchSessionTaskFn +): TaskRunCreatorFunction { + const { logger, config } = deps; + return ({ taskInstance }: RunContext) => { + const aborted$ = new BehaviorSubject<boolean>(false); + return { + async run() { + try { + const sessionConfig = config.search.sessions; + const [coreStart] = await core.getStartServices(); + if (!sessionConfig.enabled) { + logger.debug(`Search sessions are disabled. Skipping task ${title}.`); + return; + } + if (aborted$.getValue()) return; + + const internalRepo = coreStart.savedObjects.createInternalRepository([ + SEARCH_SESSION_TYPE, + ]); + const internalSavedObjectsClient = new SavedObjectsClient(internalRepo); + await checkFn( + { + logger, + client: coreStart.elasticsearch.client.asInternalUser, + savedObjectsClient: internalSavedObjectsClient, + }, + sessionConfig + ) + .pipe(takeUntil(aborted$.pipe(filter((aborted) => aborted)))) + .toPromise(); + + return { + state: {}, + }; + } catch (e) { + logger.error(`An error occurred. Skipping task ${title}.`); + } + }, + cancel: async () => { + aborted$.next(true); + }, + }; + }; +} + +export function registerSearchSessionsTask( + core: CoreSetup<DataEnhancedStartDependencies>, + deps: SearchSessionTaskSetupDeps, + taskType: string, + title: string, + checkFn: SearchSessionTaskFn +) { + deps.taskManager.registerTaskDefinitions({ + [taskType]: { + title, + createTaskRunner: searchSessionTaskRunner(core, deps, title, checkFn), + timeout: `${deps.config.search.sessions.monitoringTaskTimeout.asSeconds()}s`, + }, + }); +} + +export async function unscheduleSearchSessionsTask( + { taskManager, logger }: SearchSessionTaskStartDeps, + taskId: string +) { + try { + await taskManager.removeIfExists(taskId); + logger.debug(`${taskId} cleared`); + } catch (e) { + logger.error(`${taskId} Error clearing task ${e.message}`); + } +} + +export async function scheduleSearchSessionsTask( + { taskManager, logger }: SearchSessionTaskStartDeps, + taskId: string, + taskType: string, + interval: Duration +) { + await taskManager.removeIfExists(taskId); + + try { + await taskManager.ensureScheduled({ + id: taskId, + taskType, + schedule: { + interval: `${interval.asSeconds()}s`, + }, + state: {}, + params: {}, + }); + + logger.debug(`${taskId} scheduled to run`); + } catch (e) { + logger.error(`${taskId} Error scheduling task ${e.message}`); + } +} diff --git a/x-pack/plugins/data_enhanced/server/search/session/types.ts b/x-pack/plugins/data_enhanced/server/search/session/types.ts index 0fa384e55f7d7..eadc3821c1043 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/types.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/types.ts @@ -5,6 +5,18 @@ * 2.0. */ +import { + ElasticsearchClient, + Logger, + SavedObjectsClientContract, + SavedObjectsFindResponse, +} from 'kibana/server'; +import { Observable } from 'rxjs'; +import { KueryNode, SearchSessionSavedObjectAttributes } from 'src/plugins/data/common'; +import { + TaskManagerSetupContract, + TaskManagerStartContract, +} from '../../../../../../x-pack/plugins/task_manager/server'; import { ConfigSchema } from '../../../config'; export enum SearchStatus { @@ -14,3 +26,38 @@ export enum SearchStatus { } export type SearchSessionsConfig = ConfigSchema['search']['sessions']; + +export interface CheckSearchSessionsDeps { + savedObjectsClient: SavedObjectsClientContract; + client: ElasticsearchClient; + logger: Logger; +} + +export interface SearchSessionTaskSetupDeps { + taskManager: TaskManagerSetupContract; + logger: Logger; + config: ConfigSchema; +} + +export interface SearchSessionTaskStartDeps { + taskManager: TaskManagerStartContract; + logger: Logger; + config: ConfigSchema; +} + +export type SearchSessionTaskFn = ( + deps: CheckSearchSessionsDeps, + config: SearchSessionsConfig +) => Observable<void>; + +export type SearchSessionsResponse = SavedObjectsFindResponse< + SearchSessionSavedObjectAttributes, + unknown +>; + +export type CheckSearchSessionsFn = ( + deps: CheckSearchSessionsDeps, + config: SearchSessionsConfig, + filter: KueryNode, + page: number +) => Observable<SearchSessionsResponse>; diff --git a/x-pack/plugins/data_enhanced/server/search/session/update_session_status.test.ts b/x-pack/plugins/data_enhanced/server/search/session/update_session_status.test.ts new file mode 100644 index 0000000000000..485a30fd54951 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/update_session_status.test.ts @@ -0,0 +1,323 @@ +/* + * 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 { bulkUpdateSessions, updateSessionStatus } from './update_session_status'; +import { + SearchSessionStatus, + SearchSessionSavedObjectAttributes, +} from '../../../../../../src/plugins/data/common'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { SearchStatus } from './types'; +import moment from 'moment'; +import { + SavedObjectsBulkUpdateObject, + SavedObjectsClientContract, + SavedObjectsFindResult, +} from '../../../../../../src/core/server'; + +describe('bulkUpdateSessions', () => { + let mockClient: any; + let savedObjectsClient: jest.Mocked<SavedObjectsClientContract>; + const mockLogger: any = { + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + + beforeEach(() => { + savedObjectsClient = savedObjectsClientMock.create(); + mockClient = { + asyncSearch: { + status: jest.fn(), + delete: jest.fn(), + }, + eql: { + status: jest.fn(), + delete: jest.fn(), + }, + }; + }); + + describe('updateSessionStatus', () => { + test('updates expired session', async () => { + const so: SavedObjectsFindResult<SearchSessionSavedObjectAttributes> = { + id: '123', + attributes: { + persisted: false, + status: SearchSessionStatus.IN_PROGRESS, + expires: moment().subtract(moment.duration(5, 'd')), + idMapping: { + 'search-hash': { + id: 'search-id', + strategy: 'cool', + status: SearchStatus.IN_PROGRESS, + }, + }, + }, + } as any; + + const updated = await updateSessionStatus( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + so + ); + + expect(updated).toBeTruthy(); + expect(so.attributes.status).toBe(SearchSessionStatus.EXPIRED); + }); + + test('does nothing if the search is still running', async () => { + const so = { + id: '123', + attributes: { + persisted: false, + status: SearchSessionStatus.IN_PROGRESS, + created: moment().subtract(moment.duration(3, 'm')), + touched: moment().subtract(moment.duration(10, 's')), + expires: moment().add(moment.duration(5, 'd')), + idMapping: { + 'search-hash': { + id: 'search-id', + strategy: 'cool', + status: SearchStatus.IN_PROGRESS, + }, + }, + }, + } as any; + + mockClient.asyncSearch.status.mockResolvedValue({ + body: { + is_partial: true, + is_running: true, + }, + }); + + const updated = await updateSessionStatus( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + so + ); + + expect(updated).toBeFalsy(); + expect(so.attributes.status).toBe(SearchSessionStatus.IN_PROGRESS); + }); + + test("doesn't re-check completed or errored searches", async () => { + const so = { + id: '123', + attributes: { + expires: moment().add(moment.duration(5, 'd')), + status: SearchSessionStatus.ERROR, + idMapping: { + 'search-hash': { + id: 'search-id', + strategy: 'cool', + status: SearchStatus.COMPLETE, + }, + 'another-search-hash': { + id: 'search-id', + strategy: 'cool', + status: SearchStatus.ERROR, + }, + }, + }, + } as any; + + const updated = await updateSessionStatus( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + so + ); + + expect(updated).toBeFalsy(); + expect(mockClient.asyncSearch.status).not.toBeCalled(); + }); + + test('updates to complete if the search is done', async () => { + savedObjectsClient.bulkUpdate = jest.fn(); + const so = { + attributes: { + status: SearchSessionStatus.IN_PROGRESS, + touched: '123', + expires: moment().add(moment.duration(5, 'd')), + idMapping: { + 'search-hash': { + id: 'search-id', + strategy: 'cool', + status: SearchStatus.IN_PROGRESS, + }, + }, + }, + } as any; + mockClient.asyncSearch.status.mockResolvedValue({ + body: { + is_partial: false, + is_running: false, + completion_status: 200, + }, + }); + + const updated = await updateSessionStatus( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + so + ); + + expect(updated).toBeTruthy(); + + expect(mockClient.asyncSearch.status).toBeCalledWith({ id: 'search-id' }); + expect(so.attributes.status).toBe(SearchSessionStatus.COMPLETE); + expect(so.attributes.status).toBe(SearchSessionStatus.COMPLETE); + expect(so.attributes.touched).not.toBe('123'); + expect(so.attributes.completed).not.toBeUndefined(); + expect(so.attributes.idMapping['search-hash'].status).toBe(SearchStatus.COMPLETE); + expect(so.attributes.idMapping['search-hash'].error).toBeUndefined(); + }); + + test('updates to error if the search is errored', async () => { + savedObjectsClient.bulkUpdate = jest.fn(); + const so = { + attributes: { + expires: moment().add(moment.duration(5, 'd')), + idMapping: { + 'search-hash': { + id: 'search-id', + strategy: 'cool', + status: SearchStatus.IN_PROGRESS, + }, + }, + }, + } as any; + + mockClient.asyncSearch.status.mockResolvedValue({ + body: { + is_partial: false, + is_running: false, + completion_status: 500, + }, + }); + + const updated = await updateSessionStatus( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + so + ); + + expect(updated).toBeTruthy(); + expect(so.attributes.status).toBe(SearchSessionStatus.ERROR); + expect(so.attributes.touched).not.toBe('123'); + expect(so.attributes.idMapping['search-hash'].status).toBe(SearchStatus.ERROR); + expect(so.attributes.idMapping['search-hash'].error).toBe( + 'Search completed with a 500 status' + ); + }); + }); + + describe('bulkUpdateSessions', () => { + test('does nothing if there are no open sessions', async () => { + await bulkUpdateSessions( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + [] + ); + + expect(savedObjectsClient.bulkUpdate).not.toBeCalled(); + expect(savedObjectsClient.delete).not.toBeCalled(); + }); + + test('updates in space', async () => { + const so = { + namespaces: ['awesome'], + attributes: { + expires: moment().add(moment.duration(5, 'd')), + status: SearchSessionStatus.IN_PROGRESS, + touched: '123', + idMapping: { + 'search-hash': { + id: 'search-id', + strategy: 'cool', + status: SearchStatus.IN_PROGRESS, + }, + }, + }, + } as any; + + savedObjectsClient.bulkUpdate = jest.fn().mockResolvedValue({ + saved_objects: [so], + }); + + await bulkUpdateSessions( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + [so] + ); + + const [updateInput] = savedObjectsClient.bulkUpdate.mock.calls[0]; + const updatedAttributes = updateInput[0] as SavedObjectsBulkUpdateObject; + expect(updatedAttributes.namespace).toBe('awesome'); + }); + + test('logs failures', async () => { + const so = { + namespaces: ['awesome'], + attributes: { + expires: moment().add(moment.duration(5, 'd')), + status: SearchSessionStatus.IN_PROGRESS, + touched: '123', + idMapping: { + 'search-hash': { + id: 'search-id', + strategy: 'cool', + status: SearchStatus.IN_PROGRESS, + }, + }, + }, + } as any; + + savedObjectsClient.bulkUpdate = jest.fn().mockResolvedValue({ + saved_objects: [ + { + error: 'nope', + }, + ], + }); + + await bulkUpdateSessions( + { + savedObjectsClient, + client: mockClient, + logger: mockLogger, + }, + [so] + ); + + expect(savedObjectsClient.bulkUpdate).toBeCalledTimes(1); + expect(mockLogger.error).toBeCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/data_enhanced/server/search/session/update_session_status.ts b/x-pack/plugins/data_enhanced/server/search/session/update_session_status.ts new file mode 100644 index 0000000000000..1c484467bef63 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/update_session_status.ts @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectsFindResult, SavedObjectsUpdateResponse } from 'kibana/server'; +import { + SearchSessionRequestInfo, + SearchSessionSavedObjectAttributes, + SearchSessionStatus, +} from '../../../../../../src/plugins/data/common'; +import { getSearchStatus } from './get_search_status'; +import { getSessionStatus } from './get_session_status'; +import { CheckSearchSessionsDeps, SearchSessionsResponse, SearchStatus } from './types'; +import { isSearchSessionExpired } from './utils'; + +export async function updateSessionStatus( + { logger, client }: CheckSearchSessionsDeps, + session: SavedObjectsFindResult<SearchSessionSavedObjectAttributes> +) { + let sessionUpdated = false; + const isExpired = isSearchSessionExpired(session); + + if (!isExpired) { + // Check statuses of all running searches + await Promise.all( + Object.keys(session.attributes.idMapping).map(async (searchKey: string) => { + const updateSearchRequest = ( + currentStatus: Pick<SearchSessionRequestInfo, 'status' | 'error'> + ) => { + sessionUpdated = true; + session.attributes.idMapping[searchKey] = { + ...session.attributes.idMapping[searchKey], + ...currentStatus, + }; + }; + + const searchInfo = session.attributes.idMapping[searchKey]; + if (searchInfo.status === SearchStatus.IN_PROGRESS) { + try { + const currentStatus = await getSearchStatus(client, searchInfo.id); + + if (currentStatus.status !== searchInfo.status) { + logger.debug(`search ${searchInfo.id} | status changed to ${currentStatus.status}`); + updateSearchRequest(currentStatus); + } + } catch (e) { + logger.error(e); + updateSearchRequest({ + status: SearchStatus.ERROR, + error: e.message || e.meta.error?.caused_by?.reason, + }); + } + } + }) + ); + } + + // And only then derive the session's status + const sessionStatus = isExpired + ? SearchSessionStatus.EXPIRED + : getSessionStatus(session.attributes); + if (sessionStatus !== session.attributes.status) { + const now = new Date().toISOString(); + session.attributes.status = sessionStatus; + session.attributes.touched = now; + if (sessionStatus === SearchSessionStatus.COMPLETE) { + session.attributes.completed = now; + } else if (session.attributes.completed) { + session.attributes.completed = null; + } + sessionUpdated = true; + } + + return sessionUpdated; +} + +export async function getAllSessionsStatusUpdates( + deps: CheckSearchSessionsDeps, + searchSessions: SearchSessionsResponse +) { + const updatedSessions = new Array<SavedObjectsFindResult<SearchSessionSavedObjectAttributes>>(); + + await Promise.all( + searchSessions.saved_objects.map(async (session) => { + const updated = await updateSessionStatus(deps, session); + + if (updated) { + updatedSessions.push(session); + } + }) + ); + + return updatedSessions; +} + +export async function bulkUpdateSessions( + { logger, savedObjectsClient }: CheckSearchSessionsDeps, + updatedSessions: Array<SavedObjectsFindResult<SearchSessionSavedObjectAttributes>> +) { + if (updatedSessions.length) { + // If there's an error, we'll try again in the next iteration, so there's no need to check the output. + const updatedResponse = await savedObjectsClient.bulkUpdate<SearchSessionSavedObjectAttributes>( + updatedSessions.map((session) => ({ + ...session, + namespace: session.namespaces?.[0], + })) + ); + + const success: Array<SavedObjectsUpdateResponse<SearchSessionSavedObjectAttributes>> = []; + const fail: Array<SavedObjectsUpdateResponse<SearchSessionSavedObjectAttributes>> = []; + + updatedResponse.saved_objects.forEach((savedObjectResponse) => { + if ('error' in savedObjectResponse) { + fail.push(savedObjectResponse); + logger.error( + `Error while updating search session ${savedObjectResponse?.id}: ${savedObjectResponse.error?.message}` + ); + } else { + success.push(savedObjectResponse); + } + }); + + logger.debug(`Updating search sessions: success: ${success.length}, fail: ${fail.length}`); + } +} diff --git a/x-pack/plugins/data_enhanced/server/search/session/utils.ts b/x-pack/plugins/data_enhanced/server/search/session/utils.ts index 7b1f1a7564626..55c875602694f 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/utils.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/utils.ts @@ -7,6 +7,9 @@ import { createHash } from 'crypto'; import stringify from 'json-stable-stringify'; +import { SavedObjectsFindResult } from 'kibana/server'; +import moment from 'moment'; +import { SearchSessionSavedObjectAttributes } from 'src/plugins/data/common'; /** * Generate the hash for this request so that, in the future, this hash can be used to look up @@ -17,3 +20,9 @@ export function createRequestHash(keys: Record<any, any>) { const { preference, ...params } = keys; return createHash(`sha256`).update(stringify(params)).digest('hex'); } + +export function isSearchSessionExpired( + session: SavedObjectsFindResult<SearchSessionSavedObjectAttributes> +) { + return moment(session.attributes.expires).isBefore(moment()); +} diff --git a/x-pack/plugins/data_visualizer/kibana.json b/x-pack/plugins/data_visualizer/kibana.json index b024a52e64721..00eb3d7bf142c 100644 --- a/x-pack/plugins/data_visualizer/kibana.json +++ b/x-pack/plugins/data_visualizer/kibana.json @@ -16,7 +16,8 @@ "security", "maps", "home", - "lens" + "lens", + "indexPatternFieldEditor" ], "requiredBundles": [ "home", diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/date_picker_wrapper/date_picker_wrapper.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/date_picker_wrapper/date_picker_wrapper.tsx index f6f53f40d6b9e..52ae5e685316d 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/date_picker_wrapper/date_picker_wrapper.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/date_picker_wrapper/date_picker_wrapper.tsx @@ -18,7 +18,7 @@ import { import { useUrlState } from '../../util/url_state'; import { useDataVisualizerKibana } from '../../../kibana_context'; -import { dataVisualizerTimefilterRefresh$ } from '../../../index_data_visualizer/services/timefilter_refresh_service'; +import { dataVisualizerRefresh$ } from '../../../index_data_visualizer/services/timefilter_refresh_service'; interface TimePickerQuickRange { from: string; @@ -50,7 +50,7 @@ function getRecentlyUsedRangesFactory(timeHistory: TimeHistoryContract) { } function updateLastRefresh(timeRange: OnRefreshProps) { - dataVisualizerTimefilterRefresh$.next({ lastRefresh: Date.now(), timeRange }); + dataVisualizerRefresh$.next({ lastRefresh: Date.now(), timeRange }); } export const DatePickerWrapper: FC = () => { diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/action_menu/actions.ts b/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/action_menu/actions.ts index 414c72c33f057..a77ca1d589349 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/action_menu/actions.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_data_row/action_menu/actions.ts @@ -7,19 +7,37 @@ import { i18n } from '@kbn/i18n'; import { Action } from '@elastic/eui/src/components/basic_table/action_types'; +import { MutableRefObject } from 'react'; import { getCompatibleLensDataType, getLensAttributes } from './lens_utils'; import { IndexPattern } from '../../../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; import { CombinedQuery } from '../../../../index_data_visualizer/types/combined_query'; import { FieldVisConfig } from '../../stats_table/types'; -import { LensPublicStart } from '../../../../../../../lens/public'; +import { DataVisualizerKibanaReactContextValue } from '../../../../kibana_context'; +import { + dataVisualizerRefresh$, + Refresh, +} from '../../../../index_data_visualizer/services/timefilter_refresh_service'; + export function getActions( indexPattern: IndexPattern, - lensPlugin: LensPublicStart, - combinedQuery: CombinedQuery + services: DataVisualizerKibanaReactContextValue['services'], + combinedQuery: CombinedQuery, + actionFlyoutRef: MutableRefObject<(() => void | undefined) | undefined> ): Array<Action<FieldVisConfig>> { - const canUseLensEditor = lensPlugin.canUseEditor(); - return [ - { + const { lens: lensPlugin, indexPatternFieldEditor } = services; + + const actions: Array<Action<FieldVisConfig>> = []; + + const refreshPage = () => { + const refresh: Refresh = { + lastRefresh: Date.now(), + }; + dataVisualizerRefresh$.next(refresh); + }; + // Navigate to Lens with prefilled chart for data field + if (lensPlugin !== undefined) { + const canUseLensEditor = lensPlugin?.canUseEditor(); + actions.push({ name: i18n.translate('xpack.dataVisualizer.index.dataGrid.exploreInLensTitle', { defaultMessage: 'Explore in Lens', }), @@ -40,6 +58,56 @@ export function getActions( } }, 'data-test-subj': 'dataVisualizerActionViewInLensButton', - }, - ]; + }); + } + + // Allow to edit index pattern field + if (indexPatternFieldEditor?.userPermissions.editIndexPattern()) { + actions.push({ + name: i18n.translate('xpack.dataVisualizer.index.dataGrid.editIndexPatternFieldTitle', { + defaultMessage: 'Edit index pattern field', + }), + description: i18n.translate( + 'xpack.dataVisualizer.index.dataGrid.editIndexPatternFieldDescription', + { + defaultMessage: 'Edit index pattern field', + } + ), + type: 'icon', + icon: 'indexEdit', + onClick: (item: FieldVisConfig) => { + actionFlyoutRef.current = indexPatternFieldEditor?.openEditor({ + ctx: { indexPattern }, + fieldName: item.fieldName, + onSave: refreshPage, + }); + }, + 'data-test-subj': 'dataVisualizerActionEditIndexPatternFieldButton', + }); + actions.push({ + name: i18n.translate('xpack.dataVisualizer.index.dataGrid.deleteIndexPatternFieldTitle', { + defaultMessage: 'Delete index pattern field', + }), + description: i18n.translate( + 'xpack.dataVisualizer.index.dataGrid.deleteIndexPatternFieldDescription', + { + defaultMessage: 'Delete index pattern field', + } + ), + type: 'icon', + icon: 'trash', + available: (item: FieldVisConfig) => { + return item.deletable === true; + }, + onClick: (item: FieldVisConfig) => { + actionFlyoutRef.current = indexPatternFieldEditor?.openDeleteModal({ + ctx: { indexPattern }, + fieldName: item.fieldName!, + onDelete: refreshPage, + }); + }, + 'data-test-subj': 'dataVisualizerActionDeleteIndexPatternFieldButton', + }); + } + return actions; } diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/filebeat_config_flyout/filebeat_config_flyout.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/filebeat_config_flyout/filebeat_config_flyout.tsx index 238cdcc2f8d9e..6c9df5cf2eba7 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/filebeat_config_flyout/filebeat_config_flyout.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/filebeat_config_flyout/filebeat_config_flyout.tsx @@ -64,7 +64,7 @@ export const FilebeatConfigFlyout: FC<Props> = ({ }, [username, index, ingestPipelineId, results]); return ( - <EuiFlyout onClose={closeFlyout} hideCloseButton size={'m'}> + <EuiFlyout onClose={closeFlyout} hideCloseButton size={'m'} ownFocus={false}> <EuiFlyoutBody> <EuiFlexGroup> <Contents value={fileBeatConfig} username={username} index={index} /> diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/keyword_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/keyword_content.tsx index 22fe8244ef760..1baea4b3f2f7c 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/keyword_content.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/keyword_content.tsx @@ -14,13 +14,6 @@ import { DocumentStatsTable } from './document_stats'; import { ExpandedRowContent } from './expanded_row_content'; import { ChoroplethMap } from './choropleth_map'; -const COMMON_EMS_LAYER_IDS = [ - 'world_countries', - 'administrative_regions_lvl2', - 'usa_zip_codes', - 'usa_states', -]; - export const KeywordContent: FC<FieldDataRowProps> = ({ config }) => { const [EMSSuggestion, setEMSSuggestion] = useState<EMSTermJoinConfig | null | undefined>(); const { stats, fieldName } = config; @@ -32,7 +25,6 @@ export const KeywordContent: FC<FieldDataRowProps> = ({ config }) => { const loadEMSTermSuggestions = useCallback(async () => { if (!mapsPlugin) return; const suggestion: EMSTermJoinConfig | null = await mapsPlugin.suggestEMSTermJoinConfig({ - emsLayerIds: COMMON_EMS_LAYER_IDS, sampleValues: Array.isArray(stats?.topValues) ? stats?.topValues.map((value) => value.key) : [], diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx index afadc5c5ae4a4..02e4e29dcc05e 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx @@ -15,6 +15,7 @@ import { EuiIcon, EuiInMemoryTable, EuiText, + EuiToolTip, HorizontalAlignment, LEFT_ALIGNMENT, RIGHT_ALIGNMENT, @@ -111,6 +112,7 @@ export const DataVisualizerTable = <T extends DataVisualizerTableItem>({ width: '40px', isExpander: true, render: (item: DataVisualizerTableItem) => { + const displayName = item.displayName ?? item.fieldName; if (item.fieldName === undefined) return null; const direction = expandedRowItemIds.includes(item.fieldName) ? 'arrowUp' : 'arrowDown'; return ( @@ -121,11 +123,11 @@ export const DataVisualizerTable = <T extends DataVisualizerTableItem>({ expandedRowItemIds.includes(item.fieldName) ? i18n.translate('xpack.dataVisualizer.dataGrid.rowCollapse', { defaultMessage: 'Hide details for {fieldName}', - values: { fieldName: item.fieldName }, + values: { fieldName: displayName }, }) : i18n.translate('xpack.dataVisualizer.dataGrid.rowExpand', { defaultMessage: 'Show details for {fieldName}', - values: { fieldName: item.fieldName }, + values: { fieldName: displayName }, }) } iconType={direction} @@ -157,11 +159,15 @@ export const DataVisualizerTable = <T extends DataVisualizerTableItem>({ }), sortable: true, truncateText: true, - render: (fieldName: string) => ( - <EuiText size="s"> - <b>{fieldName}</b> - </EuiText> - ), + render: (fieldName: string, item: DataVisualizerTableItem) => { + const displayName = item.displayName ?? item.fieldName; + + return ( + <EuiText size="s"> + <b data-test-subj={`dataVisualizerDisplayName-${item.fieldName}`}>{displayName}</b> + </EuiText> + ); + }, align: LEFT_ALIGNMENT as HorizontalAlignment, 'data-test-subj': 'dataVisualizerTableColumnName', }, @@ -194,18 +200,33 @@ export const DataVisualizerTable = <T extends DataVisualizerTableItem>({ {i18n.translate('xpack.dataVisualizer.dataGrid.distributionsColumnName', { defaultMessage: 'Distributions', })} - <EuiButtonIcon - style={{ marginLeft: 4 }} - size={'s'} - iconType={showDistributions ? 'eye' : 'eyeClosed'} - onClick={() => toggleShowDistribution()} - aria-label={i18n.translate( - 'xpack.dataVisualizer.dataGrid.showDistributionsAriaLabel', - { - defaultMessage: 'Show distributions', + <EuiToolTip + content={ + !showDistributions + ? i18n.translate('xpack.dataVisualizer.dataGrid.showDistributionsTooltip', { + defaultMessage: 'Show distributions', + }) + : i18n.translate('xpack.dataVisualizer.dataGrid.hideDistributionsTooltip', { + defaultMessage: 'Hide distributions', + }) + } + > + <EuiButtonIcon + style={{ marginLeft: 4 }} + size={'s'} + iconType={showDistributions ? 'eye' : 'eyeClosed'} + onClick={() => toggleShowDistribution()} + aria-label={ + !showDistributions + ? i18n.translate('xpack.dataVisualizer.dataGrid.showDistributionsAriaLabel', { + defaultMessage: 'Show distributions', + }) + : i18n.translate('xpack.dataVisualizer.dataGrid.hideDistributionsAriaLabel', { + defaultMessage: 'Hide distributions', + }) } - )} - /> + /> + </EuiToolTip> </div> ), render: (item: DataVisualizerTableItem) => { diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_vis_config.ts b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_vis_config.ts index d58497f6cd7cc..eeb9fe12692fd 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_vis_config.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/types/field_vis_config.ts @@ -24,17 +24,20 @@ export interface MetricFieldVisStats { export interface FieldVisConfig { type: JobFieldType; fieldName?: string; + displayName?: string; existsInDocs: boolean; aggregatable: boolean; loading: boolean; stats?: FieldVisStats; fieldFormat?: any; isUnsupportedType?: boolean; + deletable?: boolean; } export interface FileBasedFieldVisConfig { type: JobFieldType; fieldName?: string; + displayName?: string; stats?: FieldVisStats; format?: string; } diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx index 12441bcfbbb23..b116b25670ad2 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC, Fragment, useEffect, useMemo, useState, useCallback } from 'react'; +import React, { FC, Fragment, useEffect, useMemo, useState, useCallback, useRef } from 'react'; import { merge } from 'rxjs'; import { EuiFlexGroup, @@ -62,10 +62,11 @@ import { kbnTypeToJobType } from '../../../common/util/field_types_utils'; import { SearchPanel } from '../search_panel'; import { ActionsPanel } from '../actions_panel'; import { DatePickerWrapper } from '../../../common/components/date_picker_wrapper'; -import { dataVisualizerTimefilterRefresh$ } from '../../services/timefilter_refresh_service'; +import { dataVisualizerRefresh$ } from '../../services/timefilter_refresh_service'; import { HelpMenu } from '../../../common/components/help_menu'; import { TimeBuckets } from '../../services/time_buckets'; import { extractSearchData } from '../../utils/saved_search_utils'; +import { DataVisualizerIndexPatternManagement } from '../index_pattern_management'; interface DataVisualizerPageState { overallStats: OverallStats; @@ -123,9 +124,8 @@ export interface IndexDataVisualizerViewProps { const restorableDefaults = getDefaultDataVisualizerListState(); export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVisualizerProps) => { - const { - services: { lens: lensPlugin, docLinks, notifications, uiSettings }, - } = useDataVisualizerKibana(); + const { services } = useDataVisualizerKibana(); + const { docLinks, notifications, uiSettings } = services; const { toasts } = notifications; const [dataVisualizerListState, setDataVisualizerListState] = usePageUrlState( @@ -299,7 +299,7 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi useEffect(() => { const timeUpdateSubscription = merge( timefilter.getTimeUpdate$(), - dataVisualizerTimefilterRefresh$ + dataVisualizerRefresh$ ).subscribe(() => { setGlobalState({ time: timefilter.getTime(), @@ -533,7 +533,7 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi }); const metricExistsFields = allMetricFields.filter((f) => { return aggregatableExistsFields.find((existsF) => { - return existsF.fieldName === f.displayName; + return existsF.fieldName === f.spec.name; }); }); @@ -562,7 +562,7 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi metricFieldsToShow.forEach((field) => { const fieldData = aggregatableFields.find((f) => { - return f.fieldName === field.displayName; + return f.fieldName === field.spec.name; }); const metricConfig: FieldVisConfig = { @@ -571,7 +571,11 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi type: JOB_FIELD_TYPES.NUMBER, loading: true, aggregatable: true, + deletable: field.runtimeField !== undefined, }; + if (field.displayName !== metricConfig.fieldName) { + metricConfig.displayName = field.displayName; + } configs.push(metricConfig); }); @@ -607,7 +611,7 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi allNonMetricFields.forEach((f) => { const checkAggregatableField = aggregatableExistsFields.find( - (existsField) => existsField.fieldName === f.displayName + (existsField) => existsField.fieldName === f.spec.name ); if (checkAggregatableField !== undefined) { @@ -615,7 +619,7 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi nonMetricFieldData.push(checkAggregatableField); } else { const checkNonAggregatableField = nonAggregatableExistsFields.find( - (existsField) => existsField.fieldName === f.displayName + (existsField) => existsField.fieldName === f.spec.name ); if (checkNonAggregatableField !== undefined) { @@ -643,7 +647,7 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi const configs: FieldVisConfig[] = []; nonMetricFieldsToShow.forEach((field) => { - const fieldData = nonMetricFieldData.find((f) => f.fieldName === field.displayName); + const fieldData = nonMetricFieldData.find((f) => f.fieldName === field.spec.name); const nonMetricConfig = { ...fieldData, @@ -651,6 +655,7 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi aggregatable: field.aggregatable, scripted: field.scripted, loading: fieldData.existsInDocs, + deletable: field.runtimeField !== undefined, }; // Map the field type from the Kibana index pattern to the field type @@ -665,6 +670,10 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi nonMetricConfig.isUnsupportedType = true; } + if (field.displayName !== nonMetricConfig.fieldName) { + nonMetricConfig.displayName = field.displayName; + } + configs.push(nonMetricConfig); }); @@ -735,13 +744,33 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi [currentIndexPattern, searchQueryLanguage, searchString] ); + // Some actions open up fly-out or popup + // This variable is used to keep track of them and clean up when unmounting + const actionFlyoutRef = useRef<() => void | undefined>(); + useEffect(() => { + const ref = actionFlyoutRef; + return () => { + // Clean up any of the flyout/editor opened from the actions + if (ref.current) { + ref.current(); + } + }; + }, []); + // Inject custom action column for the index based visualizer + // Hide the column completely if no access to any of the plugins const extendedColumns = useMemo(() => { - if (lensPlugin === undefined) { - // eslint-disable-next-line no-console - console.error('Lens plugin not available'); - return; - } + const actions = getActions( + currentIndexPattern, + services, + { + searchQueryLanguage, + searchString, + }, + actionFlyoutRef + ); + if (!Array.isArray(actions) || actions.length < 1) return; + const actionColumn: EuiTableActionsColumnType<FieldVisConfig> = { name: ( <FormattedMessage @@ -749,14 +778,15 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi defaultMessage="Actions" /> ), - actions: getActions(currentIndexPattern, lensPlugin, { searchQueryLanguage, searchString }), + actions, width: '100px', }; return [actionColumn]; - }, [currentIndexPattern, lensPlugin, searchQueryLanguage, searchString]); + }, [currentIndexPattern, services, searchQueryLanguage, searchString]); const helpLink = docLinks.links.ml.guide; + return ( <Fragment> <EuiPage data-test-subj="dataVisualizerIndexPage"> @@ -765,10 +795,24 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi <EuiFlexItem> <EuiPageContentHeader> <EuiPageContentHeaderSection> - <EuiTitle size="l"> - <h1>{currentIndexPattern.title}</h1> - </EuiTitle> + <div + style={{ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }} + > + <EuiTitle size="l"> + <h1>{currentIndexPattern.title}</h1> + </EuiTitle> + <DataVisualizerIndexPatternManagement + currentIndexPattern={currentIndexPattern} + useNewFieldsApi={true} + /> + </div> </EuiPageContentHeaderSection> + <EuiPageContentHeaderSection data-test-subj="dataVisualizerTimeRangeSelectorSection"> <EuiFlexGroup alignItems="center" justifyContent="flexEnd" gutterSize="s"> {currentIndexPattern.timeFieldName !== undefined && ( diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_pattern_management/index.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_pattern_management/index.ts new file mode 100644 index 0000000000000..c26f84a4c22fc --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_pattern_management/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 { DataVisualizerIndexPatternManagement } from './index_pattern_management'; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_pattern_management/index_pattern_management.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_pattern_management/index_pattern_management.tsx new file mode 100644 index 0000000000000..cb81640f328c5 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_pattern_management/index_pattern_management.tsx @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useRef, useState } from 'react'; +import { EuiButtonIcon, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { IndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; +import { useDataVisualizerKibana } from '../../../kibana_context'; +import { dataVisualizerRefresh$, Refresh } from '../../services/timefilter_refresh_service'; + +export interface DataVisualizerIndexPatternManagementProps { + /** + * Currently selected index pattern + */ + currentIndexPattern?: IndexPattern; + /** + * Read from the Fields API + */ + useNewFieldsApi?: boolean; +} + +export function DataVisualizerIndexPatternManagement( + props: DataVisualizerIndexPatternManagementProps +) { + const { + services: { indexPatternFieldEditor, application }, + } = useDataVisualizerKibana(); + + const { useNewFieldsApi, currentIndexPattern } = props; + const indexPatternFieldEditPermission = indexPatternFieldEditor?.userPermissions.editIndexPattern(); + const canEditIndexPatternField = !!indexPatternFieldEditPermission && useNewFieldsApi; + const [isAddIndexPatternFieldPopoverOpen, setIsAddIndexPatternFieldPopoverOpen] = useState(false); + + const closeFieldEditor = useRef<() => void | undefined>(); + useEffect(() => { + return () => { + // Make sure to close the editor when unmounting + if (closeFieldEditor.current) { + closeFieldEditor.current(); + } + }; + }, []); + + if (indexPatternFieldEditor === undefined || !currentIndexPattern || !canEditIndexPatternField) { + return null; + } + + const addField = () => { + closeFieldEditor.current = indexPatternFieldEditor.openEditor({ + ctx: { + indexPattern: currentIndexPattern, + }, + onSave: () => { + const refresh: Refresh = { + lastRefresh: Date.now(), + }; + dataVisualizerRefresh$.next(refresh); + }, + }); + }; + + return ( + <EuiPopover + panelPaddingSize="s" + isOpen={isAddIndexPatternFieldPopoverOpen} + closePopover={() => { + setIsAddIndexPatternFieldPopoverOpen(false); + }} + ownFocus + data-test-subj="dataVisualizerIndexPatternManagementPopover" + button={ + <EuiButtonIcon + color="text" + iconType="boxesHorizontal" + data-test-subj="dataVisualizerIndexPatternManagementButton" + aria-label={i18n.translate( + 'xpack.dataVisualizer.index.indexPatternManagement.actionsPopoverLabel', + { + defaultMessage: 'Index pattern settings', + } + )} + onClick={() => { + setIsAddIndexPatternFieldPopoverOpen(!isAddIndexPatternFieldPopoverOpen); + }} + /> + } + > + <EuiContextMenuPanel + data-test-subj="dataVisualizerIndexPatternManagementMenu" + size="s" + items={[ + <EuiContextMenuItem + key="add" + icon="indexOpen" + data-test-subj="dataVisualizerAddIndexPatternFieldAction" + onClick={() => { + setIsAddIndexPatternFieldPopoverOpen(false); + addField(); + }} + > + {i18n.translate('xpack.dataVisualizer.index.indexPatternManagement.addFieldButton', { + defaultMessage: 'Add field to index pattern', + })} + </EuiContextMenuItem>, + <EuiContextMenuItem + key="manage" + icon="indexSettings" + data-test-subj="dataVisualizerManageIndexPatternAction" + onClick={() => { + setIsAddIndexPatternFieldPopoverOpen(false); + application.navigateToApp('management', { + path: `/kibana/indexPatterns/patterns/${props.currentIndexPattern?.id}`, + }); + }} + > + {i18n.translate('xpack.dataVisualizer.index.indexPatternManagement.manageFieldButton', { + defaultMessage: 'Manage index pattern fields', + })} + </EuiContextMenuItem>, + ]} + /> + </EuiPopover> + ); +} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts index 3cb0d4d672f48..468bd3a2bd7ee 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/data_loader/data_loader.ts @@ -50,9 +50,9 @@ export class DataLoader { const fieldName = field.displayName !== undefined ? field.displayName : field.name; if (this.isDisplayField(fieldName) === true) { if (field.aggregatable === true && field.type !== KBN_FIELD_TYPES.GEO_SHAPE) { - aggregatableFields.push(fieldName); + aggregatableFields.push(field.name); } else { - nonAggregatableFields.push(fieldName); + nonAggregatableFields.push(field.name); } } }); diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx index 82a9b93b31a71..f9e9aece48a06 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/index_data_visualizer.tsx @@ -178,7 +178,16 @@ export const DataVisualizerUrlStateContextProvider: FC<DataVisualizerUrlStateCon export const IndexDataVisualizer: FC = () => { const coreStart = getCoreStart(); - const { data, maps, embeddable, share, security, fileUpload, lens } = getPluginsStart(); + const { + data, + maps, + embeddable, + share, + security, + fileUpload, + lens, + indexPatternFieldEditor, + } = getPluginsStart(); const services = { data, maps, @@ -187,6 +196,7 @@ export const IndexDataVisualizer: FC = () => { security, fileUpload, lens, + indexPatternFieldEditor, ...coreStart, }; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/timefilter_refresh_service.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/timefilter_refresh_service.ts index 49ef9107c3ece..11f286e781219 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/timefilter_refresh_service.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/timefilter_refresh_service.ts @@ -6,11 +6,10 @@ */ import { Subject } from 'rxjs'; -import { Required } from 'utility-types'; export interface Refresh { lastRefresh: number; timeRange?: { start: string; end: string }; } -export const dataVisualizerTimefilterRefresh$ = new Subject<Required<Refresh>>(); +export const dataVisualizerRefresh$ = new Subject<Refresh>(); diff --git a/x-pack/plugins/data_visualizer/public/application/kibana_context.ts b/x-pack/plugins/data_visualizer/public/application/kibana_context.ts index f7ce13d2fd48d..58d0ac021ff22 100644 --- a/x-pack/plugins/data_visualizer/public/application/kibana_context.ts +++ b/x-pack/plugins/data_visualizer/public/application/kibana_context.ts @@ -6,8 +6,9 @@ */ import { CoreStart } from 'kibana/public'; -import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { KibanaReactContextValue, useKibana } from '../../../../../src/plugins/kibana_react/public'; import type { DataVisualizerStartDependencies } from '../plugin'; export type StartServices = CoreStart & DataVisualizerStartDependencies; +export type DataVisualizerKibanaReactContextValue = KibanaReactContextValue<StartServices>; export const useDataVisualizerKibana = () => useKibana<StartServices>(); diff --git a/x-pack/plugins/data_visualizer/public/plugin.ts b/x-pack/plugins/data_visualizer/public/plugin.ts index 66109de1b1463..4b71b08e9cf27 100644 --- a/x-pack/plugins/data_visualizer/public/plugin.ts +++ b/x-pack/plugins/data_visualizer/public/plugin.ts @@ -17,6 +17,7 @@ import type { FileUploadPluginStart } from '../../file_upload/public'; import type { MapsStartApi } from '../../maps/public'; import type { SecurityPluginSetup } from '../../security/public'; import type { LensPublicStart } from '../../lens/public'; +import type { IndexPatternFieldEditorStart } from '../../../../src/plugins/index_pattern_field_editor/public'; import { getFileDataVisualizerComponent, getIndexDataVisualizerComponent } from './api'; import { getMaxBytesFormatted } from './application/common/util/get_max_bytes'; import { registerHomeAddData, registerHomeFeatureCatalogue } from './register_home'; @@ -32,6 +33,7 @@ export interface DataVisualizerStartDependencies { security?: SecurityPluginSetup; share: SharePluginStart; lens?: LensPublicStart; + indexPatternFieldEditor?: IndexPatternFieldEditorStart; } export type DataVisualizerPluginSetup = ReturnType<DataVisualizerPlugin['setup']>; diff --git a/x-pack/plugins/discover_enhanced/kibana.json b/x-pack/plugins/discover_enhanced/kibana.json index 01a3624d3e320..da95a0f21a020 100644 --- a/x-pack/plugins/discover_enhanced/kibana.json +++ b/x-pack/plugins/discover_enhanced/kibana.json @@ -7,5 +7,5 @@ "requiredPlugins": ["uiActions", "embeddable", "discover"], "optionalPlugins": ["share", "kibanaLegacy", "usageCollection"], "configPath": ["xpack", "discoverEnhanced"], - "requiredBundles": ["kibanaUtils", "data", "share"] + "requiredBundles": ["kibanaUtils", "data"] } diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts index 023db127ca633..44ea53fe0b870 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts @@ -11,13 +11,13 @@ import { ViewMode, IEmbeddable } from '../../../../../../src/plugins/embeddable/ import { StartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public'; import { KibanaLegacyStart } from '../../../../../../src/plugins/kibana_legacy/public'; import { CoreStart } from '../../../../../../src/core/public'; -import { KibanaURL } from '../../../../../../src/plugins/share/public'; +import { KibanaLocation } from '../../../../../../src/plugins/share/public'; import * as shared from './shared'; export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA'; export interface PluginDeps { - discover: Pick<DiscoverStart, 'urlGenerator'>; + discover: Pick<DiscoverStart, 'locator'>; kibanaLegacy?: { dashboardConfig: { getHideWriteControls: KibanaLegacyStart['dashboardConfig']['getHideWriteControls']; @@ -26,7 +26,7 @@ export interface PluginDeps { } export interface CoreDeps { - application: Pick<CoreStart['application'], 'navigateToApp'>; + application: Pick<CoreStart['application'], 'navigateToApp' | 'getUrlForApp'>; } export interface Params { @@ -43,7 +43,7 @@ export abstract class AbstractExploreDataAction<Context extends { embeddable?: I constructor(protected readonly params: Params) {} - protected abstract getUrl(context: Context): Promise<KibanaURL>; + protected abstract getLocation(context: Context): Promise<KibanaLocation>; public async isCompatible({ embeddable }: Context): Promise<boolean> { if (!embeddable) return false; @@ -52,7 +52,7 @@ export abstract class AbstractExploreDataAction<Context extends { embeddable?: I const { capabilities } = core.application; if (capabilities.discover && !capabilities.discover.show) return false; - if (!plugins.discover.urlGenerator) return false; + if (!plugins.discover.locator) return false; const isDashboardOnlyMode = !!this.params .start() .plugins.kibanaLegacy?.dashboardConfig.getHideWriteControls(); @@ -68,10 +68,10 @@ export abstract class AbstractExploreDataAction<Context extends { embeddable?: I if (!shared.hasExactlyOneIndexPattern(context.embeddable)) return; const { core } = this.params.start(); - const { appName, appPath } = await this.getUrl(context); + const { app, path } = await this.getLocation(context); - await core.application.navigateToApp(appName, { - path: appPath, + await core.application.navigateToApp(app, { + path, }); } @@ -82,8 +82,10 @@ export abstract class AbstractExploreDataAction<Context extends { embeddable?: I throw new Error(`Embeddable not supported for "${this.getDisplayName(context)}" action.`); } - const { path } = await this.getUrl(context); + const { core } = this.params.start(); + const { app, path } = await this.getLocation(context); + const url = await core.application.getUrlForApp(app, { path, absolute: false }); - return path; + return url; } } diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts index 9fae78eb156da..23ac882e4ecf7 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts @@ -8,7 +8,6 @@ import { ExploreDataChartAction } from './explore_data_chart_action'; import { Params, PluginDeps } from './abstract_explore_data_action'; import { coreMock } from '../../../../../../src/core/public/mocks'; -import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public'; import { ExploreDataChartActionContext } from './explore_data_chart_action'; import { i18n } from '@kbn/i18n'; import { @@ -17,6 +16,7 @@ import { } from '../../../../../../src/plugins/visualizations/public'; import { ViewMode } from '../../../../../../src/plugins/embeddable/public'; import { Filter, RangeFilter } from '../../../../../../src/plugins/data/public'; +import { DiscoverAppLocator } from '../../../../../../src/plugins/discover/public'; const i18nTranslateSpy = (i18n.translate as unknown) as jest.SpyInstance; @@ -43,17 +43,23 @@ const setup = ( dashboardOnlyMode?: boolean; } = { filters: [] } ) => { - type UrlGenerator = UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>; - const core = coreMock.createStart(); - - const urlGenerator: UrlGenerator = ({ - createUrl: jest.fn(() => Promise.resolve('/xyz/app/discover/foo#bar')), - } as unknown) as UrlGenerator; + const locator: DiscoverAppLocator = { + getLocation: jest.fn(() => + Promise.resolve({ + app: 'discover', + path: '/foo#bar', + state: {}, + }) + ), + navigate: jest.fn(async () => {}), + getUrl: jest.fn(), + useUrl: jest.fn(), + }; const plugins: PluginDeps = { discover: { - urlGenerator, + locator, }, kibanaLegacy: { dashboardConfig: { @@ -95,7 +101,7 @@ const setup = ( embeddable, } as ExploreDataChartActionContext; - return { core, plugins, urlGenerator, params, action, input, output, embeddable, context }; + return { core, plugins, locator, params, action, input, output, embeddable, context }; }; describe('"Explore underlying data" panel action', () => { @@ -132,7 +138,7 @@ describe('"Explore underlying data" panel action', () => { test('returns false when URL generator is not present', async () => { const { action, plugins, context } = setup(); - (plugins.discover as any).urlGenerator = undefined; + (plugins.discover as any).locator = undefined; const isCompatible = await action.isCompatible(context); @@ -205,23 +211,15 @@ describe('"Explore underlying data" panel action', () => { }); describe('getHref()', () => { - test('returns URL path generated by URL generator', async () => { - const { action, context } = setup(); - - const href = await action.getHref(context); - - expect(href).toBe('/xyz/app/discover/foo#bar'); - }); - test('calls URL generator with right arguments', async () => { - const { action, urlGenerator, context } = setup(); + const { action, locator, context } = setup(); - expect(urlGenerator.createUrl).toHaveBeenCalledTimes(0); + expect(locator.getLocation).toHaveBeenCalledTimes(0); await action.getHref(context); - expect(urlGenerator.createUrl).toHaveBeenCalledTimes(1); - expect(urlGenerator.createUrl).toHaveBeenCalledWith({ + expect(locator.getLocation).toHaveBeenCalledTimes(1); + expect(locator.getLocation).toHaveBeenCalledWith({ filters: [], indexPatternId: 'index-ptr-foo', timeRange: undefined, @@ -260,11 +258,11 @@ describe('"Explore underlying data" panel action', () => { }, ]; - const { action, context, urlGenerator } = setup({ filters, timeFieldName }); + const { action, context, locator } = setup({ filters, timeFieldName }); await action.getHref(context); - expect(urlGenerator.createUrl).toHaveBeenCalledWith({ + expect(locator.getLocation).toHaveBeenCalledWith({ filters: [ { meta: { diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts index 32264ee1deceb..7b59a4e51d042 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts @@ -7,7 +7,7 @@ import { Action } from '../../../../../../src/plugins/ui_actions/public'; import { - DiscoverUrlGeneratorState, + DiscoverAppLocatorParams, SearchInput, } from '../../../../../../src/plugins/discover/public'; import { @@ -15,7 +15,7 @@ import { esFilters, } from '../../../../../../src/plugins/data/public'; import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; -import { KibanaURL } from '../../../../../../src/plugins/share/public'; +import { KibanaLocation } from '../../../../../../src/plugins/share/public'; import * as shared from './shared'; import { AbstractExploreDataAction } from './abstract_explore_data_action'; @@ -43,14 +43,14 @@ export class ExploreDataChartAction return super.isCompatible(context); } - protected readonly getUrl = async ( + protected readonly getLocation = async ( context: ExploreDataChartActionContext - ): Promise<KibanaURL> => { + ): Promise<KibanaLocation> => { const { plugins } = this.params.start(); - const { urlGenerator } = plugins.discover; + const { locator } = plugins.discover; - if (!urlGenerator) { - throw new Error('Discover URL generator not available.'); + if (!locator) { + throw new Error('Discover URL locator not available.'); } const { embeddable } = context; @@ -59,23 +59,23 @@ export class ExploreDataChartAction context.timeFieldName ); - const state: DiscoverUrlGeneratorState = { + const params: DiscoverAppLocatorParams = { filters, timeRange, }; if (embeddable) { - state.indexPatternId = shared.getIndexPatterns(embeddable)[0] || undefined; + params.indexPatternId = shared.getIndexPatterns(embeddable)[0] || undefined; const input = embeddable.getInput() as Readonly<SearchInput>; - if (input.timeRange && !state.timeRange) state.timeRange = input.timeRange; - if (input.query) state.query = input.query; - if (input.filters) state.filters = [...input.filters, ...(state.filters || [])]; + if (input.timeRange && !params.timeRange) params.timeRange = input.timeRange; + if (input.query) params.query = input.query; + if (input.filters) params.filters = [...input.filters, ...(params.filters || [])]; } - const path = await urlGenerator.createUrl(state); + const location = await locator.getLocation(params); - return new KibanaURL(path); + return location; }; } diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts index 842c7d6b339b4..5bdac602ec271 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts @@ -8,13 +8,13 @@ import { ExploreDataContextMenuAction } from './explore_data_context_menu_action'; import { Params, PluginDeps } from './abstract_explore_data_action'; import { coreMock } from '../../../../../../src/core/public/mocks'; -import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public'; import { i18n } from '@kbn/i18n'; import { VisualizeEmbeddableContract, VISUALIZE_EMBEDDABLE_TYPE, } from '../../../../../../src/plugins/visualizations/public'; import { ViewMode } from '../../../../../../src/plugins/embeddable/public'; +import { DiscoverAppLocator } from '../../../../../../src/plugins/discover/public'; const i18nTranslateSpy = (i18n.translate as unknown) as jest.SpyInstance; @@ -29,17 +29,23 @@ afterEach(() => { }); const setup = ({ dashboardOnlyMode = false }: { dashboardOnlyMode?: boolean } = {}) => { - type UrlGenerator = UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>; - const core = coreMock.createStart(); - - const urlGenerator: UrlGenerator = ({ - createUrl: jest.fn(() => Promise.resolve('/xyz/app/discover/foo#bar')), - } as unknown) as UrlGenerator; + const locator: DiscoverAppLocator = { + getLocation: jest.fn(() => + Promise.resolve({ + app: 'discover', + path: '/foo#bar', + state: {}, + }) + ), + navigate: jest.fn(async () => {}), + getUrl: jest.fn(), + useUrl: jest.fn(), + }; const plugins: PluginDeps = { discover: { - urlGenerator, + locator, }, kibanaLegacy: { dashboardConfig: { @@ -79,7 +85,7 @@ const setup = ({ dashboardOnlyMode = false }: { dashboardOnlyMode?: boolean } = embeddable, }; - return { core, plugins, urlGenerator, params, action, input, output, embeddable, context }; + return { core, plugins, locator, params, action, input, output, embeddable, context }; }; describe('"Explore underlying data" panel action', () => { @@ -116,7 +122,7 @@ describe('"Explore underlying data" panel action', () => { test('returns false when URL generator is not present', async () => { const { action, plugins, context } = setup(); - (plugins.discover as any).urlGenerator = undefined; + (plugins.discover as any).locator = undefined; const isCompatible = await action.isCompatible(context); @@ -189,23 +195,15 @@ describe('"Explore underlying data" panel action', () => { }); describe('getHref()', () => { - test('returns URL path generated by URL generator', async () => { - const { action, context } = setup(); - - const href = await action.getHref(context); - - expect(href).toBe('/xyz/app/discover/foo#bar'); - }); - test('calls URL generator with right arguments', async () => { - const { action, urlGenerator, context } = setup(); + const { action, locator, context } = setup(); - expect(urlGenerator.createUrl).toHaveBeenCalledTimes(0); + expect(locator.getLocation).toHaveBeenCalledTimes(0); await action.getHref(context); - expect(urlGenerator.createUrl).toHaveBeenCalledTimes(1); - expect(urlGenerator.createUrl).toHaveBeenCalledWith({ + expect(locator.getLocation).toHaveBeenCalledTimes(1); + expect(locator.getLocation).toHaveBeenCalledWith({ indexPatternId: 'index-ptr-foo', }); }); diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts index 99a2afd239645..88c093a299cb9 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts @@ -12,8 +12,8 @@ import { IEmbeddable, } from '../../../../../../src/plugins/embeddable/public'; import { Query, Filter, TimeRange } from '../../../../../../src/plugins/data/public'; -import { DiscoverUrlGeneratorState } from '../../../../../../src/plugins/discover/public'; -import { KibanaURL } from '../../../../../../src/plugins/share/public'; +import { DiscoverAppLocatorParams } from '../../../../../../src/plugins/discover/public'; +import { KibanaLocation } from '../../../../../../src/plugins/share/public'; import * as shared from './shared'; import { AbstractExploreDataAction } from './abstract_explore_data_action'; @@ -40,29 +40,31 @@ export class ExploreDataContextMenuAction public readonly order = 200; - protected readonly getUrl = async (context: EmbeddableQueryContext): Promise<KibanaURL> => { + protected readonly getLocation = async ( + context: EmbeddableQueryContext + ): Promise<KibanaLocation> => { const { plugins } = this.params.start(); - const { urlGenerator } = plugins.discover; + const { locator } = plugins.discover; - if (!urlGenerator) { - throw new Error('Discover URL generator not available.'); + if (!locator) { + throw new Error('Discover URL locator not available.'); } const { embeddable } = context; - const state: DiscoverUrlGeneratorState = {}; + const params: DiscoverAppLocatorParams = {}; if (embeddable) { - state.indexPatternId = shared.getIndexPatterns(embeddable)[0] || undefined; + params.indexPatternId = shared.getIndexPatterns(embeddable)[0] || undefined; const input = embeddable.getInput(); - if (input.timeRange && !state.timeRange) state.timeRange = input.timeRange; - if (input.query) state.query = input.query; - if (input.filters) state.filters = [...input.filters, ...(state.filters || [])]; + if (input.timeRange && !params.timeRange) params.timeRange = input.timeRange; + if (input.query) params.query = input.query; + if (input.filters) params.filters = [...input.filters, ...(params.filters || [])]; } - const path = await urlGenerator.createUrl(state); + const location = await locator.getLocation(params); - return new KibanaURL(path); + return location; }; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx index 1b5a8084f5b59..d5bb525cfd332 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/api_logs_table.tsx @@ -26,6 +26,8 @@ import { ApiLogsLogic } from '../index'; import { ApiLog } from '../types'; import { getStatusColor } from '../utils'; +import { EmptyState } from './'; + import './api_logs_table.scss'; interface Props { @@ -108,6 +110,7 @@ export const ApiLogsTable: React.FC<Props> = ({ hasPagination }) => { items={apiLogs} responsive loading={dataLoading} + noItemsMessage={<EmptyState />} {...paginationProps} /> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.test.tsx index 3ad22ceac5840..19f45ced5dc5d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.test.tsx @@ -19,7 +19,7 @@ describe('EmptyState', () => { .find(EuiEmptyPrompt) .dive(); - expect(wrapper.find('h2').text()).toEqual('Perform your first API call'); + expect(wrapper.find('h2').text()).toEqual('No API events in the last 24 hours'); expect(wrapper.find(EuiButton).prop('href')).toEqual( expect.stringContaining('/api-reference.html') ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.tsx index 3f6f44adefc71..76bd0cba1731f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/components/empty_state.tsx @@ -18,14 +18,14 @@ export const EmptyState: React.FC = () => ( title={ <h2> {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyTitle', { - defaultMessage: 'Perform your first API call', + defaultMessage: 'No API events in the last 24 hours', })} </h2> } body={ <p> {i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyDescription', { - defaultMessage: "Check back after you've performed some API calls.", + defaultMessage: 'Logs will update in real-time when an API request occurs.', })} </p> } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_landing.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_landing.tsx index a2993b4d86d5a..91a0a7c5edcc0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_landing.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_landing.tsx @@ -7,29 +7,25 @@ import React from 'react'; -import { - EuiButton, - EuiLink, - EuiPageHeader, - EuiPanel, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; +import { EuiButton, EuiLink, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { getAppSearchUrl } from '../../../shared/enterprise_search_url'; import { DOCS_PREFIX, ENGINE_CRAWLER_PATH } from '../../routes'; -import { generateEnginePath } from '../engine'; +import { generateEnginePath, getEngineBreadcrumbs } from '../engine'; +import { AppSearchPageTemplate } from '../layout'; import './crawler_landing.scss'; import { CRAWLER_TITLE } from '.'; export const CrawlerLanding: React.FC = () => ( - <div data-test-subj="CrawlerLanding" className="crawlerLanding"> - <EuiPageHeader pageTitle={CRAWLER_TITLE} /> - <EuiSpacer /> - <EuiPanel grow paddingSize="l" className="crawlerLanding__panel"> + <AppSearchPageTemplate + pageChrome={getEngineBreadcrumbs([CRAWLER_TITLE])} + pageHeader={{ pageTitle: CRAWLER_TITLE }} + className="crawlerLanding" + data-test-subj="CrawlerLanding" + > + <EuiPanel hasBorder paddingSize="l" className="crawlerLanding__panel"> <div className="crawlerLanding__wrapper"> <EuiTitle size="s"> <h2> @@ -81,5 +77,5 @@ export const CrawlerLanding: React.FC = () => ( <EuiSpacer size="xl" /> </div> </EuiPanel> - </div> + </AppSearchPageTemplate> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx index affc2fd08e34c..3804ecfe7c67d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx @@ -7,14 +7,12 @@ import { setMockActions, setMockValues } from '../../../__mocks__/kea_logic'; import '../../../__mocks__/shallow_useeffect.mock'; +import '../../__mocks__/engine_logic.mock'; import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; -import { Loading } from '../../../shared/loading'; -import { rerender } from '../../../test_helpers'; - import { DomainsTable } from './components/domains_table'; import { CrawlerOverview } from './crawler_overview'; @@ -50,11 +48,4 @@ describe('CrawlerOverview', () => { // TODO test for empty state after it is built in a future PR }); - - it('shows a loading state when data is loading', () => { - setMockValues({ dataLoading: true }); - rerender(wrapper); - - expect(wrapper.find(Loading)).toHaveLength(1); - }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx index 14906378692ed..9e484df35e7a2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx @@ -9,10 +9,8 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; -import { EuiPageHeader } from '@elastic/eui'; - -import { FlashMessages } from '../../../shared/flash_messages'; -import { Loading } from '../../../shared/loading'; +import { getEngineBreadcrumbs } from '../engine'; +import { AppSearchPageTemplate } from '../layout'; import { DomainsTable } from './components/domains_table'; import { CRAWLER_TITLE } from './constants'; @@ -27,15 +25,13 @@ export const CrawlerOverview: React.FC = () => { fetchCrawlerData(); }, []); - if (dataLoading) { - return <Loading />; - } - return ( - <> - <EuiPageHeader pageTitle={CRAWLER_TITLE} /> - <FlashMessages /> + <AppSearchPageTemplate + pageChrome={getEngineBreadcrumbs([CRAWLER_TITLE])} + pageHeader={{ pageTitle: CRAWLER_TITLE }} + isLoading={dataLoading} + > <DomainsTable /> - </> + </AppSearchPageTemplate> ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.test.tsx index c11c656333010..587ba61ce27e9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.test.tsx @@ -5,9 +5,6 @@ * 2.0. */ -import { setMockValues } from '../../../__mocks__/kea_logic'; -import { mockEngineValues } from '../../__mocks__'; - import React from 'react'; import { Switch } from 'react-router-dom'; @@ -22,7 +19,6 @@ describe('CrawlerRouter', () => { beforeEach(() => { jest.clearAllMocks(); - setMockValues({ ...mockEngineValues }); }); afterEach(() => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx index 926c45b437937..a0145cf76908a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_router.tsx @@ -8,11 +8,6 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; - -import { getEngineBreadcrumbs } from '../engine'; - -import { CRAWLER_TITLE } from './constants'; import { CrawlerLanding } from './crawler_landing'; import { CrawlerOverview } from './crawler_overview'; @@ -20,7 +15,6 @@ export const CrawlerRouter: React.FC = () => { return ( <Switch> <Route> - <SetPageChrome trail={getEngineBreadcrumbs([CRAWLER_TITLE])} /> {process.env.NODE_ENV === 'development' ? <CrawlerOverview /> : <CrawlerLanding />} </Route> </Switch> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts index 37c1e9a7a1a2e..c490910184a69 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/constants.ts @@ -18,7 +18,7 @@ export const CURATIONS_OVERVIEW_TITLE = i18n.translate( ); export const CREATE_NEW_CURATION_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.curations.create.title', - { defaultMessage: 'Create new curation' } + { defaultMessage: 'Create a curation' } ); export const MANAGE_CURATION_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.curations.manage.title', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx index 937acfd84ce83..2efe1f2ffe86f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.test.tsx @@ -8,16 +8,13 @@ import '../../../../__mocks__/shallow_useeffect.mock'; import { setMockActions, setMockValues } from '../../../../__mocks__/kea_logic'; import { mockUseParams } from '../../../../__mocks__/react_router'; +import '../../../__mocks__/engine_logic.mock'; import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; -import { EuiPageHeader } from '@elastic/eui'; - -import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; -import { Loading } from '../../../../shared/loading'; -import { rerender } from '../../../../test_helpers'; +import { rerender, getPageTitle, getPageHeaderActions } from '../../../../test_helpers'; jest.mock('./curation_logic', () => ({ CurationLogic: jest.fn() })); import { CurationLogic } from './curation_logic'; @@ -27,9 +24,6 @@ import { AddResultFlyout } from './results'; import { Curation } from './'; describe('Curation', () => { - const props = { - curationsBreadcrumb: ['Engines', 'some-engine', 'Curations'], - }; const values = { dataLoading: false, queries: ['query A', 'query B'], @@ -47,39 +41,34 @@ describe('Curation', () => { }); it('renders', () => { - const wrapper = shallow(<Curation {...props} />); + const wrapper = shallow(<Curation />); - expect(wrapper.find(EuiPageHeader).prop('pageTitle')).toEqual('Manage curation'); - expect(wrapper.find(SetPageChrome).prop('trail')).toEqual([ - ...props.curationsBreadcrumb, + expect(getPageTitle(wrapper)).toEqual('Manage curation'); + expect(wrapper.prop('pageChrome')).toEqual([ + 'Engines', + 'some-engine', + 'Curations', 'query A, query B', ]); }); - it('renders a loading component on page load', () => { - setMockValues({ ...values, dataLoading: true }); - const wrapper = shallow(<Curation {...props} />); - - expect(wrapper.find(Loading)).toHaveLength(1); - }); - it('renders the add result flyout when open', () => { setMockValues({ ...values, isFlyoutOpen: true }); - const wrapper = shallow(<Curation {...props} />); + const wrapper = shallow(<Curation />); expect(wrapper.find(AddResultFlyout)).toHaveLength(1); }); it('initializes CurationLogic with a curationId prop from URL param', () => { mockUseParams.mockReturnValueOnce({ curationId: 'hello-world' }); - shallow(<Curation {...props} />); + shallow(<Curation />); expect(CurationLogic).toHaveBeenCalledWith({ curationId: 'hello-world' }); }); it('calls loadCuration on page load & whenever the curationId URL param changes', () => { mockUseParams.mockReturnValueOnce({ curationId: 'cur-123456789' }); - const wrapper = shallow(<Curation {...props} />); + const wrapper = shallow(<Curation />); expect(actions.loadCuration).toHaveBeenCalledTimes(1); mockUseParams.mockReturnValueOnce({ curationId: 'cur-987654321' }); @@ -92,9 +81,8 @@ describe('Curation', () => { let confirmSpy: jest.SpyInstance; beforeAll(() => { - const wrapper = shallow(<Curation {...props} />); - const headerActions = wrapper.find(EuiPageHeader).prop('rightSideItems'); - restoreDefaultsButton = shallow(headerActions![0] as React.ReactElement); + const wrapper = shallow(<Curation />); + restoreDefaultsButton = getPageHeaderActions(wrapper).childAt(0); confirmSpy = jest.spyOn(window, 'confirm'); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx index ffa9fd8422a1b..2a01c0db049ab 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation.tsx @@ -10,26 +10,19 @@ import { useParams } from 'react-router-dom'; import { useValues, useActions } from 'kea'; -import { EuiPageHeader, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; - -import { FlashMessages } from '../../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; -import { BreadcrumbTrail } from '../../../../shared/kibana_chrome/generate_breadcrumbs'; -import { Loading } from '../../../../shared/loading'; +import { EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; import { RESTORE_DEFAULTS_BUTTON_LABEL } from '../../../constants'; +import { AppSearchPageTemplate } from '../../layout'; import { MANAGE_CURATION_TITLE, RESTORE_CONFIRMATION } from '../constants'; +import { getCurationsBreadcrumbs } from '../utils'; import { CurationLogic } from './curation_logic'; import { PromotedDocuments, OrganicDocuments, HiddenDocuments } from './documents'; import { ActiveQuerySelect, ManageQueriesModal } from './queries'; import { AddResultLogic, AddResultFlyout } from './results'; -interface Props { - curationsBreadcrumb: BreadcrumbTrail; -} - -export const Curation: React.FC<Props> = ({ curationsBreadcrumb }) => { +export const Curation: React.FC = () => { const { curationId } = useParams() as { curationId: string }; const { loadCuration, resetCuration } = useActions(CurationLogic({ curationId })); const { dataLoading, queries } = useValues(CurationLogic({ curationId })); @@ -39,14 +32,12 @@ export const Curation: React.FC<Props> = ({ curationsBreadcrumb }) => { loadCuration(); }, [curationId]); - if (dataLoading) return <Loading />; - return ( - <> - <SetPageChrome trail={[...curationsBreadcrumb, queries.join(', ')]} /> - <EuiPageHeader - pageTitle={MANAGE_CURATION_TITLE} - rightSideItems={[ + <AppSearchPageTemplate + pageChrome={getCurationsBreadcrumbs([queries.join(', ')])} + pageHeader={{ + pageTitle: MANAGE_CURATION_TITLE, + rightSideItems: [ <EuiButton color="danger" onClick={() => { @@ -55,10 +46,10 @@ export const Curation: React.FC<Props> = ({ curationsBreadcrumb }) => { > {RESTORE_DEFAULTS_BUTTON_LABEL} </EuiButton>, - ]} - responsive={false} - /> - + ], + }} + isLoading={dataLoading} + > <EuiFlexGroup alignItems="flexEnd" gutterSize="xl" responsive={false}> <EuiFlexItem> <ActiveQuerySelect /> @@ -69,7 +60,6 @@ export const Curation: React.FC<Props> = ({ curationsBreadcrumb }) => { </EuiFlexGroup> <EuiSpacer size="xl" /> - <FlashMessages /> <PromotedDocuments /> <EuiSpacer /> @@ -78,6 +68,6 @@ export const Curation: React.FC<Props> = ({ curationsBreadcrumb }) => { <HiddenDocuments /> {isFlyoutOpen && <AddResultFlyout />} - </> + </AppSearchPageTemplate> ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/hidden_documents.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/hidden_documents.tsx index f2bc416b00341..8cb06f32d9e4e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/hidden_documents.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/documents/hidden_documents.tsx @@ -80,7 +80,7 @@ export const HiddenDocuments: React.FC = () => { <h3> {i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.curations.hiddenDocuments.emptyTitle', - { defaultMessage: 'No documents are being hidden for this query' } + { defaultMessage: "You haven't hidden any documents yet" } )} </h3> } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx index 9598212d3e0c9..a241edb8020a4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.test.tsx @@ -19,6 +19,6 @@ describe('CurationsRouter', () => { const wrapper = shallow(<CurationsRouter />); expect(wrapper.find(Switch)).toHaveLength(1); - expect(wrapper.find(Route)).toHaveLength(4); + expect(wrapper.find(Route)).toHaveLength(3); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx index 28ce311b43887..40f2d07ab61ab 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_router.tsx @@ -8,38 +8,26 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; -import { APP_SEARCH_PLUGIN } from '../../../../../common/constants'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; -import { NotFound } from '../../../shared/not_found'; import { ENGINE_CURATIONS_PATH, ENGINE_CURATIONS_NEW_PATH, ENGINE_CURATION_PATH, } from '../../routes'; -import { getEngineBreadcrumbs } from '../engine'; -import { CURATIONS_TITLE, CREATE_NEW_CURATION_TITLE } from './constants'; import { Curation } from './curation'; import { Curations, CurationCreation } from './views'; export const CurationsRouter: React.FC = () => { - const CURATIONS_BREADCRUMB = getEngineBreadcrumbs([CURATIONS_TITLE]); - return ( <Switch> <Route exact path={ENGINE_CURATIONS_PATH}> - <SetPageChrome trail={CURATIONS_BREADCRUMB} /> <Curations /> </Route> <Route exact path={ENGINE_CURATIONS_NEW_PATH}> - <SetPageChrome trail={[...CURATIONS_BREADCRUMB, CREATE_NEW_CURATION_TITLE]} /> <CurationCreation /> </Route> <Route path={ENGINE_CURATION_PATH}> - <Curation curationsBreadcrumb={CURATIONS_BREADCRUMB} /> - </Route> - <Route> - <NotFound breadcrumbs={CURATIONS_BREADCRUMB} product={APP_SEARCH_PLUGIN} /> + <Curation /> </Route> </Switch> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.test.ts index 51618ed4e3741..02641b09255e5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.test.ts @@ -5,7 +5,21 @@ * 2.0. */ -import { convertToDate, addDocument, removeDocument } from './utils'; +import '../../__mocks__/engine_logic.mock'; + +import { getCurationsBreadcrumbs, convertToDate, addDocument, removeDocument } from './utils'; + +describe('getCurationsBreadcrumbs', () => { + it('generates curation-prefixed breadcrumbs', () => { + expect(getCurationsBreadcrumbs()).toEqual(['Engines', 'some-engine', 'Curations']); + expect(getCurationsBreadcrumbs(['Some page'])).toEqual([ + 'Engines', + 'some-engine', + 'Curations', + 'Some page', + ]); + }); +}); describe('convertToDate', () => { it('converts the English-only server timestamps to a parseable Date', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.ts index 8af2636128304..978b63885fbdd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/utils.ts @@ -5,6 +5,14 @@ * 2.0. */ +import { BreadcrumbTrail } from '../../../shared/kibana_chrome/generate_breadcrumbs'; +import { getEngineBreadcrumbs } from '../engine'; + +import { CURATIONS_TITLE } from './constants'; + +export const getCurationsBreadcrumbs = (breadcrumbs: BreadcrumbTrail = []) => + getEngineBreadcrumbs([CURATIONS_TITLE, ...breadcrumbs]); + // The server API feels us an English datestring, but we want to convert // it to an actual Date() instance so that we can localize date formats. export const convertToDate = (serverDateString: string): Date => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.test.tsx index ad306dfc73080..33aab9943cc83 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.test.tsx @@ -6,6 +6,7 @@ */ import { setMockActions } from '../../../../__mocks__/kea_logic'; +import '../../../__mocks__/engine_logic.mock'; import React from 'react'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx index 32d46775a2125..9aa1759cec5c0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_creation.tsx @@ -9,10 +9,10 @@ import React from 'react'; import { useActions } from 'kea'; -import { EuiPageHeader, EuiPageContent, EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; +import { EuiPanel, EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FlashMessages } from '../../../../shared/flash_messages'; +import { AppSearchPageTemplate } from '../../layout'; import { MultiInputRows } from '../../multi_input_rows'; import { @@ -21,15 +21,17 @@ import { QUERY_INPUTS_PLACEHOLDER, } from '../constants'; import { CurationsLogic } from '../index'; +import { getCurationsBreadcrumbs } from '../utils'; export const CurationCreation: React.FC = () => { const { createCuration } = useActions(CurationsLogic); return ( - <> - <EuiPageHeader pageTitle={CREATE_NEW_CURATION_TITLE} /> - <FlashMessages /> - <EuiPageContent hasBorder> + <AppSearchPageTemplate + pageChrome={getCurationsBreadcrumbs([CREATE_NEW_CURATION_TITLE])} + pageHeader={{ pageTitle: CREATE_NEW_CURATION_TITLE }} + > + <EuiPanel hasBorder> <EuiTitle> <h2> {i18n.translate( @@ -56,7 +58,7 @@ export const CurationCreation: React.FC = () => { inputPlaceholder={QUERY_INPUTS_PLACEHOLDER} onSubmit={(queries) => createCuration(queries)} /> - </EuiPageContent> - </> + </EuiPanel> + </AppSearchPageTemplate> ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx index bcc402d6eea27..85827d5374179 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.test.tsx @@ -6,17 +6,16 @@ */ import { mockKibanaValues, setMockActions, setMockValues } from '../../../../__mocks__/kea_logic'; +import '../../../../__mocks__/react_router'; import '../../../__mocks__/engine_logic.mock'; import React from 'react'; import { shallow, ReactWrapper } from 'enzyme'; -import { EuiPageHeader, EuiBasicTable } from '@elastic/eui'; +import { EuiBasicTable } from '@elastic/eui'; -import { Loading } from '../../../../shared/loading'; -import { mountWithIntl } from '../../../../test_helpers'; -import { EmptyState } from '../components'; +import { mountWithIntl, getPageTitle } from '../../../../test_helpers'; import { Curations, CurationsTable } from './curations'; @@ -61,32 +60,34 @@ describe('Curations', () => { it('renders', () => { const wrapper = shallow(<Curations />); - expect(wrapper.find(EuiPageHeader).prop('pageTitle')).toEqual('Curated results'); + expect(getPageTitle(wrapper)).toEqual('Curated results'); expect(wrapper.find(CurationsTable)).toHaveLength(1); }); - it('renders a loading component on page load', () => { - setMockValues({ ...values, dataLoading: true, curations: [] }); - const wrapper = shallow(<Curations />); + describe('loading state', () => { + it('renders a full-page loading state on initial page load', () => { + setMockValues({ ...values, dataLoading: true, curations: [] }); + const wrapper = shallow(<Curations />); + + expect(wrapper.prop('isLoading')).toEqual(true); + }); + + it('does not re-render a full-page loading state after initial page load (uses component-level loading state instead)', () => { + setMockValues({ ...values, dataLoading: true, curations: [{}] }); + const wrapper = shallow(<Curations />); - expect(wrapper.find(Loading)).toHaveLength(1); + expect(wrapper.prop('isLoading')).toEqual(false); + }); }); it('calls loadCurations on page load', () => { + setMockValues({ ...values, myRole: {} }); // Required for AppSearchPageTemplate to load mountWithIntl(<Curations />); expect(actions.loadCurations).toHaveBeenCalledTimes(1); }); describe('CurationsTable', () => { - it('renders an empty state', () => { - setMockValues({ ...values, curations: [] }); - const table = shallow(<CurationsTable />).find(EuiBasicTable); - const noItemsMessage = table.prop('noItemsMessage') as React.ReactElement; - - expect(noItemsMessage.type).toEqual(EmptyState); - }); - it('passes loading prop based on dataLoading', () => { setMockValues({ ...values, dataLoading: true }); const wrapper = shallow(<CurationsTable />); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx index 80de9aba77258..12497ab52baf6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations.tsx @@ -9,25 +9,24 @@ import React, { useEffect } from 'react'; import { useValues, useActions } from 'kea'; -import { EuiBasicTable, EuiBasicTableColumn, EuiPageContent, EuiPageHeader } from '@elastic/eui'; +import { EuiBasicTable, EuiBasicTableColumn, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EDIT_BUTTON_LABEL, DELETE_BUTTON_LABEL } from '../../../../shared/constants'; -import { FlashMessages } from '../../../../shared/flash_messages'; import { KibanaLogic } from '../../../../shared/kibana'; -import { Loading } from '../../../../shared/loading'; import { EuiButtonTo, EuiLinkTo } from '../../../../shared/react_router_helpers'; import { convertMetaToPagination, handlePageChange } from '../../../../shared/table_pagination'; import { ENGINE_CURATIONS_NEW_PATH, ENGINE_CURATION_PATH } from '../../../routes'; import { FormattedDateTime } from '../../../utils/formatted_date_time'; import { generateEnginePath } from '../../engine'; +import { AppSearchPageTemplate } from '../../layout'; import { EmptyState } from '../components'; import { CURATIONS_OVERVIEW_TITLE, CREATE_NEW_CURATION_TITLE } from '../constants'; import { CurationsLogic } from '../curations_logic'; import { Curation } from '../types'; -import { convertToDate } from '../utils'; +import { getCurationsBreadcrumbs, convertToDate } from '../utils'; export const Curations: React.FC = () => { const { dataLoading, curations, meta } = useValues(CurationsLogic); @@ -37,23 +36,29 @@ export const Curations: React.FC = () => { loadCurations(); }, [meta.page.current]); - if (dataLoading && !curations.length) return <Loading />; - return ( - <> - <EuiPageHeader - pageTitle={CURATIONS_OVERVIEW_TITLE} - rightSideItems={[ - <EuiButtonTo to={generateEnginePath(ENGINE_CURATIONS_NEW_PATH)} fill> + <AppSearchPageTemplate + pageChrome={getCurationsBreadcrumbs()} + pageHeader={{ + pageTitle: CURATIONS_OVERVIEW_TITLE, + rightSideItems: [ + <EuiButtonTo + to={generateEnginePath(ENGINE_CURATIONS_NEW_PATH)} + iconType="plusInCircle" + fill + > {CREATE_NEW_CURATION_TITLE} </EuiButtonTo>, - ]} - /> - <EuiPageContent hasBorder> - <FlashMessages /> + ], + }} + isLoading={dataLoading && !curations.length} + isEmptyState={!curations.length} + emptyState={<EmptyState />} + > + <EuiPanel hasBorder> <CurationsTable /> - </EuiPageContent> - </> + </EuiPanel> + </AppSearchPageTemplate> ); }; @@ -139,7 +144,6 @@ export const CurationsTable: React.FC = () => { responsive hasActions loading={dataLoading} - noItemsMessage={<EmptyState />} pagination={{ ...convertMetaToPagination(meta), hidePerPageOptions: true, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/document_creation_button.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/document_creation_button.tsx index cded18094c5f2..482ee282cf464 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/document_creation_button.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/components/document_creation_button.tsx @@ -21,7 +21,7 @@ export const DocumentCreationButton: React.FC = () => { <> <EuiButton fill - color="primary" + iconType="plusInCircle" data-test-subj="IndexDocumentsButton" onClick={showCreationModes} > diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.test.tsx index b5b6dd453c9df..7e1b2acc81d18 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.test.tsx @@ -22,7 +22,6 @@ import { Documents } from '.'; describe('Documents', () => { const values = { isMetaEngine: false, - engine: { document_count: 1 }, myRole: { canManageEngineDocuments: true }, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx index 62c7759757bda..75044bfcc8fb7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx @@ -21,7 +21,7 @@ import { DOCUMENTS_TITLE } from './constants'; import { SearchExperience } from './search_experience'; export const Documents: React.FC = () => { - const { isMetaEngine, engine } = useValues(EngineLogic); + const { isMetaEngine, isEngineEmpty } = useValues(EngineLogic); const { myRole } = useValues(AppLogic); return ( @@ -32,7 +32,7 @@ export const Documents: React.FC = () => { rightSideItems: myRole.canManageEngineDocuments && !isMetaEngine ? [<DocumentCreationButton />] : [], }} - isEmptyState={!engine.document_count} + isEmptyState={isEngineEmpty} emptyState={<EmptyState />} > {isMetaEngine && ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx index 709dfc69905f0..8416974ad7a2e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx @@ -18,7 +18,7 @@ import { i18n } from '@kbn/i18n'; import './search_experience.scss'; -import { externalUrl } from '../../../../shared/enterprise_search_url'; +import { HttpLogic } from '../../../../shared/http'; import { useLocalStorage } from '../../../../shared/use_local_storage'; import { EngineLogic } from '../../engine'; @@ -52,7 +52,8 @@ const DEFAULT_SORT_OPTIONS: SortOption[] = [ export const SearchExperience: React.FC = () => { const { engine } = useValues(EngineLogic); - const endpointBase = externalUrl.enterpriseSearchUrl; + const { http } = useValues(HttpLogic); + const endpointBase = http.basePath.prepend('/api/app_search/search-ui'); const [showCustomizationModal, setShowCustomizationModal] = useState(false); const openCustomizationModal = () => setShowCustomizationModal(true); @@ -72,7 +73,9 @@ export const SearchExperience: React.FC = () => { cacheResponses: false, endpointBase, engineName: engine.name, - searchKey: engine.apiKey, + additionalHeaders: { + 'kbn-xsrf': true, + }, }); const searchProviderConfig = buildSearchUIConfig(connector, engine.schema || {}, fields); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/constants.ts new file mode 100644 index 0000000000000..9102f706fdbed --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/constants.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 { i18n } from '@kbn/i18n'; + +export const POLLING_DURATION = 5000; + +export const POLLING_ERROR_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.pollingErrorMessage', + { defaultMessage: 'Could not fetch engine data' } +); + +export const POLLING_ERROR_TEXT = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.pollingErrorDescription', + { defaultMessage: 'Please check your connection or manually reload the page.' } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts index 2b60193d4f7d3..0189edbbf871f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.test.ts @@ -5,10 +5,15 @@ * 2.0. */ -import { LogicMounter, mockHttpValues } from '../../../__mocks__/kea_logic'; +import { + LogicMounter, + mockHttpValues, + mockFlashMessageHelpers, +} from '../../../__mocks__/kea_logic'; import { nextTick } from '@kbn/test/jest'; +import { SchemaType } from '../../../shared/schema/types'; import { ApiTokenTypes } from '../credentials/constants'; import { EngineTypes } from './types'; @@ -16,8 +21,9 @@ import { EngineTypes } from './types'; import { EngineLogic } from './'; describe('EngineLogic', () => { - const { mount } = new LogicMounter(EngineLogic); + const { mount, unmount } = new LogicMounter(EngineLogic); const { http } = mockHttpValues; + const { flashErrorToast } = mockFlashMessageHelpers; const mockEngineData = { name: 'some-engine', @@ -34,7 +40,7 @@ describe('EngineLogic', () => { sample: false, isMeta: false, invalidBoosts: false, - schema: {}, + schema: { test: SchemaType.Text }, apiTokens: [], apiKey: 'some-key', }; @@ -43,6 +49,8 @@ describe('EngineLogic', () => { dataLoading: true, engine: {}, engineName: '', + isEngineEmpty: true, + isEngineSchemaEmpty: true, isMetaEngine: false, isSampleEngine: false, hasSchemaErrors: false, @@ -50,6 +58,14 @@ describe('EngineLogic', () => { hasUnconfirmedSchemaFields: false, engineNotFound: false, searchKey: '', + intervalId: null, + }; + + const DEFAULT_VALUES_WITH_ENGINE = { + ...DEFAULT_VALUES, + engine: mockEngineData, + isEngineEmpty: false, + isEngineSchemaEmpty: false, }; beforeEach(() => { @@ -69,7 +85,7 @@ describe('EngineLogic', () => { EngineLogic.actions.setEngineData(mockEngineData); expect(EngineLogic.values).toEqual({ - ...DEFAULT_VALUES, + ...DEFAULT_VALUES_WITH_ENGINE, engine: mockEngineData, dataLoading: false, }); @@ -154,6 +170,34 @@ describe('EngineLogic', () => { }); }); }); + + describe('onPollStart', () => { + it('should set intervalId', () => { + mount({ intervalId: null }); + EngineLogic.actions.onPollStart(123); + + expect(EngineLogic.values).toEqual({ + ...DEFAULT_VALUES, + intervalId: 123, + }); + }); + + describe('onPollStop', () => { + // Note: This does have to be a separate action following stopPolling(), rather + // than using stopPolling: () => null as a reducer. If you do that, then the ID + // gets cleared before the actual poll interval does & the poll interval never clears :doh: + + it('should reset intervalId', () => { + mount({ intervalId: 123 }); + EngineLogic.actions.onPollStop(); + + expect(EngineLogic.values).toEqual({ + ...DEFAULT_VALUES, + intervalId: null, + }); + }); + }); + }); }); describe('listeners', () => { @@ -170,28 +214,156 @@ describe('EngineLogic', () => { expect(EngineLogic.actions.setEngineData).toHaveBeenCalledWith(mockEngineData); }); - it('handles errors', async () => { + it('handles 4xx errors', async () => { mount(); jest.spyOn(EngineLogic.actions, 'setEngineNotFound'); - http.get.mockReturnValue(Promise.reject('An error occured')); + http.get.mockReturnValue(Promise.reject({ response: { status: 404 } })); EngineLogic.actions.initializeEngine(); await nextTick(); expect(EngineLogic.actions.setEngineNotFound).toHaveBeenCalledWith(true); }); + + it('handles 5xx errors', async () => { + mount(); + http.get.mockReturnValue(Promise.reject('An error occured')); + + EngineLogic.actions.initializeEngine(); + await nextTick(); + + expect(flashErrorToast).toHaveBeenCalledWith('Could not fetch engine data', { + text: expect.stringContaining('Please check your connection'), + toastLifeTimeMs: 3750, + }); + }); + }); + + describe('pollEmptyEngine', () => { + beforeEach(() => jest.useFakeTimers()); + afterEach(() => jest.clearAllTimers()); + afterAll(() => jest.useRealTimers()); + + it('starts a poll', () => { + mount(); + jest.spyOn(global, 'setInterval'); + jest.spyOn(EngineLogic.actions, 'onPollStart'); + + EngineLogic.actions.pollEmptyEngine(); + + expect(global.setInterval).toHaveBeenCalled(); + expect(EngineLogic.actions.onPollStart).toHaveBeenCalled(); + }); + + it('polls for engine data if the current engine is empty', () => { + mount({ engine: {} }); + jest.spyOn(EngineLogic.actions, 'initializeEngine'); + + EngineLogic.actions.pollEmptyEngine(); + + jest.advanceTimersByTime(5000); + expect(EngineLogic.actions.initializeEngine).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(5000); + expect(EngineLogic.actions.initializeEngine).toHaveBeenCalledTimes(2); + }); + + it('cancels the poll if the current engine changed from empty to non-empty', () => { + mount({ engine: mockEngineData }); + jest.spyOn(EngineLogic.actions, 'stopPolling'); + jest.spyOn(EngineLogic.actions, 'initializeEngine'); + + EngineLogic.actions.pollEmptyEngine(); + + jest.advanceTimersByTime(5000); + expect(EngineLogic.actions.stopPolling).toHaveBeenCalled(); + expect(EngineLogic.actions.initializeEngine).not.toHaveBeenCalled(); + }); + + it('does not create new polls if one already exists', () => { + jest.spyOn(global, 'setInterval'); + mount({ intervalId: 123 }); + + EngineLogic.actions.pollEmptyEngine(); + + expect(global.setInterval).not.toHaveBeenCalled(); + }); + }); + + describe('stopPolling', () => { + it('clears the poll interval and unsets the intervalId', () => { + jest.spyOn(global, 'clearInterval'); + mount({ intervalId: 123 }); + + EngineLogic.actions.stopPolling(); + + expect(global.clearInterval).toHaveBeenCalledWith(123); + expect(EngineLogic.values.intervalId).toEqual(null); + }); + + it('does not clearInterval if a poll has not been started', () => { + jest.spyOn(global, 'clearInterval'); + mount({ intervalId: null }); + + EngineLogic.actions.stopPolling(); + + expect(global.clearInterval).not.toHaveBeenCalled(); + }); }); }); describe('selectors', () => { + describe('isEngineEmpty', () => { + it('returns true if the engine contains no documents', () => { + const engine = { ...mockEngineData, document_count: 0 }; + mount({ engine }); + + expect(EngineLogic.values).toEqual({ + ...DEFAULT_VALUES_WITH_ENGINE, + engine, + isEngineEmpty: true, + }); + }); + + it('returns true if the engine is not yet initialized', () => { + mount({ engine: {} }); + + expect(EngineLogic.values).toEqual({ + ...DEFAULT_VALUES, + isEngineEmpty: true, + }); + }); + }); + + describe('isEngineSchemaEmpty', () => { + it('returns true if the engine schema contains no fields', () => { + const engine = { ...mockEngineData, schema: {} }; + mount({ engine }); + + expect(EngineLogic.values).toEqual({ + ...DEFAULT_VALUES_WITH_ENGINE, + engine, + isEngineSchemaEmpty: true, + }); + }); + + it('returns true if the engine is not yet initialized', () => { + mount({ engine: {} }); + + expect(EngineLogic.values).toEqual({ + ...DEFAULT_VALUES, + isEngineSchemaEmpty: true, + }); + }); + }); + describe('isSampleEngine', () => { it('should be set based on engine.sample', () => { - const mockSampleEngine = { ...mockEngineData, sample: true }; - mount({ engine: mockSampleEngine }); + const engine = { ...mockEngineData, sample: true }; + mount({ engine }); expect(EngineLogic.values).toEqual({ - ...DEFAULT_VALUES, - engine: mockSampleEngine, + ...DEFAULT_VALUES_WITH_ENGINE, + engine, isSampleEngine: true, }); }); @@ -199,12 +371,12 @@ describe('EngineLogic', () => { describe('isMetaEngine', () => { it('should be set based on engine.type', () => { - const mockMetaEngine = { ...mockEngineData, type: EngineTypes.meta }; - mount({ engine: mockMetaEngine }); + const engine = { ...mockEngineData, type: EngineTypes.meta }; + mount({ engine }); expect(EngineLogic.values).toEqual({ - ...DEFAULT_VALUES, - engine: mockMetaEngine, + ...DEFAULT_VALUES_WITH_ENGINE, + engine, isMetaEngine: true, }); }); @@ -212,17 +384,17 @@ describe('EngineLogic', () => { describe('hasSchemaErrors', () => { it('should be set based on engine.activeReindexJob.numDocumentsWithErrors', () => { - const mockSchemaEngine = { + const engine = { ...mockEngineData, activeReindexJob: { numDocumentsWithErrors: 10, }, }; - mount({ engine: mockSchemaEngine }); + mount({ engine }); expect(EngineLogic.values).toEqual({ - ...DEFAULT_VALUES, - engine: mockSchemaEngine, + ...DEFAULT_VALUES_WITH_ENGINE, + engine, hasSchemaErrors: true, }); }); @@ -230,7 +402,7 @@ describe('EngineLogic', () => { describe('hasSchemaConflicts', () => { it('should be set based on engine.schemaConflicts', () => { - const mockSchemaEngine = { + const engine = { ...mockEngineData, schemaConflicts: { someSchemaField: { @@ -241,11 +413,11 @@ describe('EngineLogic', () => { }, }, }; - mount({ engine: mockSchemaEngine }); + mount({ engine }); expect(EngineLogic.values).toEqual({ - ...DEFAULT_VALUES, - engine: mockSchemaEngine, + ...DEFAULT_VALUES_WITH_ENGINE, + engine, hasSchemaConflicts: true, }); }); @@ -253,15 +425,15 @@ describe('EngineLogic', () => { describe('hasUnconfirmedSchemaFields', () => { it('should be set based on engine.unconfirmedFields', () => { - const mockUnconfirmedFieldsEngine = { + const engine = { ...mockEngineData, unconfirmedFields: ['new_field_1', 'new_field_2'], }; - mount({ engine: mockUnconfirmedFieldsEngine }); + mount({ engine }); expect(EngineLogic.values).toEqual({ - ...DEFAULT_VALUES, - engine: mockUnconfirmedFieldsEngine, + ...DEFAULT_VALUES_WITH_ENGINE, + engine, hasUnconfirmedSchemaFields: true, }); }); @@ -292,7 +464,7 @@ describe('EngineLogic', () => { mount({ engine }); expect(EngineLogic.values).toEqual({ - ...DEFAULT_VALUES, + ...DEFAULT_VALUES_WITH_ENGINE, engine, searchKey: 'search-123xyz', }); @@ -312,11 +484,22 @@ describe('EngineLogic', () => { mount({ engine }); expect(EngineLogic.values).toEqual({ - ...DEFAULT_VALUES, + ...DEFAULT_VALUES_WITH_ENGINE, engine, searchKey: '', }); }); }); }); + + describe('events', () => { + it('calls stopPolling before unmount', () => { + mount(); + // Has to be a const to check state after unmount + const stopPollingSpy = jest.spyOn(EngineLogic.actions, 'stopPolling'); + + unmount(); + expect(stopPollingSpy).toHaveBeenCalled(); + }); + }); }); 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 5cbe89b364859..bfa77450176f6 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 @@ -7,16 +7,20 @@ import { kea, MakeLogicType } from 'kea'; +import { flashErrorToast } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { ApiTokenTypes } from '../credentials/constants'; import { ApiToken } from '../credentials/types'; +import { POLLING_DURATION, POLLING_ERROR_TITLE, POLLING_ERROR_TEXT } from './constants'; import { EngineDetails, EngineTypes } from './types'; interface EngineValues { dataLoading: boolean; engine: Partial<EngineDetails>; engineName: string; + isEngineEmpty: boolean; + isEngineSchemaEmpty: boolean; isMetaEngine: boolean; isSampleEngine: boolean; hasSchemaErrors: boolean; @@ -24,6 +28,7 @@ interface EngineValues { hasUnconfirmedSchemaFields: boolean; engineNotFound: boolean; searchKey: string; + intervalId: number | null; } interface EngineActions { @@ -32,6 +37,10 @@ interface EngineActions { setEngineNotFound(notFound: boolean): { notFound: boolean }; clearEngine(): void; initializeEngine(): void; + pollEmptyEngine(): void; + onPollStart(intervalId: number): { intervalId: number }; + stopPolling(): void; + onPollStop(): void; } export const EngineLogic = kea<MakeLogicType<EngineValues, EngineActions>>({ @@ -42,6 +51,10 @@ export const EngineLogic = kea<MakeLogicType<EngineValues, EngineActions>>({ setEngineNotFound: (notFound) => ({ notFound }), clearEngine: true, initializeEngine: true, + pollEmptyEngine: true, + onPollStart: (intervalId) => ({ intervalId }), + stopPolling: true, + onPollStop: true, }, reducers: { dataLoading: [ @@ -72,8 +85,20 @@ export const EngineLogic = kea<MakeLogicType<EngineValues, EngineActions>>({ clearEngine: () => false, }, ], + intervalId: [ + null, + { + onPollStart: (_, { intervalId }) => intervalId, + onPollStop: () => null, + }, + ], }, selectors: ({ selectors }) => ({ + isEngineEmpty: [() => [selectors.engine], (engine) => !engine.document_count], + isEngineSchemaEmpty: [ + () => [selectors.engine], + (engine) => Object.keys(engine.schema || {}).length === 0, + ], isMetaEngine: [() => [selectors.engine], (engine) => engine?.type === EngineTypes.meta], isSampleEngine: [() => [selectors.engine], (engine) => !!engine?.sample], // Indexed engines @@ -100,7 +125,9 @@ export const EngineLogic = kea<MakeLogicType<EngineValues, EngineActions>>({ ], }), listeners: ({ actions, values }) => ({ - initializeEngine: async () => { + initializeEngine: async (_, breakpoint) => { + breakpoint(); // Prevents errors if logic unmounts while fetching + const { engineName } = values; const { http } = HttpLogic.values; @@ -108,8 +135,39 @@ export const EngineLogic = kea<MakeLogicType<EngineValues, EngineActions>>({ const response = await http.get(`/api/app_search/engines/${engineName}`); actions.setEngineData(response); } catch (error) { - actions.setEngineNotFound(true); + if (error?.response?.status >= 400 && error?.response?.status < 500) { + actions.setEngineNotFound(true); + } else { + flashErrorToast(POLLING_ERROR_TITLE, { + text: POLLING_ERROR_TEXT, + toastLifeTimeMs: POLLING_DURATION * 0.75, + }); + } } }, + pollEmptyEngine: () => { + if (values.intervalId) return; // Ensure we only have one poll at a time + + const id = window.setInterval(() => { + if (values.isEngineEmpty && values.isEngineSchemaEmpty) { + actions.initializeEngine(); // Re-fetch engine data when engine is empty + } else { + actions.stopPolling(); + } + }, POLLING_DURATION); + + actions.onPollStart(id); + }, + stopPolling: () => { + if (values.intervalId !== null) { + clearInterval(values.intervalId); + actions.onPollStop(); + } + }, + }), + events: ({ actions }) => ({ + beforeUnmount: () => { + actions.stopPolling(); + }, }), }); 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 ee1c0578debfc..ed35bfbe97842 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 @@ -41,7 +41,13 @@ describe('EngineRouter', () => { engineNotFound: false, myRole: {}, }; - const actions = { setEngineName: jest.fn(), initializeEngine: jest.fn(), clearEngine: jest.fn() }; + const actions = { + setEngineName: jest.fn(), + initializeEngine: jest.fn(), + pollEmptyEngine: jest.fn(), + stopPolling: jest.fn(), + clearEngine: jest.fn(), + }; beforeEach(() => { setMockValues(values); @@ -58,12 +64,14 @@ describe('EngineRouter', () => { expect(actions.setEngineName).toHaveBeenCalledWith('some-engine'); }); - it('initializes/fetches engine API data', () => { + it('initializes/fetches engine API data and starts a poll for empty engines', () => { expect(actions.initializeEngine).toHaveBeenCalled(); + expect(actions.pollEmptyEngine).toHaveBeenCalled(); }); - it('clears engine on unmount and on update', () => { + it('clears engine and stops polling on unmount / on engine change', () => { unmountHandler(); + expect(actions.stopPolling).toHaveBeenCalled(); expect(actions.clearEngine).toHaveBeenCalled(); }); }); 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 91a21847107a9..da8dd8467bb61 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 @@ -13,9 +13,7 @@ import { useValues, useActions } from 'kea'; import { i18n } from '@kbn/i18n'; import { setQueuedErrorMessage } from '../../../shared/flash_messages'; -import { Layout } from '../../../shared/layout'; import { AppLogic } from '../../app_logic'; -import { AppSearchNav } from '../../index'; import { ENGINE_PATH, @@ -68,12 +66,19 @@ export const EngineRouter: React.FC = () => { const { engineName: engineNameFromUrl } = useParams() as { engineName: string }; const { engineName, dataLoading, engineNotFound } = useValues(EngineLogic); - const { setEngineName, initializeEngine, clearEngine } = useActions(EngineLogic); + const { setEngineName, initializeEngine, pollEmptyEngine, stopPolling, clearEngine } = useActions( + EngineLogic + ); useEffect(() => { setEngineName(engineNameFromUrl); initializeEngine(); - return clearEngine; + pollEmptyEngine(); + + return () => { + stopPolling(); + clearEngine(); + }; }, [engineNameFromUrl]); if (engineNotFound) { @@ -114,6 +119,36 @@ export const EngineRouter: React.FC = () => { <SchemaRouter /> </Route> )} + {canViewMetaEngineSourceEngines && ( + <Route path={META_ENGINE_SOURCE_ENGINES_PATH}> + <SourceEngines /> + </Route> + )} + {canViewEngineCrawler && ( + <Route path={ENGINE_CRAWLER_PATH}> + <CrawlerRouter /> + </Route> + )} + {canManageEngineRelevanceTuning && ( + <Route path={ENGINE_RELEVANCE_TUNING_PATH}> + <RelevanceTuning /> + </Route> + )} + {canManageEngineSynonyms && ( + <Route path={ENGINE_SYNONYMS_PATH}> + <Synonyms /> + </Route> + )} + {canManageEngineCurations && ( + <Route path={ENGINE_CURATIONS_PATH}> + <CurationsRouter /> + </Route> + )} + {canManageEngineResultSettings && ( + <Route path={ENGINE_RESULT_SETTINGS_PATH}> + <ResultSettings /> + </Route> + )} {canManageEngineSearchUi && ( <Route path={ENGINE_SEARCH_UI_PATH}> <SearchUI /> @@ -124,39 +159,6 @@ export const EngineRouter: React.FC = () => { <ApiLogs /> </Route> )} - {/* TODO: Remove layout once page template migration is over */} - <Layout navigation={<AppSearchNav />}> - {canManageEngineCurations && ( - <Route path={ENGINE_CURATIONS_PATH}> - <CurationsRouter /> - </Route> - )} - {canManageEngineRelevanceTuning && ( - <Route path={ENGINE_RELEVANCE_TUNING_PATH}> - <RelevanceTuning /> - </Route> - )} - {canManageEngineSynonyms && ( - <Route path={ENGINE_SYNONYMS_PATH}> - <Synonyms /> - </Route> - )} - {canManageEngineResultSettings && ( - <Route path={ENGINE_RESULT_SETTINGS_PATH}> - <ResultSettings /> - </Route> - )} - {canViewMetaEngineSourceEngines && ( - <Route path={META_ENGINE_SOURCE_ENGINES_PATH}> - <SourceEngines /> - </Route> - )} - {canViewEngineCrawler && ( - <Route path={ENGINE_CRAWLER_PATH}> - <CrawlerRouter /> - </Route> - )} - </Layout> </Switch> ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx index 913aa4f0ec845..18b8390081467 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_creation/engine_creation.tsx @@ -22,6 +22,7 @@ import { EuiButton, } from '@elastic/eui'; +import { ENGINES_TITLE } from '../engines'; import { AppSearchPageTemplate } from '../layout'; import { @@ -43,7 +44,7 @@ export const EngineCreation: React.FC = () => { return ( <AppSearchPageTemplate - pageChrome={[ENGINE_CREATION_TITLE]} + pageChrome={[ENGINES_TITLE, ENGINE_CREATION_TITLE]} pageHeader={{ pageTitle: ENGINE_CREATION_TITLE }} data-test-subj="EngineCreation" > diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx index edacd74e046a2..a2e0ba4fcd44d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import '../../../__mocks__/shallow_useeffect.mock'; -import { setMockValues, setMockActions } from '../../../__mocks__/kea_logic'; +import { setMockValues } from '../../../__mocks__/kea_logic'; import React from 'react'; @@ -20,18 +19,14 @@ import { EngineOverview } from './'; describe('EngineOverview', () => { const values = { dataLoading: false, - documentCount: 0, myRole: {}, + isEngineEmpty: true, isMetaEngine: false, }; - const actions = { - pollForOverviewMetrics: jest.fn(), - }; beforeEach(() => { jest.clearAllMocks(); setMockValues(values); - setMockActions(actions); }); it('renders', () => { @@ -39,21 +34,10 @@ describe('EngineOverview', () => { expect(wrapper.find('[data-test-subj="EngineOverview"]')).toHaveLength(1); }); - it('initializes data on mount', () => { - shallow(<EngineOverview />); - expect(actions.pollForOverviewMetrics).toHaveBeenCalledTimes(1); - }); - - it('renders a loading page template if async data is still loading', () => { - setMockValues({ ...values, dataLoading: true }); - const wrapper = shallow(<EngineOverview />); - expect(wrapper.prop('isLoading')).toEqual(true); - }); - describe('EmptyEngineOverview', () => { it('renders when the engine has no documents & the user can add documents', () => { const myRole = { canManageEngineDocuments: true, canViewEngineCredentials: true }; - setMockValues({ ...values, myRole, documentCount: 0 }); + setMockValues({ ...values, myRole }); const wrapper = shallow(<EngineOverview />); expect(wrapper.find(EmptyEngineOverview)).toHaveLength(1); }); @@ -61,7 +45,7 @@ describe('EngineOverview', () => { describe('EngineOverviewMetrics', () => { it('renders when the engine has documents', () => { - setMockValues({ ...values, documentCount: 1 }); + setMockValues({ ...values, isEngineEmpty: false }); const wrapper = shallow(<EngineOverview />); expect(wrapper.find(EngineOverviewMetrics)).toHaveLength(1); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx index 4c15ffd8b7f94..a3f98d8c13e8e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx @@ -5,38 +5,25 @@ * 2.0. */ -import React, { useEffect } from 'react'; +import React from 'react'; -import { useActions, useValues } from 'kea'; +import { useValues } from 'kea'; import { AppLogic } from '../../app_logic'; import { EngineLogic } from '../engine'; -import { AppSearchPageTemplate } from '../layout'; import { EmptyEngineOverview } from './engine_overview_empty'; import { EngineOverviewMetrics } from './engine_overview_metrics'; -import { EngineOverviewLogic } from './'; - export const EngineOverview: React.FC = () => { const { myRole: { canManageEngineDocuments, canViewEngineCredentials }, } = useValues(AppLogic); - const { isMetaEngine } = useValues(EngineLogic); - - const { pollForOverviewMetrics } = useActions(EngineOverviewLogic); - const { dataLoading, documentCount } = useValues(EngineOverviewLogic); - - useEffect(() => { - pollForOverviewMetrics(); - }, []); - - if (dataLoading) return <AppSearchPageTemplate isLoading />; + const { isEngineEmpty, isMetaEngine } = useValues(EngineLogic); - const engineHasDocuments = documentCount > 0; const canAddDocuments = canManageEngineDocuments && canViewEngineCredentials; - const showEngineOverview = engineHasDocuments || !canAddDocuments || isMetaEngine; + const showEngineOverview = !isEngineEmpty || !canAddDocuments || isMetaEngine; return ( <div data-test-subj="EngineOverview"> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts index c9c1defd46032..cc677d2642702 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts @@ -20,7 +20,7 @@ import { nextTick } from '@kbn/test/jest'; import { EngineOverviewLogic } from './'; describe('EngineOverviewLogic', () => { - const { mount, unmount } = new LogicMounter(EngineOverviewLogic); + const { mount } = new LogicMounter(EngineOverviewLogic); const { http } = mockHttpValues; const { flashAPIErrors } = mockFlashMessageHelpers; @@ -41,7 +41,6 @@ describe('EngineOverviewLogic', () => { queriesPerDay: [], totalClicks: 0, totalQueries: 0, - timeoutId: null, }; beforeEach(() => { @@ -54,10 +53,10 @@ describe('EngineOverviewLogic', () => { }); describe('actions', () => { - describe('setPolledData', () => { + describe('onOverviewMetricsLoad', () => { it('should set all received data as top-level values and set dataLoading to false', () => { mount(); - EngineOverviewLogic.actions.setPolledData(mockEngineMetrics); + EngineOverviewLogic.actions.onOverviewMetricsLoad(mockEngineMetrics); expect(EngineOverviewLogic.values).toEqual({ ...DEFAULT_VALUES, @@ -66,34 +65,20 @@ describe('EngineOverviewLogic', () => { }); }); }); - - describe('setTimeoutId', () => { - describe('timeoutId', () => { - it('should be set to the provided value', () => { - mount(); - EngineOverviewLogic.actions.setTimeoutId(123); - - expect(EngineOverviewLogic.values).toEqual({ - ...DEFAULT_VALUES, - timeoutId: 123, - }); - }); - }); - }); }); describe('listeners', () => { - describe('pollForOverviewMetrics', () => { - it('fetches data and calls onPollingSuccess', async () => { + describe('loadOverviewMetrics', () => { + it('fetches data and calls onOverviewMetricsLoad', async () => { mount(); - jest.spyOn(EngineOverviewLogic.actions, 'onPollingSuccess'); + jest.spyOn(EngineOverviewLogic.actions, 'onOverviewMetricsLoad'); http.get.mockReturnValueOnce(Promise.resolve(mockEngineMetrics)); - EngineOverviewLogic.actions.pollForOverviewMetrics(); + EngineOverviewLogic.actions.loadOverviewMetrics(); await nextTick(); expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine/overview'); - expect(EngineOverviewLogic.actions.onPollingSuccess).toHaveBeenCalledWith( + expect(EngineOverviewLogic.actions.onOverviewMetricsLoad).toHaveBeenCalledWith( mockEngineMetrics ); }); @@ -102,47 +87,11 @@ describe('EngineOverviewLogic', () => { mount(); http.get.mockReturnValue(Promise.reject('An error occurred')); - EngineOverviewLogic.actions.pollForOverviewMetrics(); + EngineOverviewLogic.actions.loadOverviewMetrics(); await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('An error occurred'); }); }); - - describe('onPollingSuccess', () => { - it('starts a polling timeout and sets data', async () => { - mount(); - jest.useFakeTimers(); - jest.spyOn(EngineOverviewLogic.actions, 'setTimeoutId'); - jest.spyOn(EngineOverviewLogic.actions, 'setPolledData'); - - EngineOverviewLogic.actions.onPollingSuccess(mockEngineMetrics); - - expect(setTimeout).toHaveBeenCalledWith( - EngineOverviewLogic.actions.pollForOverviewMetrics, - 5000 - ); - expect(EngineOverviewLogic.actions.setTimeoutId).toHaveBeenCalledWith(expect.any(Number)); - expect(EngineOverviewLogic.actions.setPolledData).toHaveBeenCalledWith(mockEngineMetrics); - }); - }); - }); - - describe('unmount', () => { - beforeEach(() => { - jest.useFakeTimers(); - mount(); - }); - - it('clears existing polling timeouts on unmount', () => { - EngineOverviewLogic.actions.setTimeoutId(123); - unmount(); - expect(clearTimeout).toHaveBeenCalled(); - }); - - it("does not clear timeout if one hasn't been set", () => { - unmount(); - expect(clearTimeout).not.toHaveBeenCalled(); - }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.ts index 78d5358fc4909..3f9c2e43a332b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.ts @@ -11,8 +11,6 @@ import { flashAPIErrors } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { EngineLogic } from '../engine'; -const POLLING_DURATION = 5000; - interface EngineOverviewApiData { documentCount: number; startDate: string; @@ -23,95 +21,74 @@ interface EngineOverviewApiData { } interface EngineOverviewValues extends EngineOverviewApiData { dataLoading: boolean; - timeoutId: number | null; } interface EngineOverviewActions { - setPolledData(engineMetrics: EngineOverviewApiData): EngineOverviewApiData; - setTimeoutId(timeoutId: number): { timeoutId: number }; - pollForOverviewMetrics(): void; - onPollingSuccess(engineMetrics: EngineOverviewApiData): EngineOverviewApiData; + loadOverviewMetrics(): void; + onOverviewMetricsLoad(response: EngineOverviewApiData): EngineOverviewApiData; } export const EngineOverviewLogic = kea<MakeLogicType<EngineOverviewValues, EngineOverviewActions>>({ path: ['enterprise_search', 'app_search', 'engine_overview_logic'], actions: () => ({ - setPolledData: (engineMetrics) => engineMetrics, - setTimeoutId: (timeoutId) => ({ timeoutId }), - pollForOverviewMetrics: true, - onPollingSuccess: (engineMetrics) => engineMetrics, + loadOverviewMetrics: true, + onOverviewMetricsLoad: (engineMetrics) => engineMetrics, }), reducers: () => ({ dataLoading: [ true, { - setPolledData: () => false, + onOverviewMetricsLoad: () => false, }, ], startDate: [ '', { - setPolledData: (_, { startDate }) => startDate, + onOverviewMetricsLoad: (_, { startDate }) => startDate, }, ], queriesPerDay: [ [], { - setPolledData: (_, { queriesPerDay }) => queriesPerDay, + onOverviewMetricsLoad: (_, { queriesPerDay }) => queriesPerDay, }, ], operationsPerDay: [ [], { - setPolledData: (_, { operationsPerDay }) => operationsPerDay, + onOverviewMetricsLoad: (_, { operationsPerDay }) => operationsPerDay, }, ], totalQueries: [ 0, { - setPolledData: (_, { totalQueries }) => totalQueries, + onOverviewMetricsLoad: (_, { totalQueries }) => totalQueries, }, ], totalClicks: [ 0, { - setPolledData: (_, { totalClicks }) => totalClicks, + onOverviewMetricsLoad: (_, { totalClicks }) => totalClicks, }, ], documentCount: [ 0, { - setPolledData: (_, { documentCount }) => documentCount, - }, - ], - timeoutId: [ - null, - { - setTimeoutId: (_, { timeoutId }) => timeoutId, + onOverviewMetricsLoad: (_, { documentCount }) => documentCount, }, ], }), listeners: ({ actions }) => ({ - pollForOverviewMetrics: async () => { + loadOverviewMetrics: async () => { const { http } = HttpLogic.values; const { engineName } = EngineLogic.values; try { const response = await http.get(`/api/app_search/engines/${engineName}/overview`); - actions.onPollingSuccess(response); + actions.onOverviewMetricsLoad(response); } catch (e) { flashAPIErrors(e); } }, - onPollingSuccess: (engineMetrics) => { - const timeoutId = window.setTimeout(actions.pollForOverviewMetrics, POLLING_DURATION); - actions.setTimeoutId(timeoutId); - actions.setPolledData(engineMetrics); - }, - }), - events: ({ values }) => ({ - beforeUnmount() { - if (values.timeoutId !== null) clearTimeout(values.timeoutId); - }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx index 620d913c5f9a7..14f182463d837 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx @@ -5,6 +5,8 @@ * 2.0. */ +import { setMockValues, setMockActions } from '../../../__mocks__/kea_logic'; +import '../../../__mocks__/shallow_useeffect.mock'; import '../../__mocks__/engine_logic.mock'; import React from 'react'; @@ -17,6 +19,19 @@ import { TotalStats, TotalCharts, RecentApiLogs } from './components'; import { EngineOverviewMetrics } from './engine_overview_metrics'; describe('EngineOverviewMetrics', () => { + const values = { + dataLoading: false, + }; + const actions = { + loadOverviewMetrics: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + it('renders', () => { const wrapper = shallow(<EngineOverviewMetrics />); @@ -25,4 +40,9 @@ describe('EngineOverviewMetrics', () => { expect(wrapper.find(TotalCharts)).toHaveLength(1); expect(wrapper.find(RecentApiLogs)).toHaveLength(1); }); + + it('initializes data on mount', () => { + shallow(<EngineOverviewMetrics />); + expect(actions.loadOverviewMetrics).toHaveBeenCalledTimes(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx index b47ae21104ae9..3cc7138623735 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx @@ -5,7 +5,9 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; + +import { useActions, useValues } from 'kea'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -15,7 +17,16 @@ import { AppSearchPageTemplate } from '../layout'; import { TotalStats, TotalCharts, RecentApiLogs } from './components'; +import { EngineOverviewLogic } from './'; + export const EngineOverviewMetrics: React.FC = () => { + const { loadOverviewMetrics } = useActions(EngineOverviewLogic); + const { dataLoading } = useValues(EngineOverviewLogic); + + useEffect(() => { + loadOverviewMetrics(); + }, []); + return ( <AppSearchPageTemplate pageChrome={getEngineBreadcrumbs()} @@ -24,6 +35,7 @@ export const EngineOverviewMetrics: React.FC = () => { defaultMessage: 'Engine overview', }), }} + isLoading={dataLoading} > <EuiFlexGroup> <EuiFlexItem grow={1}> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.test.tsx index 159a986096ae2..9117fdd0be87d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.test.tsx @@ -53,7 +53,7 @@ describe('EmptyState', () => { }); it('sends a user to engine creation', () => { - expect(button.prop('to')).toEqual('/engine_creation'); + expect(button.prop('to')).toEqual('/engines/new'); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts index 1d8e578e0edf2..63235f8a992f0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/index.ts @@ -5,6 +5,5 @@ * 2.0. */ -export { LaunchAppSearchButton } from './launch_as_button'; export { EmptyState } from './empty_state'; export { EmptyMetaEnginesState } from './empty_meta_engines_state'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.test.tsx deleted file mode 100644 index 93c91cc3830f4..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.test.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import '../../../../__mocks__/enterprise_search_url.mock'; -import { mockTelemetryActions } from '../../../../__mocks__/kea_logic'; - -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { LaunchAppSearchButton } from './'; - -describe('LaunchAppSearchButton', () => { - it('renders a launch app search button that sends telemetry on click', () => { - const button = shallow(<LaunchAppSearchButton />); - - expect(button.prop('href')).toBe('http://localhost:3002/as'); - expect(button.prop('isDisabled')).toBeFalsy(); - - button.simulate('click'); - expect(mockTelemetryActions.sendAppSearchTelemetry).toHaveBeenCalled(); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.tsx deleted file mode 100644 index 41102cb4fba2e..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/launch_as_button.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { useActions } from 'kea'; - -import { EuiButton } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { getAppSearchUrl } from '../../../../shared/enterprise_search_url'; -import { TelemetryLogic } from '../../../../shared/telemetry'; - -export const LaunchAppSearchButton: React.FC = () => { - const { sendAppSearchTelemetry } = useActions(TelemetryLogic); - - return ( - // eslint-disable-next-line @elastic/eui/href-or-on-click - <EuiButton - size="s" - iconType="popout" - href={getAppSearchUrl()} - target="_blank" - onClick={() => - sendAppSearchTelemetry({ - action: 'clicked', - metric: 'header_launch_button', - }) - } - data-test-subj="launchButton" - > - {i18n.translate('xpack.enterpriseSearch.appSearch.productCta', { - defaultMessage: 'Launch App Search', - })} - </EuiButton> - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx index 4dff246052138..d1dd5514757d7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx @@ -20,7 +20,7 @@ import { ENGINE_CREATION_PATH, META_ENGINE_CREATION_PATH } from '../../routes'; import { DataPanel } from '../data_panel'; import { AppSearchPageTemplate } from '../layout'; -import { LaunchAppSearchButton, EmptyState, EmptyMetaEnginesState } from './components'; +import { EmptyState, EmptyMetaEnginesState } from './components'; import { EnginesTable } from './components/tables/engines_table'; import { MetaEnginesTable } from './components/tables/meta_engines_table'; import { @@ -65,10 +65,7 @@ export const EnginesOverview: React.FC = () => { <AppSearchPageTemplate pageViewTelemetry="engines_overview" pageChrome={[ENGINES_TITLE]} - pageHeader={{ - pageTitle: ENGINES_OVERVIEW_TITLE, - rightSideItems: [<LaunchAppSearchButton />], - }} + pageHeader={{ pageTitle: ENGINES_OVERVIEW_TITLE }} isLoading={dataLoading} isEmptyState={!engines.length} emptyState={<EmptyState />} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx index 80230394ce2a2..ce4a118bef095 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.test.tsx @@ -8,7 +8,7 @@ import { setMockValues } from '../../../__mocks__/kea_logic'; jest.mock('../../../shared/layout', () => ({ - generateNavLink: jest.fn(({ to }) => ({ href: to })), + generateNavLink: jest.fn(({ to, items }) => ({ href: to, items })), })); jest.mock('../engine/engine_nav', () => ({ useEngineNav: () => [], @@ -100,8 +100,8 @@ describe('useAppSearchNav', () => { }, { id: 'usersRoles', - name: 'Users & roles', - href: '/role_mappings', + name: 'Users and roles', + href: '/users_and_roles', }, ], }, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx index 4737fbcf07e23..793a36f48fe82 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/nav.tsx @@ -13,7 +13,7 @@ import { generateNavLink } from '../../../shared/layout'; import { ROLE_MAPPINGS_TITLE } from '../../../shared/role_mapping/constants'; import { AppLogic } from '../../app_logic'; -import { ENGINES_PATH, SETTINGS_PATH, CREDENTIALS_PATH, ROLE_MAPPINGS_PATH } from '../../routes'; +import { ENGINES_PATH, SETTINGS_PATH, CREDENTIALS_PATH, USERS_AND_ROLES_PATH } from '../../routes'; import { CREDENTIALS_TITLE } from '../credentials'; import { useEngineNav } from '../engine/engine_nav'; import { ENGINES_TITLE } from '../engines'; @@ -28,8 +28,12 @@ export const useAppSearchNav = () => { { id: 'engines', name: ENGINES_TITLE, - ...generateNavLink({ to: ENGINES_PATH, isRoot: true }), - items: useEngineNav(), + ...generateNavLink({ + to: ENGINES_PATH, + isRoot: true, + shouldShowActiveForSubroutes: true, + items: useEngineNav(), + }), }, ]; @@ -53,7 +57,7 @@ export const useAppSearchNav = () => { navItems.push({ id: 'usersRoles', name: ROLE_MAPPINGS_TITLE, - ...generateNavLink({ to: ROLE_MAPPINGS_PATH }), + ...generateNavLink({ to: USERS_AND_ROLES_PATH }), }); } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx index 325e557acec0c..1455444ab2b4b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation.tsx @@ -25,6 +25,7 @@ import { } from '@elastic/eui'; import { AppLogic } from '../../app_logic'; +import { ENGINES_TITLE } from '../engines'; import { AppSearchPageTemplate } from '../layout'; import { @@ -73,7 +74,7 @@ export const MetaEngineCreation: React.FC = () => { return ( <AppSearchPageTemplate - pageChrome={[META_ENGINE_CREATION_TITLE]} + pageChrome={[ENGINES_TITLE, META_ENGINE_CREATION_TITLE]} pageHeader={{ pageTitle: META_ENGINE_CREATION_TITLE, description: ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.tsx index e6a14d7b5cd72..df29010bd682f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/components/empty_state.tsx @@ -7,42 +7,40 @@ import React from 'react'; -import { EuiButton, EuiEmptyPrompt, EuiPanel } from '@elastic/eui'; +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DOCS_PREFIX } from '../../../routes'; export const EmptyState: React.FC = () => ( - <EuiPanel color="subdued"> - <EuiEmptyPrompt - iconType="wrench" - title={ - <h2> - {i18n.translate('xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.title', { - defaultMessage: 'Add documents to tune relevance', - })} - </h2> + <EuiEmptyPrompt + iconType="wrench" + title={ + <h2> + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.title', { + defaultMessage: 'Add documents to tune relevance', + })} + </h2> + } + body={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.description', + { + defaultMessage: + 'A schema will be automatically created for you after you index some documents.', } - body={i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.description', - { - defaultMessage: - 'A schema will be automatically created for you after you index some documents.', - } - )} - actions={ - <EuiButton - size="s" - target="_blank" - iconType="popout" - href={`${DOCS_PREFIX}/relevance-tuning-guide.html`} - > - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.buttonLabel', - { defaultMessage: 'Read the relevance tuning guide' } - )} - </EuiButton> - } - /> - </EuiPanel> + )} + actions={ + <EuiButton + size="s" + target="_blank" + iconType="popout" + href={`${DOCS_PREFIX}/relevance-tuning-guide.html`} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.empty.buttonLabel', + { defaultMessage: 'Read the relevance tuning guide' } + )} + </EuiButton> + } + /> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx index 092740ac5d3cc..48b536a954ed5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx @@ -13,14 +13,14 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiEmptyPrompt } from '@elastic/eui'; - -import { Loading } from '../../../shared/loading'; import { UnsavedChangesPrompt } from '../../../shared/unsaved_changes_prompt'; +import { getPageHeaderActions } from '../../../test_helpers'; -import { EmptyState } from './components'; import { RelevanceTuning } from './relevance_tuning'; + +import { RelevanceTuningCallouts } from './relevance_tuning_callouts'; import { RelevanceTuningForm } from './relevance_tuning_form'; +import { RelevanceTuningPreview } from './relevance_tuning_preview'; describe('RelevanceTuning', () => { const values = { @@ -50,9 +50,9 @@ describe('RelevanceTuning', () => { it('renders', () => { const wrapper = subject(); + expect(wrapper.find(RelevanceTuningCallouts).exists()).toBe(true); expect(wrapper.find(RelevanceTuningForm).exists()).toBe(true); - expect(wrapper.find(Loading).exists()).toBe(false); - expect(wrapper.find(EmptyState).exists()).toBe(false); + expect(wrapper.find(RelevanceTuningPreview).exists()).toBe(true); }); it('initializes relevance tuning data', () => { @@ -60,33 +60,38 @@ describe('RelevanceTuning', () => { expect(actions.initializeRelevanceTuning).toHaveBeenCalled(); }); - it('will render an empty message when the engine has no schema', () => { + it('will prevent user from leaving the page if there are unsaved changes', () => { setMockValues({ ...values, - engineHasSchemaFields: false, + unsavedChanges: true, }); - const wrapper = subject(); - expect(wrapper.find(EmptyState).dive().find(EuiEmptyPrompt).exists()).toBe(true); - expect(wrapper.find(Loading).exists()).toBe(false); - expect(wrapper.find(RelevanceTuningForm).exists()).toBe(false); + expect(subject().find(UnsavedChangesPrompt).prop('hasUnsavedChanges')).toBe(true); }); - it('will show a loading message if data is loading', () => { - setMockValues({ - ...values, - dataLoading: true, + describe('header actions', () => { + it('renders a Save button that will save the current changes', () => { + const buttons = getPageHeaderActions(subject()); + expect(buttons.children().length).toBe(2); + const saveButton = buttons.find('[data-test-subj="SaveRelevanceTuning"]'); + saveButton.simulate('click'); + expect(actions.updateSearchSettings).toHaveBeenCalled(); }); - const wrapper = subject(); - expect(wrapper.find(Loading).exists()).toBe(true); - expect(wrapper.find(EmptyState).exists()).toBe(false); - expect(wrapper.find(RelevanceTuningForm).exists()).toBe(false); - }); - it('will prevent user from leaving the page if there are unsaved changes', () => { - setMockValues({ - ...values, - unsavedChanges: true, + it('renders a Reset button that will remove all weights and boosts', () => { + const buttons = getPageHeaderActions(subject()); + expect(buttons.children().length).toBe(2); + const resetButton = buttons.find('[data-test-subj="ResetRelevanceTuning"]'); + resetButton.simulate('click'); + expect(actions.resetSearchSettings).toHaveBeenCalled(); + }); + + it('will not render buttons if the engine has no schema', () => { + setMockValues({ + ...values, + engineHasSchemaFields: false, + }); + const buttons = getPageHeaderActions(subject()); + expect(buttons.children().length).toBe(0); }); - expect(subject().find(UnsavedChangesPrompt).prop('hasUnsavedChanges')).toBe(true); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx index b98541a963890..2e87d6836199b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx @@ -9,43 +9,77 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; -import { Loading } from '../../../shared/loading'; +import { SAVE_BUTTON_LABEL } from '../../../shared/constants'; import { UnsavedChangesPrompt } from '../../../shared/unsaved_changes_prompt'; +import { RESTORE_DEFAULTS_BUTTON_LABEL } from '../../constants'; +import { getEngineBreadcrumbs } from '../engine'; +import { AppSearchPageTemplate } from '../layout'; import { EmptyState } from './components'; +import { RELEVANCE_TUNING_TITLE } from './constants'; +import { RelevanceTuningCallouts } from './relevance_tuning_callouts'; import { RelevanceTuningForm } from './relevance_tuning_form'; -import { RelevanceTuningLayout } from './relevance_tuning_layout'; import { RelevanceTuningPreview } from './relevance_tuning_preview'; import { RelevanceTuningLogic } from '.'; export const RelevanceTuning: React.FC = () => { const { dataLoading, engineHasSchemaFields, unsavedChanges } = useValues(RelevanceTuningLogic); - const { initializeRelevanceTuning } = useActions(RelevanceTuningLogic); + const { initializeRelevanceTuning, resetSearchSettings, updateSearchSettings } = useActions( + RelevanceTuningLogic + ); useEffect(() => { initializeRelevanceTuning(); }, []); - if (dataLoading) return <Loading />; - return ( - <RelevanceTuningLayout> + <AppSearchPageTemplate + pageChrome={getEngineBreadcrumbs([RELEVANCE_TUNING_TITLE])} + pageHeader={{ + pageTitle: RELEVANCE_TUNING_TITLE, + description: i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.description', + { defaultMessage: 'Set field weights and boosts.' } + ), + rightSideItems: engineHasSchemaFields + ? [ + <EuiButton + data-test-subj="SaveRelevanceTuning" + color="primary" + fill + onClick={updateSearchSettings} + > + {SAVE_BUTTON_LABEL} + </EuiButton>, + <EuiButton + data-test-subj="ResetRelevanceTuning" + color="danger" + onClick={resetSearchSettings} + > + {RESTORE_DEFAULTS_BUTTON_LABEL} + </EuiButton>, + ] + : [], + }} + isLoading={dataLoading} + isEmptyState={!engineHasSchemaFields} + emptyState={<EmptyState />} + > <UnsavedChangesPrompt hasUnsavedChanges={unsavedChanges} /> - {engineHasSchemaFields ? ( - <EuiFlexGroup alignItems="flexStart"> - <EuiFlexItem grow={3}> - <RelevanceTuningForm /> - </EuiFlexItem> - <EuiFlexItem grow={4}> - <RelevanceTuningPreview /> - </EuiFlexItem> - </EuiFlexGroup> - ) : ( - <EmptyState /> - )} - </RelevanceTuningLayout> + <RelevanceTuningCallouts /> + + <EuiFlexGroup alignItems="flexStart"> + <EuiFlexItem grow={3}> + <RelevanceTuningForm /> + </EuiFlexItem> + <EuiFlexItem grow={4}> + <RelevanceTuningPreview /> + </EuiFlexItem> + </EuiFlexGroup> + </AppSearchPageTemplate> ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_callouts.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_callouts.tsx index c981d35ff20cb..bdbc414a22eaa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_callouts.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_callouts.tsx @@ -7,12 +7,11 @@ import React from 'react'; -import { FormattedMessage } from 'react-intl'; - import { useValues } from 'kea'; import { EuiCallOut, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiLinkTo } from '../../../shared/react_router_helpers'; import { DOCS_PREFIX, ENGINE_SCHEMA_PATH } from '../../routes'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx index 5cbd291f85deb..c35cd280c7a05 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx @@ -42,7 +42,7 @@ export const RelevanceTuningForm: React.FC = () => { return ( <section className="relevanceTuningForm"> <form> - <EuiSpacer size="s" /> + <EuiSpacer size="m" /> <EuiTitle size="m"> <h2> {i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.test.tsx deleted file mode 100644 index 20b1a16879234..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.test.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { setMockActions, setMockValues } from '../../../__mocks__/kea_logic'; -import '../../__mocks__/engine_logic.mock'; - -import React from 'react'; - -import { shallow, ShallowWrapper } from 'enzyme'; - -import { EuiPageHeader } from '@elastic/eui'; - -import { RelevanceTuningLayout } from './relevance_tuning_layout'; - -describe('RelevanceTuningLayout', () => { - const values = { - engineHasSchemaFields: true, - schemaFieldsWithConflicts: [], - }; - - const actions = { - updateSearchSettings: jest.fn(), - resetSearchSettings: jest.fn(), - }; - - beforeEach(() => { - jest.clearAllMocks(); - setMockValues(values); - setMockActions(actions); - }); - - const subject = () => shallow(<RelevanceTuningLayout />); - const findButtons = (wrapper: ShallowWrapper) => - wrapper.find(EuiPageHeader).prop('rightSideItems') as React.ReactElement[]; - - it('renders a Save button that will save the current changes', () => { - const buttons = findButtons(subject()); - expect(buttons.length).toBe(2); - const saveButton = shallow(buttons[0]); - saveButton.simulate('click'); - expect(actions.updateSearchSettings).toHaveBeenCalled(); - }); - - it('renders a Reset button that will remove all weights and boosts', () => { - const buttons = findButtons(subject()); - expect(buttons.length).toBe(2); - const resetButton = shallow(buttons[1]); - resetButton.simulate('click'); - expect(actions.resetSearchSettings).toHaveBeenCalled(); - }); - - it('will not render buttons if the engine has no schema', () => { - setMockValues({ - ...values, - engineHasSchemaFields: false, - }); - const buttons = findButtons(subject()); - expect(buttons.length).toBe(0); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.tsx deleted file mode 100644 index 4fa694300a779..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_layout.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { useActions, useValues } from 'kea'; - -import { EuiPageHeader, EuiButton } from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; - -import { SAVE_BUTTON_LABEL } from '../../../shared/constants'; -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; -import { RESTORE_DEFAULTS_BUTTON_LABEL } from '../../constants'; -import { getEngineBreadcrumbs } from '../engine'; - -import { RELEVANCE_TUNING_TITLE } from './constants'; -import { RelevanceTuningCallouts } from './relevance_tuning_callouts'; -import { RelevanceTuningLogic } from './relevance_tuning_logic'; - -export const RelevanceTuningLayout: React.FC = ({ children }) => { - const { resetSearchSettings, updateSearchSettings } = useActions(RelevanceTuningLogic); - const { engineHasSchemaFields } = useValues(RelevanceTuningLogic); - - const pageHeader = () => ( - <EuiPageHeader - className="relevanceTuningHeader" - pageTitle={RELEVANCE_TUNING_TITLE} - description={i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.description', - { - defaultMessage: 'Set field weights and boosts.', - } - )} - rightSideItems={ - engineHasSchemaFields - ? [ - <EuiButton - data-test-subj="SaveRelevanceTuning" - color="primary" - fill - onClick={updateSearchSettings} - > - {SAVE_BUTTON_LABEL} - </EuiButton>, - <EuiButton - data-test-subj="ResetRelevanceTuning" - color="danger" - onClick={resetSearchSettings} - > - {RESTORE_DEFAULTS_BUTTON_LABEL} - </EuiButton>, - ] - : [] - } - /> - ); - - return ( - <> - <SetPageChrome trail={getEngineBreadcrumbs([RELEVANCE_TUNING_TITLE])} /> - {pageHeader()} - <FlashMessages /> - <RelevanceTuningCallouts /> - {children} - </> - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx index 911e97de5b53f..4f3b20b419e80 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_preview.tsx @@ -21,6 +21,7 @@ import { RelevanceTuningLogic } from '.'; const emptyCallout = ( <EuiEmptyPrompt data-test-subj="EmptyQueryPrompt" + iconType="glasses" body={i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.preview.enterQueryMessage', { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/components/empty_state.tsx index 299156984724e..dae8390a35fd7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/components/empty_state.tsx @@ -7,42 +7,40 @@ import React from 'react'; -import { EuiButton, EuiEmptyPrompt, EuiPanel } from '@elastic/eui'; +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DOCS_PREFIX } from '../../../routes'; export const EmptyState: React.FC = () => ( - <EuiPanel color="subdued"> - <EuiEmptyPrompt - iconType="gear" - title={ - <h2> - {i18n.translate('xpack.enterpriseSearch.appSearch.engine.resultSettings.empty.title', { - defaultMessage: 'Add documents to adjust settings', - })} - </h2> + <EuiEmptyPrompt + iconType="gear" + title={ + <h2> + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.resultSettings.empty.title', { + defaultMessage: 'Add documents to adjust settings', + })} + </h2> + } + body={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.empty.description', + { + defaultMessage: + 'A schema will be automatically created for you after you index some documents.', } - body={i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.resultSettings.empty.description', - { - defaultMessage: - 'A schema will be automatically created for you after you index some documents.', - } - )} - actions={ - <EuiButton - size="s" - target="_blank" - iconType="popout" - href={`${DOCS_PREFIX}/result-settings-guide.html`} - > - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.resultSettings.empty.buttonLabel', - { defaultMessage: 'Read the result settings guide' } - )} - </EuiButton> - } - /> - </EuiPanel> + )} + actions={ + <EuiButton + size="s" + target="_blank" + iconType="popout" + href={`${DOCS_PREFIX}/result-settings-guide.html`} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.resultSettings.empty.buttonLabel', + { defaultMessage: 'Read the result settings guide' } + )} + </EuiButton> + } + /> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx index ec521b4959535..440acaf136dda 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.test.tsx @@ -13,11 +13,9 @@ import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; -import { EuiPageHeader } from '@elastic/eui'; - import { UnsavedChangesPrompt } from '../../../shared/unsaved_changes_prompt'; +import { getPageHeaderActions } from '../../../test_helpers'; -import { EmptyState } from './components'; import { ResultSettings } from './result_settings'; import { ResultSettingsTable } from './result_settings_table'; import { SampleResponse } from './sample_response'; @@ -46,8 +44,6 @@ describe('ResultSettings', () => { }); const subject = () => shallow(<ResultSettings />); - const findButtons = (wrapper: ShallowWrapper) => - wrapper.find(EuiPageHeader).prop('rightSideItems') as React.ReactElement[]; it('renders', () => { const wrapper = subject(); @@ -60,19 +56,10 @@ describe('ResultSettings', () => { expect(actions.initializeResultSettingsData).toHaveBeenCalled(); }); - it('renders a loading screen if data has not loaded yet', () => { - setMockValues({ - dataLoading: true, - }); - const wrapper = subject(); - expect(wrapper.find(ResultSettingsTable).exists()).toBe(false); - expect(wrapper.find(SampleResponse).exists()).toBe(false); - }); - it('renders a "save" button that will save the current changes', () => { - const buttons = findButtons(subject()); - expect(buttons.length).toBe(3); - const saveButton = shallow(buttons[0]); + const buttons = getPageHeaderActions(subject()); + expect(buttons.children().length).toBe(3); + const saveButton = buttons.find('[data-test-subj="SaveResultSettings"]'); saveButton.simulate('click'); expect(actions.saveResultSettings).toHaveBeenCalled(); }); @@ -82,8 +69,8 @@ describe('ResultSettings', () => { ...values, stagedUpdates: false, }); - const buttons = findButtons(subject()); - const saveButton = shallow(buttons[0]); + const buttons = getPageHeaderActions(subject()); + const saveButton = buttons.find('[data-test-subj="SaveResultSettings"]'); expect(saveButton.prop('disabled')).toBe(true); }); @@ -93,15 +80,15 @@ describe('ResultSettings', () => { stagedUpdates: true, resultFieldsEmpty: true, }); - const buttons = findButtons(subject()); - const saveButton = shallow(buttons[0]); + const buttons = getPageHeaderActions(subject()); + const saveButton = buttons.find('[data-test-subj="SaveResultSettings"]'); expect(saveButton.prop('disabled')).toBe(true); }); it('renders a "restore defaults" button that will reset all values to their defaults', () => { - const buttons = findButtons(subject()); - expect(buttons.length).toBe(3); - const resetButton = shallow(buttons[1]); + const buttons = getPageHeaderActions(subject()); + expect(buttons.children().length).toBe(3); + const resetButton = buttons.find('[data-test-subj="ResetResultSettings"]'); resetButton.simulate('click'); expect(actions.confirmResetAllFields).toHaveBeenCalled(); }); @@ -111,15 +98,15 @@ describe('ResultSettings', () => { ...values, resultFieldsAtDefaultSettings: true, }); - const buttons = findButtons(subject()); - const resetButton = shallow(buttons[1]); + const buttons = getPageHeaderActions(subject()); + const resetButton = buttons.find('[data-test-subj="ResetResultSettings"]'); expect(resetButton.prop('disabled')).toBe(true); }); it('renders a "clear" button that will remove all selected options', () => { - const buttons = findButtons(subject()); - expect(buttons.length).toBe(3); - const clearButton = shallow(buttons[2]); + const buttons = getPageHeaderActions(subject()); + expect(buttons.children().length).toBe(3); + const clearButton = buttons.find('[data-test-subj="ClearResultSettings"]'); clearButton.simulate('click'); expect(actions.clearAllFields).toHaveBeenCalled(); }); @@ -143,17 +130,12 @@ describe('ResultSettings', () => { }); it('will not render action buttons', () => { - const buttons = findButtons(wrapper); - expect(buttons.length).toBe(0); - }); - - it('will not render the main page content', () => { - expect(wrapper.find(ResultSettingsTable).exists()).toBe(false); - expect(wrapper.find(SampleResponse).exists()).toBe(false); + const buttons = getPageHeaderActions(wrapper); + expect(buttons.children().length).toBe(0); }); it('will render an empty state', () => { - expect(wrapper.find(EmptyState).exists()).toBe(true); + expect(wrapper.prop('isEmptyState')).toBe(true); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx index 45cb9ea1cfcb4..c315927433a0a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings.tsx @@ -9,17 +9,15 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; -import { EuiPageHeader, EuiFlexGroup, EuiFlexItem, EuiButton, EuiButtonEmpty } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { SAVE_BUTTON_LABEL } from '../../../shared/constants'; -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; -import { Loading } from '../../../shared/loading'; import { UnsavedChangesPrompt } from '../../../shared/unsaved_changes_prompt'; import { RESTORE_DEFAULTS_BUTTON_LABEL } from '../../constants'; import { getEngineBreadcrumbs } from '../engine'; +import { AppSearchPageTemplate } from '../layout'; import { EmptyState } from './components'; import { RESULT_SETTINGS_TITLE } from './constants'; @@ -57,59 +55,56 @@ export const ResultSettings: React.FC = () => { initializeResultSettingsData(); }, []); - if (dataLoading) return <Loading />; const hasSchema = Object.keys(schema).length > 0; return ( - <> - <SetPageChrome trail={getEngineBreadcrumbs([RESULT_SETTINGS_TITLE])} /> - <UnsavedChangesPrompt hasUnsavedChanges={stagedUpdates} messageText={UNSAVED_MESSAGE} /> - <EuiPageHeader - pageTitle={RESULT_SETTINGS_TITLE} - description={i18n.translate( + <AppSearchPageTemplate + pageChrome={getEngineBreadcrumbs([RESULT_SETTINGS_TITLE])} + pageHeader={{ + pageTitle: RESULT_SETTINGS_TITLE, + description: i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.resultSettings.pageDescription', { defaultMessage: 'Enrich search results and select which fields will appear.' } - )} - rightSideItems={ - hasSchema - ? [ - <EuiButton - data-test-subj="SaveResultSettings" - color="primary" - fill - onClick={saveResultSettings} - disabled={resultFieldsEmpty || !stagedUpdates} - > - {SAVE_BUTTON_LABEL} - </EuiButton>, - <EuiButton - data-test-subj="ResetResultSettings" - color="danger" - onClick={confirmResetAllFields} - disabled={resultFieldsAtDefaultSettings} - > - {RESTORE_DEFAULTS_BUTTON_LABEL} - </EuiButton>, - <EuiButtonEmpty data-test-subj="ClearResultSettings" onClick={clearAllFields}> - {CLEAR_BUTTON_LABEL} - </EuiButtonEmpty>, - ] - : [] - } - /> - <FlashMessages /> - {hasSchema ? ( - <EuiFlexGroup alignItems="flexStart"> - <EuiFlexItem grow={5}> - <ResultSettingsTable /> - </EuiFlexItem> - <EuiFlexItem grow={3}> - <SampleResponse /> - </EuiFlexItem> - </EuiFlexGroup> - ) : ( - <EmptyState /> - )} - </> + ), + rightSideItems: hasSchema + ? [ + <EuiButton + data-test-subj="SaveResultSettings" + color="primary" + fill + onClick={saveResultSettings} + disabled={resultFieldsEmpty || !stagedUpdates} + > + {SAVE_BUTTON_LABEL} + </EuiButton>, + <EuiButton + data-test-subj="ResetResultSettings" + color="danger" + onClick={confirmResetAllFields} + disabled={resultFieldsAtDefaultSettings} + > + {RESTORE_DEFAULTS_BUTTON_LABEL} + </EuiButton>, + <EuiButtonEmpty data-test-subj="ClearResultSettings" onClick={clearAllFields}> + {CLEAR_BUTTON_LABEL} + </EuiButtonEmpty>, + ] + : [], + }} + isLoading={dataLoading} + isEmptyState={!hasSchema} + emptyState={<EmptyState />} + > + <UnsavedChangesPrompt hasUnsavedChanges={stagedUpdates} messageText={UNSAVED_MESSAGE} /> + + <EuiFlexGroup alignItems="flexStart"> + <EuiFlexItem grow={5}> + <ResultSettingsTable /> + </EuiFlexItem> + <EuiFlexItem grow={3}> + <SampleResponse /> + </EuiFlexItem> + </EuiFlexGroup> + </AppSearchPageTemplate> ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts index df1e19e264c75..cce18cbeffd0a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts @@ -9,14 +9,6 @@ import { i18n } from '@kbn/i18n'; import { AdvanceRoleType } from '../../types'; -export const DELETE_ROLE_MAPPING_MESSAGE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.deleteRoleMappingMessage', - { - defaultMessage: - 'Are you sure you want to permanently delete this mapping? This action is not reversible and some users might lose access.', - } -); - export const ROLE_MAPPING_DELETED_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.appSearch.roleMappingDeletedMessage', { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx index b6a9dd72cfd05..dbebd8e46a219 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mapping.tsx @@ -28,7 +28,7 @@ export const RoleMapping: React.FC = () => { handleAuthProviderChange, handleRoleChange, handleSaveMapping, - closeRoleMappingFlyout, + closeUsersAndRolesFlyout, } = useActions(RoleMappingsLogic); const { @@ -68,7 +68,7 @@ export const RoleMapping: React.FC = () => { <RoleMappingFlyout disabled={attributeValueInvalid || !hasEngineAssignment} isNew={isNew} - closeRoleMappingFlyout={closeRoleMappingFlyout} + closeUsersAndRolesFlyout={closeUsersAndRolesFlyout} handleSaveMapping={handleSaveMapping} > <EuiForm isInvalid={roleMappingErrors.length > 0} error={roleMappingErrors}> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.test.tsx index 308022ccb2e5a..64bf41a50a2f0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.test.tsx @@ -12,26 +12,39 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { RoleMappingsTable, RoleMappingsHeading } from '../../../shared/role_mapping'; -import { wsRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; +import { + RoleMappingsTable, + RoleMappingsHeading, + UsersHeading, + UsersEmptyPrompt, +} from '../../../shared/role_mapping'; +import { + asRoleMapping, + asSingleUserRoleMapping, +} from '../../../shared/role_mapping/__mocks__/roles'; import { RoleMapping } from './role_mapping'; import { RoleMappings } from './role_mappings'; +import { User } from './user'; describe('RoleMappings', () => { const initializeRoleMappings = jest.fn(); const initializeRoleMapping = jest.fn(); + const initializeSingleUserRoleMapping = jest.fn(); const handleDeleteMapping = jest.fn(); const mockValues = { - roleMappings: [wsRoleMapping], + roleMappings: [asRoleMapping], dataLoading: false, multipleAuthProvidersConfig: false, + singleUserRoleMappings: [asSingleUserRoleMapping], + singleUserRoleMappingFlyoutOpen: false, }; beforeEach(() => { setMockActions({ initializeRoleMappings, initializeRoleMapping, + initializeSingleUserRoleMapping, handleDeleteMapping, }); setMockValues(mockValues); @@ -50,10 +63,31 @@ describe('RoleMappings', () => { expect(wrapper.find(RoleMapping)).toHaveLength(1); }); - it('handles onClick', () => { + it('renders User flyout', () => { + setMockValues({ ...mockValues, singleUserRoleMappingFlyoutOpen: true }); + const wrapper = shallow(<RoleMappings />); + + expect(wrapper.find(User)).toHaveLength(1); + }); + + it('handles RoleMappingsHeading onClick', () => { const wrapper = shallow(<RoleMappings />); wrapper.find(RoleMappingsHeading).prop('onClick')(); expect(initializeRoleMapping).toHaveBeenCalled(); }); + + it('handles UsersHeading onClick', () => { + const wrapper = shallow(<RoleMappings />); + wrapper.find(UsersHeading).prop('onClick')(); + + expect(initializeSingleUserRoleMapping).toHaveBeenCalled(); + }); + + it('handles empty users state', () => { + setMockValues({ ...mockValues, singleUserRoleMappings: [] }); + const wrapper = shallow(<RoleMappings />); + + expect(wrapper.find(UsersEmptyPrompt)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx index db0e6e6dead11..3e692aa48623e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings.tsx @@ -9,27 +9,45 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; +import { EuiSpacer } from '@elastic/eui'; + import { APP_SEARCH_PLUGIN } from '../../../../../common/constants'; -import { RoleMappingsTable, RoleMappingsHeading } from '../../../shared/role_mapping'; +import { + RoleMappingsTable, + RoleMappingsHeading, + RolesEmptyPrompt, + UsersTable, + UsersHeading, + UsersEmptyPrompt, +} from '../../../shared/role_mapping'; import { ROLE_MAPPINGS_TITLE } from '../../../shared/role_mapping/constants'; + +import { DOCS_PREFIX } from '../../routes'; import { AppSearchPageTemplate } from '../layout'; import { ROLE_MAPPINGS_ENGINE_ACCESS_HEADING } from './constants'; import { RoleMapping } from './role_mapping'; import { RoleMappingsLogic } from './role_mappings_logic'; +import { User } from './user'; + +const ROLES_DOCS_LINK = `${DOCS_PREFIX}/security-and-users.html`; export const RoleMappings: React.FC = () => { const { + enableRoleBasedAccess, initializeRoleMappings, initializeRoleMapping, + initializeSingleUserRoleMapping, handleDeleteMapping, resetState, } = useActions(RoleMappingsLogic); const { roleMappings, + singleUserRoleMappings, multipleAuthProvidersConfig, dataLoading, roleMappingFlyoutOpen, + singleUserRoleMappingFlyoutOpen, } = useValues(RoleMappingsLogic); useEffect(() => { @@ -37,10 +55,21 @@ export const RoleMappings: React.FC = () => { return resetState; }, []); + const hasUsers = singleUserRoleMappings.length > 0; + + const rolesEmptyState = ( + <RolesEmptyPrompt + productName={APP_SEARCH_PLUGIN.NAME} + docsLink={ROLES_DOCS_LINK} + onEnable={enableRoleBasedAccess} + /> + ); + const roleMappingsSection = ( <section> <RoleMappingsHeading productName={APP_SEARCH_PLUGIN.NAME} + docsLink={ROLES_DOCS_LINK} onClick={() => initializeRoleMapping()} /> <RoleMappingsTable @@ -54,14 +83,36 @@ export const RoleMappings: React.FC = () => { </section> ); + const usersTable = ( + <UsersTable + accessItemKey="engines" + singleUserRoleMappings={singleUserRoleMappings} + initializeSingleUserRoleMapping={initializeSingleUserRoleMapping} + handleDeleteMapping={handleDeleteMapping} + /> + ); + + const usersSection = ( + <> + <UsersHeading onClick={() => initializeSingleUserRoleMapping()} /> + <EuiSpacer /> + {hasUsers ? usersTable : <UsersEmptyPrompt />} + </> + ); + return ( <AppSearchPageTemplate pageChrome={[ROLE_MAPPINGS_TITLE]} pageHeader={{ pageTitle: ROLE_MAPPINGS_TITLE }} isLoading={dataLoading} + isEmptyState={roleMappings.length < 1} + emptyState={rolesEmptyState} > {roleMappingFlyoutOpen && <RoleMapping />} + {singleUserRoleMappingFlyoutOpen && <User />} {roleMappingsSection} + <EuiSpacer size="xxl" /> + {usersSection} </AppSearchPageTemplate> ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts index 870e303a2930d..16b44e9ec1f11 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts @@ -15,11 +15,18 @@ import { engines } from '../../__mocks__/engines.mock'; import { nextTick } from '@kbn/test/jest'; -import { asRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; +import { elasticsearchUsers } from '../../../shared/role_mapping/__mocks__/elasticsearch_users'; + +import { + asRoleMapping, + asSingleUserRoleMapping, +} from '../../../shared/role_mapping/__mocks__/roles'; import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants'; import { RoleMappingsLogic } from './role_mappings_logic'; +const emptyUser = { username: '', email: '' }; + describe('RoleMappingsLogic', () => { const { http } = mockHttpValues; const { clearFlashMessages, flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers; @@ -28,6 +35,8 @@ describe('RoleMappingsLogic', () => { attributes: [], availableAuthProviders: [], elasticsearchRoles: [], + elasticsearchUser: emptyUser, + elasticsearchUsers: [], roleMapping: null, roleMappingFlyoutOpen: false, roleMappings: [], @@ -43,6 +52,12 @@ describe('RoleMappingsLogic', () => { selectedAuthProviders: [ANY_AUTH_PROVIDER], selectedOptions: [], roleMappingErrors: [], + singleUserRoleMapping: null, + singleUserRoleMappings: [], + singleUserRoleMappingFlyoutOpen: false, + userCreated: false, + userFormIsNewUser: true, + userFormUserIsExisting: true, }; const mappingsServerProps = { @@ -53,6 +68,8 @@ describe('RoleMappingsLogic', () => { availableEngines: engines, elasticsearchRoles: [], hasAdvancedRoles: false, + singleUserRoleMappings: [asSingleUserRoleMapping], + elasticsearchUsers, }; beforeEach(() => { @@ -83,8 +100,47 @@ describe('RoleMappingsLogic', () => { elasticsearchRoles: mappingsServerProps.elasticsearchRoles, selectedEngines: new Set(), selectedOptions: [], + elasticsearchUsers, + elasticsearchUser: elasticsearchUsers[0], + singleUserRoleMappings: [asSingleUserRoleMapping], }); }); + + it('handles fallback if no elasticsearch users present', () => { + RoleMappingsLogic.actions.setRoleMappingsData({ + ...mappingsServerProps, + elasticsearchUsers: [], + }); + + expect(RoleMappingsLogic.values.elasticsearchUser).toEqual(emptyUser); + }); + }); + + it('setRoleMappings', () => { + RoleMappingsLogic.actions.setRoleMappings({ roleMappings: [asRoleMapping] }); + + expect(RoleMappingsLogic.values.roleMappings).toEqual([asRoleMapping]); + expect(RoleMappingsLogic.values.dataLoading).toEqual(false); + }); + + describe('setElasticsearchUser', () => { + it('sets user', () => { + RoleMappingsLogic.actions.setElasticsearchUser(elasticsearchUsers[0]); + + expect(RoleMappingsLogic.values.elasticsearchUser).toEqual(elasticsearchUsers[0]); + }); + + it('handles fallback if no user present', () => { + RoleMappingsLogic.actions.setElasticsearchUser(undefined); + + expect(RoleMappingsLogic.values.elasticsearchUser).toEqual(emptyUser); + }); + }); + + it('setSingleUserRoleMapping', () => { + RoleMappingsLogic.actions.setSingleUserRoleMapping(asSingleUserRoleMapping); + + expect(RoleMappingsLogic.values.singleUserRoleMapping).toEqual(asSingleUserRoleMapping); }); it('handleRoleChange', () => { @@ -145,6 +201,12 @@ describe('RoleMappingsLogic', () => { }); }); + it('setUserExistingRadioValue', () => { + RoleMappingsLogic.actions.setUserExistingRadioValue(false); + + expect(RoleMappingsLogic.values.userFormUserIsExisting).toEqual(false); + }); + describe('handleAttributeSelectorChange', () => { const elasticsearchRoles = ['foo', 'bar']; @@ -167,6 +229,8 @@ describe('RoleMappingsLogic', () => { attributeName: 'role', elasticsearchRoles, selectedEngines: new Set(), + elasticsearchUsers, + singleUserRoleMappings: [asSingleUserRoleMapping], }); }); @@ -253,19 +317,86 @@ describe('RoleMappingsLogic', () => { expect(clearFlashMessages).toHaveBeenCalled(); }); - it('closeRoleMappingFlyout', () => { + it('openSingleUserRoleMappingFlyout', () => { + mount(mappingsServerProps); + RoleMappingsLogic.actions.openSingleUserRoleMappingFlyout(); + + expect(RoleMappingsLogic.values.singleUserRoleMappingFlyoutOpen).toEqual(true); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + + it('closeUsersAndRolesFlyout', () => { mount({ ...mappingsServerProps, roleMappingFlyoutOpen: true, }); - RoleMappingsLogic.actions.closeRoleMappingFlyout(); + RoleMappingsLogic.actions.closeUsersAndRolesFlyout(); expect(RoleMappingsLogic.values.roleMappingFlyoutOpen).toEqual(false); expect(clearFlashMessages).toHaveBeenCalled(); }); + + it('setElasticsearchUsernameValue', () => { + const username = 'newName'; + RoleMappingsLogic.actions.setElasticsearchUsernameValue(username); + + expect(RoleMappingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + elasticsearchUser: { + ...RoleMappingsLogic.values.elasticsearchUser, + username, + }, + }); + }); + + it('setElasticsearchEmailValue', () => { + const email = 'newEmail@foo.cats'; + RoleMappingsLogic.actions.setElasticsearchEmailValue(email); + + expect(RoleMappingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + elasticsearchUser: { + ...RoleMappingsLogic.values.elasticsearchUser, + email, + }, + }); + }); + + it('setUserCreated', () => { + RoleMappingsLogic.actions.setUserCreated(); + + expect(RoleMappingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + userCreated: true, + }); + }); }); describe('listeners', () => { + describe('enableRoleBasedAccess', () => { + it('calls API and sets values', async () => { + const setRoleMappingsSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappings'); + http.post.mockReturnValue(Promise.resolve(mappingsServerProps)); + RoleMappingsLogic.actions.enableRoleBasedAccess(); + + expect(RoleMappingsLogic.values.dataLoading).toEqual(true); + + expect(http.post).toHaveBeenCalledWith( + '/api/app_search/role_mappings/enable_role_based_access' + ); + await nextTick(); + expect(setRoleMappingsSpy).toHaveBeenCalledWith(mappingsServerProps); + }); + + it('handles error', async () => { + http.post.mockReturnValue(Promise.reject('this is an error')); + RoleMappingsLogic.actions.enableRoleBasedAccess(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + }); + describe('initializeRoleMappings', () => { it('calls API and sets values', async () => { const setRoleMappingsDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingsData'); @@ -304,6 +435,39 @@ describe('RoleMappingsLogic', () => { }); }); + describe('initializeSingleUserRoleMapping', () => { + let setElasticsearchUserSpy: jest.MockedFunction<any>; + let setRoleMappingSpy: jest.MockedFunction<any>; + let setSingleUserRoleMappingSpy: jest.MockedFunction<any>; + beforeEach(() => { + setElasticsearchUserSpy = jest.spyOn(RoleMappingsLogic.actions, 'setElasticsearchUser'); + setRoleMappingSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMapping'); + setSingleUserRoleMappingSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setSingleUserRoleMapping' + ); + }); + + it('should handle the new user state and only set an empty mapping', () => { + RoleMappingsLogic.actions.initializeSingleUserRoleMapping(); + + expect(setElasticsearchUserSpy).not.toHaveBeenCalled(); + expect(setRoleMappingSpy).not.toHaveBeenCalled(); + expect(setSingleUserRoleMappingSpy).toHaveBeenCalledWith(undefined); + }); + + it('should handle an existing user state and set mapping', () => { + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); + RoleMappingsLogic.actions.initializeSingleUserRoleMapping( + asSingleUserRoleMapping.roleMapping.id + ); + + expect(setElasticsearchUserSpy).toHaveBeenCalled(); + expect(setRoleMappingSpy).toHaveBeenCalled(); + expect(setSingleUserRoleMappingSpy).toHaveBeenCalledWith(asSingleUserRoleMapping); + }); + }); + describe('handleSaveMapping', () => { const body = { roleType: 'owner', @@ -399,19 +563,97 @@ describe('RoleMappingsLogic', () => { }); }); - describe('handleDeleteMapping', () => { - let confirmSpy: any; - const roleMappingId = 'r1'; + describe('handleSaveUser', () => { + it('calls API and refreshes list when new mapping', async () => { + const initializeRoleMappingsSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'initializeRoleMappings' + ); + const setUserCreatedSpy = jest.spyOn(RoleMappingsLogic.actions, 'setUserCreated'); + const setSingleUserRoleMappingSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setSingleUserRoleMapping' + ); + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); - beforeEach(() => { - confirmSpy = jest.spyOn(window, 'confirm'); - confirmSpy.mockImplementation(jest.fn(() => true)); + http.post.mockReturnValue(Promise.resolve(mappingsServerProps)); + RoleMappingsLogic.actions.handleSaveUser(); + + expect(http.post).toHaveBeenCalledWith('/api/app_search/single_user_role_mapping', { + body: JSON.stringify({ + roleMapping: { + engines: [], + roleType: 'owner', + accessAllEngines: true, + }, + elasticsearchUser: { + username: elasticsearchUsers[0].username, + email: elasticsearchUsers[0].email, + }, + }), + }); + await nextTick(); + + expect(initializeRoleMappingsSpy).toHaveBeenCalled(); + expect(setUserCreatedSpy).toHaveBeenCalled(); + expect(setSingleUserRoleMappingSpy).toHaveBeenCalled(); }); - afterEach(() => { - confirmSpy.mockRestore(); + it('calls API and refreshes list when existing mapping', async () => { + const initializeRoleMappingsSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'initializeRoleMappings' + ); + RoleMappingsLogic.actions.setSingleUserRoleMapping(asSingleUserRoleMapping); + RoleMappingsLogic.actions.handleAccessAllEnginesChange(false); + + http.put.mockReturnValue(Promise.resolve(mappingsServerProps)); + RoleMappingsLogic.actions.handleSaveUser(); + + expect(http.post).toHaveBeenCalledWith('/api/app_search/single_user_role_mapping', { + body: JSON.stringify({ + roleMapping: { + engines: [], + roleType: 'owner', + accessAllEngines: false, + id: asSingleUserRoleMapping.roleMapping.id, + }, + elasticsearchUser: { + username: '', + email: '', + }, + }), + }); + await nextTick(); + + expect(initializeRoleMappingsSpy).toHaveBeenCalled(); }); + it('handles error', async () => { + const setRoleMappingErrorsSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setRoleMappingErrors' + ); + + http.post.mockReturnValue( + Promise.reject({ + body: { + attributes: { + errors: ['this is an error'], + }, + }, + }) + ); + RoleMappingsLogic.actions.handleSaveUser(); + await nextTick(); + + expect(setRoleMappingErrorsSpy).toHaveBeenCalledWith(['this is an error']); + }); + }); + + describe('handleDeleteMapping', () => { + const roleMappingId = 'r1'; + it('calls API and refreshes list', async () => { mount(mappingsServerProps); const initializeRoleMappingsSpy = jest.spyOn( @@ -436,13 +678,52 @@ describe('RoleMappingsLogic', () => { expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); + }); - it('will do nothing if not confirmed', () => { - mount(mappingsServerProps); - jest.spyOn(window, 'confirm').mockReturnValueOnce(false); - RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId); + describe('handleUsernameSelectChange', () => { + it('sets elasticsearchUser when match found', () => { + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); + const setElasticsearchUserSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setElasticsearchUser' + ); + RoleMappingsLogic.actions.handleUsernameSelectChange(elasticsearchUsers[0].username); + + expect(setElasticsearchUserSpy).toHaveBeenCalledWith(elasticsearchUsers[0]); + }); + + it('does not set elasticsearchUser when no match found', () => { + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); + const setElasticsearchUserSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setElasticsearchUser' + ); + RoleMappingsLogic.actions.handleUsernameSelectChange('bogus'); + + expect(setElasticsearchUserSpy).not.toHaveBeenCalled(); + }); + }); + + describe('setUserExistingRadioValue', () => { + it('handles existing user', () => { + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); + const setElasticsearchUserSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setElasticsearchUser' + ); + RoleMappingsLogic.actions.setUserExistingRadioValue(true); + + expect(setElasticsearchUserSpy).toHaveBeenCalledWith(elasticsearchUsers[0]); + }); + + it('handles new user', () => { + const setElasticsearchUserSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setElasticsearchUser' + ); + RoleMappingsLogic.actions.setUserExistingRadioValue(false); - expect(http.delete).not.toHaveBeenCalled(); + expect(setElasticsearchUserSpy).toHaveBeenCalledWith(emptyUser); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts index fc0a235b23c77..0b57e1d08a294 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts @@ -16,32 +16,36 @@ import { } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants'; -import { AttributeName } from '../../../shared/types'; +import { AttributeName, SingleUserRoleMapping, ElasticsearchUser } from '../../../shared/types'; import { ASRoleMapping, RoleTypes } from '../../types'; import { roleHasScopedEngines } from '../../utils/role/has_scoped_engines'; import { Engine } from '../engine/types'; import { - DELETE_ROLE_MAPPING_MESSAGE, ROLE_MAPPING_DELETED_MESSAGE, ROLE_MAPPING_CREATED_MESSAGE, ROLE_MAPPING_UPDATED_MESSAGE, } from './constants'; +type UserMapping = SingleUserRoleMapping<ASRoleMapping>; + interface RoleMappingsServerDetails { roleMappings: ASRoleMapping[]; attributes: string[]; authProviders: string[]; availableEngines: Engine[]; elasticsearchRoles: string[]; + elasticsearchUsers: ElasticsearchUser[]; hasAdvancedRoles: boolean; multipleAuthProvidersConfig: boolean; + singleUserRoleMappings: UserMapping[]; } const getFirstAttributeName = (roleMapping: ASRoleMapping) => Object.entries(roleMapping.rules)[0][0] as AttributeName; const getFirstAttributeValue = (roleMapping: ASRoleMapping) => Object.entries(roleMapping.rules)[0][1] as AttributeName; +const emptyUser = { username: '', email: '' } as ElasticsearchUser; interface RoleMappingsActions { handleAccessAllEnginesChange(selected: boolean): { selected: boolean }; @@ -54,15 +58,34 @@ interface RoleMappingsActions { handleDeleteMapping(roleMappingId: string): { roleMappingId: string }; handleEngineSelectionChange(engineNames: string[]): { engineNames: string[] }; handleRoleChange(roleType: RoleTypes): { roleType: RoleTypes }; + handleUsernameSelectChange(username: string): { username: string }; handleSaveMapping(): void; + handleSaveUser(): void; initializeRoleMapping(roleMappingId?: string): { roleMappingId?: string }; + initializeSingleUserRoleMapping(roleMappingId?: string): { roleMappingId?: string }; initializeRoleMappings(): void; resetState(): void; setRoleMapping(roleMapping: ASRoleMapping): { roleMapping: ASRoleMapping }; + setSingleUserRoleMapping(data?: UserMapping): { singleUserRoleMapping: UserMapping }; + setRoleMappings({ + roleMappings, + }: { + roleMappings: ASRoleMapping[]; + }): { roleMappings: ASRoleMapping[] }; setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails; + setElasticsearchUser( + elasticsearchUser?: ElasticsearchUser + ): { elasticsearchUser: ElasticsearchUser }; openRoleMappingFlyout(): void; - closeRoleMappingFlyout(): void; + openSingleUserRoleMappingFlyout(): void; + closeUsersAndRolesFlyout(): void; setRoleMappingErrors(errors: string[]): { errors: string[] }; + enableRoleBasedAccess(): void; + setUserExistingRadioValue(userFormUserIsExisting: boolean): { userFormUserIsExisting: boolean }; + setElasticsearchUsernameValue(username: string): { username: string }; + setElasticsearchEmailValue(email: string): { email: string }; + setUserCreated(): void; + setUserFormIsNewUser(userFormIsNewUser: boolean): { userFormIsNewUser: boolean }; } interface RoleMappingsValues { @@ -74,26 +97,38 @@ interface RoleMappingsValues { availableEngines: Engine[]; dataLoading: boolean; elasticsearchRoles: string[]; + elasticsearchUsers: ElasticsearchUser[]; + elasticsearchUser: ElasticsearchUser; hasAdvancedRoles: boolean; multipleAuthProvidersConfig: boolean; roleMapping: ASRoleMapping | null; roleMappings: ASRoleMapping[]; + singleUserRoleMapping: UserMapping | null; + singleUserRoleMappings: UserMapping[]; roleType: RoleTypes; selectedAuthProviders: string[]; selectedEngines: Set<string>; roleMappingFlyoutOpen: boolean; + singleUserRoleMappingFlyoutOpen: boolean; selectedOptions: EuiComboBoxOptionOption[]; roleMappingErrors: string[]; + userFormUserIsExisting: boolean; + userCreated: boolean; + userFormIsNewUser: boolean; } export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappingsActions>>({ - path: ['enterprise_search', 'app_search', 'role_mappings'], + path: ['enterprise_search', 'app_search', 'users_and_roles'], actions: { setRoleMappingsData: (data: RoleMappingsServerDetails) => data, setRoleMapping: (roleMapping: ASRoleMapping) => ({ roleMapping }), + setElasticsearchUser: (elasticsearchUser: ElasticsearchUser) => ({ elasticsearchUser }), + setSingleUserRoleMapping: (singleUserRoleMapping: UserMapping) => ({ singleUserRoleMapping }), + setRoleMappings: ({ roleMappings }: { roleMappings: ASRoleMapping[] }) => ({ roleMappings }), setRoleMappingErrors: (errors: string[]) => ({ errors }), handleAuthProviderChange: (value: string) => ({ value }), handleRoleChange: (roleType: RoleTypes) => ({ roleType }), + handleUsernameSelectChange: (username: string) => ({ username }), handleEngineSelectionChange: (engineNames: string[]) => ({ engineNames }), handleAttributeSelectorChange: (value: string, firstElasticsearchRole: string) => ({ value, @@ -101,26 +136,45 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi }), handleAttributeValueChange: (value: string) => ({ value }), handleAccessAllEnginesChange: (selected: boolean) => ({ selected }), + enableRoleBasedAccess: true, + openSingleUserRoleMappingFlyout: true, + setUserExistingRadioValue: (userFormUserIsExisting: boolean) => ({ userFormUserIsExisting }), resetState: true, initializeRoleMappings: true, + initializeSingleUserRoleMapping: (roleMappingId?: string) => ({ roleMappingId }), initializeRoleMapping: (roleMappingId) => ({ roleMappingId }), handleDeleteMapping: (roleMappingId: string) => ({ roleMappingId }), handleSaveMapping: true, + handleSaveUser: true, openRoleMappingFlyout: true, - closeRoleMappingFlyout: false, + closeUsersAndRolesFlyout: false, + setElasticsearchUsernameValue: (username: string) => ({ username }), + setElasticsearchEmailValue: (email: string) => ({ email }), + setUserCreated: true, + setUserFormIsNewUser: (userFormIsNewUser: boolean) => ({ userFormIsNewUser }), }, reducers: { dataLoading: [ true, { setRoleMappingsData: () => false, + setRoleMappings: () => false, resetState: () => true, + enableRoleBasedAccess: () => true, }, ], roleMappings: [ [], { setRoleMappingsData: (_, { roleMappings }) => roleMappings, + setRoleMappings: (_, { roleMappings }) => roleMappings, + resetState: () => [], + }, + ], + singleUserRoleMappings: [ + [], + { + setRoleMappingsData: (_, { singleUserRoleMappings }) => singleUserRoleMappings, resetState: () => [], }, ], @@ -155,6 +209,14 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi [], { setRoleMappingsData: (_, { elasticsearchRoles }) => elasticsearchRoles, + closeUsersAndRolesFlyout: () => [ANY_AUTH_PROVIDER], + }, + ], + elasticsearchUsers: [ + [], + { + setRoleMappingsData: (_, { elasticsearchUsers }) => elasticsearchUsers, + resetState: () => [], }, ], roleMapping: [ @@ -162,7 +224,7 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi { setRoleMapping: (_, { roleMapping }) => roleMapping, resetState: () => null, - closeRoleMappingFlyout: () => null, + closeUsersAndRolesFlyout: () => null, }, ], roleType: [ @@ -178,6 +240,7 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi setRoleMapping: (_, { roleMapping }) => roleMapping.accessAllEngines, handleRoleChange: (_, { roleType }) => !roleHasScopedEngines(roleType), handleAccessAllEnginesChange: (_, { selected }) => selected, + closeUsersAndRolesFlyout: () => true, }, ], attributeValue: [ @@ -188,7 +251,7 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi value === 'role' ? firstElasticsearchRole : '', handleAttributeValueChange: (_, { value }) => value, resetState: () => '', - closeRoleMappingFlyout: () => '', + closeUsersAndRolesFlyout: () => '', }, ], attributeName: [ @@ -197,7 +260,7 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi setRoleMapping: (_, { roleMapping }) => getFirstAttributeName(roleMapping), handleAttributeSelectorChange: (_, { value }) => value, resetState: () => 'username', - closeRoleMappingFlyout: () => 'username', + closeUsersAndRolesFlyout: () => 'username', }, ], selectedEngines: [ @@ -212,6 +275,7 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi return newSelectedEngineNames; }, + closeUsersAndRolesFlyout: () => new Set(), }, ], availableAuthProviders: [ @@ -241,17 +305,68 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi false, { openRoleMappingFlyout: () => true, - closeRoleMappingFlyout: () => false, + closeUsersAndRolesFlyout: () => false, initializeRoleMappings: () => false, initializeRoleMapping: () => true, }, ], + singleUserRoleMappingFlyoutOpen: [ + false, + { + openSingleUserRoleMappingFlyout: () => true, + closeUsersAndRolesFlyout: () => false, + initializeSingleUserRoleMapping: () => true, + }, + ], + singleUserRoleMapping: [ + null, + { + setSingleUserRoleMapping: (_, { singleUserRoleMapping }) => singleUserRoleMapping || null, + closeUsersAndRolesFlyout: () => null, + }, + ], roleMappingErrors: [ [], { setRoleMappingErrors: (_, { errors }) => errors, handleSaveMapping: () => [], - closeRoleMappingFlyout: () => [], + closeUsersAndRolesFlyout: () => [], + }, + ], + userFormUserIsExisting: [ + true, + { + setUserExistingRadioValue: (_, { userFormUserIsExisting }) => userFormUserIsExisting, + closeUsersAndRolesFlyout: () => true, + }, + ], + elasticsearchUser: [ + emptyUser, + { + setRoleMappingsData: (_, { elasticsearchUsers }) => elasticsearchUsers[0] || emptyUser, + setElasticsearchUser: (_, { elasticsearchUser }) => elasticsearchUser || emptyUser, + setElasticsearchUsernameValue: (state, { username }) => ({ + ...state, + username, + }), + setElasticsearchEmailValue: (state, { email }) => ({ + ...state, + email, + }), + closeUsersAndRolesFlyout: () => emptyUser, + }, + ], + userCreated: [ + false, + { + setUserCreated: () => true, + closeUsersAndRolesFlyout: () => false, + }, + ], + userFormIsNewUser: [ + true, + { + setUserFormIsNewUser: (_, { userFormIsNewUser }) => userFormIsNewUser, }, ], }, @@ -267,6 +382,17 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi ], }), listeners: ({ actions, values }) => ({ + enableRoleBasedAccess: async () => { + const { http } = HttpLogic.values; + const route = '/api/app_search/role_mappings/enable_role_based_access'; + + try { + const response = await http.post(route); + actions.setRoleMappings(response); + } catch (e) { + flashAPIErrors(e); + } + }, initializeRoleMappings: async () => { const { http } = HttpLogic.values; const route = '/api/app_search/role_mappings'; @@ -282,18 +408,27 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi const roleMapping = values.roleMappings.find(({ id }) => id === roleMappingId); if (roleMapping) actions.setRoleMapping(roleMapping); }, + initializeSingleUserRoleMapping: ({ roleMappingId }) => { + const singleUserRoleMapping = values.singleUserRoleMappings.find( + ({ roleMapping }) => roleMapping.id === roleMappingId + ); + if (singleUserRoleMapping) { + actions.setElasticsearchUser(singleUserRoleMapping.elasticsearchUser); + actions.setRoleMapping(singleUserRoleMapping.roleMapping); + } + actions.setSingleUserRoleMapping(singleUserRoleMapping); + actions.setUserFormIsNewUser(!singleUserRoleMapping); + }, handleDeleteMapping: async ({ roleMappingId }) => { const { http } = HttpLogic.values; const route = `/api/app_search/role_mappings/${roleMappingId}`; - if (window.confirm(DELETE_ROLE_MAPPING_MESSAGE)) { - try { - await http.delete(route); - actions.initializeRoleMappings(); - setSuccessMessage(ROLE_MAPPING_DELETED_MESSAGE); - } catch (e) { - flashAPIErrors(e); - } + try { + await http.delete(route); + actions.initializeRoleMappings(); + setSuccessMessage(ROLE_MAPPING_DELETED_MESSAGE); + } catch (e) { + flashAPIErrors(e); } }, handleSaveMapping: async () => { @@ -338,11 +473,56 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi resetState: () => { clearFlashMessages(); }, - closeRoleMappingFlyout: () => { + handleSaveUser: async () => { + const { http } = HttpLogic.values; + const { + roleType, + singleUserRoleMapping, + accessAllEngines, + selectedEngines, + elasticsearchUser: { email, username }, + } = values; + + const body = JSON.stringify({ + roleMapping: { + engines: accessAllEngines ? [] : Array.from(selectedEngines), + roleType, + accessAllEngines, + id: singleUserRoleMapping?.roleMapping?.id, + }, + elasticsearchUser: { + username, + email, + }, + }); + + try { + const response = await http.post('/api/app_search/single_user_role_mapping', { body }); + actions.setSingleUserRoleMapping(response); + actions.setUserCreated(); + actions.initializeRoleMappings(); + } catch (e) { + actions.setRoleMappingErrors(e?.body?.attributes?.errors); + } + }, + closeUsersAndRolesFlyout: () => { clearFlashMessages(); + const firstUser = values.elasticsearchUsers[0]; + actions.setElasticsearchUser(firstUser); }, openRoleMappingFlyout: () => { clearFlashMessages(); }, + openSingleUserRoleMappingFlyout: () => { + clearFlashMessages(); + }, + setUserExistingRadioValue: ({ userFormUserIsExisting }) => { + const firstUser = values.elasticsearchUsers[0]; + actions.setElasticsearchUser(userFormUserIsExisting ? firstUser : emptyUser); + }, + handleUsernameSelectChange: ({ username }) => { + const user = values.elasticsearchUsers.find((u) => u.username === username); + if (user) actions.setElasticsearchUser(user); + }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.test.tsx new file mode 100644 index 0000000000000..88103532bd149 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.test.tsx @@ -0,0 +1,124 @@ +/* + * 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'; +import '../../../__mocks__/shallow_useeffect.mock'; +import { setMockActions, setMockValues } from '../../../__mocks__/kea_logic'; +import { engines } from '../../__mocks__/engines.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { UserFlyout, UserAddedInfo, UserInvitationCallout } from '../../../shared/role_mapping'; +import { elasticsearchUsers } from '../../../shared/role_mapping/__mocks__/elasticsearch_users'; +import { wsSingleUserRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; + +import { EngineAssignmentSelector } from './engine_assignment_selector'; +import { User } from './user'; + +describe('User', () => { + const handleSaveUser = jest.fn(); + const closeUsersAndRolesFlyout = jest.fn(); + const setUserExistingRadioValue = jest.fn(); + const setElasticsearchUsernameValue = jest.fn(); + const setElasticsearchEmailValue = jest.fn(); + const handleRoleChange = jest.fn(); + const handleUsernameSelectChange = jest.fn(); + + const mockValues = { + availableEngines: [], + singleUserRoleMapping: null, + userFormUserIsExisting: false, + elasticsearchUsers: [], + elasticsearchUser: {}, + roleType: 'admin', + roleMappingErrors: [], + userCreated: false, + userFormIsNewUser: false, + hasAdvancedRoles: false, + }; + + beforeEach(() => { + setMockActions({ + handleSaveUser, + closeUsersAndRolesFlyout, + setUserExistingRadioValue, + setElasticsearchUsernameValue, + setElasticsearchEmailValue, + handleRoleChange, + handleUsernameSelectChange, + }); + + setMockValues(mockValues); + }); + + it('renders', () => { + const wrapper = shallow(<User />); + + expect(wrapper.find(UserFlyout)).toHaveLength(1); + }); + + it('renders engine assignment selector when groups present', () => { + setMockValues({ ...mockValues, availableEngines: engines, hasAdvancedRoles: true }); + const wrapper = shallow(<User />); + + expect(wrapper.find(EngineAssignmentSelector)).toHaveLength(1); + }); + + it('renders userInvitationCallout', () => { + setMockValues({ + ...mockValues, + singleUserRoleMapping: wsSingleUserRoleMapping, + }); + const wrapper = shallow(<User />); + + expect(wrapper.find(UserInvitationCallout)).toHaveLength(1); + }); + + it('renders user added info when user created', () => { + setMockValues({ + ...mockValues, + singleUserRoleMapping: wsSingleUserRoleMapping, + userCreated: true, + }); + const wrapper = shallow(<User />); + + expect(wrapper.find(UserAddedInfo)).toHaveLength(1); + }); + + it('disables form when username value not present', () => { + setMockValues({ + ...mockValues, + singleUserRoleMapping: wsSingleUserRoleMapping, + elasticsearchUsers, + elasticsearchUser: { + username: null, + email: 'email@user.com', + }, + }); + const wrapper = shallow(<User />); + + expect(wrapper.find(UserFlyout).prop('disabled')).toEqual(true); + }); + + it('enables form when userFormUserIsExisting', () => { + setMockValues({ + ...mockValues, + userFormUserIsExisting: true.valueOf, + singleUserRoleMapping: wsSingleUserRoleMapping, + elasticsearchUsers, + elasticsearchUser: { + username: null, + email: 'email@user.com', + }, + }); + const wrapper = shallow(<User />); + + expect(wrapper.find(UserFlyout).prop('disabled')).toEqual(false); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.tsx new file mode 100644 index 0000000000000..df231fac64df7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.tsx @@ -0,0 +1,106 @@ +/* + * 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 { useActions, useValues } from 'kea'; + +import { EuiForm } from '@elastic/eui'; + +import { getAppSearchUrl } from '../../../shared/enterprise_search_url'; +import { + UserFlyout, + UserSelector, + UserAddedInfo, + UserInvitationCallout, +} from '../../../shared/role_mapping'; +import { RoleTypes } from '../../types'; + +import { EngineAssignmentSelector } from './engine_assignment_selector'; +import { RoleMappingsLogic } from './role_mappings_logic'; + +const standardRoles = (['owner', 'admin'] as unknown) as RoleTypes[]; +const advancedRoles = (['dev', 'editor', 'analyst'] as unknown) as RoleTypes[]; + +export const User: React.FC = () => { + const { + handleSaveUser, + closeUsersAndRolesFlyout, + setUserExistingRadioValue, + setElasticsearchUsernameValue, + setElasticsearchEmailValue, + handleRoleChange, + handleUsernameSelectChange, + } = useActions(RoleMappingsLogic); + + const { + availableEngines, + singleUserRoleMapping, + hasAdvancedRoles, + userFormUserIsExisting, + elasticsearchUsers, + elasticsearchUser, + roleType, + roleMappingErrors, + userCreated, + userFormIsNewUser, + } = useValues(RoleMappingsLogic); + + const roleTypes = hasAdvancedRoles ? [...standardRoles, ...advancedRoles] : standardRoles; + const hasEngines = availableEngines.length > 0; + const showEngineAssignmentSelector = hasEngines && hasAdvancedRoles; + const flyoutDisabled = + !userFormUserIsExisting && (!elasticsearchUser.email || !elasticsearchUser.username); + + const userAddedInfo = singleUserRoleMapping && ( + <UserAddedInfo + username={singleUserRoleMapping.elasticsearchUser.username} + email={singleUserRoleMapping.elasticsearchUser.email as string} + roleType={singleUserRoleMapping.roleMapping.roleType} + /> + ); + + const userInvitationCallout = singleUserRoleMapping?.invitation && ( + <UserInvitationCallout + isNew={userCreated} + invitationCode={singleUserRoleMapping!.invitation.code} + urlPrefix={getAppSearchUrl()} + /> + ); + + const createUserForm = ( + <EuiForm isInvalid={roleMappingErrors.length > 0} error={roleMappingErrors}> + <UserSelector + isNewUser={userFormIsNewUser} + elasticsearchUsers={elasticsearchUsers} + handleRoleChange={handleRoleChange} + elasticsearchUser={elasticsearchUser} + setUserExisting={setUserExistingRadioValue} + setElasticsearchEmailValue={setElasticsearchEmailValue} + setElasticsearchUsernameValue={setElasticsearchUsernameValue} + handleUsernameSelectChange={handleUsernameSelectChange} + userFormUserIsExisting={userFormUserIsExisting} + roleTypes={roleTypes} + roleType={roleType} + /> + {showEngineAssignmentSelector && <EngineAssignmentSelector />} + </EuiForm> + ); + + return ( + <UserFlyout + disabled={flyoutDisabled} + isComplete={userCreated} + isNew={userFormIsNewUser} + closeUserFlyout={closeUsersAndRolesFlyout} + handleSaveUser={handleSaveUser} + > + {userCreated ? userAddedInfo : createUserForm} + {userInvitationCallout} + </UserFlyout> + ); +}; 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 0ac59a33068ba..9f84bf4bd3b75 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 @@ -24,7 +24,7 @@ import { SearchUILogic } from './search_ui_logic'; export const SearchUI: React.FC = () => { const { loadFieldData } = useActions(SearchUILogic); - const { engine } = useValues(EngineLogic); + const { isEngineSchemaEmpty } = useValues(EngineLogic); useEffect(() => { loadFieldData(); @@ -34,7 +34,7 @@ export const SearchUI: React.FC = () => { <AppSearchPageTemplate pageChrome={getEngineBreadcrumbs([SEARCH_UI_TITLE])} pageHeader={{ pageTitle: SEARCH_UI_TITLE }} - isEmptyState={Object.keys(engine.schema || {}).length === 0} + isEmptyState={isEngineSchemaEmpty} emptyState={<EmptyState />} > <EuiFlexGroup alignItems="flexStart"> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx index 004217d88987b..3076e14d6329b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/components/add_source_engines_button.tsx @@ -18,7 +18,7 @@ export const AddSourceEnginesButton: React.FC = () => { const { openModal } = useActions(SourceEnginesLogic); return ( - <EuiButton color="secondary" fill onClick={openModal}> + <EuiButton fill iconType="plusInCircle" onClick={openModal}> {ADD_SOURCE_ENGINES_BUTTON_LABEL} </EuiButton> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx index 9d2fe653150c3..e2398209e630d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.test.tsx @@ -11,11 +11,9 @@ import '../../__mocks__/engine_logic.mock'; import React from 'react'; -import { shallow, ShallowWrapper } from 'enzyme'; +import { shallow } from 'enzyme'; -import { EuiPageHeader } from '@elastic/eui'; - -import { Loading } from '../../../shared/loading'; +import { getPageHeaderActions } from '../../../test_helpers'; import { AddSourceEnginesButton, AddSourceEnginesModal, SourceEnginesTable } from './components'; @@ -61,20 +59,10 @@ describe('SourceEngines', () => { expect(wrapper.find(AddSourceEnginesModal)).toHaveLength(1); }); - it('renders a loading component before data has loaded', () => { - setMockValues({ ...MOCK_VALUES, dataLoading: true }); - const wrapper = shallow(<SourceEngines />); - - expect(wrapper.find(Loading)).toHaveLength(1); - }); - describe('page actions', () => { - const getPageHeader = (wrapper: ShallowWrapper) => - wrapper.find(EuiPageHeader).dive().children().dive(); - it('contains a button to add source engines', () => { const wrapper = shallow(<SourceEngines />); - expect(getPageHeader(wrapper).find(AddSourceEnginesButton)).toHaveLength(1); + expect(getPageHeaderActions(wrapper).find(AddSourceEnginesButton)).toHaveLength(1); }); it('hides the add source engines button if the user does not have permissions', () => { @@ -86,7 +74,7 @@ describe('SourceEngines', () => { }); const wrapper = shallow(<SourceEngines />); - expect(getPageHeader(wrapper).find(AddSourceEnginesButton)).toHaveLength(0); + expect(getPageHeaderActions(wrapper).find(AddSourceEnginesButton)).toHaveLength(0); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx index 190c44c919020..d2476faf4f3f5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines.tsx @@ -9,13 +9,11 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; -import { EuiPageHeader, EuiPageContent } from '@elastic/eui'; +import { EuiPanel } from '@elastic/eui'; -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; -import { Loading } from '../../../shared/loading'; import { AppLogic } from '../../app_logic'; import { getEngineBreadcrumbs } from '../engine'; +import { AppSearchPageTemplate } from '../layout'; import { AddSourceEnginesButton, AddSourceEnginesModal, SourceEnginesTable } from './components'; import { SOURCE_ENGINES_TITLE } from './i18n'; @@ -33,20 +31,19 @@ export const SourceEngines: React.FC = () => { fetchSourceEngines(); }, []); - if (dataLoading) return <Loading />; - return ( - <> - <SetPageChrome trail={getEngineBreadcrumbs([SOURCE_ENGINES_TITLE])} /> - <EuiPageHeader - pageTitle={SOURCE_ENGINES_TITLE} - rightSideItems={canManageMetaEngineSourceEngines ? [<AddSourceEnginesButton />] : []} - /> - <FlashMessages /> - <EuiPageContent hasBorder> + <AppSearchPageTemplate + pageChrome={getEngineBreadcrumbs([SOURCE_ENGINES_TITLE])} + pageHeader={{ + pageTitle: SOURCE_ENGINES_TITLE, + rightSideItems: canManageMetaEngineSourceEngines ? [<AddSourceEnginesButton />] : [], + }} + isLoading={dataLoading} + > + <EuiPanel hasBorder> <SourceEnginesTable /> {isModalOpen && <AddSourceEnginesModal />} - </EuiPageContent> - </> + </EuiPanel> + </AppSearchPageTemplate> ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.test.tsx index f1382bb5972b2..a43f170e5822f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.test.tsx @@ -11,7 +11,7 @@ import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; -import { EmptyState } from './'; +import { EmptyState, SynonymModal } from './'; describe('EmptyState', () => { it('renders', () => { @@ -24,4 +24,10 @@ describe('EmptyState', () => { expect.stringContaining('/synonyms-guide.html') ); }); + + it('renders the add synonym modal', () => { + const wrapper = shallow(<EmptyState />); + + expect(wrapper.find(SynonymModal)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.tsx index 2eb6643bda503..f856a5c035f81 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/components/empty_state.tsx @@ -7,16 +7,16 @@ import React from 'react'; -import { EuiPanel, EuiEmptyPrompt, EuiButton } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DOCS_PREFIX } from '../../../routes'; -import { SynonymIcon } from './'; +import { SynonymModal, SynonymIcon } from './'; export const EmptyState: React.FC = () => { return ( - <EuiPanel color="subdued"> + <> <EuiEmptyPrompt iconType={SynonymIcon} title={ @@ -47,6 +47,7 @@ export const EmptyState: React.FC = () => { </EuiButton> } /> - </EuiPanel> + <SynonymModal /> + </> ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.test.tsx index c8f65c4bdbc6c..64ac3066b51a5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.test.tsx @@ -13,12 +13,11 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiPageHeader, EuiButton, EuiPagination } from '@elastic/eui'; +import { EuiButton, EuiPagination } from '@elastic/eui'; -import { Loading } from '../../../shared/loading'; -import { rerender } from '../../../test_helpers'; +import { rerender, getPageHeaderActions } from '../../../test_helpers'; -import { SynonymCard, SynonymModal, EmptyState } from './components'; +import { SynonymCard, SynonymModal } from './components'; import { Synonyms } from './'; @@ -53,21 +52,9 @@ describe('Synonyms', () => { }); it('renders a create action button', () => { - const wrapper = shallow(<Synonyms />) - .find(EuiPageHeader) - .dive() - .children() - .dive(); - - wrapper.find(EuiButton).simulate('click'); - expect(actions.openModal).toHaveBeenCalled(); - }); - - it('renders an empty state if no synonyms exist', () => { - setMockValues({ ...values, synonymSets: [] }); const wrapper = shallow(<Synonyms />); - - expect(wrapper.find(EmptyState)).toHaveLength(1); + getPageHeaderActions(wrapper).find(EuiButton).simulate('click'); + expect(actions.openModal).toHaveBeenCalled(); }); describe('loading', () => { @@ -75,14 +62,14 @@ describe('Synonyms', () => { setMockValues({ ...values, synonymSets: [], dataLoading: true }); const wrapper = shallow(<Synonyms />); - expect(wrapper.find(Loading)).toHaveLength(1); + expect(wrapper.prop('isLoading')).toEqual(true); }); it('does not render a full loading state after initial page load', () => { setMockValues({ ...values, synonymSets: [MOCK_SYNONYM_SET], dataLoading: true }); const wrapper = shallow(<Synonyms />); - expect(wrapper.find(Loading)).toHaveLength(0); + expect(wrapper.prop('isLoading')).toEqual(false); }); }); @@ -108,7 +95,7 @@ describe('Synonyms', () => { const wrapper = shallow(<Synonyms />); expect(actions.onPaginate).not.toHaveBeenCalled(); - expect(wrapper.find(EmptyState)).toHaveLength(1); + expect(wrapper.prop('isEmptyState')).toEqual(true); }); it('handles off-by-one shenanigans between EuiPagination and our API', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.tsx index d3ba53819f7de..4a68bc381f764 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.tsx @@ -9,21 +9,11 @@ import React, { useEffect } from 'react'; import { useValues, useActions } from 'kea'; -import { - EuiPageHeader, - EuiButton, - EuiPageContentBody, - EuiSpacer, - EuiFlexGrid, - EuiFlexItem, - EuiPagination, -} from '@elastic/eui'; +import { EuiButton, EuiSpacer, EuiFlexGrid, EuiFlexItem, EuiPagination } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FlashMessages } from '../../../shared/flash_messages'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; -import { Loading } from '../../../shared/loading'; import { getEngineBreadcrumbs } from '../engine'; +import { AppSearchPageTemplate } from '../layout'; import { SynonymCard, SynonymModal, EmptyState } from './components'; import { SYNONYMS_TITLE } from './constants'; @@ -46,46 +36,45 @@ export const Synonyms: React.FC = () => { } }, [synonymSets]); - if (dataLoading && !hasSynonyms) return <Loading />; - return ( - <> - <SetPageChrome trail={getEngineBreadcrumbs([SYNONYMS_TITLE])} /> - <EuiPageHeader - pageTitle={SYNONYMS_TITLE} - rightSideItems={[ - <EuiButton fill onClick={() => openModal(null)}> + <AppSearchPageTemplate + pageChrome={getEngineBreadcrumbs([SYNONYMS_TITLE])} + pageHeader={{ + pageTitle: SYNONYMS_TITLE, + description: i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.synonyms.description', + { + defaultMessage: + 'Use synonyms to relate queries together that contextually have the same meaning in your dataset.', + } + ), + rightSideItems: [ + <EuiButton fill iconType="plusInCircle" onClick={() => openModal(null)}> {i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.synonyms.createSynonymSetButtonLabel', { defaultMessage: 'Create a synonym set' } )} </EuiButton>, - ]} + ], + }} + isLoading={dataLoading && !hasSynonyms} + isEmptyState={!hasSynonyms} + emptyState={<EmptyState />} + > + <EuiFlexGrid columns={3}> + {synonymSets.map(({ id, synonyms }) => ( + <EuiFlexItem key={id}> + <SynonymCard id={id} synonyms={synonyms} /> + </EuiFlexItem> + ))} + </EuiFlexGrid> + <EuiSpacer /> + <EuiPagination + pageCount={meta.page.total_pages} + activePage={meta.page.current - 1} + onPageClick={(pageIndex) => onPaginate(pageIndex + 1)} /> - <FlashMessages /> - <EuiPageContentBody> - <EuiSpacer size="s" /> - {hasSynonyms ? ( - <> - <EuiFlexGrid columns={3}> - {synonymSets.map(({ id, synonyms }) => ( - <EuiFlexItem key={id}> - <SynonymCard id={id} synonyms={synonyms} /> - </EuiFlexItem> - ))} - </EuiFlexGrid> - <EuiSpacer /> - <EuiPagination - pageCount={meta.page.total_pages} - activePage={meta.page.current - 1} - onPageClick={(pageIndex) => onPaginate(pageIndex + 1)} - /> - </> - ) : ( - <EmptyState /> - )} - <SynonymModal /> - </EuiPageContentBody> - </> + <SynonymModal /> + </AppSearchPageTemplate> ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 2402a6ecc6401..00acea945177a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -196,6 +196,6 @@ describe('AppSearchNav', () => { setMockValues({ myRole: { canViewRoleMappings: true } }); const wrapper = shallow(<AppSearchNav />); - expect(wrapper.find(SideNavLink).last().prop('to')).toEqual('/role_mappings'); + expect(wrapper.find(SideNavLink).last().prop('to')).toEqual('/users_and_roles'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index 7b3b13aef05d6..d7ddad5683f38 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -37,7 +37,7 @@ import { SETUP_GUIDE_PATH, SETTINGS_PATH, CREDENTIALS_PATH, - ROLE_MAPPINGS_PATH, + USERS_AND_ROLES_PATH, ENGINES_PATH, ENGINE_PATH, LIBRARY_PATH, @@ -104,9 +104,6 @@ export const AppSearchConfigured: React.FC<Required<InitialAppData>> = (props) = <Route exact path={ENGINES_PATH}> <EnginesOverview /> </Route> - <Route path={ENGINE_PATH}> - <EngineRouter /> - </Route> {canManageEngines && ( <Route exact path={ENGINE_CREATION_PATH}> <EngineCreation /> @@ -117,6 +114,9 @@ export const AppSearchConfigured: React.FC<Required<InitialAppData>> = (props) = <MetaEngineCreation /> </Route> )} + <Route path={ENGINE_PATH}> + <EngineRouter /> + </Route> {canViewSettings && ( <Route exact path={SETTINGS_PATH}> <Settings /> @@ -128,7 +128,7 @@ export const AppSearchConfigured: React.FC<Required<InitialAppData>> = (props) = </Route> )} {canViewRoleMappings && ( - <Route path={ROLE_MAPPINGS_PATH}> + <Route path={USERS_AND_ROLES_PATH}> <RoleMappings /> </Route> )} @@ -162,7 +162,7 @@ export const AppSearchNav: React.FC = () => { <SideNavLink to={CREDENTIALS_PATH}>{CREDENTIALS_TITLE}</SideNavLink> )} {canViewRoleMappings && ( - <SideNavLink shouldShowActiveForSubroutes to={ROLE_MAPPINGS_PATH}> + <SideNavLink shouldShowActiveForSubroutes to={USERS_AND_ROLES_PATH}> {ROLE_MAPPINGS_TITLE} </SideNavLink> )} 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 bd5bdb7b2f665..f086a32bbf590 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 @@ -15,10 +15,10 @@ export const LIBRARY_PATH = '/library'; export const SETTINGS_PATH = '/settings'; export const CREDENTIALS_PATH = '/credentials'; -export const ROLE_MAPPINGS_PATH = '/role_mappings'; +export const USERS_AND_ROLES_PATH = '/users_and_roles'; export const ENGINES_PATH = '/engines'; -export const ENGINE_CREATION_PATH = '/engine_creation'; +export const ENGINE_CREATION_PATH = `${ENGINES_PATH}/new`; // This is safe from conflicting with an :engineName path because new is a reserved name export const ENGINE_PATH = `${ENGINES_PATH}/:engineName`; export const ENGINE_ANALYTICS_PATH = `${ENGINE_PATH}/analytics`; @@ -39,7 +39,7 @@ export const ENGINE_REINDEX_JOB_PATH = `${ENGINE_SCHEMA_PATH}/reindex_job/:reind export const ENGINE_CRAWLER_PATH = `${ENGINE_PATH}/crawler`; export const ENGINE_CRAWLER_DOMAIN_PATH = `${ENGINE_CRAWLER_PATH}/domains/:domainId`; -export const META_ENGINE_CREATION_PATH = '/meta_engine_creation'; +export const META_ENGINE_CREATION_PATH = `${ENGINES_PATH}/new_meta_engine`; // This is safe from conflicting with an :engineName path because engine names cannot have underscores export const META_ENGINE_SOURCE_ENGINES_PATH = `${ENGINE_PATH}/engines`; export const ENGINE_RELEVANCE_TUNING_PATH = `${ENGINE_PATH}/relevance_tuning`; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts index 70990727b8a62..b15bd9e1155cc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/constants/index.ts @@ -6,4 +6,5 @@ */ export * from './actions'; +export * from './labels'; export { DEFAULT_META } from './default_meta'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/constants/labels.ts b/x-pack/plugins/enterprise_search/public/applications/shared/constants/labels.ts new file mode 100644 index 0000000000000..8e6159d2b5b2a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/constants/labels.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. + */ + +import { i18n } from '@kbn/i18n'; + +export const USERNAME_LABEL = i18n.translate('xpack.enterpriseSearch.usernameLabel', { + defaultMessage: 'Username', +}); +export const EMAIL_LABEL = i18n.translate('xpack.enterpriseSearch.emailLabel', { + defaultMessage: 'Email', +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.test.ts index b51416ac76ca7..8cfca3bade993 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.test.ts @@ -19,21 +19,23 @@ import { generateNavLink, getNavLinkActive } from './nav_link_helpers'; describe('generateNavLink', () => { beforeEach(() => { jest.clearAllMocks(); - mockKibanaValues.history.location.pathname = '/current_page'; + mockKibanaValues.history.location.pathname = '/'; }); - it('generates React Router props & isSelected (active) state for use within an EuiSideNavItem obj', () => { + it('generates React Router props for use within an EuiSideNavItem obj', () => { const navItem = generateNavLink({ to: '/test' }); - expect(navItem.href).toEqual('/app/enterprise_search/test'); + expect(navItem).toEqual({ + href: '/app/enterprise_search/test', + onClick: expect.any(Function), + isSelected: false, + }); navItem.onClick({} as any); expect(mockKibanaValues.navigateToUrl).toHaveBeenCalledWith('/test'); - - expect(navItem.isSelected).toEqual(false); }); - describe('getNavLinkActive', () => { + describe('isSelected / getNavLinkActive', () => { it('returns true when the current path matches the link path', () => { mockKibanaValues.history.location.pathname = '/test'; const isSelected = getNavLinkActive({ to: '/test' }); @@ -41,6 +43,13 @@ describe('generateNavLink', () => { expect(isSelected).toEqual(true); }); + it('return false when the current path does not match the link path', () => { + mockKibanaValues.history.location.pathname = '/hello'; + const isSelected = getNavLinkActive({ to: '/world' }); + + expect(isSelected).toEqual(false); + }); + describe('isRoot', () => { it('returns true if the current path is "/"', () => { mockKibanaValues.history.location.pathname = '/'; @@ -58,7 +67,31 @@ describe('generateNavLink', () => { expect(isSelected).toEqual(true); }); - it('returns false if not', () => { + /* NOTE: This logic is primarily used for the following routing scenario: + * 1. /item/{itemId} shows a child subnav, e.g. /items/{itemId}/settings + * - BUT when the child subnav is open, the parent `Item` nav link should not show as active - its child nav links should + * 2. /item/create_item (example) does *not* show a child subnav + * - BUT the parent `Item` nav link should highlight when on this non-subnav route + */ + it('returns false if subroutes already have their own items subnav (with active state)', () => { + mockKibanaValues.history.location.pathname = '/items/123/settings'; + const isSelected = getNavLinkActive({ + to: '/items', + shouldShowActiveForSubroutes: true, + items: [{ id: 'settings', name: 'Settings' }], + }); + + expect(isSelected).toEqual(false); + }); + + it('returns false if not a valid subroute', () => { + mockKibanaValues.history.location.pathname = '/hello/world'; + const isSelected = getNavLinkActive({ to: '/world', shouldShowActiveForSubroutes: true }); + + expect(isSelected).toEqual(false); + }); + + it('returns false for subroutes if the flag is not passed', () => { mockKibanaValues.history.location.pathname = '/hello/world'; const isSelected = getNavLinkActive({ to: '/hello' }); @@ -66,4 +99,10 @@ describe('generateNavLink', () => { }); }); }); + + it('optionally passes items', () => { + const navItem = generateNavLink({ to: '/test', items: [] }); + + expect(navItem.items).toEqual([]); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.ts b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.ts index 6124636af3f99..9caf58886c52e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav_link_helpers.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { EuiSideNavItemType } from '@elastic/eui'; + import { stripTrailingSlash } from '../../../../common/strip_slashes'; import { KibanaLogic } from '../kibana'; @@ -14,12 +16,14 @@ interface Params { to: string; isRoot?: boolean; shouldShowActiveForSubroutes?: boolean; + items?: Array<EuiSideNavItemType<unknown>>; // Primarily passed if using `items` to determine isSelected - if not, you can just set `items` outside of this helper } -export const generateNavLink = ({ to, ...rest }: Params & ReactRouterProps) => { +export const generateNavLink = ({ to, items, ...rest }: Params & ReactRouterProps) => { return { ...generateReactRouterProps({ to, ...rest }), - isSelected: getNavLinkActive({ to, ...rest }), + isSelected: getNavLinkActive({ to, items, ...rest }), + items, }; }; @@ -27,14 +31,19 @@ export const getNavLinkActive = ({ to, isRoot = false, shouldShowActiveForSubroutes = false, + items = [], }: Params): boolean => { const { pathname } = KibanaLogic.values.history.location; const currentPath = stripTrailingSlash(pathname); - const isActive = - currentPath === to || - (shouldShowActiveForSubroutes && currentPath.startsWith(to)) || - (isRoot && currentPath === ''); + if (currentPath === to) return true; + + if (isRoot && currentPath === '') return true; + + if (shouldShowActiveForSubroutes) { + if (items.length) return false; // If a nav link has sub-nav items open, never show it as active + if (currentPath.startsWith(to)) return true; + } - return isActive; + return false; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/loading/loading.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/loading/loading.test.tsx index 4ed242c6ed677..eb3e5f027a2d4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/loading/loading.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/loading/loading.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiLoadingSpinner } from '@elastic/eui'; +import { EuiLoadingLogo, EuiLoadingSpinner } from '@elastic/eui'; import { Loading, LoadingOverlay } from './'; @@ -17,7 +17,7 @@ describe('Loading', () => { it('renders', () => { const wrapper = shallow(<Loading />); expect(wrapper.hasClass('enterpriseSearchLoading')).toBe(true); - expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(1); + expect(wrapper.find(EuiLoadingLogo)).toHaveLength(1); }); }); @@ -25,6 +25,6 @@ describe('LoadingOverlay', () => { it('renders', () => { const wrapper = shallow(<LoadingOverlay />); expect(wrapper.hasClass('enterpriseSearchLoadingOverlay')).toBe(true); - expect(wrapper.find(Loading)).toHaveLength(1); + expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/loading/loading.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/loading/loading.tsx index 627d8386dc1c0..477cc27f5c8ef 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/loading/loading.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/loading/loading.tsx @@ -7,18 +7,20 @@ import React from 'react'; -import { EuiLoadingSpinner } from '@elastic/eui'; +import { EuiLoadingLogo, EuiLoadingSpinner } from '@elastic/eui'; import './loading.scss'; export const Loading: React.FC = () => ( <div className="enterpriseSearchLoading"> - <EuiLoadingSpinner size="xl" /> + <EuiLoadingLogo size="xl" logo="logoEnterpriseSearch" /> </div> ); export const LoadingOverlay: React.FC = () => ( <div className="enterpriseSearchLoadingOverlay"> - <Loading /> + <div className="enterpriseSearchLoading"> + <EuiLoadingSpinner size="xl" /> + </div> </div> ); diff --git a/x-pack/plugins/ml/common/constants/embeddable_map.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/elasticsearch_users.ts similarity index 66% rename from x-pack/plugins/ml/common/constants/embeddable_map.ts rename to x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/elasticsearch_users.ts index 6cb345bae630e..500f560675679 100644 --- a/x-pack/plugins/ml/common/constants/embeddable_map.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/elasticsearch_users.ts @@ -5,9 +5,9 @@ * 2.0. */ -export const COMMON_EMS_LAYER_IDS = [ - 'world_countries', - 'administrative_regions_lvl2', - 'usa_zip_codes', - 'usa_states', +export const elasticsearchUsers = [ + { + email: 'user1@user.com', + username: 'user1', + }, ]; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts index 15dec753351ba..486c1ba6c9af6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts @@ -9,6 +9,8 @@ import { engines } from '../../../app_search/__mocks__/engines.mock'; import { AttributeName } from '../../types'; +import { elasticsearchUsers } from './elasticsearch_users'; + export const asRoleMapping = { id: 'sdgfasdgadf123', attributeName: 'role' as AttributeName, @@ -70,3 +72,20 @@ export const wsRoleMapping = { }, ], }; + +export const invitation = { + email: 'foo@example.com', + code: '123fooqwe', +}; + +export const wsSingleUserRoleMapping = { + invitation, + elasticsearchUser: elasticsearchUsers[0], + roleMapping: wsRoleMapping, +}; + +export const asSingleUserRoleMapping = { + invitation, + elasticsearchUser: elasticsearchUsers[0], + roleMapping: asRoleMapping, +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts index 9f40844e52470..215c76ffb7ef4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts @@ -50,10 +50,26 @@ export const ROLE_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.rol defaultMessage: 'Role', }); +export const USERNAME_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.usernameLabel', { + defaultMessage: 'Username', +}); + +export const EMAIL_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.emailLabel', { + defaultMessage: 'Email', +}); + export const ALL_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.allLabel', { defaultMessage: 'All', }); +export const GROUPS_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.groupsLabel', { + defaultMessage: 'Groups', +}); + +export const ENGINES_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.enginesLabel', { + defaultMessage: 'Engines', +}); + export const AUTH_PROVIDER_LABEL = i18n.translate( 'xpack.enterpriseSearch.roleMapping.authProviderLabel', { @@ -82,10 +98,10 @@ export const ATTRIBUTE_VALUE_ERROR = i18n.translate( } ); -export const DELETE_ROLE_MAPPING_TITLE = i18n.translate( - 'xpack.enterpriseSearch.roleMapping.deleteRoleMappingTitle', +export const REMOVE_ROLE_MAPPING_TITLE = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.removeRoleMappingTitle', { - defaultMessage: 'Remove this role mapping', + defaultMessage: 'Remove role mapping', } ); @@ -96,10 +112,17 @@ export const DELETE_ROLE_MAPPING_DESCRIPTION = i18n.translate( } ); -export const DELETE_ROLE_MAPPING_BUTTON = i18n.translate( - 'xpack.enterpriseSearch.roleMapping.deleteRoleMappingButton', +export const REMOVE_ROLE_MAPPING_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.removeRoleMappingButton', { - defaultMessage: 'Delete mapping', + defaultMessage: 'Remove mapping', + } +); + +export const REMOVE_USER_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.removeUserButton', + { + defaultMessage: 'Remove user', } ); @@ -113,7 +136,7 @@ export const FILTER_ROLE_MAPPINGS_PLACEHOLDER = i18n.translate( export const ROLE_MAPPINGS_TITLE = i18n.translate( 'xpack.enterpriseSearch.roleMapping.roleMappingsTitle', { - defaultMessage: 'Users & roles', + defaultMessage: 'Users and roles', } ); @@ -205,3 +228,197 @@ export const ROLE_MAPPINGS_NO_RESULTS_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.roleMapping.noResults.message', { defaultMessage: 'Create a new role mapping' } ); + +export const ROLES_DISABLED_TITLE = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.rolesDisabledTitle', + { defaultMessage: 'Role-based access is disabled' } +); + +export const ROLES_DISABLED_DESCRIPTION = (productName: ProductName) => + i18n.translate('xpack.enterpriseSearch.roleMapping.rolesDisabledDescription', { + defaultMessage: + 'All users set for this deployment currently have full access to {productName}. To restrict access and manage permissions, you must enable role-based access for Enterprise Search.', + values: { productName }, + }); + +export const ROLES_DISABLED_NOTE = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.rolesDisabledNote', + { + defaultMessage: + 'Note: enabling role-based access restricts access for both App Search and Workplace Search. Once enabled, review access management for both products, if applicable.', + } +); + +export const ENABLE_ROLES_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.enableRolesButton', + { defaultMessage: 'Enable role-based access' } +); + +export const ENABLE_ROLES_LINK = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.enableRolesLink', + { defaultMessage: 'Learn more about role-based access' } +); + +export const INVITATION_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.invitationDescription', + { + defaultMessage: + 'This URL can be shared with the user, allowing them to accept the Enterprise Search invitation and set a new password', + } +); + +export const NEW_INVITATION_LABEL = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.newInvitationLabel', + { defaultMessage: 'Invitation URL' } +); + +export const EXISTING_INVITATION_LABEL = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.existingInvitationLabel', + { defaultMessage: 'The user has not yet accepted the invitation.' } +); + +export const INVITATION_LINK = i18n.translate('xpack.enterpriseSearch.roleMapping.invitationLink', { + defaultMessage: 'Enterprise Search Invitation Link', +}); + +export const NO_USERS_TITLE = i18n.translate('xpack.enterpriseSearch.roleMapping.noUsersTitle', { + defaultMessage: 'No user added', +}); + +export const NO_USERS_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.noUsersDescription', + { + defaultMessage: + 'Users can be added individually, for flexibility. Role mappings provide a broader interface for adding large number of users using user attributes.', + } +); + +export const ENABLE_USERS_LINK = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.enableUsersLink', + { defaultMessage: 'Learn more about user management' } +); + +export const NEW_USER_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.newUserLabel', { + defaultMessage: 'Create new user', +}); + +export const EXISTING_USER_LABEL = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.existingUserLabel', + { defaultMessage: 'Add existing user' } +); + +export const USERNAME_NO_USERS_TEXT = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.usernameNoUsersText', + { defaultMessage: 'No existing user eligible for addition.' } +); + +export const REQUIRED_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.requiredLabel', { + defaultMessage: 'Required', +}); + +export const USERS_HEADING_TITLE = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.usersHeadingTitle', + { defaultMessage: 'Users' } +); + +export const USERS_HEADING_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.usersHeadingDescription', + { + defaultMessage: + 'User management provides granular access for individual or special permission needs. Users from federated sources such as SAML are managed by role mappings, and excluded from this list.', + } +); + +export const USERS_HEADING_LABEL = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.usersHeadingLabel', + { defaultMessage: 'Add a new user' } +); + +export const UPDATE_USER_LABEL = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.updateUserLabel', + { + defaultMessage: 'Update user', + } +); + +export const ADD_USER_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.addUserLabel', { + defaultMessage: 'Add user', +}); + +export const USER_ADDED_LABEL = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.userAddedLabel', + { + defaultMessage: 'User added', + } +); + +export const USER_UPDATED_LABEL = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.userUpdatedLabel', + { + defaultMessage: 'User updated', + } +); + +export const NEW_USER_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.newUserDescription', + { + defaultMessage: 'Provide granular access and permissions', + } +); + +export const UPDATE_USER_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.updateUserDescription', + { + defaultMessage: 'Manage granular access and permissions', + } +); + +export const INVITATION_PENDING_LABEL = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.invitationPendingLabel', + { + defaultMessage: 'Invitation pending', + } +); + +export const ROLE_MODAL_TEXT = i18n.translate('xpack.enterpriseSearch.roleMapping.roleModalText', { + defaultMessage: + 'Removing a role mapping revokes access to any user corresponding to the mapping attributes, but may not take effect immediately for SAML-governed roles. Users with an active SAML session will retain access until it expires.', +}); + +export const USER_MODAL_TITLE = (username: string) => + i18n.translate('xpack.enterpriseSearch.roleMapping.userModalTitle', { + defaultMessage: 'Remove {username}', + values: { username }, + }); + +export const USER_MODAL_TEXT = i18n.translate('xpack.enterpriseSearch.roleMapping.userModalText', { + defaultMessage: + 'Removing a user immediately revokes access to the experience, unless this user’s attributes also corresponds to a role mapping for native and SAML-governed authentication, in which case associated role mappings should also be reviewed and adjusted, as needed.', +}); + +export const FILTER_USERS_LABEL = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.filterUsersLabel', + { + defaultMessage: 'Filter users', + } +); + +export const NO_USERS_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.noUsersLabel', { + defaultMessage: 'No matching users found', +}); + +export const EXTERNAL_ATTRIBUTE_TOOLTIP = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.externalAttributeTooltip', + { + defaultMessage: + 'External attributes are defined by the identity provider, and varies from service to service.', + } +); + +export const AUTH_PROVIDER_TOOLTIP = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.authProviderTooltip', + { + defaultMessage: + 'Provider-specific role mapping is still applied, but configuration is now deprecated.', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts index b0d10e9692714..8096b86939ff3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/index.ts @@ -6,9 +6,17 @@ */ export { AttributeSelector } from './attribute_selector'; +export { RolesEmptyPrompt } from './roles_empty_prompt'; export { RoleMappingsTable } from './role_mappings_table'; export { RoleOptionLabel } from './role_option_label'; export { RoleSelector } from './role_selector'; export { RoleMappingFlyout } from './role_mapping_flyout'; export { RoleMappingsHeading } from './role_mappings_heading'; +export { UserAddedInfo } from './user_added_info'; +export { UserFlyout } from './user_flyout'; +export { UsersHeading } from './users_heading'; +export { UserInvitationCallout } from './user_invitation_callout'; +export { UserSelector } from './user_selector'; +export { UsersTable } from './users_table'; export { UsersAndRolesRowActions } from './users_and_roles_row_actions'; +export { UsersEmptyPrompt } from './users_empty_prompt'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx index c0973bb2c9504..ffcf5508233fc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.test.tsx @@ -20,13 +20,13 @@ import { import { RoleMappingFlyout } from './role_mapping_flyout'; describe('RoleMappingFlyout', () => { - const closeRoleMappingFlyout = jest.fn(); + const closeUsersAndRolesFlyout = jest.fn(); const handleSaveMapping = jest.fn(); const props = { isNew: true, disabled: false, - closeRoleMappingFlyout, + closeUsersAndRolesFlyout, handleSaveMapping, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx index bae991fef3655..4416a2de28011 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mapping_flyout.tsx @@ -36,7 +36,7 @@ interface Props { children: React.ReactNode; isNew: boolean; disabled: boolean; - closeRoleMappingFlyout(): void; + closeUsersAndRolesFlyout(): void; handleSaveMapping(): void; } @@ -44,13 +44,13 @@ export const RoleMappingFlyout: React.FC<Props> = ({ children, isNew, disabled, - closeRoleMappingFlyout, + closeUsersAndRolesFlyout, handleSaveMapping, }) => ( <EuiPortal> <EuiFlyout ownFocus - onClose={closeRoleMappingFlyout} + onClose={closeUsersAndRolesFlyout} size="s" aria-labelledby="flyoutLargeTitle" > @@ -71,7 +71,9 @@ export const RoleMappingFlyout: React.FC<Props> = ({ <EuiFlyoutFooter> <EuiFlexGroup justifyContent="spaceBetween"> <EuiFlexItem grow={false}> - <EuiButtonEmpty onClick={closeRoleMappingFlyout}>{CANCEL_BUTTON_LABEL}</EuiButtonEmpty> + <EuiButtonEmpty onClick={closeUsersAndRolesFlyout}> + {CANCEL_BUTTON_LABEL} + </EuiButtonEmpty> </EuiFlexItem> <EuiFlexItem grow={false}> <EuiButton diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.test.tsx index f0bf86fb306c6..5a2958d60dc2c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.test.tsx @@ -15,7 +15,13 @@ import { RoleMappingsHeading } from './role_mappings_heading'; describe('RoleMappingsHeading', () => { it('renders ', () => { - const wrapper = shallow(<RoleMappingsHeading productName="App Search" onClick={jest.fn()} />); + const wrapper = shallow( + <RoleMappingsHeading + docsLink="http://elastic.co" + productName="App Search" + onClick={jest.fn()} + /> + ); expect(wrapper.find(EuiTitle)).toHaveLength(1); expect(wrapper.find(EuiText)).toHaveLength(1); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.tsx index eee8b180d3281..1984cc6c60a34 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_heading.tsx @@ -28,13 +28,11 @@ import { interface Props { productName: ProductName; + docsLink: string; onClick(): void; } -// TODO: Replace EuiLink href with acutal docs link when available -const ROLE_MAPPINGS_DOCS_HREF = '#TODO'; - -export const RoleMappingsHeading: React.FC<Props> = ({ productName, onClick }) => ( +export const RoleMappingsHeading: React.FC<Props> = ({ productName, docsLink, onClick }) => ( <header> <EuiFlexGroup justifyContent="spaceBetween"> <EuiFlexItem> @@ -45,7 +43,7 @@ export const RoleMappingsHeading: React.FC<Props> = ({ productName, onClick }) = <EuiText color="subdued"> <p> {ROLE_MAPPINGS_HEADING_DESCRIPTION(productName)}{' '} - <EuiLink external href={ROLE_MAPPINGS_DOCS_HREF} target="_blank"> + <EuiLink external href={docsLink} target="_blank"> {ROLE_MAPPINGS_HEADING_DOCS_LINK} </EuiLink> </p> diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx index 156b52a4016c3..61043aa6ad9a8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx @@ -13,7 +13,7 @@ import { mount } from 'enzyme'; import { EuiInMemoryTable, EuiTableHeaderCell } from '@elastic/eui'; -import { ALL_LABEL, ANY_AUTH_PROVIDER_OPTION_LABEL } from './constants'; +import { engines } from '../../app_search/__mocks__/engines.mock'; import { RoleMappingsTable } from './role_mappings_table'; import { UsersAndRolesRowActions } from './users_and_roles_row_actions'; @@ -57,11 +57,13 @@ describe('RoleMappingsTable', () => { }); it('renders auth provider display names', () => { - const wrapper = mount(<RoleMappingsTable {...props} />); + const roleMappingWithAuths = { + ...wsRoleMapping, + authProvider: ['saml', 'native'], + }; + const wrapper = mount(<RoleMappingsTable {...props} roleMappings={[roleMappingWithAuths]} />); - expect(wrapper.find('[data-test-subj="AuthProviderDisplayValue"]').prop('children')).toEqual( - `${ANY_AUTH_PROVIDER_OPTION_LABEL}, other_auth` - ); + expect(wrapper.find('[data-test-subj="ProviderSpecificList"]')).toHaveLength(1); }); it('handles manage click', () => { @@ -78,28 +80,30 @@ describe('RoleMappingsTable', () => { expect(handleDeleteMapping).toHaveBeenCalled(); }); - it('shows default message when "accessAllEngines" is true', () => { + it('handles access items display for all items', () => { const wrapper = mount( <RoleMappingsTable {...props} roleMappings={[asRoleMapping as any]} accessItemKey="engines" /> ); - expect(wrapper.find('[data-test-subj="AccessItemsList"]').prop('children')).toEqual(ALL_LABEL); + expect(wrapper.find('[data-test-subj="AllItems"]')).toHaveLength(1); }); - it('handles display when no items present', () => { - const noItemsRoleMapping = { ...asRoleMapping, engines: [] }; - noItemsRoleMapping.accessAllEngines = false; - + it('handles access items display more than 2 items', () => { + const extraEngine = { + ...engines[0], + id: '3', + }; + + const roleMapping = { + ...asRoleMapping, + engines: [...engines, extraEngine], + accessAllEngines: false, + }; const wrapper = mount( - <RoleMappingsTable - {...props} - roleMappings={[noItemsRoleMapping as any]} - accessItemKey="engines" - /> + <RoleMappingsTable {...props} roleMappings={[roleMapping as any]} accessItemKey="engines" /> ); - - expect(wrapper.find('[data-test-subj="AccessItemsList"]').children().children().text()).toEqual( - '—' + expect(wrapper.find('[data-test-subj="AccessItems"]').prop('children')).toEqual( + `${engines[0].name}, ${engines[1].name} + 1` ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx index 7696cf03ed4b1..4136d114d3420 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx @@ -5,16 +5,19 @@ * 2.0. */ -import React, { Fragment } from 'react'; +import React from 'react'; -import { EuiIconTip, EuiTextColor, EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui'; +import { EuiIconTip, EuiInMemoryTable, EuiBasicTableColumn, EuiLink } from '@elastic/eui'; import { ASRoleMapping } from '../../app_search/types'; import { WSRoleMapping } from '../../workplace_search/types'; +import { docLinks } from '../doc_links'; import { RoleRules } from '../types'; import './role_mappings_table.scss'; +const AUTH_PROVIDER_DOCUMENTATION_URL = `${docLinks.enterpriseSearchBase}/users-access.html`; + import { ANY_AUTH_PROVIDER, ANY_AUTH_PROVIDER_OPTION_LABEL, @@ -25,6 +28,8 @@ import { ATTRIBUTE_VALUE_LABEL, FILTER_ROLE_MAPPINGS_PLACEHOLDER, ROLE_MAPPINGS_NO_RESULTS_MESSAGE, + EXTERNAL_ATTRIBUTE_TOOLTIP, + AUTH_PROVIDER_TOOLTIP, } from './constants'; import { UsersAndRolesRowActions } from './users_and_roles_row_actions'; @@ -46,11 +51,6 @@ interface Props { handleDeleteMapping(roleMappingId: string): void; } -const noItemsPlaceholder = <EuiTextColor color="subdued">—</EuiTextColor>; - -const getAuthProviderDisplayValue = (authProvider: string) => - authProvider === ANY_AUTH_PROVIDER ? ANY_AUTH_PROVIDER_OPTION_LABEL : authProvider; - export const RoleMappingsTable: React.FC<Props> = ({ accessItemKey, accessHeader, @@ -71,7 +71,19 @@ export const RoleMappingsTable: React.FC<Props> = ({ const attributeNameCol: EuiBasicTableColumn<SharedRoleMapping> = { field: 'attribute', - name: EXTERNAL_ATTRIBUTE_LABEL, + name: ( + <span> + {EXTERNAL_ATTRIBUTE_LABEL}{' '} + <EuiIconTip + type="iInCircle" + color="subdued" + content={EXTERNAL_ATTRIBUTE_TOOLTIP} + iconProps={{ + className: 'eui-alignTop', + }} + /> + </span> + ), render: (_, { rules }: SharedRoleMapping) => getFirstAttributeName(rules), }; @@ -90,34 +102,36 @@ export const RoleMappingsTable: React.FC<Props> = ({ const accessItemsCol: EuiBasicTableColumn<SharedRoleMapping> = { field: 'accessItems', name: accessHeader, - render: (_, { accessAllEngines, accessItems }: SharedRoleMapping) => ( - <span data-test-subj="AccessItemsList"> - {accessAllEngines ? ( - ALL_LABEL - ) : ( - <> - {accessItems.length === 0 - ? noItemsPlaceholder - : accessItems.map(({ name }) => ( - <Fragment key={name}> - {name} - <br /> - </Fragment> - ))} - </> - )} - </span> - ), + render: (_, { accessAllEngines, accessItems }: SharedRoleMapping) => { + // Design calls for showing the first 2 items followed by a +x after those 2. + // ['foo', 'bar', 'baz'] would display as: "foo, bar + 1" + const numItems = accessItems.length; + if (accessAllEngines || numItems === 0) + return <span data-test-subj="AllItems">{ALL_LABEL}</span>; + const additionalItems = numItems > 2 ? ` + ${numItems - 2}` : ''; + const names = accessItems.map((item) => item.name); + return ( + <span data-test-subj="AccessItems">{names.slice(0, 2).join(', ') + additionalItems}</span> + ); + }, }; const authProviderCol: EuiBasicTableColumn<SharedRoleMapping> = { field: 'authProvider', name: AUTH_PROVIDER_LABEL, - render: (_, { authProvider }: SharedRoleMapping) => ( - <span data-test-subj="AuthProviderDisplayValue"> - {authProvider.map(getAuthProviderDisplayValue).join(', ')} - </span> - ), + render: (_, { authProvider }: SharedRoleMapping) => { + if (authProvider[0] === ANY_AUTH_PROVIDER) { + return ANY_AUTH_PROVIDER_OPTION_LABEL; + } + return ( + <span data-test-subj="ProviderSpecificList"> + {authProvider.join(', ')}{' '} + <EuiLink href={AUTH_PROVIDER_DOCUMENTATION_URL} target="_blank"> + <EuiIconTip type="alert" color="warning" content={AUTH_PROVIDER_TOOLTIP} /> + </EuiLink> + </span> + ); + }, }; const actionsCol: EuiBasicTableColumn<SharedRoleMapping> = { @@ -143,6 +157,7 @@ export const RoleMappingsTable: React.FC<Props> = ({ const pagination = { hidePerPageOptions: true, + pageSize: 10, }; const search = { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/roles_empty_prompt.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/roles_empty_prompt.test.tsx new file mode 100644 index 0000000000000..8331a45849e3a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/roles_empty_prompt.test.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 { shallow } from 'enzyme'; + +import { EuiButton, EuiLink, EuiEmptyPrompt } from '@elastic/eui'; + +import { RolesEmptyPrompt } from './roles_empty_prompt'; + +describe('RolesEmptyPrompt', () => { + const onEnable = jest.fn(); + + const props = { + productName: 'App Search', + docsLink: 'http://elastic.co', + onEnable, + }; + + it('renders', () => { + const wrapper = shallow(<RolesEmptyPrompt {...props} />); + + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + expect(wrapper.find(EuiEmptyPrompt).dive().find(EuiLink).prop('href')).toEqual(props.docsLink); + }); + + it('calls onEnable on change', () => { + const wrapper = shallow(<RolesEmptyPrompt {...props} />); + const prompt = wrapper.find(EuiEmptyPrompt).dive(); + prompt.find(EuiButton).simulate('click'); + + expect(onEnable).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/roles_empty_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/roles_empty_prompt.tsx new file mode 100644 index 0000000000000..11d50573c45f6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/roles_empty_prompt.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 React from 'react'; + +import { EuiEmptyPrompt, EuiButton, EuiLink, EuiSpacer } from '@elastic/eui'; + +import { ProductName } from '../types'; + +import { + ROLES_DISABLED_TITLE, + ROLES_DISABLED_DESCRIPTION, + ROLES_DISABLED_NOTE, + ENABLE_ROLES_BUTTON, + ENABLE_ROLES_LINK, +} from './constants'; + +interface Props { + productName: ProductName; + docsLink: string; + onEnable(): void; +} + +export const RolesEmptyPrompt: React.FC<Props> = ({ onEnable, docsLink, productName }) => ( + <EuiEmptyPrompt + iconType="lockOpen" + title={<h2>{ROLES_DISABLED_TITLE}</h2>} + body={ + <> + <p>{ROLES_DISABLED_DESCRIPTION(productName)}</p> + <p>{ROLES_DISABLED_NOTE}</p> + </> + } + actions={[ + <EuiButton key="enableRolesButton" fill onClick={onEnable}> + {ENABLE_ROLES_BUTTON} + </EuiButton>, + <EuiSpacer key="spacer" size="xs" />, + <EuiLink key="enableRolesLink" href={docsLink} target="_blank" external> + {ENABLE_ROLES_LINK} + </EuiLink>, + ]} + /> +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.test.tsx new file mode 100644 index 0000000000000..57200b389591d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.test.tsx @@ -0,0 +1,120 @@ +/* + * 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 { UserAddedInfo } from './'; + +describe('UserAddedInfo', () => { + const props = { + username: 'user1', + email: 'test@test.com', + roleType: 'user', + }; + + it('renders with email', () => { + const wrapper = shallow(<UserAddedInfo {...props} />); + + expect(wrapper).toMatchInlineSnapshot(` + <Fragment> + <EuiText + size="s" + > + <strong> + Username + </strong> + </EuiText> + <EuiText + size="s" + > + user1 + </EuiText> + <EuiSpacer /> + <EuiText + size="s" + > + <strong> + Email + </strong> + </EuiText> + <EuiText + size="s" + > + test@test.com + </EuiText> + <EuiSpacer /> + <EuiText + size="s" + > + <strong> + Role + </strong> + </EuiText> + <EuiText + size="s" + > + user + </EuiText> + <EuiSpacer /> + </Fragment> + `); + }); + + it('renders without email', () => { + const wrapper = shallow(<UserAddedInfo {...props} email="" />); + + expect(wrapper).toMatchInlineSnapshot(` + <Fragment> + <EuiText + size="s" + > + <strong> + Username + </strong> + </EuiText> + <EuiText + size="s" + > + user1 + </EuiText> + <EuiSpacer /> + <EuiText + size="s" + > + <strong> + Email + </strong> + </EuiText> + <EuiText + size="s" + > + <EuiTextColor + color="subdued" + > + — + </EuiTextColor> + </EuiText> + <EuiSpacer /> + <EuiText + size="s" + > + <strong> + Role + </strong> + </EuiText> + <EuiText + size="s" + > + user + </EuiText> + <EuiSpacer /> + </Fragment> + `); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.tsx new file mode 100644 index 0000000000000..37804414a94a9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiSpacer, EuiText, EuiTextColor } from '@elastic/eui'; + +import { USERNAME_LABEL, EMAIL_LABEL } from '../constants'; + +import { ROLE_LABEL } from './constants'; + +interface Props { + username: string; + email: string; + roleType: string; +} + +const noItemsPlaceholder = <EuiTextColor color="subdued">—</EuiTextColor>; + +export const UserAddedInfo: React.FC<Props> = ({ username, email, roleType }) => ( + <> + <EuiText size="s"> + <strong>{USERNAME_LABEL}</strong> + </EuiText> + <EuiText size="s">{username}</EuiText> + <EuiSpacer /> + <EuiText size="s"> + <strong>{EMAIL_LABEL}</strong> + </EuiText> + <EuiText size="s">{email || noItemsPlaceholder}</EuiText> + <EuiSpacer /> + <EuiText size="s"> + <strong>{ROLE_LABEL}</strong> + </EuiText> + <EuiText size="s">{roleType}</EuiText> + <EuiSpacer /> + </> +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.test.tsx new file mode 100644 index 0000000000000..43333fe048f23 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.test.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiFlyout, EuiText, EuiIcon } from '@elastic/eui'; + +import { + USERS_HEADING_LABEL, + UPDATE_USER_LABEL, + USER_UPDATED_LABEL, + NEW_USER_DESCRIPTION, + UPDATE_USER_DESCRIPTION, +} from './constants'; + +import { UserFlyout } from './'; + +describe('UserFlyout', () => { + const closeUserFlyout = jest.fn(); + const handleSaveUser = jest.fn(); + + const props = { + children: <div />, + isNew: true, + isComplete: false, + disabled: false, + closeUserFlyout, + handleSaveUser, + }; + + it('renders for new user', () => { + const wrapper = shallow(<UserFlyout {...props} />); + + expect(wrapper.find(EuiFlyout)).toHaveLength(1); + expect(wrapper.find('h2').prop('children')).toEqual(USERS_HEADING_LABEL); + expect(wrapper.find(EuiText).prop('children')).toEqual(<p>{NEW_USER_DESCRIPTION}</p>); + }); + + it('renders for existing user', () => { + const wrapper = shallow(<UserFlyout {...props} isNew={false} />); + + expect(wrapper.find('h2').prop('children')).toEqual(UPDATE_USER_LABEL); + expect(wrapper.find(EuiText).prop('children')).toEqual(<p>{UPDATE_USER_DESCRIPTION}</p>); + }); + + it('renders icon and message for completed user', () => { + const wrapper = shallow(<UserFlyout {...props} isNew={false} isComplete />); + const icon = ( + <EuiIcon + color="secondary" + size="l" + type="checkInCircleFilled" + style={{ marginLeft: 5, marginTop: -5 }} + /> + ); + const children = ( + <span> + {USER_UPDATED_LABEL} {icon} + </span> + ); + + expect(wrapper.find('h2').prop('children')).toEqual(children); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.tsx new file mode 100644 index 0000000000000..a3be5e295ddfe --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_flyout.tsx @@ -0,0 +1,118 @@ +/* + * 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 { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiIcon, + EuiPortal, + EuiText, + EuiTitle, + EuiSpacer, +} from '@elastic/eui'; + +interface Props { + children: React.ReactNode; + isNew: boolean; + isComplete: boolean; + disabled: boolean; + closeUserFlyout(): void; + handleSaveUser(): void; +} + +import { CANCEL_BUTTON_LABEL, CLOSE_BUTTON_LABEL } from '../constants'; + +import { + USERS_HEADING_LABEL, + UPDATE_USER_LABEL, + ADD_USER_LABEL, + USER_ADDED_LABEL, + USER_UPDATED_LABEL, + NEW_USER_DESCRIPTION, + UPDATE_USER_DESCRIPTION, +} from './constants'; + +export const UserFlyout: React.FC<Props> = ({ + children, + isNew, + isComplete, + disabled, + closeUserFlyout, + handleSaveUser, +}) => { + const savedIcon = ( + <EuiIcon + color="secondary" + size="l" + type="checkInCircleFilled" + style={{ marginLeft: 5, marginTop: -5 }} + /> + ); + const IS_EDITING_HEADING = isNew ? USERS_HEADING_LABEL : UPDATE_USER_LABEL; + const IS_EDITING_DESCRIPTION = isNew ? NEW_USER_DESCRIPTION : UPDATE_USER_DESCRIPTION; + const USER_SAVED_HEADING = isNew ? USER_ADDED_LABEL : USER_UPDATED_LABEL; + const IS_COMPLETE_HEADING = ( + <span> + {USER_SAVED_HEADING} {savedIcon} + </span> + ); + + const editingFooterActions = ( + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty onClick={closeUserFlyout}>{CANCEL_BUTTON_LABEL}</EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton disabled={disabled} onClick={handleSaveUser} fill> + {isNew ? ADD_USER_LABEL : UPDATE_USER_LABEL} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + ); + + const completedFooterAction = ( + <EuiFlexGroup justifyContent="center"> + <EuiFlexItem grow={false}> + <EuiButton fill onClick={closeUserFlyout}> + {CLOSE_BUTTON_LABEL} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + ); + + return ( + <EuiPortal> + <EuiFlyout ownFocus onClose={closeUserFlyout} size="s" aria-labelledby="userFlyoutTitle"> + <EuiFlyoutHeader hasBorder> + <EuiTitle size="m"> + <h2 id="userFlyoutTitle">{isComplete ? IS_COMPLETE_HEADING : IS_EDITING_HEADING}</h2> + </EuiTitle> + {!isComplete && ( + <EuiText size="xs"> + <p>{IS_EDITING_DESCRIPTION}</p> + </EuiText> + )} + </EuiFlyoutHeader> + <EuiFlyoutBody> + {children} + <EuiSpacer /> + </EuiFlyoutBody> + <EuiFlyoutFooter> + {isComplete ? completedFooterAction : editingFooterActions} + </EuiFlyoutFooter> + </EuiFlyout> + </EuiPortal> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_invitation_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_invitation_callout.test.tsx new file mode 100644 index 0000000000000..d5272a26715b6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_invitation_callout.test.tsx @@ -0,0 +1,46 @@ +/* + * 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 { EuiText, EuiButtonIcon, EuiCopy } from '@elastic/eui'; + +import { EXISTING_INVITATION_LABEL } from './constants'; + +import { UserInvitationCallout } from './'; + +describe('UserInvitationCallout', () => { + const props = { + isNew: true, + invitationCode: 'test@test.com', + urlPrefix: 'http://foo', + }; + + it('renders', () => { + const wrapper = shallow(<UserInvitationCallout {...props} />); + + expect(wrapper.find(EuiText)).toHaveLength(2); + }); + + it('renders the copy button', () => { + const copyMock = jest.fn(); + const wrapper = shallow(<UserInvitationCallout {...props} />); + + const copyEl = shallow(<div>{wrapper.find(EuiCopy).props().children(copyMock)}</div>); + expect(copyEl.find(EuiButtonIcon).props().onClick).toEqual(copyMock); + }); + + it('renders existing invitation label', () => { + const wrapper = shallow(<UserInvitationCallout {...props} isNew={false} />); + + expect(wrapper.find(EuiText).first().prop('children')).toEqual( + <strong>{EXISTING_INVITATION_LABEL}</strong> + ); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_invitation_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_invitation_callout.tsx new file mode 100644 index 0000000000000..d6d0ce7b050ab --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_invitation_callout.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiCopy, EuiButtonIcon, EuiSpacer, EuiText, EuiLink } from '@elastic/eui'; + +import { + INVITATION_DESCRIPTION, + NEW_INVITATION_LABEL, + EXISTING_INVITATION_LABEL, + INVITATION_LINK, +} from './constants'; + +interface Props { + isNew: boolean; + invitationCode: string; + urlPrefix: string; +} + +export const UserInvitationCallout: React.FC<Props> = ({ isNew, invitationCode, urlPrefix }) => { + const link = `${urlPrefix}/invitations/${invitationCode}`; + const label = isNew ? NEW_INVITATION_LABEL : EXISTING_INVITATION_LABEL; + + return ( + <> + {!isNew && <EuiSpacer />} + <EuiText size="s"> + <strong>{label}</strong> + </EuiText> + <EuiSpacer size="xs" /> + <EuiText size="s">{INVITATION_DESCRIPTION}</EuiText> + <EuiSpacer size="xs" /> + <EuiLink href={link} target="_blank" external> + {INVITATION_LINK} + </EuiLink>{' '} + <EuiCopy textToCopy={link}> + {(copy) => <EuiButtonIcon iconType="copy" onClick={copy} />} + </EuiCopy> + <EuiSpacer /> + </> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.test.tsx new file mode 100644 index 0000000000000..60bac97d09835 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.test.tsx @@ -0,0 +1,111 @@ +/* + * 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 { elasticsearchUsers } from './__mocks__/elasticsearch_users'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiFormRow } from '@elastic/eui'; + +import { RoleTypes as ASRole } from '../../app_search/types'; + +import { REQUIRED_LABEL, USERNAME_NO_USERS_TEXT } from './constants'; + +import { UserSelector } from './'; + +const simulatedEvent = { + target: { value: 'foo' }, +}; + +describe('UserSelector', () => { + const setUserExisting = jest.fn(); + const setElasticsearchUsernameValue = jest.fn(); + const setElasticsearchEmailValue = jest.fn(); + const handleRoleChange = jest.fn(); + const handleUsernameSelectChange = jest.fn(); + + const roleType = ('user' as unknown) as ASRole; + + const props = { + isNewUser: true, + userFormUserIsExisting: true, + elasticsearchUsers, + elasticsearchUser: elasticsearchUsers[0], + roleTypes: [roleType], + roleType, + setUserExisting, + setElasticsearchUsernameValue, + setElasticsearchEmailValue, + handleRoleChange, + handleUsernameSelectChange, + }; + + it('renders Role select and calls method', () => { + const wrapper = shallow(<UserSelector {...props} />); + wrapper.find('[data-test-subj="RoleSelect"]').simulate('change', simulatedEvent); + + expect(handleRoleChange).toHaveBeenCalled(); + }); + + it('renders when updating user', () => { + const wrapper = shallow(<UserSelector {...props} isNewUser={false} />); + + expect(wrapper.find('[data-test-subj="UsernameInput"]')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="EmailInput"]')).toHaveLength(1); + }); + + it('renders Username select and calls method', () => { + const wrapper = shallow(<UserSelector {...props} />); + wrapper.find('[data-test-subj="UsernameSelect"]').simulate('change', simulatedEvent); + + expect(handleUsernameSelectChange).toHaveBeenCalled(); + }); + + it('renders Existing user radio and calls method', () => { + const wrapper = shallow(<UserSelector {...props} />); + wrapper.find('[data-test-subj="ExistingUserRadio"]').simulate('change'); + + expect(setUserExisting).toHaveBeenCalledWith(true); + }); + + it('renders Email input and calls method', () => { + const wrapper = shallow(<UserSelector {...props} userFormUserIsExisting={false} />); + wrapper.find('[data-test-subj="EmailInput"]').simulate('change', simulatedEvent); + + expect(setElasticsearchEmailValue).toHaveBeenCalled(); + }); + + it('renders Username input and calls method', () => { + const wrapper = shallow(<UserSelector {...props} userFormUserIsExisting={false} />); + wrapper.find('[data-test-subj="UsernameInput"]').simulate('change', simulatedEvent); + + expect(setElasticsearchUsernameValue).toHaveBeenCalled(); + }); + + it('renders New user radio and calls method', () => { + const wrapper = shallow(<UserSelector {...props} />); + wrapper.find('[data-test-subj="NewUserRadio"]').simulate('change'); + + expect(setUserExisting).toHaveBeenCalledWith(false); + }); + + it('renders helpText when values are empty', () => { + const wrapper = shallow( + <UserSelector + {...props} + userFormUserIsExisting={false} + elasticsearchUsers={[]} + elasticsearchUser={{ email: '', username: '' }} + /> + ); + + expect(wrapper.find(EuiFormRow).at(0).prop('helpText')).toEqual(USERNAME_NO_USERS_TEXT); + expect(wrapper.find(EuiFormRow).at(1).prop('helpText')).toEqual(REQUIRED_LABEL); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.tsx new file mode 100644 index 0000000000000..d65f97265f6a3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_selector.tsx @@ -0,0 +1,160 @@ +/* + * 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 { + EuiFieldText, + EuiRadio, + EuiFormRow, + EuiSelect, + EuiSelectOption, + EuiSpacer, +} from '@elastic/eui'; + +import { RoleTypes as ASRole } from '../../app_search/types'; +import { ElasticsearchUser } from '../../shared/types'; +import { Role as WSRole } from '../../workplace_search/types'; + +import { USERNAME_LABEL, EMAIL_LABEL } from '../constants'; + +import { + NEW_USER_LABEL, + EXISTING_USER_LABEL, + USERNAME_NO_USERS_TEXT, + REQUIRED_LABEL, + ROLE_LABEL, +} from './constants'; + +type SharedRole = WSRole | ASRole; + +interface Props { + isNewUser: boolean; + userFormUserIsExisting: boolean; + elasticsearchUsers: ElasticsearchUser[]; + elasticsearchUser: ElasticsearchUser; + roleTypes: SharedRole[]; + roleType: SharedRole; + setUserExisting(userFormUserIsExisting: boolean): void; + setElasticsearchUsernameValue(username: string): void; + setElasticsearchEmailValue(email: string): void; + handleRoleChange(roleType: SharedRole): void; + handleUsernameSelectChange(username: string): void; +} + +export const UserSelector: React.FC<Props> = ({ + isNewUser, + userFormUserIsExisting, + elasticsearchUsers, + elasticsearchUser, + roleTypes, + roleType, + setUserExisting, + setElasticsearchUsernameValue, + setElasticsearchEmailValue, + handleRoleChange, + handleUsernameSelectChange, +}) => { + const roleOptions = roleTypes.map((role) => ({ id: role, text: role })); + const usernameOptions = elasticsearchUsers.map(({ username }) => ({ + id: username, + text: username, + })); + const hasElasticsearchUsers = elasticsearchUsers.length > 0; + const showNewUserExistingUserControls = userFormUserIsExisting && hasElasticsearchUsers; + + const roleSelect = ( + <EuiFormRow label={ROLE_LABEL}> + <EuiSelect + name={ROLE_LABEL} + data-test-subj="RoleSelect" + options={roleOptions as EuiSelectOption[]} + value={roleType as string} + onChange={(e) => handleRoleChange(e.target.value as SharedRole)} + /> + </EuiFormRow> + ); + + const emailInput = ( + <EuiFormRow label={EMAIL_LABEL}> + <EuiFieldText + name={EMAIL_LABEL} + data-test-subj="EmailInput" + value={elasticsearchUser.email as string} + onChange={(e) => setElasticsearchEmailValue(e.target.value)} + /> + </EuiFormRow> + ); + + const usernameAndEmailControls = ( + <> + <EuiFormRow label={USERNAME_LABEL} helpText={!elasticsearchUser.username && REQUIRED_LABEL}> + <EuiFieldText + name={USERNAME_LABEL} + disabled={!isNewUser} + data-test-subj="UsernameInput" + value={elasticsearchUser.username} + onChange={(e) => setElasticsearchUsernameValue(e.target.value)} + /> + </EuiFormRow> + {elasticsearchUser.email !== null && emailInput} + {roleSelect} + </> + ); + + const existingUserControls = ( + <> + <EuiSpacer size="s" /> + <EuiFormRow label={USERNAME_LABEL}> + <EuiSelect + name="Username select" + data-test-subj="UsernameSelect" + options={usernameOptions} + value={elasticsearchUser.username} + disabled={!hasElasticsearchUsers} + onChange={(e) => handleUsernameSelectChange(e.target.value)} + /> + </EuiFormRow> + {roleSelect} + </> + ); + + const newUserControls = ( + <> + <EuiSpacer size="s" /> + {usernameAndEmailControls} + </> + ); + + const createUserControls = ( + <> + <EuiFormRow helpText={!hasElasticsearchUsers && USERNAME_NO_USERS_TEXT}> + <EuiRadio + id="existingUser" + data-test-subj="ExistingUserRadio" + label={EXISTING_USER_LABEL} + checked={userFormUserIsExisting && hasElasticsearchUsers} + onChange={() => setUserExisting(true)} + disabled={!hasElasticsearchUsers} + /> + </EuiFormRow> + + {showNewUserExistingUserControls && existingUserControls} + <EuiSpacer /> + <EuiRadio + id="newUser" + data-test-subj="NewUserRadio" + label={NEW_USER_LABEL} + checked={!userFormUserIsExisting || !hasElasticsearchUsers} + onChange={() => setUserExisting(false)} + /> + {!showNewUserExistingUserControls && newUserControls} + </> + ); + + return isNewUser ? createUserControls : usernameAndEmailControls; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.test.tsx index dbb47b50d4066..5f1fefc688c77 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.test.tsx @@ -9,15 +9,23 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiButtonIcon } from '@elastic/eui'; +import { EuiButtonIcon, EuiConfirmModal } from '@elastic/eui'; + +import { + REMOVE_ROLE_MAPPING_TITLE, + REMOVE_ROLE_MAPPING_BUTTON, + ROLE_MODAL_TEXT, +} from './constants'; import { UsersAndRolesRowActions } from './users_and_roles_row_actions'; describe('UsersAndRolesRowActions', () => { const onManageClick = jest.fn(); const onDeleteClick = jest.fn(); + const username = 'foo'; const props = { + username, onManageClick, onDeleteClick, }; @@ -40,7 +48,19 @@ describe('UsersAndRolesRowActions', () => { const wrapper = shallow(<UsersAndRolesRowActions {...props} />); const button = wrapper.find(EuiButtonIcon).last(); button.simulate('click'); + wrapper.find(EuiConfirmModal).prop('onConfirm')!({} as any); expect(onDeleteClick).toHaveBeenCalled(); }); + + it('renders role mapping confirm modal text', () => { + const wrapper = shallow(<UsersAndRolesRowActions {...props} username={undefined} />); + const button = wrapper.find(EuiButtonIcon).last(); + button.simulate('click'); + const modal = wrapper.find(EuiConfirmModal); + + expect(modal.prop('title')).toEqual(REMOVE_ROLE_MAPPING_TITLE); + expect(modal.prop('children')).toEqual(<p>{ROLE_MODAL_TEXT}</p>); + expect(modal.prop('confirmButtonText')).toEqual(REMOVE_ROLE_MAPPING_BUTTON); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.tsx index 3d956c0aabd68..a3b0d24769bf6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_and_roles_row_actions.tsx @@ -5,20 +5,65 @@ * 2.0. */ -import React from 'react'; +import React, { useState } from 'react'; -import { EuiButtonIcon } from '@elastic/eui'; +import { EuiButtonIcon, EuiConfirmModal } from '@elastic/eui'; -import { MANAGE_BUTTON_LABEL, DELETE_BUTTON_LABEL } from '../constants'; +import { CANCEL_BUTTON_LABEL, MANAGE_BUTTON_LABEL, DELETE_BUTTON_LABEL } from '../constants'; + +import { + REMOVE_ROLE_MAPPING_TITLE, + REMOVE_ROLE_MAPPING_BUTTON, + REMOVE_USER_BUTTON, + ROLE_MODAL_TEXT, + USER_MODAL_TITLE, + USER_MODAL_TEXT, +} from './constants'; interface Props { + username?: string; onManageClick(): void; onDeleteClick(): void; } -export const UsersAndRolesRowActions: React.FC<Props> = ({ onManageClick, onDeleteClick }) => ( - <> - <EuiButtonIcon onClick={onManageClick} iconType="pencil" aria-label={MANAGE_BUTTON_LABEL} />{' '} - <EuiButtonIcon onClick={onDeleteClick} iconType="trash" aria-label={DELETE_BUTTON_LABEL} /> - </> -); +export const UsersAndRolesRowActions: React.FC<Props> = ({ + onManageClick, + onDeleteClick, + username, +}) => { + const [deleteModalVisible, setVisible] = useState(false); + const showDeleteModal = () => setVisible(true); + const closeDeleteModal = () => setVisible(false); + const title = username ? USER_MODAL_TITLE(username) : REMOVE_ROLE_MAPPING_TITLE; + const text = username ? USER_MODAL_TEXT : ROLE_MODAL_TEXT; + const confirmButton = username ? REMOVE_USER_BUTTON : REMOVE_ROLE_MAPPING_BUTTON; + + const deleteModal = ( + <EuiConfirmModal + title={title} + onCancel={closeDeleteModal} + onConfirm={() => { + onDeleteClick(); + closeDeleteModal(); + }} + cancelButtonText={CANCEL_BUTTON_LABEL} + confirmButtonText={confirmButton} + buttonColor="danger" + defaultFocusedButton="confirm" + > + <p>{text}</p> + </EuiConfirmModal> + ); + + return ( + <> + {deleteModalVisible && deleteModal} + <EuiButtonIcon + onClick={onManageClick} + iconType="pencil" + aria-label={MANAGE_BUTTON_LABEL} + />{' '} + <EuiButtonIcon onClick={showDeleteModal} iconType="trash" aria-label={DELETE_BUTTON_LABEL} /> + </> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_empty_prompt.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_empty_prompt.test.tsx new file mode 100644 index 0000000000000..9110c09827c49 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_empty_prompt.test.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { EuiEmptyPrompt } from '@elastic/eui'; + +import { UsersEmptyPrompt } from './'; + +describe('UsersEmptyPrompt', () => { + it('renders', () => { + const wrapper = shallow(<UsersEmptyPrompt />); + + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_empty_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_empty_prompt.tsx new file mode 100644 index 0000000000000..42bf690c388c4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_empty_prompt.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiEmptyPrompt, + EuiLink, + EuiSpacer, +} from '@elastic/eui'; + +import { docLinks } from '../doc_links'; + +import { NO_USERS_TITLE, NO_USERS_DESCRIPTION, ENABLE_USERS_LINK } from './constants'; + +const USERS_DOCS_URL = `${docLinks.enterpriseSearchBase}/users-access.html`; + +export const UsersEmptyPrompt: React.FC = () => ( + <EuiFlexGroup alignItems="center" justifyContent="center"> + <EuiFlexItem> + <EuiSpacer /> + <EuiPanel style={{ maxWidth: 700, margin: '0 auto' }}> + <EuiEmptyPrompt + iconType="user" + title={<h2>{NO_USERS_TITLE}</h2>} + body={<p>{NO_USERS_DESCRIPTION}</p>} + actions={ + <EuiLink href={USERS_DOCS_URL} target="_blank" external> + {ENABLE_USERS_LINK} + </EuiLink> + } + /> + </EuiPanel> + </EuiFlexItem> + </EuiFlexGroup> +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_heading.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_heading.test.tsx new file mode 100644 index 0000000000000..9bae93079e89f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_heading.test.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 { shallow } from 'enzyme'; + +import { EuiButton, EuiText, EuiTitle } from '@elastic/eui'; + +import { UsersHeading } from './'; + +describe('UsersHeading', () => { + const onClick = jest.fn(); + + it('renders', () => { + const wrapper = shallow(<UsersHeading onClick={onClick} />); + + expect(wrapper.find(EuiText)).toHaveLength(1); + expect(wrapper.find(EuiTitle)).toHaveLength(1); + }); + + it('handles button click', () => { + const wrapper = shallow(<UsersHeading onClick={onClick} />); + wrapper.find(EuiButton).simulate('click'); + + expect(onClick).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_heading.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_heading.tsx new file mode 100644 index 0000000000000..8d097e21e9c3f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_heading.tsx @@ -0,0 +1,37 @@ +/* + * 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 { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; + +import { USERS_HEADING_TITLE, USERS_HEADING_DESCRIPTION, USERS_HEADING_LABEL } from './constants'; + +interface Props { + onClick(): void; +} + +export const UsersHeading: React.FC<Props> = ({ onClick }) => ( + <> + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem> + <EuiTitle> + <h2>{USERS_HEADING_TITLE}</h2> + </EuiTitle> + <EuiText> + <p>{USERS_HEADING_DESCRIPTION}</p> + </EuiText> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton fill onClick={onClick}> + {USERS_HEADING_LABEL} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer size="s" /> + </> +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.test.tsx new file mode 100644 index 0000000000000..dc1a2713ced12 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.test.tsx @@ -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 { asSingleUserRoleMapping, wsSingleUserRoleMapping, asRoleMapping } from './__mocks__/roles'; + +import React from 'react'; + +import { shallow, mount } from 'enzyme'; + +import { EuiInMemoryTable, EuiTextColor } from '@elastic/eui'; + +import { engines } from '../../app_search/__mocks__/engines.mock'; + +import { UsersAndRolesRowActions } from './users_and_roles_row_actions'; + +import { UsersTable } from './'; + +describe('UsersTable', () => { + const initializeSingleUserRoleMapping = jest.fn(); + const handleDeleteMapping = jest.fn(); + const props = { + accessItemKey: 'groups' as 'groups' | 'engines', + singleUserRoleMappings: [wsSingleUserRoleMapping], + initializeSingleUserRoleMapping, + handleDeleteMapping, + }; + + it('renders', () => { + const wrapper = shallow(<UsersTable {...props} />); + + expect(wrapper.find(EuiInMemoryTable)).toHaveLength(1); + }); + + it('handles manage click', () => { + const wrapper = mount(<UsersTable {...props} />); + wrapper.find(UsersAndRolesRowActions).prop('onManageClick')(); + + expect(initializeSingleUserRoleMapping).toHaveBeenCalled(); + }); + + it('handles delete click', () => { + const wrapper = mount(<UsersTable {...props} />); + wrapper.find(UsersAndRolesRowActions).prop('onDeleteClick')(); + + expect(handleDeleteMapping).toHaveBeenCalled(); + }); + + it('handles display when no email present', () => { + const userWithNoEmail = { + ...wsSingleUserRoleMapping, + elasticsearchUser: { + email: null, + username: 'foo', + }, + }; + const wrapper = mount(<UsersTable {...props} singleUserRoleMappings={[userWithNoEmail]} />); + + expect(wrapper.find(EuiTextColor)).toHaveLength(1); + }); + + it('handles access items display for all items', () => { + const userWithAllItems = { + ...asSingleUserRoleMapping, + roleMapping: { + ...asRoleMapping, + engines: [], + }, + }; + const wrapper = mount( + <UsersTable {...props} accessItemKey="engines" singleUserRoleMappings={[userWithAllItems]} /> + ); + + expect(wrapper.find('[data-test-subj="AllItems"]')).toHaveLength(1); + }); + + it('handles access items display more than 2 items', () => { + const extraEngine = { + ...engines[0], + id: '3', + }; + const userWithAllItems = { + ...asSingleUserRoleMapping, + roleMapping: { + ...asRoleMapping, + engines: [...engines, extraEngine], + }, + }; + const wrapper = mount( + <UsersTable {...props} accessItemKey="engines" singleUserRoleMappings={[userWithAllItems]} /> + ); + + expect(wrapper.find('[data-test-subj="AccessItems"]').prop('children')).toEqual( + `${engines[0].name}, ${engines[1].name} + 1` + ); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.tsx new file mode 100644 index 0000000000000..674796775b1d3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.tsx @@ -0,0 +1,148 @@ +/* + * 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 { EuiBadge, EuiBasicTableColumn, EuiInMemoryTable, EuiTextColor } from '@elastic/eui'; + +import { ASRoleMapping } from '../../app_search/types'; +import { SingleUserRoleMapping } from '../../shared/types'; +import { WSRoleMapping } from '../../workplace_search/types'; + +import { + INVITATION_PENDING_LABEL, + ALL_LABEL, + FILTER_USERS_LABEL, + NO_USERS_LABEL, + ROLE_LABEL, + USERNAME_LABEL, + EMAIL_LABEL, + GROUPS_LABEL, + ENGINES_LABEL, +} from './constants'; + +import { UsersAndRolesRowActions } from './'; + +interface AccessItem { + name: string; +} + +interface SharedUser extends SingleUserRoleMapping<ASRoleMapping | WSRoleMapping> { + accessItems: AccessItem[]; + username: string; + email: string | null; + roleType: string; + id: string; +} + +interface SharedRoleMapping extends ASRoleMapping, WSRoleMapping { + accessItems: AccessItem[]; +} + +interface Props { + accessItemKey: 'groups' | 'engines'; + singleUserRoleMappings: Array<SingleUserRoleMapping<ASRoleMapping | WSRoleMapping>>; + initializeSingleUserRoleMapping(roleMappingId: string): void; + handleDeleteMapping(roleMappingId: string): void; +} + +const noItemsPlaceholder = <EuiTextColor color="subdued">—</EuiTextColor>; +const invitationBadge = <EuiBadge color="hollow">{INVITATION_PENDING_LABEL}</EuiBadge>; + +export const UsersTable: React.FC<Props> = ({ + accessItemKey, + singleUserRoleMappings, + initializeSingleUserRoleMapping, + handleDeleteMapping, +}) => { + // 'accessItems' is needed because App Search has `engines` and Workplace Search has `groups`. + const users = ((singleUserRoleMappings as SharedUser[]).map((user) => ({ + username: user.elasticsearchUser.username, + email: user.elasticsearchUser.email, + roleType: user.roleMapping.roleType, + id: user.roleMapping.id, + accessItems: (user.roleMapping as SharedRoleMapping)[accessItemKey], + invitation: user.invitation, + })) as unknown) as Array<Omit<SharedUser, 'elasticsearchUser | roleMapping'>>; + + const columns: Array<EuiBasicTableColumn<SharedUser>> = [ + { + field: 'username', + name: USERNAME_LABEL, + render: (_, { username }: SharedUser) => username, + }, + { + field: 'email', + name: EMAIL_LABEL, + render: (_, { email, invitation }: SharedUser) => { + if (!email) return noItemsPlaceholder; + return ( + <div data-test-subj="EmailDisplayValue"> + {email} {invitation && invitationBadge} + </div> + ); + }, + }, + { + field: 'roleType', + name: ROLE_LABEL, + render: (_, user: SharedUser) => user.roleType, + }, + { + field: 'accessItems', + name: accessItemKey === 'groups' ? GROUPS_LABEL : ENGINES_LABEL, + render: (_, { accessItems }: SharedUser) => { + // Design calls for showing the first 2 items followed by a +x after those 2. + // ['foo', 'bar', 'baz'] would display as: "foo, bar + 1" + const numItems = accessItems.length; + if (numItems === 0) return <span data-test-subj="AllItems">{ALL_LABEL}</span>; + const additionalItems = numItems > 2 ? ` + ${numItems - 2}` : ''; + const names = accessItems.map((item) => item.name); + return ( + <span data-test-subj="AccessItems">{names.slice(0, 2).join(', ') + additionalItems}</span> + ); + }, + }, + { + field: 'id', + name: '', + align: 'right', + render: (_, { id, username }: SharedUser) => ( + <UsersAndRolesRowActions + username={username} + onManageClick={() => initializeSingleUserRoleMapping(id)} + onDeleteClick={() => handleDeleteMapping(id)} + /> + ), + }, + ]; + + const pagination = { + hidePerPageOptions: true, + pageSize: 10, + }; + + const search = { + box: { + incremental: true, + fullWidth: false, + placeholder: FILTER_USERS_LABEL, + 'data-test-subj': 'UsersTableSearchInput', + }, + }; + + return ( + <EuiInMemoryTable + data-test-subj="UsersTable" + columns={columns} + items={users} + search={search} + pagination={pagination} + message={NO_USERS_LABEL} + /> + ); +}; 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 67208c63ddf4c..e6d2c67d1baf8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -40,3 +40,19 @@ export interface RoleMapping { const productNames = [APP_SEARCH_PLUGIN.NAME, WORKPLACE_SEARCH_PLUGIN.NAME] as const; export type ProductName = typeof productNames[number]; + +export interface Invitation { + email: string; + code: string; +} + +export interface ElasticsearchUser { + email: string | null; + username: string; +} + +export interface SingleUserRoleMapping<T> { + invitation: Invitation; + elasticsearchUser: ElasticsearchUser; + roleMapping: T; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx index 04b0880a7351c..04576e981e104 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx @@ -7,7 +7,7 @@ jest.mock('../../../shared/layout', () => ({ ...jest.requireActual('../../../shared/layout'), - generateNavLink: jest.fn(({ to }) => ({ href: to })), + generateNavLink: jest.fn(({ to, items }) => ({ href: to, items })), })); jest.mock('../../views/content_sources/components/source_sub_nav', () => ({ useSourceSubNav: () => [], @@ -53,8 +53,8 @@ describe('useWorkplaceSearchNav', () => { }, { id: 'usersRoles', - name: 'Users & roles', - href: '/role_mappings', + name: 'Users and roles', + href: '/users_and_roles', }, { id: 'security', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx index 99225bc36e892..c8d821dcdae2e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx @@ -15,7 +15,7 @@ import { NAV } from '../../constants'; import { SOURCES_PATH, SECURITY_PATH, - ROLE_MAPPINGS_PATH, + USERS_AND_ROLES_PATH, GROUPS_PATH, ORG_SETTINGS_PATH, } from '../../routes'; @@ -33,8 +33,11 @@ export const useWorkplaceSearchNav = () => { { id: 'sources', name: NAV.SOURCES, - ...generateNavLink({ to: SOURCES_PATH }), - items: useSourceSubNav(), + ...generateNavLink({ + to: SOURCES_PATH, + shouldShowActiveForSubroutes: true, + items: useSourceSubNav(), + }), }, { id: 'groups', @@ -45,7 +48,7 @@ export const useWorkplaceSearchNav = () => { { id: 'usersRoles', name: NAV.ROLE_MAPPINGS, - ...generateNavLink({ to: ROLE_MAPPINGS_PATH }), + ...generateNavLink({ to: USERS_AND_ROLES_PATH }), }, { id: 'security', @@ -89,7 +92,7 @@ export const WorkplaceSearchNav: React.FC<Props> = ({ <SideNavLink to={GROUPS_PATH} subNav={groupsSubNav}> {NAV.GROUPS} </SideNavLink> - <SideNavLink shouldShowActiveForSubroutes to={ROLE_MAPPINGS_PATH}> + <SideNavLink shouldShowActiveForSubroutes to={USERS_AND_ROLES_PATH}> {NAV.ROLE_MAPPINGS} </SideNavLink> <SideNavLink to={SECURITY_PATH}>{NAV.SECURITY}</SideNavLink> diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx index 36496b83b3123..3f6863175e29b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/personal_dashboard_sidebar/private_sources_sidebar.tsx @@ -43,7 +43,6 @@ export const PrivateSourcesSidebar = () => { return ( <> <ViewContentHeader title={PAGE_TITLE} description={PAGE_DESCRIPTION} /> - {/* @ts-expect-error: TODO, uncomment this once EUI 34.x lands in Kibana & `mobileBreakpoints` is a valid prop */} {id && <EuiSideNav items={navItems} mobileBreakpoints={[]} />} </> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts index aa5419f12c7f3..cf459171a808a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts @@ -40,7 +40,7 @@ export const NAV = { defaultMessage: 'Content', }), ROLE_MAPPINGS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.roleMappings', { - defaultMessage: 'Users & roles', + defaultMessage: 'Users and roles', }), SECURITY: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.security', { defaultMessage: 'Security', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index 8a1e9c0275322..05018be2934b4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -26,7 +26,7 @@ import { SOURCE_ADDED_PATH, PERSONAL_SOURCES_PATH, ORG_SETTINGS_PATH, - ROLE_MAPPINGS_PATH, + USERS_AND_ROLES_PATH, SECURITY_PATH, PERSONAL_SETTINGS_PATH, PERSONAL_PATH, @@ -103,7 +103,7 @@ export const WorkplaceSearchConfigured: React.FC<InitialAppData> = (props) => { <Route path={GROUPS_PATH}> <GroupsRouter /> </Route> - <Route path={ROLE_MAPPINGS_PATH}> + <Route path={USERS_AND_ROLES_PATH}> <RoleMappings /> </Route> <Route path={SECURITY_PATH}> diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 3c564c1f912ec..b9309ffd94809 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -48,7 +48,7 @@ export const ENT_SEARCH_LICENSE_MANAGEMENT = `${docLinks.enterpriseSearchBase}/l export const PERSONAL_PATH = '/p'; -export const ROLE_MAPPINGS_PATH = '/role_mappings'; +export const USERS_AND_ROLES_PATH = '/users_and_roles'; export const USERS_PATH = '/users'; export const SECURITY_PATH = '/security'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx index a4eb228eff92f..050aaf1dadf89 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx @@ -11,8 +11,8 @@ import { useValues } from 'kea'; import { EuiTable, EuiTableBody, EuiTablePagination } from '@elastic/eui'; import { Pager } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; +import { USERNAME_LABEL, EMAIL_LABEL } from '../../../../shared/constants'; import { TableHeader } from '../../../../shared/table_header'; import { AppLogic } from '../../../app_logic'; import { UserRow } from '../../../components/shared/user_row'; @@ -20,27 +20,15 @@ import { User } from '../../../types'; import { GroupLogic } from '../group_logic'; const USERS_PER_PAGE = 10; -const USERNAME_TABLE_HEADER = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.groups.groupsUsersTable.usernameTableHeader', - { - defaultMessage: 'Username', - } -); -const EMAIL_TABLE_HEADER = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.groups.groupsUsersTable.emailTableHeader', - { - defaultMessage: 'Email', - } -); export const GroupUsersTable: React.FC = () => { const { isFederatedAuth } = useValues(AppLogic); const { group: { users }, } = useValues(GroupLogic); - const headerItems = [USERNAME_TABLE_HEADER]; + const headerItems = [USERNAME_LABEL]; if (!isFederatedAuth) { - headerItems.push(EMAIL_TABLE_HEADER); + headerItems.push(EMAIL_LABEL); } const [firstItem, setFirstItem] = useState(0); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts index 92c8b7827b9b6..809b631c78391 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts @@ -7,14 +7,6 @@ import { i18n } from '@kbn/i18n'; -export const DELETE_ROLE_MAPPING_MESSAGE = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.roleMapping.deleteRoleMappingButtonMessage', - { - defaultMessage: - 'Are you sure you want to permanently delete this mapping? This action is not reversible and some users might lose access.', - } -); - export const ROLE_MAPPING_DELETED_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.roleMappingDeletedMessage', { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx index cc773895bff1c..20211d40d7010 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx @@ -43,7 +43,7 @@ export const RoleMapping: React.FC = () => { handleAttributeSelectorChange, handleRoleChange, handleAuthProviderChange, - closeRoleMappingFlyout, + closeUsersAndRolesFlyout, } = useActions(RoleMappingsLogic); const { @@ -70,7 +70,7 @@ export const RoleMapping: React.FC = () => { <RoleMappingFlyout disabled={attributeValueInvalid || !hasGroupAssignment} isNew={isNew} - closeRoleMappingFlyout={closeRoleMappingFlyout} + closeUsersAndRolesFlyout={closeUsersAndRolesFlyout} handleSaveMapping={handleSaveMapping} > <EuiForm isInvalid={roleMappingErrors.length > 0} error={roleMappingErrors}> diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.test.tsx index 308022ccb2e5a..2e13f24a13eee 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.test.tsx @@ -12,26 +12,39 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { RoleMappingsTable, RoleMappingsHeading } from '../../../shared/role_mapping'; -import { wsRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; +import { + RoleMappingsTable, + RoleMappingsHeading, + UsersHeading, + UsersEmptyPrompt, +} from '../../../shared/role_mapping'; +import { + wsRoleMapping, + wsSingleUserRoleMapping, +} from '../../../shared/role_mapping/__mocks__/roles'; import { RoleMapping } from './role_mapping'; import { RoleMappings } from './role_mappings'; +import { User } from './user'; describe('RoleMappings', () => { const initializeRoleMappings = jest.fn(); const initializeRoleMapping = jest.fn(); + const initializeSingleUserRoleMapping = jest.fn(); const handleDeleteMapping = jest.fn(); const mockValues = { roleMappings: [wsRoleMapping], dataLoading: false, multipleAuthProvidersConfig: false, + singleUserRoleMappings: [wsSingleUserRoleMapping], + singleUserRoleMappingFlyoutOpen: false, }; beforeEach(() => { setMockActions({ initializeRoleMappings, initializeRoleMapping, + initializeSingleUserRoleMapping, handleDeleteMapping, }); setMockValues(mockValues); @@ -50,10 +63,31 @@ describe('RoleMappings', () => { expect(wrapper.find(RoleMapping)).toHaveLength(1); }); - it('handles onClick', () => { + it('renders User flyout', () => { + setMockValues({ ...mockValues, singleUserRoleMappingFlyoutOpen: true }); + const wrapper = shallow(<RoleMappings />); + + expect(wrapper.find(User)).toHaveLength(1); + }); + + it('handles RoleMappingsHeading onClick', () => { const wrapper = shallow(<RoleMappings />); wrapper.find(RoleMappingsHeading).prop('onClick')(); expect(initializeRoleMapping).toHaveBeenCalled(); }); + + it('handles UsersHeading onClick', () => { + const wrapper = shallow(<RoleMappings />); + wrapper.find(UsersHeading).prop('onClick')(); + + expect(initializeSingleUserRoleMapping).toHaveBeenCalled(); + }); + + it('handles empty users state', () => { + setMockValues({ ...mockValues, singleUserRoleMappings: [] }); + const wrapper = shallow(<RoleMappings />); + + expect(wrapper.find(UsersEmptyPrompt)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx index b153d01224193..df5d7e4267690 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx @@ -9,36 +9,64 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; +import { EuiSpacer } from '@elastic/eui'; + import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; -import { RoleMappingsTable, RoleMappingsHeading } from '../../../shared/role_mapping'; +import { + RoleMappingsTable, + RoleMappingsHeading, + RolesEmptyPrompt, + UsersTable, + UsersHeading, + UsersEmptyPrompt, +} from '../../../shared/role_mapping'; import { ROLE_MAPPINGS_TITLE } from '../../../shared/role_mapping/constants'; import { WorkplaceSearchPageTemplate } from '../../components/layout'; +import { SECURITY_DOCS_URL } from '../../routes'; import { ROLE_MAPPINGS_TABLE_HEADER } from './constants'; import { RoleMapping } from './role_mapping'; import { RoleMappingsLogic } from './role_mappings_logic'; +import { User } from './user'; export const RoleMappings: React.FC = () => { - const { initializeRoleMappings, initializeRoleMapping, handleDeleteMapping } = useActions( - RoleMappingsLogic - ); + const { + enableRoleBasedAccess, + initializeRoleMappings, + initializeRoleMapping, + initializeSingleUserRoleMapping, + handleDeleteMapping, + } = useActions(RoleMappingsLogic); const { roleMappings, + singleUserRoleMappings, dataLoading, multipleAuthProvidersConfig, roleMappingFlyoutOpen, + singleUserRoleMappingFlyoutOpen, } = useValues(RoleMappingsLogic); useEffect(() => { initializeRoleMappings(); }, []); + const hasUsers = singleUserRoleMappings.length > 0; + + const rolesEmptyState = ( + <RolesEmptyPrompt + productName={WORKPLACE_SEARCH_PLUGIN.NAME} + docsLink={SECURITY_DOCS_URL} + onEnable={enableRoleBasedAccess} + /> + ); + const roleMappingsSection = ( <section> <RoleMappingsHeading productName={WORKPLACE_SEARCH_PLUGIN.NAME} + docsLink={SECURITY_DOCS_URL} onClick={() => initializeRoleMapping()} /> <RoleMappingsTable @@ -52,14 +80,36 @@ export const RoleMappings: React.FC = () => { </section> ); + const usersTable = ( + <UsersTable + accessItemKey="groups" + singleUserRoleMappings={singleUserRoleMappings} + initializeSingleUserRoleMapping={initializeSingleUserRoleMapping} + handleDeleteMapping={handleDeleteMapping} + /> + ); + + const usersSection = ( + <> + <UsersHeading onClick={() => initializeSingleUserRoleMapping()} /> + <EuiSpacer /> + {hasUsers ? usersTable : <UsersEmptyPrompt />} + </> + ); + return ( <WorkplaceSearchPageTemplate pageChrome={[ROLE_MAPPINGS_TITLE]} pageHeader={{ pageTitle: ROLE_MAPPINGS_TITLE }} isLoading={dataLoading} + isEmptyState={roleMappings.length < 1} + emptyState={rolesEmptyState} > {roleMappingFlyoutOpen && <RoleMapping />} + {singleUserRoleMappingFlyoutOpen && <User />} {roleMappingsSection} + <EuiSpacer size="xxl" /> + {usersSection} </WorkplaceSearchPageTemplate> ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts index 4ee530870284e..c85e86ebcca2c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts @@ -15,11 +15,18 @@ import { groups } from '../../__mocks__/groups.mock'; import { nextTick } from '@kbn/test/jest'; -import { wsRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; +import { elasticsearchUsers } from '../../../shared/role_mapping/__mocks__/elasticsearch_users'; + +import { + wsRoleMapping, + wsSingleUserRoleMapping, +} from '../../../shared/role_mapping/__mocks__/roles'; import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants'; import { RoleMappingsLogic } from './role_mappings_logic'; +const emptyUser = { username: '', email: '' }; + describe('RoleMappingsLogic', () => { const { http } = mockHttpValues; const { clearFlashMessages, flashAPIErrors } = mockFlashMessageHelpers; @@ -28,6 +35,8 @@ describe('RoleMappingsLogic', () => { attributes: [], availableAuthProviders: [], elasticsearchRoles: [], + elasticsearchUser: emptyUser, + elasticsearchUsers: [], roleMapping: null, roleMappingFlyoutOpen: false, roleMappings: [], @@ -42,6 +51,12 @@ describe('RoleMappingsLogic', () => { selectedAuthProviders: [ANY_AUTH_PROVIDER], selectedOptions: [], roleMappingErrors: [], + singleUserRoleMapping: null, + singleUserRoleMappings: [], + singleUserRoleMappingFlyoutOpen: false, + userCreated: false, + userFormIsNewUser: true, + userFormUserIsExisting: true, }; const roleGroup = { id: '123', @@ -59,6 +74,8 @@ describe('RoleMappingsLogic', () => { authProviders: [], availableGroups: [roleGroup, defaultGroup], elasticsearchRoles: [], + singleUserRoleMappings: [wsSingleUserRoleMapping], + elasticsearchUsers, }; beforeEach(() => { @@ -71,23 +88,63 @@ describe('RoleMappingsLogic', () => { }); describe('actions', () => { - it('setRoleMappingsData', () => { - RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); + describe('setRoleMappingsData', () => { + it('sets data based on server response from the `mappings` (plural) endpoint', () => { + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); + + expect(RoleMappingsLogic.values.roleMappings).toEqual([wsRoleMapping]); + expect(RoleMappingsLogic.values.dataLoading).toEqual(false); + expect(RoleMappingsLogic.values.multipleAuthProvidersConfig).toEqual(true); + expect(RoleMappingsLogic.values.dataLoading).toEqual(false); + expect(RoleMappingsLogic.values.attributes).toEqual(mappingsServerProps.attributes); + expect(RoleMappingsLogic.values.availableGroups).toEqual( + mappingsServerProps.availableGroups + ); + expect(RoleMappingsLogic.values.includeInAllGroups).toEqual(false); + expect(RoleMappingsLogic.values.elasticsearchRoles).toEqual( + mappingsServerProps.elasticsearchRoles + ); + expect(RoleMappingsLogic.values.selectedOptions).toEqual([ + { label: defaultGroup.name, value: defaultGroup.id }, + ]); + expect(RoleMappingsLogic.values.selectedGroups).toEqual(new Set([defaultGroup.id])); + }); + + it('handles fallback if no elasticsearch users present', () => { + RoleMappingsLogic.actions.setRoleMappingsData({ + ...mappingsServerProps, + elasticsearchUsers: [], + }); + + expect(RoleMappingsLogic.values.elasticsearchUser).toEqual(emptyUser); + }); + }); + + it('setRoleMappings', () => { + RoleMappingsLogic.actions.setRoleMappings({ roleMappings: [wsRoleMapping] }); expect(RoleMappingsLogic.values.roleMappings).toEqual([wsRoleMapping]); expect(RoleMappingsLogic.values.dataLoading).toEqual(false); - expect(RoleMappingsLogic.values.multipleAuthProvidersConfig).toEqual(true); - expect(RoleMappingsLogic.values.dataLoading).toEqual(false); - expect(RoleMappingsLogic.values.attributes).toEqual(mappingsServerProps.attributes); - expect(RoleMappingsLogic.values.availableGroups).toEqual(mappingsServerProps.availableGroups); - expect(RoleMappingsLogic.values.includeInAllGroups).toEqual(false); - expect(RoleMappingsLogic.values.elasticsearchRoles).toEqual( - mappingsServerProps.elasticsearchRoles - ); - expect(RoleMappingsLogic.values.selectedOptions).toEqual([ - { label: defaultGroup.name, value: defaultGroup.id }, - ]); - expect(RoleMappingsLogic.values.selectedGroups).toEqual(new Set([defaultGroup.id])); + }); + + describe('setElasticsearchUser', () => { + it('sets user', () => { + RoleMappingsLogic.actions.setElasticsearchUser(elasticsearchUsers[0]); + + expect(RoleMappingsLogic.values.elasticsearchUser).toEqual(elasticsearchUsers[0]); + }); + + it('handles fallback if no user present', () => { + RoleMappingsLogic.actions.setElasticsearchUser(undefined); + + expect(RoleMappingsLogic.values.elasticsearchUser).toEqual(emptyUser); + }); + }); + + it('setSingleUserRoleMapping', () => { + RoleMappingsLogic.actions.setSingleUserRoleMapping(wsSingleUserRoleMapping); + + expect(RoleMappingsLogic.values.singleUserRoleMapping).toEqual(wsSingleUserRoleMapping); }); it('handleRoleChange', () => { @@ -126,6 +183,12 @@ describe('RoleMappingsLogic', () => { expect(RoleMappingsLogic.values.includeInAllGroups).toEqual(true); }); + it('setUserExistingRadioValue', () => { + RoleMappingsLogic.actions.setUserExistingRadioValue(false); + + expect(RoleMappingsLogic.values.userFormUserIsExisting).toEqual(false); + }); + describe('handleAttributeSelectorChange', () => { const elasticsearchRoles = ['foo', 'bar']; @@ -221,19 +284,77 @@ describe('RoleMappingsLogic', () => { expect(clearFlashMessages).toHaveBeenCalled(); }); - it('closeRoleMappingFlyout', () => { + it('openSingleUserRoleMappingFlyout', () => { + mount(mappingsServerProps); + RoleMappingsLogic.actions.openSingleUserRoleMappingFlyout(); + + expect(RoleMappingsLogic.values.singleUserRoleMappingFlyoutOpen).toEqual(true); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + + it('closeUsersAndRolesFlyout', () => { mount({ ...mappingsServerProps, roleMappingFlyoutOpen: true, }); - RoleMappingsLogic.actions.closeRoleMappingFlyout(); + RoleMappingsLogic.actions.closeUsersAndRolesFlyout(); expect(RoleMappingsLogic.values.roleMappingFlyoutOpen).toEqual(false); expect(clearFlashMessages).toHaveBeenCalled(); }); + + it('setElasticsearchUsernameValue', () => { + const username = 'newName'; + RoleMappingsLogic.actions.setElasticsearchUsernameValue(username); + + expect(RoleMappingsLogic.values.elasticsearchUser).toEqual({ + ...RoleMappingsLogic.values.elasticsearchUser, + username, + }); + }); + + it('setElasticsearchEmailValue', () => { + const email = 'newEmail@foo.cats'; + RoleMappingsLogic.actions.setElasticsearchEmailValue(email); + + expect(RoleMappingsLogic.values.elasticsearchUser).toEqual({ + ...RoleMappingsLogic.values.elasticsearchUser, + email, + }); + }); + + it('setUserCreated', () => { + RoleMappingsLogic.actions.setUserCreated(); + + expect(RoleMappingsLogic.values.userCreated).toEqual(true); + }); }); describe('listeners', () => { + describe('enableRoleBasedAccess', () => { + it('calls API and sets values', async () => { + const setRoleMappingsSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappings'); + http.post.mockReturnValue(Promise.resolve(mappingsServerProps)); + RoleMappingsLogic.actions.enableRoleBasedAccess(); + + expect(RoleMappingsLogic.values.dataLoading).toEqual(true); + + expect(http.post).toHaveBeenCalledWith( + '/api/workplace_search/org/role_mappings/enable_role_based_access' + ); + await nextTick(); + expect(setRoleMappingsSpy).toHaveBeenCalledWith(mappingsServerProps); + }); + + it('handles error', async () => { + http.post.mockReturnValue(Promise.reject('this is an error')); + RoleMappingsLogic.actions.enableRoleBasedAccess(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + }); + describe('initializeRoleMappings', () => { it('calls API and sets values', async () => { const setRoleMappingsDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingsData'); @@ -272,6 +393,39 @@ describe('RoleMappingsLogic', () => { }); }); + describe('initializeSingleUserRoleMapping', () => { + let setElasticsearchUserSpy: jest.MockedFunction<any>; + let setRoleMappingSpy: jest.MockedFunction<any>; + let setSingleUserRoleMappingSpy: jest.MockedFunction<any>; + beforeEach(() => { + setElasticsearchUserSpy = jest.spyOn(RoleMappingsLogic.actions, 'setElasticsearchUser'); + setRoleMappingSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMapping'); + setSingleUserRoleMappingSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setSingleUserRoleMapping' + ); + }); + + it('should handle the new user state and only set an empty mapping', () => { + RoleMappingsLogic.actions.initializeSingleUserRoleMapping(); + + expect(setElasticsearchUserSpy).not.toHaveBeenCalled(); + expect(setRoleMappingSpy).not.toHaveBeenCalled(); + expect(setSingleUserRoleMappingSpy).toHaveBeenCalledWith(undefined); + }); + + it('should handle an existing user state and set mapping', () => { + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); + RoleMappingsLogic.actions.initializeSingleUserRoleMapping( + wsSingleUserRoleMapping.roleMapping.id + ); + + expect(setElasticsearchUserSpy).toHaveBeenCalled(); + expect(setRoleMappingSpy).toHaveBeenCalled(); + expect(setSingleUserRoleMappingSpy).toHaveBeenCalledWith(wsSingleUserRoleMapping); + }); + }); + describe('handleSaveMapping', () => { it('calls API and refreshes list when new mapping', async () => { const initializeRoleMappingsSpy = jest.spyOn( @@ -350,18 +504,102 @@ describe('RoleMappingsLogic', () => { }); }); - describe('handleDeleteMapping', () => { - let confirmSpy: any; - const roleMappingId = 'r1'; + describe('handleSaveUser', () => { + it('calls API and refreshes list when new mapping', async () => { + const initializeRoleMappingsSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'initializeRoleMappings' + ); + const setUserCreatedSpy = jest.spyOn(RoleMappingsLogic.actions, 'setUserCreated'); + const setSingleUserRoleMappingSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setSingleUserRoleMapping' + ); + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); - beforeEach(() => { - confirmSpy = jest.spyOn(window, 'confirm'); - confirmSpy.mockImplementation(jest.fn(() => true)); + http.post.mockReturnValue(Promise.resolve(mappingsServerProps)); + RoleMappingsLogic.actions.handleSaveUser(); + + expect(http.post).toHaveBeenCalledWith( + '/api/workplace_search/org/single_user_role_mapping', + { + body: JSON.stringify({ + roleMapping: { + groups: [defaultGroup.id], + roleType: 'admin', + allGroups: false, + }, + elasticsearchUser: { + username: elasticsearchUsers[0].username, + email: elasticsearchUsers[0].email, + }, + }), + } + ); + await nextTick(); + + expect(initializeRoleMappingsSpy).toHaveBeenCalled(); + expect(setUserCreatedSpy).toHaveBeenCalled(); + expect(setSingleUserRoleMappingSpy).toHaveBeenCalled(); + }); + + it('calls API and refreshes list when existing mapping', async () => { + const initializeRoleMappingsSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'initializeRoleMappings' + ); + RoleMappingsLogic.actions.setSingleUserRoleMapping(wsSingleUserRoleMapping); + RoleMappingsLogic.actions.handleAllGroupsSelectionChange(true); + + http.put.mockReturnValue(Promise.resolve(mappingsServerProps)); + RoleMappingsLogic.actions.handleSaveUser(); + + expect(http.post).toHaveBeenCalledWith( + '/api/workplace_search/org/single_user_role_mapping', + { + body: JSON.stringify({ + roleMapping: { + groups: [], + roleType: 'admin', + allGroups: true, + id: wsSingleUserRoleMapping.roleMapping.id, + }, + elasticsearchUser: { + username: '', + email: '', + }, + }), + } + ); + await nextTick(); + + expect(initializeRoleMappingsSpy).toHaveBeenCalled(); }); - afterEach(() => { - confirmSpy.mockRestore(); + it('handles error', async () => { + const setRoleMappingErrorsSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setRoleMappingErrors' + ); + + http.post.mockReturnValue( + Promise.reject({ + body: { + attributes: { + errors: ['this is an error'], + }, + }, + }) + ); + RoleMappingsLogic.actions.handleSaveUser(); + await nextTick(); + + expect(setRoleMappingErrorsSpy).toHaveBeenCalledWith(['this is an error']); }); + }); + + describe('handleDeleteMapping', () => { + const roleMappingId = 'r1'; it('calls API and refreshes list', async () => { const initializeRoleMappingsSpy = jest.spyOn( @@ -388,14 +626,52 @@ describe('RoleMappingsLogic', () => { expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); + }); - it('will do nothing if not confirmed', async () => { - RoleMappingsLogic.actions.setRoleMapping(wsRoleMapping); - window.confirm = () => false; - RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId); + describe('handleUsernameSelectChange', () => { + it('sets elasticsearchUser when match found', () => { + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); + const setElasticsearchUserSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setElasticsearchUser' + ); + RoleMappingsLogic.actions.handleUsernameSelectChange(elasticsearchUsers[0].username); - expect(http.delete).not.toHaveBeenCalled(); - await nextTick(); + expect(setElasticsearchUserSpy).toHaveBeenCalledWith(elasticsearchUsers[0]); + }); + + it('does not set elasticsearchUser when no match found', () => { + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); + const setElasticsearchUserSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setElasticsearchUser' + ); + RoleMappingsLogic.actions.handleUsernameSelectChange('bogus'); + + expect(setElasticsearchUserSpy).not.toHaveBeenCalled(); + }); + }); + + describe('setUserExistingRadioValue', () => { + it('handles existing user', () => { + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); + const setElasticsearchUserSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setElasticsearchUser' + ); + RoleMappingsLogic.actions.setUserExistingRadioValue(true); + + expect(setElasticsearchUserSpy).toHaveBeenCalledWith(elasticsearchUsers[0]); + }); + + it('handles new user', () => { + const setElasticsearchUserSpy = jest.spyOn( + RoleMappingsLogic.actions, + 'setElasticsearchUser' + ); + RoleMappingsLogic.actions.setUserExistingRadioValue(false); + + expect(setElasticsearchUserSpy).toHaveBeenCalledWith(emptyUser); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts index 361425b7a78a1..7f26c8738786c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts @@ -16,30 +16,34 @@ import { } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants'; -import { AttributeName } from '../../../shared/types'; +import { AttributeName, SingleUserRoleMapping, ElasticsearchUser } from '../../../shared/types'; import { RoleGroup, WSRoleMapping, Role } from '../../types'; import { - DELETE_ROLE_MAPPING_MESSAGE, ROLE_MAPPING_DELETED_MESSAGE, ROLE_MAPPING_CREATED_MESSAGE, ROLE_MAPPING_UPDATED_MESSAGE, DEFAULT_GROUP_NAME, } from './constants'; +type UserMapping = SingleUserRoleMapping<WSRoleMapping>; + interface RoleMappingsServerDetails { roleMappings: WSRoleMapping[]; attributes: string[]; authProviders: string[]; availableGroups: RoleGroup[]; + elasticsearchUsers: ElasticsearchUser[]; elasticsearchRoles: string[]; multipleAuthProvidersConfig: boolean; + singleUserRoleMappings: UserMapping[]; } const getFirstAttributeName = (roleMapping: WSRoleMapping): AttributeName => Object.entries(roleMapping.rules)[0][0] as AttributeName; const getFirstAttributeValue = (roleMapping: WSRoleMapping): string => Object.entries(roleMapping.rules)[0][1] as string; +const emptyUser = { username: '', email: '' } as ElasticsearchUser; interface RoleMappingsActions { handleAllGroupsSelectionChange(selected: boolean): { selected: boolean }; @@ -52,15 +56,35 @@ interface RoleMappingsActions { handleDeleteMapping(roleMappingId: string): { roleMappingId: string }; handleGroupSelectionChange(groupIds: string[]): { groupIds: string[] }; handleRoleChange(roleType: Role): { roleType: Role }; + handleUsernameSelectChange(username: string): { username: string }; handleSaveMapping(): void; + handleSaveUser(): void; initializeRoleMapping(roleMappingId?: string): { roleMappingId?: string }; + initializeSingleUserRoleMapping(roleMappingId?: string): { roleMappingId?: string }; initializeRoleMappings(): void; resetState(): void; setRoleMapping(roleMapping: WSRoleMapping): { roleMapping: WSRoleMapping }; + setSingleUserRoleMapping(data?: UserMapping): { singleUserRoleMapping: UserMapping }; + setRoleMappings({ + roleMappings, + }: { + roleMappings: WSRoleMapping[]; + }): { roleMappings: WSRoleMapping[] }; setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails; + setElasticsearchUser( + elasticsearchUser?: ElasticsearchUser + ): { elasticsearchUser: ElasticsearchUser }; + setDefaultGroup(availableGroups: RoleGroup[]): { availableGroups: RoleGroup[] }; openRoleMappingFlyout(): void; - closeRoleMappingFlyout(): void; + openSingleUserRoleMappingFlyout(): void; + closeUsersAndRolesFlyout(): void; setRoleMappingErrors(errors: string[]): { errors: string[] }; + enableRoleBasedAccess(): void; + setUserExistingRadioValue(userFormUserIsExisting: boolean): { userFormUserIsExisting: boolean }; + setElasticsearchUsernameValue(username: string): { username: string }; + setElasticsearchEmailValue(email: string): { email: string }; + setUserCreated(): void; + setUserFormIsNewUser(userFormIsNewUser: boolean): { userFormIsNewUser: boolean }; } interface RoleMappingsValues { @@ -72,25 +96,37 @@ interface RoleMappingsValues { availableGroups: RoleGroup[]; dataLoading: boolean; elasticsearchRoles: string[]; + elasticsearchUsers: ElasticsearchUser[]; + elasticsearchUser: ElasticsearchUser; multipleAuthProvidersConfig: boolean; roleMapping: WSRoleMapping | null; roleMappings: WSRoleMapping[]; + singleUserRoleMapping: UserMapping | null; + singleUserRoleMappings: UserMapping[]; roleType: Role; selectedAuthProviders: string[]; selectedGroups: Set<string>; roleMappingFlyoutOpen: boolean; + singleUserRoleMappingFlyoutOpen: boolean; selectedOptions: EuiComboBoxOptionOption[]; roleMappingErrors: string[]; + userFormUserIsExisting: boolean; + userCreated: boolean; + userFormIsNewUser: boolean; } export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappingsActions>>({ - path: ['enterprise_search', 'workplace_search', 'role_mappings'], + path: ['enterprise_search', 'workplace_search', 'users_and_roles'], actions: { setRoleMappingsData: (data: RoleMappingsServerDetails) => data, setRoleMapping: (roleMapping: WSRoleMapping) => ({ roleMapping }), + setElasticsearchUser: (elasticsearchUser: ElasticsearchUser) => ({ elasticsearchUser }), + setSingleUserRoleMapping: (singleUserRoleMapping: UserMapping) => ({ singleUserRoleMapping }), + setRoleMappings: ({ roleMappings }: { roleMappings: WSRoleMapping[] }) => ({ roleMappings }), setRoleMappingErrors: (errors: string[]) => ({ errors }), handleAuthProviderChange: (value: string[]) => ({ value }), handleRoleChange: (roleType: Role) => ({ roleType }), + handleUsernameSelectChange: (username: string) => ({ username }), handleGroupSelectionChange: (groupIds: string[]) => ({ groupIds }), handleAttributeSelectorChange: (value: string, firstElasticsearchRole: string) => ({ value, @@ -98,26 +134,46 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi }), handleAttributeValueChange: (value: string) => ({ value }), handleAllGroupsSelectionChange: (selected: boolean) => ({ selected }), + enableRoleBasedAccess: true, + openSingleUserRoleMappingFlyout: true, + setUserExistingRadioValue: (userFormUserIsExisting: boolean) => ({ userFormUserIsExisting }), resetState: true, initializeRoleMappings: true, + initializeSingleUserRoleMapping: (roleMappingId?: string) => ({ roleMappingId }), initializeRoleMapping: (roleMappingId?: string) => ({ roleMappingId }), handleDeleteMapping: (roleMappingId: string) => ({ roleMappingId }), handleSaveMapping: true, + handleSaveUser: true, + setDefaultGroup: (availableGroups: RoleGroup[]) => ({ availableGroups }), openRoleMappingFlyout: true, - closeRoleMappingFlyout: false, + closeUsersAndRolesFlyout: false, + setElasticsearchUsernameValue: (username: string) => ({ username }), + setElasticsearchEmailValue: (email: string) => ({ email }), + setUserCreated: true, + setUserFormIsNewUser: (userFormIsNewUser: boolean) => ({ userFormIsNewUser }), }, reducers: { dataLoading: [ true, { setRoleMappingsData: () => false, + setRoleMappings: () => false, resetState: () => true, + enableRoleBasedAccess: () => true, }, ], roleMappings: [ [], { setRoleMappingsData: (_, { roleMappings }) => roleMappings, + setRoleMappings: (_, { roleMappings }) => roleMappings, + resetState: () => [], + }, + ], + singleUserRoleMappings: [ + [], + { + setRoleMappingsData: (_, { singleUserRoleMappings }) => singleUserRoleMappings, resetState: () => [], }, ], @@ -144,6 +200,13 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi [], { setRoleMappingsData: (_, { elasticsearchRoles }) => elasticsearchRoles, + closeUsersAndRolesFlyout: () => [ANY_AUTH_PROVIDER], + }, + ], + elasticsearchUsers: [ + [], + { + setRoleMappingsData: (_, { elasticsearchUsers }) => elasticsearchUsers, }, ], roleMapping: [ @@ -151,7 +214,14 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi { setRoleMapping: (_, { roleMapping }) => roleMapping, resetState: () => null, - closeRoleMappingFlyout: () => null, + closeUsersAndRolesFlyout: () => null, + }, + ], + singleUserRoleMapping: [ + null, + { + setSingleUserRoleMapping: (_, { singleUserRoleMapping }) => singleUserRoleMapping || null, + closeUsersAndRolesFlyout: () => null, }, ], roleType: [ @@ -166,6 +236,7 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi { setRoleMapping: (_, { roleMapping }) => roleMapping.allGroups, handleAllGroupsSelectionChange: (_, { selected }) => selected, + closeUsersAndRolesFlyout: () => false, }, ], attributeValue: [ @@ -176,7 +247,7 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi value === 'role' ? firstElasticsearchRole : '', handleAttributeValueChange: (_, { value }) => value, resetState: () => '', - closeRoleMappingFlyout: () => '', + closeUsersAndRolesFlyout: () => '', }, ], attributeName: [ @@ -185,7 +256,7 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi setRoleMapping: (_, { roleMapping }) => getFirstAttributeName(roleMapping), handleAttributeSelectorChange: (_, { value }) => value, resetState: () => 'username', - closeRoleMappingFlyout: () => 'username', + closeUsersAndRolesFlyout: () => 'username', }, ], selectedGroups: [ @@ -197,6 +268,12 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi .filter((group) => group.name === DEFAULT_GROUP_NAME) .map((group) => group.id) ), + setDefaultGroup: (_, { availableGroups }) => + new Set( + availableGroups + .filter((group) => group.name === DEFAULT_GROUP_NAME) + .map((group) => group.id) + ), setRoleMapping: (_, { roleMapping }) => new Set(roleMapping.groups.map((group: RoleGroup) => group.id)), handleGroupSelectionChange: (_, { groupIds }) => { @@ -205,6 +282,7 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi return newSelectedGroupNames; }, + closeUsersAndRolesFlyout: () => new Set(), }, ], availableAuthProviders: [ @@ -234,17 +312,61 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi false, { openRoleMappingFlyout: () => true, - closeRoleMappingFlyout: () => false, + closeUsersAndRolesFlyout: () => false, initializeRoleMappings: () => false, initializeRoleMapping: () => true, }, ], + singleUserRoleMappingFlyoutOpen: [ + false, + { + openSingleUserRoleMappingFlyout: () => true, + closeUsersAndRolesFlyout: () => false, + initializeSingleUserRoleMapping: () => true, + }, + ], roleMappingErrors: [ [], { setRoleMappingErrors: (_, { errors }) => errors, handleSaveMapping: () => [], - closeRoleMappingFlyout: () => [], + closeUsersAndRolesFlyout: () => [], + }, + ], + userFormUserIsExisting: [ + true, + { + setUserExistingRadioValue: (_, { userFormUserIsExisting }) => userFormUserIsExisting, + closeUsersAndRolesFlyout: () => true, + }, + ], + elasticsearchUser: [ + emptyUser, + { + setRoleMappingsData: (_, { elasticsearchUsers }) => elasticsearchUsers[0] || emptyUser, + setElasticsearchUser: (_, { elasticsearchUser }) => elasticsearchUser || emptyUser, + setElasticsearchUsernameValue: (state, { username }) => ({ + ...state, + username, + }), + setElasticsearchEmailValue: (state, { email }) => ({ + ...state, + email, + }), + closeUsersAndRolesFlyout: () => emptyUser, + }, + ], + userCreated: [ + false, + { + setUserCreated: () => true, + closeUsersAndRolesFlyout: () => false, + }, + ], + userFormIsNewUser: [ + true, + { + setUserFormIsNewUser: (_, { userFormIsNewUser }) => userFormIsNewUser, }, ], }, @@ -260,6 +382,17 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi ], }), listeners: ({ actions, values }) => ({ + enableRoleBasedAccess: async () => { + const { http } = HttpLogic.values; + const route = '/api/workplace_search/org/role_mappings/enable_role_based_access'; + + try { + const response = await http.post(route); + actions.setRoleMappings(response); + } catch (e) { + flashAPIErrors(e); + } + }, initializeRoleMappings: async () => { const { http } = HttpLogic.values; const route = '/api/workplace_search/org/role_mappings'; @@ -275,18 +408,28 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi const roleMapping = values.roleMappings.find(({ id }) => id === roleMappingId); if (roleMapping) actions.setRoleMapping(roleMapping); }, + initializeSingleUserRoleMapping: ({ roleMappingId }) => { + const singleUserRoleMapping = values.singleUserRoleMappings.find( + ({ roleMapping }) => roleMapping.id === roleMappingId + ); + + if (singleUserRoleMapping) { + actions.setElasticsearchUser(singleUserRoleMapping.elasticsearchUser); + actions.setRoleMapping(singleUserRoleMapping.roleMapping); + } + actions.setSingleUserRoleMapping(singleUserRoleMapping); + actions.setUserFormIsNewUser(!singleUserRoleMapping); + }, handleDeleteMapping: async ({ roleMappingId }) => { const { http } = HttpLogic.values; const route = `/api/workplace_search/org/role_mappings/${roleMappingId}`; - if (window.confirm(DELETE_ROLE_MAPPING_MESSAGE)) { - try { - await http.delete(route); - actions.initializeRoleMappings(); - setSuccessMessage(ROLE_MAPPING_DELETED_MESSAGE); - } catch (e) { - flashAPIErrors(e); - } + try { + await http.delete(route); + actions.initializeRoleMappings(); + setSuccessMessage(ROLE_MAPPING_DELETED_MESSAGE); + } catch (e) { + flashAPIErrors(e); } }, handleSaveMapping: async () => { @@ -330,11 +473,59 @@ export const RoleMappingsLogic = kea<MakeLogicType<RoleMappingsValues, RoleMappi resetState: () => { clearFlashMessages(); }, - closeRoleMappingFlyout: () => { + handleSaveUser: async () => { + const { http } = HttpLogic.values; + const { + roleType, + singleUserRoleMapping, + includeInAllGroups, + selectedGroups, + elasticsearchUser: { email, username }, + } = values; + + const body = JSON.stringify({ + roleMapping: { + groups: includeInAllGroups ? [] : Array.from(selectedGroups), + roleType, + allGroups: includeInAllGroups, + id: singleUserRoleMapping?.roleMapping?.id, + }, + elasticsearchUser: { + username, + email, + }, + }); + + try { + const response = await http.post('/api/workplace_search/org/single_user_role_mapping', { + body, + }); + actions.setSingleUserRoleMapping(response); + actions.setUserCreated(); + actions.initializeRoleMappings(); + } catch (e) { + actions.setRoleMappingErrors(e?.body?.attributes?.errors); + } + }, + closeUsersAndRolesFlyout: () => { clearFlashMessages(); + const firstUser = values.elasticsearchUsers[0]; + actions.setElasticsearchUser(firstUser); + actions.setDefaultGroup(values.availableGroups); }, openRoleMappingFlyout: () => { clearFlashMessages(); }, + openSingleUserRoleMappingFlyout: () => { + clearFlashMessages(); + }, + setUserExistingRadioValue: ({ userFormUserIsExisting }) => { + const firstUser = values.elasticsearchUsers[0]; + actions.setElasticsearchUser(userFormUserIsExisting ? firstUser : emptyUser); + }, + handleUsernameSelectChange: ({ username }) => { + const user = values.elasticsearchUsers.find((u) => u.username === username); + if (user) actions.setElasticsearchUser(user); + }, }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.test.tsx new file mode 100644 index 0000000000000..32ee1a7f22875 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.test.tsx @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import '../../../__mocks__/react_router'; +import '../../../__mocks__/shallow_useeffect.mock'; +import { setMockActions, setMockValues } from '../../../__mocks__/kea_logic'; +import { groups } from '../../__mocks__/groups.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { UserFlyout, UserAddedInfo, UserInvitationCallout } from '../../../shared/role_mapping'; +import { elasticsearchUsers } from '../../../shared/role_mapping/__mocks__/elasticsearch_users'; +import { wsSingleUserRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; + +import { GroupAssignmentSelector } from './group_assignment_selector'; +import { User } from './user'; + +describe('User', () => { + const handleSaveUser = jest.fn(); + const closeUsersAndRolesFlyout = jest.fn(); + const setUserExistingRadioValue = jest.fn(); + const setElasticsearchUsernameValue = jest.fn(); + const setElasticsearchEmailValue = jest.fn(); + const handleRoleChange = jest.fn(); + const handleUsernameSelectChange = jest.fn(); + + const mockValues = { + availableGroups: [], + singleUserRoleMapping: null, + userFormUserIsExisting: false, + elasticsearchUsers: [], + elasticsearchUser: {}, + roleType: 'admin', + roleMappingErrors: [], + userCreated: false, + userFormIsNewUser: false, + }; + + beforeEach(() => { + setMockActions({ + handleSaveUser, + closeUsersAndRolesFlyout, + setUserExistingRadioValue, + setElasticsearchUsernameValue, + setElasticsearchEmailValue, + handleRoleChange, + handleUsernameSelectChange, + }); + + setMockValues(mockValues); + }); + + it('renders', () => { + const wrapper = shallow(<User />); + + expect(wrapper.find(UserFlyout)).toHaveLength(1); + }); + + it('renders group assignment selector when groups present', () => { + setMockValues({ ...mockValues, availableGroups: groups }); + const wrapper = shallow(<User />); + + expect(wrapper.find(GroupAssignmentSelector)).toHaveLength(1); + }); + + it('renders userInvitationCallout', () => { + setMockValues({ + ...mockValues, + singleUserRoleMapping: wsSingleUserRoleMapping, + }); + const wrapper = shallow(<User />); + + expect(wrapper.find(UserInvitationCallout)).toHaveLength(1); + }); + + it('renders user added info when user created', () => { + setMockValues({ + ...mockValues, + singleUserRoleMapping: wsSingleUserRoleMapping, + userCreated: true, + }); + const wrapper = shallow(<User />); + + expect(wrapper.find(UserAddedInfo)).toHaveLength(1); + }); + + it('disables form when username value not present', () => { + setMockValues({ + ...mockValues, + singleUserRoleMapping: wsSingleUserRoleMapping, + elasticsearchUsers, + elasticsearchUser: { + username: null, + email: 'email@user.com', + }, + }); + const wrapper = shallow(<User />); + + expect(wrapper.find(UserFlyout).prop('disabled')).toEqual(true); + }); + + it('enables form when userFormUserIsExisting', () => { + setMockValues({ + ...mockValues, + userFormUserIsExisting: true.valueOf, + singleUserRoleMapping: wsSingleUserRoleMapping, + elasticsearchUsers, + elasticsearchUser: { + username: null, + email: 'email@user.com', + }, + }); + const wrapper = shallow(<User />); + + expect(wrapper.find(UserFlyout).prop('disabled')).toEqual(false); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.tsx new file mode 100644 index 0000000000000..bfb32ee31c121 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.tsx @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { EuiForm } from '@elastic/eui'; + +import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; +import { + UserFlyout, + UserSelector, + UserAddedInfo, + UserInvitationCallout, +} from '../../../shared/role_mapping'; +import { Role } from '../../types'; + +import { GroupAssignmentSelector } from './group_assignment_selector'; +import { RoleMappingsLogic } from './role_mappings_logic'; + +const roleTypes = (['admin', 'user'] as unknown) as Role[]; + +export const User: React.FC = () => { + const { + handleSaveUser, + closeUsersAndRolesFlyout, + setUserExistingRadioValue, + setElasticsearchUsernameValue, + setElasticsearchEmailValue, + handleRoleChange, + handleUsernameSelectChange, + } = useActions(RoleMappingsLogic); + + const { + availableGroups, + singleUserRoleMapping, + userFormUserIsExisting, + elasticsearchUsers, + elasticsearchUser, + roleType, + roleMappingErrors, + userCreated, + userFormIsNewUser, + } = useValues(RoleMappingsLogic); + + const showGroupAssignmentSelector = availableGroups.length > 0; + const hasAvailableUsers = elasticsearchUsers.length > 0; + const flyoutDisabled = + (!userFormUserIsExisting || !hasAvailableUsers) && !elasticsearchUser.username; + + const userAddedInfo = singleUserRoleMapping && ( + <UserAddedInfo + username={singleUserRoleMapping.elasticsearchUser.username} + email={singleUserRoleMapping.elasticsearchUser.email as string} + roleType={singleUserRoleMapping.roleMapping.roleType} + /> + ); + + const userInvitationCallout = singleUserRoleMapping?.invitation && ( + <UserInvitationCallout + isNew={userCreated} + invitationCode={singleUserRoleMapping!.invitation.code} + urlPrefix={getWorkplaceSearchUrl()} + /> + ); + + const createUserForm = ( + <EuiForm isInvalid={roleMappingErrors.length > 0} error={roleMappingErrors}> + <UserSelector + isNewUser={userFormIsNewUser} + elasticsearchUsers={elasticsearchUsers} + handleRoleChange={handleRoleChange} + elasticsearchUser={elasticsearchUser} + setUserExisting={setUserExistingRadioValue} + setElasticsearchEmailValue={setElasticsearchEmailValue} + setElasticsearchUsernameValue={setElasticsearchUsernameValue} + handleUsernameSelectChange={handleUsernameSelectChange} + userFormUserIsExisting={userFormUserIsExisting} + roleTypes={roleTypes} + roleType={roleType} + /> + {showGroupAssignmentSelector && <GroupAssignmentSelector />} + </EuiForm> + ); + + return ( + <UserFlyout + disabled={flyoutDisabled} + isComplete={userCreated} + isNew={userFormIsNewUser} + closeUserFlyout={closeUsersAndRolesFlyout} + handleSaveUser={handleSaveUser} + > + {userCreated ? userAddedInfo : createUserForm} + {userInvitationCallout} + </UserFlyout> + ); +}; diff --git a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts index 350c27fa43cd3..5580c3dac5996 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts @@ -25,8 +25,7 @@ describe('App Search Telemetry Usage Collector', () => { 'ui_error.cannot_connect': 3, 'ui_error.not_found': 7, 'ui_clicked.create_first_engine_button': 40, - 'ui_clicked.header_launch_button': 50, - 'ui_clicked.engine_table_link': 60, + 'ui_clicked.engine_table_link': 50, }, }), incrementCounter: jest.fn(), @@ -66,8 +65,7 @@ describe('App Search Telemetry Usage Collector', () => { }, ui_clicked: { create_first_engine_button: 40, - header_launch_button: 50, - engine_table_link: 60, + engine_table_link: 50, }, }); }); @@ -93,7 +91,6 @@ describe('App Search Telemetry Usage Collector', () => { }, ui_clicked: { create_first_engine_button: 0, - header_launch_button: 0, engine_table_link: 0, }, }); diff --git a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts index 36ba2976f929a..4dca6ed58e0c5 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts @@ -23,7 +23,6 @@ interface Telemetry { }; ui_clicked: { create_first_engine_button: number; - header_launch_button: number; engine_table_link: number; }; } @@ -54,7 +53,6 @@ export const registerTelemetryUsageCollector = ( }, ui_clicked: { create_first_engine_button: { type: 'long' }, - header_launch_button: { type: 'long' }, engine_table_link: { type: 'long' }, }, }, @@ -85,7 +83,6 @@ const fetchTelemetryMetrics = async (savedObjects: SavedObjectsServiceStart, log }, ui_clicked: { create_first_engine_button: 0, - header_launch_button: 0, engine_table_link: 0, }, }; @@ -110,7 +107,6 @@ const fetchTelemetryMetrics = async (savedObjects: SavedObjectsServiceStart, log 'ui_clicked.create_first_engine_button', 0 ), - header_launch_button: get(savedObjectAttributes, 'ui_clicked.header_launch_button', 0), engine_table_link: get(savedObjectAttributes, 'ui_clicked.engine_table_link', 0), }, } as Telemetry; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts index 718597c12e9c5..dfb9765f834b6 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts @@ -7,7 +7,12 @@ import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; -import { registerRoleMappingsRoute, registerRoleMappingRoute } from './role_mappings'; +import { + registerEnableRoleMappingsRoute, + registerRoleMappingsRoute, + registerRoleMappingRoute, + registerUserRoute, +} from './role_mappings'; const roleMappingBaseSchema = { rules: { username: 'user' }, @@ -18,6 +23,29 @@ const roleMappingBaseSchema = { }; describe('role mappings routes', () => { + describe('POST /api/app_search/role_mappings/enable_role_based_access', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/role_mappings/enable_role_based_access', + }); + + registerEnableRoleMappingsRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/role_mappings/enable_role_based_access', + }); + }); + }); + describe('GET /api/app_search/role_mappings', () => { let mockRouter: MockRouter; @@ -36,7 +64,7 @@ describe('role mappings routes', () => { it('creates a request handler', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/role_mappings', + path: '/as/role_mappings', }); }); }); @@ -59,7 +87,7 @@ describe('role mappings routes', () => { it('creates a request handler', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/role_mappings', + path: '/as/role_mappings', }); }); @@ -94,7 +122,7 @@ describe('role mappings routes', () => { it('creates a request handler', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/role_mappings/:id', + path: '/as/role_mappings/:id', }); }); @@ -129,7 +157,55 @@ describe('role mappings routes', () => { it('creates a request handler', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ - path: '/role_mappings/:id', + path: '/as/role_mappings/:id', + }); + }); + }); + + describe('POST /api/app_search/single_user_role_mapping', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/single_user_role_mapping', + }); + + registerUserRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + describe('validates', () => { + it('correctly', () => { + const request = { + body: { + roleMapping: { + engines: ['foo', 'bar'], + roleType: 'admin', + accessAllEngines: true, + id: '123asf', + }, + elasticsearchUser: { + username: 'user2@elastic.co', + email: 'user2', + }, + }, + }; + mockRouter.shouldValidate(request); + }); + + it('missing required fields', () => { + const request = { body: {} }; + mockRouter.shouldThrow(request); + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/role_mappings/upsert_single_user_role_mapping', }); }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts index 75724a3344d6d..d90a005cb2532 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts @@ -17,6 +17,21 @@ const roleMappingBaseSchema = { authProvider: schema.arrayOf(schema.string()), }; +export function registerEnableRoleMappingsRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.post( + { + path: '/api/app_search/role_mappings/enable_role_based_access', + validate: false, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/role_mappings/enable_role_based_access', + }) + ); +} + export function registerRoleMappingsRoute({ router, enterpriseSearchRequestHandler, @@ -27,7 +42,7 @@ export function registerRoleMappingsRoute({ validate: false, }, enterpriseSearchRequestHandler.createRequest({ - path: '/role_mappings', + path: '/as/role_mappings', }) ); @@ -39,7 +54,7 @@ export function registerRoleMappingsRoute({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/role_mappings', + path: '/as/role_mappings', }) ); } @@ -59,7 +74,7 @@ export function registerRoleMappingRoute({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/role_mappings/:id', + path: '/as/role_mappings/:id', }) ); @@ -73,12 +88,39 @@ export function registerRoleMappingRoute({ }, }, enterpriseSearchRequestHandler.createRequest({ - path: '/role_mappings/:id', + path: '/as/role_mappings/:id', + }) + ); +} + +export function registerUserRoute({ router, enterpriseSearchRequestHandler }: RouteDependencies) { + router.post( + { + path: '/api/app_search/single_user_role_mapping', + validate: { + body: schema.object({ + roleMapping: schema.object({ + engines: schema.arrayOf(schema.string()), + roleType: schema.string(), + accessAllEngines: schema.boolean(), + id: schema.maybe(schema.string()), + }), + elasticsearchUser: schema.object({ + username: schema.string(), + email: schema.string(), + }), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/role_mappings/upsert_single_user_role_mapping', }) ); } export const registerRoleMappingsRoutes = (dependencies: RouteDependencies) => { + registerEnableRoleMappingsRoute(dependencies); registerRoleMappingsRoute(dependencies); registerRoleMappingRoute(dependencies); + registerUserRoute(dependencies); }; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/search.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/search.ts index 016f71e7e65b8..216bffc683265 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/search.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/search.ts @@ -14,6 +14,8 @@ import { schema } from '@kbn/config-schema'; +import { skipBodyValidation } from '../../lib/route_config_helpers'; + import { RouteDependencies } from '../../plugin'; export function registerSearchRoutes({ @@ -36,4 +38,25 @@ export function registerSearchRoutes({ path: '/api/as/v1/engines/:engineName/search.json', }) ); + + // For the Search UI routes below, Search UI always uses the full API path, like: + // "/api/as/v1/engines/{engineName}/search.json". We only have control over the base path + // in Search UI, so we created a common basepath of "/api/app_search/search-ui" here that + // Search UI can use. + // + // Search UI *also* uses the click tracking and query suggestion endpoints, however, since the + // App Search plugin doesn't use that portion of Search UI, we only set up a proxy for the search endpoint below. + router.post( + skipBodyValidation({ + path: '/api/app_search/search-ui/api/as/v1/engines/{engineName}/search.json', + validate: { + params: schema.object({ + engineName: schema.string(), + }), + }, + }), + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v1/engines/:engineName/search.json', + }) + ); } diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts index a945866da5ef2..ef8f1bd63f5d3 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts @@ -7,9 +7,37 @@ import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; -import { registerOrgRoleMappingsRoute, registerOrgRoleMappingRoute } from './role_mappings'; +import { + registerOrgEnableRoleMappingsRoute, + registerOrgRoleMappingsRoute, + registerOrgRoleMappingRoute, + registerOrgUserRoute, +} from './role_mappings'; describe('role mappings routes', () => { + describe('POST /api/workplace_search/org/role_mappings/enable_role_based_access', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/workplace_search/org/role_mappings/enable_role_based_access', + }); + + registerOrgEnableRoleMappingsRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/role_mappings/enable_role_based_access', + }); + }); + }); + describe('GET /api/workplace_search/org/role_mappings', () => { let mockRouter: MockRouter; @@ -101,4 +129,52 @@ describe('role mappings routes', () => { }); }); }); + + describe('POST /api/workplace_search/org/single_user_role_mapping', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/workplace_search/org/single_user_role_mapping', + }); + + registerOrgUserRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + describe('validates', () => { + it('correctly', () => { + const request = { + body: { + roleMapping: { + groups: ['foo', 'bar'], + roleType: 'admin', + allGroups: true, + id: '123asf', + }, + elasticsearchUser: { + username: 'user2@elastic.co', + email: 'user2', + }, + }, + }; + mockRouter.shouldValidate(request); + }); + + it('missing required fields', () => { + const request = { body: {} }; + mockRouter.shouldThrow(request); + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/role_mappings/upsert_single_user_role_mapping', + }); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts index a0fcec63cbb27..e6f4919ed2a2f 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts @@ -17,6 +17,21 @@ const roleMappingBaseSchema = { authProvider: schema.arrayOf(schema.string()), }; +export function registerOrgEnableRoleMappingsRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.post( + { + path: '/api/workplace_search/org/role_mappings/enable_role_based_access', + validate: false, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/ws/org/role_mappings/enable_role_based_access', + }) + ); +} + export function registerOrgRoleMappingsRoute({ router, enterpriseSearchRequestHandler, @@ -78,7 +93,37 @@ export function registerOrgRoleMappingRoute({ ); } +export function registerOrgUserRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.post( + { + path: '/api/workplace_search/org/single_user_role_mapping', + validate: { + body: schema.object({ + roleMapping: schema.object({ + groups: schema.arrayOf(schema.string()), + roleType: schema.string(), + allGroups: schema.boolean(), + id: schema.maybe(schema.string()), + }), + elasticsearchUser: schema.object({ + username: schema.string(), + email: schema.string(), + }), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/ws/org/role_mappings/upsert_single_user_role_mapping', + }) + ); +} + export const registerRoleMappingsRoutes = (dependencies: RouteDependencies) => { + registerOrgEnableRoleMappingsRoute(dependencies); registerOrgRoleMappingsRoute(dependencies); registerOrgRoleMappingRoute(dependencies); + registerOrgUserRoute(dependencies); }; diff --git a/x-pack/plugins/event_log/README.md b/x-pack/plugins/event_log/README.md index ffbd20dd6f2be..682bf2660c78b 100644 --- a/x-pack/plugins/event_log/README.md +++ b/x-pack/plugins/event_log/README.md @@ -127,6 +127,10 @@ Below is a document in the expected structure, with descriptions of the fields: // Custom fields that are not part of ECS. kibana: { server_uuid: "UUID of kibana server, for diagnosing multi-Kibana scenarios", + task: { + scheduled: "ISO date of when the task for this event was supposed to start", + schedule_delay: "delay in nanoseconds between when this task was supposed to start and when it actually started", + }, alerting: { instance_id: "alert instance id, for relevant documents", action_group_id: "alert action group, for relevant documents", diff --git a/x-pack/plugins/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json index 3eadcc21257b0..0f5f4af2052ee 100644 --- a/x-pack/plugins/event_log/generated/mappings.json +++ b/x-pack/plugins/event_log/generated/mappings.json @@ -214,10 +214,6 @@ "version": { "ignore_above": 1024, "type": "keyword" - }, - "namespace": { - "ignore_above": 1024, - "type": "keyword" } } }, @@ -241,6 +237,16 @@ "type": "keyword", "ignore_above": 1024 }, + "task": { + "properties": { + "scheduled": { + "type": "date" + }, + "schedule_delay": { + "type": "long" + } + } + }, "alerting": { "properties": { "instance_id": { diff --git a/x-pack/plugins/event_log/generated/schemas.ts b/x-pack/plugins/event_log/generated/schemas.ts index 2a066ca0bd15b..556ddec5a7001 100644 --- a/x-pack/plugins/event_log/generated/schemas.ts +++ b/x-pack/plugins/event_log/generated/schemas.ts @@ -91,7 +91,6 @@ export const EventSchema = schema.maybe( ruleset: ecsString(), uuid: ecsString(), version: ecsString(), - namespace: ecsString(), }) ), user: schema.maybe( @@ -102,6 +101,12 @@ export const EventSchema = schema.maybe( kibana: schema.maybe( schema.object({ server_uuid: ecsString(), + task: schema.maybe( + schema.object({ + scheduled: ecsDate(), + schedule_delay: ecsNumber(), + }) + ), alerting: schema.maybe( schema.object({ instance_id: ecsString(), diff --git a/x-pack/plugins/event_log/scripts/mappings.js b/x-pack/plugins/event_log/scripts/mappings.js index f2020e76b46ba..93fe053bd0cdf 100644 --- a/x-pack/plugins/event_log/scripts/mappings.js +++ b/x-pack/plugins/event_log/scripts/mappings.js @@ -17,6 +17,17 @@ exports.EcsCustomPropertyMappings = { type: 'keyword', ignore_above: 1024, }, + // task specific fields + task: { + properties: { + scheduled: { + type: 'date', + }, + schedule_delay: { + type: 'long', + }, + }, + }, // alerting specific fields alerting: { properties: { diff --git a/x-pack/plugins/event_log/server/event_logger.ts b/x-pack/plugins/event_log/server/event_logger.ts index 4af69de0f47a0..b985a173ccdbf 100644 --- a/x-pack/plugins/event_log/server/event_logger.ts +++ b/x-pack/plugins/event_log/server/event_logger.ts @@ -88,7 +88,7 @@ export class EventLogger implements IEventLogger { try { validatedEvent = validateEvent(this.eventLogService, event); } catch (err) { - this.systemLogger.warn(`invalid event logged: ${err.message}`); + this.systemLogger.warn(`invalid event logged: ${err.message}; ${JSON.stringify(event)})`); return; } diff --git a/x-pack/plugins/fleet/common/constants/epm.ts b/x-pack/plugins/fleet/common/constants/epm.ts index e9dd968d3f048..81ea2a630d3db 100644 --- a/x-pack/plugins/fleet/common/constants/epm.ts +++ b/x-pack/plugins/fleet/common/constants/epm.ts @@ -48,6 +48,9 @@ export const dataTypes = { Metrics: 'metrics', } as const; +// currently identical but may be a subset or otherwise different some day +export const monitoringTypes = Object.values(dataTypes); + export const installationStatuses = { Installed: 'installed', NotInstalled: 'not_installed', diff --git a/x-pack/plugins/fleet/common/constants/preconfiguration.ts b/x-pack/plugins/fleet/common/constants/preconfiguration.ts index 937c08b7e8cb5..2ec67393df76b 100644 --- a/x-pack/plugins/fleet/common/constants/preconfiguration.ts +++ b/x-pack/plugins/fleet/common/constants/preconfiguration.ts @@ -12,6 +12,7 @@ import { FLEET_SYSTEM_PACKAGE, FLEET_SERVER_PACKAGE, autoUpdatePackages, + monitoringTypes, } from './epm'; export const PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE = @@ -40,7 +41,7 @@ export const DEFAULT_AGENT_POLICY: PreconfiguredAgentPolicyWithDefaultInputs = { ], is_default: true, is_managed: false, - monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>, + monitoring_enabled: monitoringTypes, }; export const DEFAULT_FLEET_SERVER_AGENT_POLICY: PreconfiguredAgentPolicyWithDefaultInputs = { @@ -58,7 +59,7 @@ export const DEFAULT_FLEET_SERVER_AGENT_POLICY: PreconfiguredAgentPolicyWithDefa is_default: false, is_default_fleet_server: true, is_managed: false, - monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>, + monitoring_enabled: monitoringTypes, }; export const DEFAULT_PACKAGES = defaultPackages.map((name) => ({ diff --git a/x-pack/plugins/fleet/common/constants/routes.ts b/x-pack/plugins/fleet/common/constants/routes.ts index 037c0ee506a05..0b892bacf53a7 100644 --- a/x-pack/plugins/fleet/common/constants/routes.ts +++ b/x-pack/plugins/fleet/common/constants/routes.ts @@ -117,5 +117,5 @@ export const INSTALL_SCRIPT_API_ROUTES = `${API_ROOT}/install/{osType}`; // Policy preconfig API routes export const PRECONFIGURATION_API_ROUTES = { - PUT_PRECONFIG: `${API_ROOT}/setup/preconfiguration`, + UPDATE_PATTERN: `${API_ROOT}/setup/preconfiguration`, }; diff --git a/x-pack/plugins/fleet/server/services/hosts_utils.test.ts b/x-pack/plugins/fleet/common/services/hosts_utils.test.ts similarity index 100% rename from x-pack/plugins/fleet/server/services/hosts_utils.test.ts rename to x-pack/plugins/fleet/common/services/hosts_utils.test.ts diff --git a/x-pack/plugins/fleet/server/services/hosts_utils.ts b/x-pack/plugins/fleet/common/services/hosts_utils.ts similarity index 100% rename from x-pack/plugins/fleet/server/services/hosts_utils.ts rename to x-pack/plugins/fleet/common/services/hosts_utils.ts diff --git a/x-pack/plugins/fleet/common/services/index.ts b/x-pack/plugins/fleet/common/services/index.ts index 86361ae163399..a6f4cd319b970 100644 --- a/x-pack/plugins/fleet/common/services/index.ts +++ b/x-pack/plugins/fleet/common/services/index.ts @@ -30,3 +30,5 @@ export { validationHasErrors, countValidationErrors, } from './validate_package_policy'; + +export { normalizeHostsForAgents } from './hosts_utils'; diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts index 95f91165aaf94..59691bf32d099 100644 --- a/x-pack/plugins/fleet/common/types/index.ts +++ b/x-pack/plugins/fleet/common/types/index.ts @@ -25,6 +25,7 @@ export interface FleetConfigType { }; agentPolicies?: PreconfiguredAgentPolicy[]; packages?: PreconfiguredPackage[]; + agentIdVerificationEnabled?: boolean; } // Calling Object.entries(PackagesGroupedByStatus) gave `status: string` diff --git a/x-pack/plugins/fleet/common/types/models/agent_policy.ts b/x-pack/plugins/fleet/common/types/models/agent_policy.ts index a9393abcc57ef..f64467ca674fb 100644 --- a/x-pack/plugins/fleet/common/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/agent_policy.ts @@ -6,7 +6,7 @@ */ import type { agentPolicyStatuses } from '../../constants'; -import type { DataType, ValueOf } from '../../types'; +import type { MonitoringType, ValueOf } from '../../types'; import type { PackagePolicy, PackagePolicyPackage } from './package_policy'; import type { Output } from './output'; @@ -20,7 +20,8 @@ export interface NewAgentPolicy { is_default?: boolean; is_default_fleet_server?: boolean; // Optional when creating a policy is_managed?: boolean; // Optional when creating a policy - monitoring_enabled?: Array<ValueOf<DataType>>; + monitoring_enabled?: MonitoringType; + unenroll_timeout?: number; is_preconfigured?: boolean; } @@ -138,4 +139,8 @@ export interface FleetServerPolicy { * True when this policy is the default policy to start Fleet Server */ default_fleet_server: boolean; + /** + * Auto unenroll any Elastic Agents which have not checked in for this many seconds + */ + unenroll_timeout?: number; } diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index aece658083196..36554b8409364 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { estypes } from '@elastic/elasticsearch'; // Follow pattern from https://github.com/elastic/kibana/pull/52447 // TODO: Update when https://github.com/elastic/kibana/issues/53021 is closed import type { SavedObject, SavedObjectAttributes, SavedObjectReference } from 'src/core/public'; @@ -13,6 +14,7 @@ import type { ASSETS_SAVED_OBJECT_TYPE, agentAssetTypes, dataTypes, + monitoringTypes, installationStatuses, } from '../../constants'; import type { ValueOf } from '../../types'; @@ -91,7 +93,7 @@ export enum ElasticsearchAssetType { } export type DataType = typeof dataTypes; - +export type MonitoringType = typeof monitoringTypes; export type InstallablePackage = RegistryPackage | ArchivePackage; export type ArchivePackage = PackageSpecManifest & @@ -299,8 +301,8 @@ export interface RegistryDataStream { } export interface RegistryElasticsearch { - 'index_template.settings'?: object; - 'index_template.mappings'?: object; + 'index_template.settings'?: estypes.IndicesIndexSettings; + 'index_template.mappings'?: estypes.MappingTypeMapping; } export interface RegistryDataStreamPermissions { @@ -425,7 +427,7 @@ export interface IndexTemplate { _meta: object; } -export interface TemplateRef { +export interface IndexTemplateEntry { templateName: string; indexTemplate: IndexTemplate; } diff --git a/x-pack/plugins/fleet/public/applications/fleet/app.tsx b/x-pack/plugins/fleet/public/applications/fleet/app.tsx index c4cc4d92f5d95..8be6232733def 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/app.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/app.tsx @@ -5,12 +5,13 @@ * 2.0. */ +import type { FunctionComponent } from 'react'; import React, { memo, useEffect, useState } from 'react'; import type { AppMountParameters } from 'kibana/public'; import { EuiCode, EuiEmptyPrompt, EuiErrorBoundary, EuiPanel, EuiPortal } from '@elastic/eui'; import type { History } from 'history'; import { createHashHistory } from 'history'; -import { Router, Redirect, Route, Switch } from 'react-router-dom'; +import { Router, Redirect, Route, Switch, useRouteMatch } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; @@ -39,7 +40,7 @@ import { Error, Loading, SettingFlyout, FleetSetupLoading } from './components'; import type { UIExtensionsStorage } from './types'; import { FLEET_ROUTING_PATHS } from './constants'; -import { DefaultLayout, WithoutHeaderLayout } from './layouts'; +import { DefaultLayout, DefaultPageTitle, WithoutHeaderLayout, WithHeaderLayout } from './layouts'; import { AgentPolicyApp } from './sections/agent_policy'; import { DataStreamApp } from './sections/data_stream'; import { AgentsApp } from './sections/agents'; @@ -48,11 +49,18 @@ import { EnrollmentTokenListPage } from './sections/agents/enrollment_token_list const FEEDBACK_URL = 'https://ela.st/fleet-feedback'; -const ErrorLayout = ({ children }: { children: JSX.Element }) => ( +const ErrorLayout: FunctionComponent<{ isAddIntegrationsPath: boolean }> = ({ + isAddIntegrationsPath, + children, +}) => ( <EuiErrorBoundary> - <DefaultLayout> - <WithoutHeaderLayout>{children}</WithoutHeaderLayout> - </DefaultLayout> + {isAddIntegrationsPath ? ( + <WithHeaderLayout leftColumn={<DefaultPageTitle />}>{children}</WithHeaderLayout> + ) : ( + <DefaultLayout> + <WithoutHeaderLayout>{children}</WithoutHeaderLayout> + </DefaultLayout> + )} </EuiErrorBoundary> ); @@ -71,6 +79,8 @@ export const WithPermissionsAndSetup: React.FC = memo(({ children }) => { const [isInitialized, setIsInitialized] = useState(false); const [initializationError, setInitializationError] = useState<Error | null>(null); + const isAddIntegrationsPath = !!useRouteMatch(FLEET_ROUTING_PATHS.add_integration_to_policy); + useEffect(() => { (async () => { setIsPermissionsLoading(false); @@ -109,7 +119,7 @@ export const WithPermissionsAndSetup: React.FC = memo(({ children }) => { if (isPermissionsLoading || permissionsError) { return ( - <ErrorLayout> + <ErrorLayout isAddIntegrationsPath={isAddIntegrationsPath}> {isPermissionsLoading ? ( <Loading /> ) : permissionsError === 'REQUEST_ERROR' ? ( @@ -168,7 +178,7 @@ export const WithPermissionsAndSetup: React.FC = memo(({ children }) => { if (!isInitialized || initializationError) { return ( - <ErrorLayout> + <ErrorLayout isAddIntegrationsPath={isAddIntegrationsPath}> {initializationError ? ( <Error title={ @@ -314,9 +324,7 @@ export const AppRoutes = memo( {/* TODO: Move this route to the Integrations app */} <Route path={FLEET_ROUTING_PATHS.add_integration_to_policy}> - <DefaultLayout> - <CreatePackagePolicyPage /> - </DefaultLayout> + <CreatePackagePolicyPage /> </Route> <Redirect to={FLEET_ROUTING_PATHS.agents} /> diff --git a/x-pack/plugins/fleet/public/applications/fleet/layouts/default.tsx b/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default.tsx similarity index 65% rename from x-pack/plugins/fleet/public/applications/fleet/layouts/default.tsx rename to x-pack/plugins/fleet/public/applications/fleet/layouts/default/default.tsx index f312ff374d792..c6ef212b3995e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/layouts/default.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default.tsx @@ -6,12 +6,13 @@ */ import React from 'react'; -import { EuiText, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { Section } from '../sections'; -import { useLink, useConfig } from '../hooks'; -import { WithHeaderLayout } from '../../../layouts'; +import type { Section } from '../../sections'; +import { useLink, useConfig } from '../../hooks'; +import { WithHeaderLayout } from '../../../../layouts'; + +import { DefaultPageTitle } from './default_page_title'; interface Props { section?: Section; @@ -24,31 +25,7 @@ export const DefaultLayout: React.FunctionComponent<Props> = ({ section, childre return ( <WithHeaderLayout - leftColumn={ - <EuiFlexGroup direction="column" gutterSize="m"> - <EuiFlexItem> - <EuiFlexGroup responsive={false} gutterSize="s" alignItems="center"> - <EuiFlexItem grow={false}> - <EuiTitle size="l"> - <h1> - <FormattedMessage id="xpack.fleet.overviewPageTitle" defaultMessage="Fleet" /> - </h1> - </EuiTitle> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - <EuiFlexItem> - <EuiText color="subdued"> - <p> - <FormattedMessage - id="xpack.fleet.overviewPageSubtitle" - defaultMessage="Centralized management for Elastic Agents" - /> - </p> - </EuiText> - </EuiFlexItem> - </EuiFlexGroup> - } + leftColumn={<DefaultPageTitle />} tabs={[ { name: ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default_page_title.tsx b/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default_page_title.tsx new file mode 100644 index 0000000000000..e525a059b7837 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/layouts/default/default_page_title.tsx @@ -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 type { FunctionComponent } from 'react'; +import React from 'react'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiText } from '@elastic/eui'; + +export const DefaultPageTitle: FunctionComponent = () => { + return ( + <EuiFlexGroup direction="column" gutterSize="m"> + <EuiFlexItem> + <EuiFlexGroup responsive={false} gutterSize="s" alignItems="center"> + <EuiFlexItem grow={false}> + <EuiTitle size="l"> + <h1> + <FormattedMessage id="xpack.fleet.overviewPageTitle" defaultMessage="Fleet" /> + </h1> + </EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem> + <EuiText color="subdued"> + <p> + <FormattedMessage + id="xpack.fleet.overviewPageSubtitle" + defaultMessage="Centralized management for Elastic Agents" + /> + </p> + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/layouts/default/index.ts b/x-pack/plugins/fleet/public/applications/fleet/layouts/default/index.ts new file mode 100644 index 0000000000000..9b0d3ee06138f --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/layouts/default/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 { DefaultLayout } from './default'; +export { DefaultPageTitle } from './default_page_title'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/layouts/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/layouts/index.tsx index 71cb8d3aeeb36..0c07f1ffecb79 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/layouts/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/layouts/index.tsx @@ -7,4 +7,4 @@ export * from '../../../layouts'; -export { DefaultLayout } from './default'; +export { DefaultLayout, DefaultPageTitle } from './default'; 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 25a0993242822..633f8a2c57409 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 @@ -21,6 +21,7 @@ import { EuiCheckboxGroup, EuiButton, EuiLink, + EuiFieldNumber, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -158,6 +159,10 @@ export const AgentPolicyForm: React.FunctionComponent<Props> = ({ </EuiFormRow> ); }); + const unenrollmentTimeoutText = i18n.translate( + 'xpack.fleet.agentPolicyForm.unenrollmentTimeoutLabel', + { defaultMessage: 'Unenrollment timeout' } + ); const advancedOptionsContent = ( <> @@ -297,6 +302,27 @@ export const AgentPolicyForm: React.FunctionComponent<Props> = ({ }} /> </EuiDescribedFormGroup> + <EuiDescribedFormGroup + title={<h4>{unenrollmentTimeoutText}</h4>} + description={ + <FormattedMessage + id="xpack.fleet.agentPolicyForm.unenrollmentTimeoutDescription" + defaultMessage="An optional timeout in seconds. If provided, an agent will automatically unenroll after being gone for this period of time." + /> + } + > + <EuiFormRow fullWidth> + <EuiFieldNumber + fullWidth + value={agentPolicy.unenroll_timeout} + min={1} + onChange={(e) => updateAgentPolicy({ unenroll_timeout: Number(e.target.value) })} + isInvalid={Boolean(touchedFields.unenroll_timeout && validation.unenroll_timeout)} + onBlur={() => setTouchedFields({ ...touchedFields, unenroll_timeout: true })} + placeholder={unenrollmentTimeoutText} + /> + </EuiFormRow> + </EuiDescribedFormGroup> {isEditing && 'id' in agentPolicy && !agentPolicy.is_managed && diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx index b3b0d6ed51cb4..3fbaea67d8973 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx @@ -495,17 +495,13 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { onCancel={() => setFormState('VALID')} /> )} - {from === 'package' - ? packageInfo && ( - <IntegrationBreadcrumb - pkgTitle={integrationInfo?.title || packageInfo.title} - pkgkey={pkgKeyFromPackageInfo(packageInfo)} - integration={integrationInfo?.name} - /> - ) - : agentPolicy && ( - <PolicyBreadcrumb policyName={agentPolicy.name} policyId={agentPolicy.id} /> - )} + {packageInfo && ( + <IntegrationBreadcrumb + pkgTitle={integrationInfo?.title || packageInfo.title} + pkgkey={pkgKeyFromPackageInfo(packageInfo)} + integration={integrationInfo?.name} + /> + )} <StepsWithLessPadding steps={steps} /> <EuiSpacer size="xl" /> <EuiSpacer size="xl" /> @@ -559,14 +555,6 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { ); }; -const PolicyBreadcrumb: React.FunctionComponent<{ - policyName: string; - policyId: string; -}> = ({ policyName, policyId }) => { - useBreadcrumbs('add_integration_from_policy', { policyName, policyId }); - return null; -}; - const IntegrationBreadcrumb: React.FunctionComponent<{ pkgTitle: string; pkgkey: string; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx index 1ea1a7de53b95..0c6451e3f34a2 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx @@ -65,12 +65,13 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>( setIsLoading(true); try { // eslint-disable-next-line @typescript-eslint/naming-convention - const { name, description, namespace, monitoring_enabled } = agentPolicy; + const { name, description, namespace, monitoring_enabled, unenroll_timeout } = agentPolicy; const { data, error } = await sendUpdateAgentPolicy(agentPolicy.id, { name, description, namespace, monitoring_enabled, + unenroll_timeout, }); if (data) { notifications.toasts.addSuccess( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx index 33dbbb25c5d42..5992888564e7f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx @@ -5,7 +5,9 @@ * 2.0. */ +import type { ReactNode } from 'react'; import React, { useState } from 'react'; +import type { StyledComponent } from 'styled-components'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -29,7 +31,13 @@ import type { NewAgentPolicy, AgentPolicy } from '../../../../types'; import { useCapabilities, useStartServices, sendCreateAgentPolicy } from '../../../../hooks'; import { AgentPolicyForm, agentPolicyFormValidation } from '../../components'; -const FlyoutWithHigherZIndex = styled(EuiFlyout)` +// TODO: EUI team follow up on complex types and styled-components `styled` +// https://github.com/elastic/eui/issues/4855 +const FlyoutWithHigherZIndex: StyledComponent< + typeof EuiFlyout, + {}, + { children?: ReactNode } +> = styled(EuiFlyout)` z-index: ${(props) => props.theme.eui.euiZLevel5}; `; @@ -39,6 +47,7 @@ interface Props extends EuiFlyoutProps { export const CreateAgentPolicyFlyout: React.FunctionComponent<Props> = ({ onClose, + as, ...restOfProps }) => { const { notifications } = useStartServices(); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx index 995423ea91f96..9e8d200344b01 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx @@ -233,7 +233,7 @@ export const SettingsPage: React.FC<Props> = memo(({ packageInfo }: Props) => { <EuiText color="subdued"> <FormattedMessage id="xpack.fleet.integrations.settings.packageUninstallNoteDescription.packageUninstallUninstallableNoteDetail" - defaultMessage="{strongNote} The {title} integration is installed by default and cannot be removed." + defaultMessage="{strongNote} The {title} integration is a system integration and cannot be removed." values={{ title, strongNote: <NoteLabel />, diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/advanced_agent_authentication_settings.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/advanced_agent_authentication_settings.tsx index 25602b7e108fd..96fab27a55050 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/advanced_agent_authentication_settings.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/advanced_agent_authentication_settings.tsx @@ -21,20 +21,19 @@ import { interface Props { agentPolicyId?: string; + selectedApiKeyId?: string; onKeyChange: (key?: string) => void; } export const AdvancedAgentAuthenticationSettings: FunctionComponent<Props> = ({ agentPolicyId, + selectedApiKeyId, onKeyChange, }) => { const { notifications } = useStartServices(); const [enrollmentAPIKeys, setEnrollmentAPIKeys] = useState<GetEnrollmentAPIKeysResponse['list']>( [] ); - // TODO: Remove this piece of state since we don't need it here. The currently selected enrollment API key only - // needs to live on the form - const [selectedEnrollmentApiKey, setSelectedEnrollmentApiKey] = useState<undefined | string>(); const [isLoadingEnrollmentKey, setIsLoadingEnrollmentKey] = useState(false); const [isAuthenticationSettingsOpen, setIsAuthenticationSettingsOpen] = useState<boolean>(false); @@ -51,7 +50,7 @@ export const AdvancedAgentAuthenticationSettings: FunctionComponent<Props> = ({ return; } setEnrollmentAPIKeys([res.data.item]); - setSelectedEnrollmentApiKey(res.data.item.id); + onKeyChange(res.data.item.id); notifications.toasts.addSuccess( i18n.translate('xpack.fleet.newEnrollmentKey.keyCreatedToasts', { defaultMessage: 'Enrollment token created', @@ -66,15 +65,6 @@ export const AdvancedAgentAuthenticationSettings: FunctionComponent<Props> = ({ } }; - useEffect( - function triggerOnKeyChangeEffect() { - if (onKeyChange) { - onKeyChange(selectedEnrollmentApiKey); - } - }, - [onKeyChange, selectedEnrollmentApiKey] - ); - useEffect( function useEnrollmentKeysForAgentPolicyEffect() { if (!agentPolicyId) { @@ -97,9 +87,13 @@ export const AdvancedAgentAuthenticationSettings: FunctionComponent<Props> = ({ throw new Error('No data while fetching enrollment API keys'); } - setEnrollmentAPIKeys( - res.data.list.filter((key) => key.policy_id === agentPolicyId && key.active === true) + const enrollmentAPIKeysResponse = res.data.list.filter( + (key) => key.policy_id === agentPolicyId && key.active === true ); + + setEnrollmentAPIKeys(enrollmentAPIKeysResponse); + // Default to the first enrollment key if there is one. + onKeyChange(enrollmentAPIKeysResponse[0]?.id); } catch (error) { notifications.toasts.addError(error, { title: 'Error', @@ -108,21 +102,21 @@ export const AdvancedAgentAuthenticationSettings: FunctionComponent<Props> = ({ } fetchEnrollmentAPIKeys(); }, - [agentPolicyId, notifications.toasts] + [onKeyChange, agentPolicyId, notifications.toasts] ); useEffect( function useDefaultEnrollmentKeyForAgentPolicyEffect() { if ( - !selectedEnrollmentApiKey && + !selectedApiKeyId && enrollmentAPIKeys.length > 0 && enrollmentAPIKeys[0].policy_id === agentPolicyId ) { const enrollmentAPIKeyId = enrollmentAPIKeys[0].id; - setSelectedEnrollmentApiKey(enrollmentAPIKeyId); + onKeyChange(enrollmentAPIKeyId); } }, - [enrollmentAPIKeys, selectedEnrollmentApiKey, agentPolicyId] + [enrollmentAPIKeys, selectedApiKeyId, agentPolicyId, onKeyChange] ); return ( <> @@ -139,14 +133,14 @@ export const AdvancedAgentAuthenticationSettings: FunctionComponent<Props> = ({ {isAuthenticationSettingsOpen && ( <> <EuiSpacer size="m" /> - {enrollmentAPIKeys.length && selectedEnrollmentApiKey ? ( + {enrollmentAPIKeys.length && selectedApiKeyId ? ( <EuiSelect fullWidth options={enrollmentAPIKeys.map((key) => ({ value: key.id, text: key.name, }))} - value={selectedEnrollmentApiKey || undefined} + value={selectedApiKeyId || undefined} prepend={ <EuiText> <FormattedMessage @@ -156,7 +150,7 @@ export const AdvancedAgentAuthenticationSettings: FunctionComponent<Props> = ({ </EuiText> } onChange={(e) => { - setSelectedEnrollmentApiKey(e.target.value); + onKeyChange(e.target.value); }} /> ) : ( diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_selection.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_selection.tsx index f92b2d4825935..d9d1aa2e77f86 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_selection.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/agent_policy_selection.tsx @@ -22,6 +22,7 @@ type Props = { } & ( | { withKeySelection: true; + selectedApiKeyId?: string; onKeyChange?: (key?: string) => void; } | { @@ -31,9 +32,9 @@ type Props = { const resolveAgentId = ( agentPolicies?: AgentPolicy[], - selectedAgentId?: string + selectedAgentPolicyId?: string ): undefined | string => { - if (agentPolicies && agentPolicies.length && !selectedAgentId) { + if (agentPolicies && agentPolicies.length && !selectedAgentPolicyId) { if (agentPolicies.length === 1) { return agentPolicies[0].id; } @@ -44,33 +45,33 @@ const resolveAgentId = ( } } - return selectedAgentId; + return selectedAgentPolicyId; }; export const EnrollmentStepAgentPolicy: React.FC<Props> = (props) => { - const { withKeySelection, agentPolicies, onAgentPolicyChange, excludeFleetServer } = props; - const onKeyChange = props.withKeySelection && props.onKeyChange; - const [selectedAgentId, setSelectedAgentId] = useState<undefined | string>( + const { agentPolicies, onAgentPolicyChange, excludeFleetServer } = props; + + const [selectedAgentPolicyId, setSelectedAgentPolicyId] = useState<undefined | string>( () => resolveAgentId(agentPolicies, undefined) // no agent id selected yet ); useEffect( function triggerOnAgentPolicyChangeEffect() { if (onAgentPolicyChange) { - onAgentPolicyChange(selectedAgentId); + onAgentPolicyChange(selectedAgentPolicyId); } }, - [selectedAgentId, onAgentPolicyChange] + [selectedAgentPolicyId, onAgentPolicyChange] ); useEffect( function useDefaultAgentPolicyEffect() { - const resolvedId = resolveAgentId(agentPolicies, selectedAgentId); - if (resolvedId !== selectedAgentId) { - setSelectedAgentId(resolvedId); + const resolvedId = resolveAgentId(agentPolicies, selectedAgentPolicyId); + if (resolvedId !== selectedAgentPolicyId) { + setSelectedAgentPolicyId(resolvedId); } }, - [agentPolicies, selectedAgentId] + [agentPolicies, selectedAgentPolicyId] ); return ( @@ -90,25 +91,26 @@ export const EnrollmentStepAgentPolicy: React.FC<Props> = (props) => { value: agentPolicy.id, text: agentPolicy.name, }))} - value={selectedAgentId || undefined} - onChange={(e) => setSelectedAgentId(e.target.value)} + value={selectedAgentPolicyId || undefined} + onChange={(e) => setSelectedAgentPolicyId(e.target.value)} aria-label={i18n.translate('xpack.fleet.enrollmentStepAgentPolicy.policySelectAriaLabel', { defaultMessage: 'Agent policy', })} /> <EuiSpacer size="m" /> - {selectedAgentId && ( + {selectedAgentPolicyId && ( <AgentPolicyPackageBadges - agentPolicyId={selectedAgentId} + agentPolicyId={selectedAgentPolicyId} excludeFleetServer={excludeFleetServer} /> )} - {withKeySelection && onKeyChange && ( + {props.withKeySelection && props.onKeyChange && ( <> <EuiSpacer /> <AdvancedAgentAuthenticationSettings - onKeyChange={onKeyChange} - agentPolicyId={selectedAgentId} + selectedApiKeyId={props.selectedApiKeyId} + onKeyChange={props.onKeyChange} + agentPolicyId={selectedAgentPolicyId} /> </> )} diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx index 919f0c3052db9..efae8db377f7f 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/managed_instructions.tsx @@ -62,10 +62,10 @@ export const ManagedInstructions = React.memo<Props>( ({ agentPolicy, agentPolicies, viewDataStepContent }) => { const fleetStatus = useFleetStatus(); - const [selectedAPIKeyId, setSelectedAPIKeyId] = useState<string | undefined>(); + const [selectedApiKeyId, setSelectedAPIKeyId] = useState<string | undefined>(); const [isFleetServerPolicySelected, setIsFleetServerPolicySelected] = useState<boolean>(false); - const apiKey = useGetOneEnrollmentAPIKey(selectedAPIKeyId); + const apiKey = useGetOneEnrollmentAPIKey(selectedApiKeyId); const settings = useGetSettings(); const fleetServerInstructions = useFleetServerInstructions(apiKey?.data?.item?.policy_id); @@ -84,10 +84,11 @@ export const ManagedInstructions = React.memo<Props>( !agentPolicy ? AgentPolicySelectionStep({ agentPolicies, + selectedApiKeyId, setSelectedAPIKeyId, setIsFleetServerPolicySelected, }) - : AgentEnrollmentKeySelectionStep({ agentPolicy, setSelectedAPIKeyId }), + : AgentEnrollmentKeySelectionStep({ agentPolicy, selectedApiKeyId, setSelectedAPIKeyId }), ]; if (isFleetServerPolicySelected) { baseSteps.push( @@ -101,7 +102,7 @@ export const ManagedInstructions = React.memo<Props>( title: i18n.translate('xpack.fleet.agentEnrollment.stepEnrollAndRunAgentTitle', { defaultMessage: 'Enroll and start the Elastic Agent', }), - children: selectedAPIKeyId && apiKey.data && ( + children: selectedApiKeyId && apiKey.data && ( <ManualInstructions apiKey={apiKey.data.item} fleetServerHosts={fleetServerHosts} /> ), }); @@ -115,7 +116,7 @@ export const ManagedInstructions = React.memo<Props>( }, [ agentPolicy, agentPolicies, - selectedAPIKeyId, + selectedApiKeyId, apiKey.data, isFleetServerPolicySelected, settings.data?.item?.fleet_server_hosts, diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx index 03cff88e63969..8b12994473e34 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps.tsx @@ -49,14 +49,16 @@ export const DownloadStep = () => { export const AgentPolicySelectionStep = ({ agentPolicies, - setSelectedAPIKeyId, setSelectedPolicyId, - setIsFleetServerPolicySelected, + selectedApiKeyId, + setSelectedAPIKeyId, excludeFleetServer, + setIsFleetServerPolicySelected, }: { agentPolicies?: AgentPolicy[]; - setSelectedAPIKeyId?: (key?: string) => void; setSelectedPolicyId?: (policyId?: string) => void; + selectedApiKeyId?: string; + setSelectedAPIKeyId?: (key?: string) => void; setIsFleetServerPolicySelected?: (selected: boolean) => void; excludeFleetServer?: boolean; }) => { @@ -99,6 +101,7 @@ export const AgentPolicySelectionStep = ({ <EnrollmentStepAgentPolicy agentPolicies={regularAgentPolicies} withKeySelection={setSelectedAPIKeyId ? true : false} + selectedApiKeyId={selectedApiKeyId} onKeyChange={setSelectedAPIKeyId} onAgentPolicyChange={onAgentPolicyChange} excludeFleetServer={excludeFleetServer} @@ -109,9 +112,11 @@ export const AgentPolicySelectionStep = ({ export const AgentEnrollmentKeySelectionStep = ({ agentPolicy, + selectedApiKeyId, setSelectedAPIKeyId, }: { agentPolicy: AgentPolicy; + selectedApiKeyId?: string; setSelectedAPIKeyId: (key?: string) => void; }) => { return { @@ -132,6 +137,7 @@ export const AgentEnrollmentKeySelectionStep = ({ <EuiSpacer size="l" /> <AdvancedAgentAuthenticationSettings agentPolicyId={agentPolicy.id} + selectedApiKeyId={selectedApiKeyId} onKeyChange={setSelectedAPIKeyId} /> </> diff --git a/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx b/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx index d748e655bd506..9bc1bc977b786 100644 --- a/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx @@ -38,7 +38,7 @@ import { useGetOutputs, sendPutOutput, } from '../../hooks'; -import { isDiffPathProtocol } from '../../../common'; +import { isDiffPathProtocol, normalizeHostsForAgents } from '../../../common'; import { SettingsConfirmModal } from './confirm_modal'; import type { SettingsConfirmModalProps } from './confirm_modal'; @@ -53,8 +53,20 @@ interface Props { onClose: () => void; } -function isSameArrayValue(arrayA: string[] = [], arrayB: string[] = []) { - return arrayA.length === arrayB.length && arrayA.every((val, index) => val === arrayB[index]); +function normalizeHosts(hostsInput: string[]) { + return hostsInput.map((host) => { + try { + return normalizeHostsForAgents(host); + } catch (err) { + return host; + } + }); +} + +function isSameArrayValueWithNormalizedHosts(arrayA: string[] = [], arrayB: string[] = []) { + const hostsA = normalizeHosts(arrayA); + const hostsB = normalizeHosts(arrayB); + return hostsA.length === hostsB.length && hostsA.every((val, index) => val === hostsB[index]); } function useSettingsForm(outputId: string | undefined, onSuccess: () => void) { @@ -234,8 +246,11 @@ export const SettingFlyout: React.FunctionComponent<Props> = ({ onClose }) => { return false; } return ( - !isSameArrayValue(settings.fleet_server_hosts, inputs.fleetServerHosts.value) || - !isSameArrayValue(output.hosts, inputs.elasticsearchUrl.value) || + !isSameArrayValueWithNormalizedHosts( + settings.fleet_server_hosts, + inputs.fleetServerHosts.value + ) || + !isSameArrayValueWithNormalizedHosts(output.hosts, inputs.elasticsearchUrl.value) || (output.config_yaml || '') !== inputs.additionalYamlConfig.value ); }, [settings, inputs, output]); @@ -246,32 +261,37 @@ export const SettingFlyout: React.FunctionComponent<Props> = ({ onClose }) => { } const tmpChanges: SettingsConfirmModalProps['changes'] = []; - if (!isSameArrayValue(output.hosts, inputs.elasticsearchUrl.value)) { + if (!isSameArrayValueWithNormalizedHosts(output.hosts, inputs.elasticsearchUrl.value)) { tmpChanges.push( { type: 'elasticsearch', direction: 'removed', - urls: output.hosts || [], + urls: normalizeHosts(output.hosts || []), }, { type: 'elasticsearch', direction: 'added', - urls: inputs.elasticsearchUrl.value, + urls: normalizeHosts(inputs.elasticsearchUrl.value), } ); } - if (!isSameArrayValue(settings.fleet_server_hosts, inputs.fleetServerHosts.value)) { + if ( + !isSameArrayValueWithNormalizedHosts( + settings.fleet_server_hosts, + inputs.fleetServerHosts.value + ) + ) { tmpChanges.push( { type: 'fleet_server', direction: 'removed', - urls: settings.fleet_server_hosts, + urls: normalizeHosts(settings.fleet_server_hosts || []), }, { type: 'fleet_server', direction: 'added', - urls: inputs.fleetServerHosts.value, + urls: normalizeHosts(inputs.fleetServerHosts.value), } ); } @@ -300,7 +320,7 @@ export const SettingFlyout: React.FunctionComponent<Props> = ({ onClose }) => { helpText={ <FormattedMessage id="xpack.fleet.settings.fleetServerHostsHelpTect" - defaultMessage="Specify the URLs that your agents will use to connect to a Fleet Server. If multiple URLs exist, Fleet shows the first provided URL for enrollment purposes. Refer to the {link}." + defaultMessage="Specify the URLs that your agents will use to connect to a Fleet Server. If multiple URLs exist, Fleet shows the first provided URL for enrollment purposes. Fleet Server uses port 8220 by default. Refer to the {link}." values={{ link: ( <EuiLink @@ -327,7 +347,8 @@ export const SettingFlyout: React.FunctionComponent<Props> = ({ onClose }) => { defaultMessage: 'Elasticsearch hosts', })} helpText={i18n.translate('xpack.fleet.settings.elasticsearchUrlsHelpTect', { - defaultMessage: 'Specify the Elasticsearch URLs where agents send data.', + defaultMessage: + 'Specify the Elasticsearch URLs where agents send data. Elasticsearch uses port 9200 by default.', })} /> </EuiPanel> diff --git a/x-pack/plugins/fleet/public/constants/page_paths.ts b/x-pack/plugins/fleet/public/constants/page_paths.ts index 1688a396cd5a1..317241358a381 100644 --- a/x-pack/plugins/fleet/public/constants/page_paths.ts +++ b/x-pack/plugins/fleet/public/constants/page_paths.ts @@ -54,6 +54,7 @@ export const FLEET_ROUTING_PATHS = { policy_details: '/policies/:policyId/:tabId?', policy_details_settings: '/policies/:policyId/settings', edit_integration: '/policies/:policyId/edit-integration/:packagePolicyId', + // TODO: Review uses and remove if it is no longer used or linked to in any UX flows add_integration_from_policy: '/policies/:policyId/add-integration', enrollment_tokens: '/enrollment-tokens', data_streams: '/data-streams', diff --git a/x-pack/plugins/fleet/public/mock/plugin_configuration.ts b/x-pack/plugins/fleet/public/mock/plugin_configuration.ts index 097b6aa98c067..5dad8ad504979 100644 --- a/x-pack/plugins/fleet/public/mock/plugin_configuration.ts +++ b/x-pack/plugins/fleet/public/mock/plugin_configuration.ts @@ -12,6 +12,7 @@ export const createConfigurationMock = (): FleetConfigType => { enabled: true, registryUrl: '', registryProxyUrl: '', + agentIdVerificationEnabled: true, agents: { enabled: true, elasticsearch: { diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/final_pipeline.ts b/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts similarity index 82% rename from x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/final_pipeline.ts rename to x-pack/plugins/fleet/server/constants/fleet_es_assets.ts index f929a4f139981..8e9dac11db799 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/final_pipeline.ts +++ b/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts @@ -5,9 +5,37 @@ * 2.0. */ -export const FINAL_PIPELINE_ID = '.fleet_final_pipeline'; +export const FLEET_FINAL_PIPELINE_ID = '.fleet_final_pipeline-1'; -export const FINAL_PIPELINE = `--- +export const FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME = '.fleet_component_template-1'; + +export const FLEET_GLOBAL_COMPONENT_TEMPLATE_CONTENT = { + _meta: {}, + template: { + settings: { + index: { + final_pipeline: FLEET_FINAL_PIPELINE_ID, + }, + }, + mappings: { + properties: { + event: { + properties: { + ingested: { + type: 'date', + }, + agent_id_status: { + ignore_above: 1024, + type: 'keyword', + }, + }, + }, + }, + }, + }, +}; + +export const FLEET_FINAL_PIPELINE_CONTENT = `--- description: > Final pipeline for processing all incoming Fleet Agent documents. processors: diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index 16a92a2ffa1aa..3aca5e8800dc5 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -57,3 +57,10 @@ export { PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, PRECONFIGURATION_LATEST_KEYWORD, } from '../../common'; + +export { + FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME, + FLEET_GLOBAL_COMPONENT_TEMPLATE_CONTENT, + FLEET_FINAL_PIPELINE_ID, + FLEET_FINAL_PIPELINE_CONTENT, +} from './fleet_es_assets'; diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts index 0a886ffedbd6c..ab1cd9002d04a 100644 --- a/x-pack/plugins/fleet/server/index.ts +++ b/x-pack/plugins/fleet/server/index.ts @@ -77,6 +77,7 @@ export const config: PluginConfigDescriptor = { }), packages: PreconfiguredPackagesSchema, agentPolicies: PreconfiguredAgentPoliciesSchema, + agentIdVerificationEnabled: schema.boolean({ defaultValue: true }), }), }; diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index 1e2a0bc7649f0..2632b7f9dd85a 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -4,6 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { of } from 'rxjs'; + import { elasticsearchServiceMock, loggingSystemMock, @@ -22,6 +24,14 @@ import type { FleetAppContext } from '../plugin'; export * from '../services/artifacts/mocks'; export const createAppContextStartContractMock = (): FleetAppContext => { + const config = { + agents: { enabled: true, elasticsearch: {} }, + enabled: true, + agentIdVerificationEnabled: true, + }; + + const config$ = of(config); + return { elasticsearch: elasticsearchServiceMock.createStart(), data: dataPluginMock.createStartContract(), @@ -33,7 +43,9 @@ export const createAppContextStartContractMock = (): FleetAppContext => { configInitialValue: { agents: { enabled: true, elasticsearch: {} }, enabled: true, + agentIdVerificationEnabled: true, }, + config$, kibanaVersion: '8.0.0', kibanaBranch: 'master', }; diff --git a/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts b/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts index 77fe74fda54d9..d6c483ffe30d9 100644 --- a/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts +++ b/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts @@ -15,7 +15,7 @@ import { PutPreconfigurationSchema } from '../../types'; import { defaultIngestErrorHandler } from '../../errors'; import { ensurePreconfiguredPackagesAndPolicies, outputService } from '../../services'; -export const putPreconfigurationHandler: RequestHandler< +export const updatePreconfigurationHandler: RequestHandler< undefined, undefined, TypeOf<typeof PutPreconfigurationSchema.body> @@ -43,10 +43,10 @@ export const putPreconfigurationHandler: RequestHandler< export const registerRoutes = (router: IRouter) => { router.put( { - path: PRECONFIGURATION_API_ROUTES.PUT_PRECONFIG, + path: PRECONFIGURATION_API_ROUTES.UPDATE_PATTERN, validate: PutPreconfigurationSchema, options: { tags: [`access:${PLUGIN_ID}-all`] }, }, - putPreconfigurationHandler + updatePreconfigurationHandler ); }; diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index bd7bb98eb7c07..fe8771115a217 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -149,6 +149,7 @@ const getSavedObjectTypes = ( is_managed: { type: 'boolean' }, status: { type: 'keyword' }, package_policies: { type: 'keyword' }, + unenroll_timeout: { type: 'integer' }, updated_at: { type: 'date' }, updated_by: { type: 'keyword' }, revision: { type: 'integer' }, diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 2a6036d99281e..465075cca7a0b 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -642,6 +642,7 @@ class AgentPolicyService { data: (fullPolicy as unknown) as FleetServerPolicy['data'], policy_id: fullPolicy.id, default_fleet_server: policy.is_default_fleet_server === true, + unenroll_timeout: policy.unenroll_timeout, }; await esClient.create({ diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts index 1d212f188120f..a6aa87c5ed0f5 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts @@ -14,9 +14,9 @@ import { getAsset, getPathParts } from '../../archive'; import type { ArchiveEntry } from '../../archive'; import { saveInstalledEsRefs } from '../../packages/install'; import { getInstallationObject } from '../../packages'; +import { FLEET_FINAL_PIPELINE_CONTENT, FLEET_FINAL_PIPELINE_ID } from '../../../../constants'; import { deletePipelineRefs } from './remove'; -import { FINAL_PIPELINE, FINAL_PIPELINE_ID } from './final_pipeline'; interface RewriteSubstitution { source: string; @@ -190,22 +190,24 @@ export async function ensureFleetFinalPipelineIsInstalled(esClient: Elasticsearc const esClientRequestOptions: TransportRequestOptions = { ignore: [404], }; - const res = await esClient.ingest.getPipeline({ id: FINAL_PIPELINE_ID }, esClientRequestOptions); + const res = await esClient.ingest.getPipeline( + { id: FLEET_FINAL_PIPELINE_ID }, + esClientRequestOptions + ); if (res.statusCode === 404) { - await esClient.ingest.putPipeline( - // @ts-ignore pipeline is define in yaml - { id: FINAL_PIPELINE_ID, body: FINAL_PIPELINE }, - { - headers: { - // pipeline is YAML - 'Content-Type': 'application/yaml', - // but we want JSON responses (to extract error messages, status code, or other metadata) - Accept: 'application/json', - }, - } - ); + await installPipeline({ + esClient, + pipeline: { + nameForInstallation: FLEET_FINAL_PIPELINE_ID, + contentForInstallation: FLEET_FINAL_PIPELINE_CONTENT, + extension: 'yml', + }, + }); + return { isCreated: true }; } + + return { isCreated: false }; } const isDirectory = ({ path }: ArchiveEntry) => path.endsWith('/'); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap index acf8ae742bf8f..6a4476316bfa5 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap @@ -25,8 +25,7 @@ exports[`EPM template tests loading base.yml: base.yml 1`] = ` "default_field": [ "long.nested.foo" ] - }, - "final_pipeline": ".fleet_final_pipeline" + } } }, "mappings": { @@ -99,7 +98,9 @@ exports[`EPM template tests loading base.yml: base.yml 1`] = ` } }, "data_stream": {}, - "composed_of": [], + "composed_of": [ + ".fleet_component_template-1" + ], "_meta": { "package": { "name": "nginx" @@ -140,8 +141,7 @@ exports[`EPM template tests loading coredns.logs.yml: coredns.logs.yml 1`] = ` "coredns.response.code", "coredns.response.flags" ] - }, - "final_pipeline": ".fleet_final_pipeline" + } } }, "mappings": { @@ -214,7 +214,9 @@ exports[`EPM template tests loading coredns.logs.yml: coredns.logs.yml 1`] = ` } }, "data_stream": {}, - "composed_of": [], + "composed_of": [ + ".fleet_component_template-1" + ], "_meta": { "package": { "name": "coredns" @@ -283,8 +285,7 @@ exports[`EPM template tests loading system.yml: system.yml 1`] = ` "system.users.scope", "system.users.remote_host" ] - }, - "final_pipeline": ".fleet_final_pipeline" + } } }, "mappings": { @@ -1741,7 +1742,9 @@ exports[`EPM template tests loading system.yml: system.yml 1`] = ` } }, "data_stream": {}, - "composed_of": [], + "composed_of": [ + ".fleet_component_template-1" + ], "_meta": { "package": { "name": "system" diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts index d202dab54f5bd..e8dac60ddba1a 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts @@ -11,7 +11,7 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/s import { ElasticsearchAssetType } from '../../../../types'; import type { RegistryDataStream, - TemplateRef, + IndexTemplateEntry, RegistryElasticsearch, InstallablePackage, } from '../../../../types'; @@ -19,7 +19,11 @@ import { loadFieldsFromYaml, processFields } from '../../fields/field'; import type { Field } from '../../fields/field'; import { getPipelineNameForInstallation } from '../ingest_pipeline/install'; import { getAsset, getPathParts } from '../../archive'; -import { removeAssetsFromInstalledEsByType, saveInstalledEsRefs } from '../../packages/install'; +import { removeAssetTypesFromInstalledEs, saveInstalledEsRefs } from '../../packages/install'; +import { + FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME, + FLEET_GLOBAL_COMPONENT_TEMPLATE_CONTENT, +} from '../../../../constants'; import { generateMappings, @@ -34,7 +38,7 @@ export const installTemplates = async ( esClient: ElasticsearchClient, paths: string[], savedObjectsClient: SavedObjectsClientContract -): Promise<TemplateRef[]> => { +): Promise<IndexTemplateEntry[]> => { // install any pre-built index template assets, // atm, this is only the base package's global index templates // Install component templates first, as they are used by the index templates @@ -42,44 +46,36 @@ export const installTemplates = async ( await installPreBuiltTemplates(paths, esClient); // remove package installation's references to index templates - await removeAssetsFromInstalledEsByType( - savedObjectsClient, - installablePackage.name, - ElasticsearchAssetType.indexTemplate - ); + await removeAssetTypesFromInstalledEs(savedObjectsClient, installablePackage.name, [ + ElasticsearchAssetType.indexTemplate, + ElasticsearchAssetType.componentTemplate, + ]); // build templates per data stream from yml files const dataStreams = installablePackage.data_streams; if (!dataStreams) return []; + + const installedTemplatesNested = await Promise.all( + dataStreams.map((dataStream) => + installTemplateForDataStream({ + pkg: installablePackage, + esClient, + dataStream, + }) + ) + ); + const installedTemplates = installedTemplatesNested.flat(); + // get template refs to save - const installedTemplateRefs = dataStreams.map((dataStream) => ({ - id: generateTemplateName(dataStream), - type: ElasticsearchAssetType.indexTemplate, - })); + const installedIndexTemplateRefs = getAllTemplateRefs(installedTemplates); // add package installation's references to index templates - await saveInstalledEsRefs(savedObjectsClient, installablePackage.name, installedTemplateRefs); - - if (dataStreams) { - const installTemplatePromises = dataStreams.reduce<Array<Promise<TemplateRef>>>( - (acc, dataStream) => { - acc.push( - installTemplateForDataStream({ - pkg: installablePackage, - esClient, - dataStream, - }) - ); - return acc; - }, - [] - ); - - const res = await Promise.all(installTemplatePromises); - const installedTemplates = res.flat(); + await saveInstalledEsRefs( + savedObjectsClient, + installablePackage.name, + installedIndexTemplateRefs + ); - return installedTemplates; - } - return []; + return installedTemplates; }; const installPreBuiltTemplates = async (paths: string[], esClient: ElasticsearchClient) => { @@ -160,7 +156,7 @@ export async function installTemplateForDataStream({ pkg: InstallablePackage; esClient: ElasticsearchClient; dataStream: RegistryDataStream; -}): Promise<TemplateRef> { +}): Promise<IndexTemplateEntry> { const fields = await loadFieldsFromYaml(pkg, dataStream.path); return installTemplate({ esClient, @@ -171,84 +167,140 @@ export async function installTemplateForDataStream({ }); } +interface TemplateMapEntry { + _meta: { package?: { name: string } }; + template: + | { + mappings: NonNullable<RegistryElasticsearch['index_template.mappings']>; + } + | { + settings: NonNullable<RegistryElasticsearch['index_template.settings']> | object; + }; +} +type TemplateMap = Record<string, TemplateMapEntry>; function putComponentTemplate( - body: object | undefined, - name: string, - esClient: ElasticsearchClient -): { clusterPromise: Promise<any>; name: string } | undefined { - if (body) { - const esClientParams = { - name, - body, - }; - - return { - // @ts-expect-error body expected to be ClusterPutComponentTemplateRequest - clusterPromise: esClient.cluster.putComponentTemplate(esClientParams, { ignore: [404] }), - name, - }; + esClient: ElasticsearchClient, + params: { + body: TemplateMapEntry; + name: string; + create?: boolean; } +): { clusterPromise: Promise<any>; name: string } { + const { name, body, create = false } = params; + return { + clusterPromise: esClient.cluster.putComponentTemplate( + // @ts-expect-error body is missing required key `settings`. TemplateMapEntry has settings *or* mappings + { name, body, create }, + { ignore: [404] } + ), + name, + }; } -function buildComponentTemplates(registryElasticsearch: RegistryElasticsearch | undefined) { - let mappingsTemplate; - let settingsTemplate; +const mappingsSuffix = '@mappings'; +const settingsSuffix = '@settings'; +const userSettingsSuffix = '@custom'; +type TemplateBaseName = string; +type UserSettingsTemplateName = `${TemplateBaseName}${typeof userSettingsSuffix}`; + +const isUserSettingsTemplate = (name: string): name is UserSettingsTemplateName => + name.endsWith(userSettingsSuffix); + +function buildComponentTemplates(params: { + templateName: string; + registryElasticsearch: RegistryElasticsearch | undefined; + packageName: string; +}) { + const { templateName, registryElasticsearch, packageName } = params; + const mappingsTemplateName = `${templateName}${mappingsSuffix}`; + const settingsTemplateName = `${templateName}${settingsSuffix}`; + const userSettingsTemplateName = `${templateName}${userSettingsSuffix}`; + + const templatesMap: TemplateMap = {}; + const _meta = { package: { name: packageName } }; if (registryElasticsearch && registryElasticsearch['index_template.mappings']) { - mappingsTemplate = { + templatesMap[mappingsTemplateName] = { template: { - mappings: { - ...registryElasticsearch['index_template.mappings'], - }, + mappings: registryElasticsearch['index_template.mappings'], }, + _meta, }; } if (registryElasticsearch && registryElasticsearch['index_template.settings']) { - settingsTemplate = { + templatesMap[settingsTemplateName] = { template: { settings: registryElasticsearch['index_template.settings'], }, + _meta, }; } - return { settingsTemplate, mappingsTemplate }; -} -async function installDataStreamComponentTemplates( - templateName: string, - registryElasticsearch: RegistryElasticsearch | undefined, - esClient: ElasticsearchClient -) { - const templates: string[] = []; - const componentPromises: Array<Promise<any>> = []; + // return empty/stub template + templatesMap[userSettingsTemplateName] = { + template: { + settings: {}, + }, + _meta, + }; - const compTemplates = buildComponentTemplates(registryElasticsearch); + return templatesMap; +} - const mappings = putComponentTemplate( - compTemplates.mappingsTemplate, - `${templateName}-mappings`, - esClient - ); +async function installDataStreamComponentTemplates(params: { + templateName: string; + registryElasticsearch: RegistryElasticsearch | undefined; + esClient: ElasticsearchClient; + packageName: string; +}) { + const { templateName, registryElasticsearch, esClient, packageName } = params; + const templates = buildComponentTemplates({ templateName, registryElasticsearch, packageName }); + const templateNames = Object.keys(templates); + const templateEntries = Object.entries(templates); - const settings = putComponentTemplate( - compTemplates.settingsTemplate, - `${templateName}-settings`, - esClient + // TODO: Check return values for errors + await Promise.all( + templateEntries.map(async ([name, body]) => { + if (isUserSettingsTemplate(name)) { + // look for existing user_settings template + const result = await esClient.cluster.getComponentTemplate({ name }, { ignore: [404] }); + const hasUserSettingsTemplate = result.body.component_templates?.length === 1; + if (!hasUserSettingsTemplate) { + // only add if one isn't already present + const { clusterPromise } = putComponentTemplate(esClient, { body, name, create: true }); + return clusterPromise; + } + } else { + const { clusterPromise } = putComponentTemplate(esClient, { body, name }); + return clusterPromise; + } + }) ); - if (mappings) { - templates.push(mappings.name); - componentPromises.push(mappings.clusterPromise); - } + return templateNames; +} - if (settings) { - templates.push(settings.name); - componentPromises.push(settings.clusterPromise); +export async function ensureDefaultComponentTemplate(esClient: ElasticsearchClient) { + const { body: getTemplateRes } = await esClient.cluster.getComponentTemplate( + { + name: FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME, + }, + { + ignore: [404], + } + ); + + const existingTemplate = getTemplateRes?.component_templates?.[0]; + if (!existingTemplate) { + await putComponentTemplate(esClient, { + name: FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME, + body: FLEET_GLOBAL_COMPONENT_TEMPLATE_CONTENT, + create: true, + }); } - // TODO: Check return values for errors - await Promise.all(componentPromises); - return templates; + return { isCreated: !existingTemplate }; } export async function installTemplate({ @@ -263,7 +315,7 @@ export async function installTemplate({ dataStream: RegistryDataStream; packageVersion: string; packageName: string; -}): Promise<TemplateRef> { +}): Promise<IndexTemplateEntry> { const validFields = processFields(fields); const mappings = generateMappings(validFields); const templateName = generateTemplateName(dataStream); @@ -310,11 +362,12 @@ export async function installTemplate({ await esClient.indices.putIndexTemplate(updateIndexTemplateParams, { ignore: [404] }); } - const composedOfTemplates = await installDataStreamComponentTemplates( + const composedOfTemplates = await installDataStreamComponentTemplates({ templateName, - dataStream.elasticsearch, - esClient - ); + registryElasticsearch: dataStream.elasticsearch, + esClient, + packageName, + }); const template = getTemplate({ type: dataStream.type, @@ -342,3 +395,22 @@ export async function installTemplate({ indexTemplate: template, }; } + +export function getAllTemplateRefs(installedTemplates: IndexTemplateEntry[]) { + return installedTemplates.flatMap((installedTemplate) => { + const indexTemplates = [ + { + id: installedTemplate.templateName, + type: ElasticsearchAssetType.indexTemplate, + }, + ]; + const componentTemplates = installedTemplate.indexTemplate.composed_of + // Filter global component template shared between integrations + .filter((componentTemplateId) => componentTemplateId !== FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME) + .map((componentTemplateId) => ({ + id: componentTemplateId, + type: ElasticsearchAssetType.componentTemplate, + })); + return indexTemplates.concat(componentTemplates); + }); +} diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts index ae7bff618dba2..d1f806f67ca5c 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts @@ -24,6 +24,8 @@ import { generateTemplateIndexPattern, } from './template'; +const FLEET_COMPONENT_TEMPLATE = '.fleet_component_template-1'; + // Add our own serialiser to just do JSON.stringify expect.addSnapshotSerializer({ print(val) { @@ -67,7 +69,7 @@ describe('EPM template', () => { composedOfTemplates, templatePriority: 200, }); - expect(template.composed_of).toStrictEqual(composedOfTemplates); + expect(template.composed_of).toStrictEqual([...composedOfTemplates, FLEET_COMPONENT_TEMPLATE]); }); it('adds empty composed_of correctly', () => { @@ -82,7 +84,7 @@ describe('EPM template', () => { composedOfTemplates, templatePriority: 200, }); - expect(template.composed_of).toStrictEqual(composedOfTemplates); + expect(template.composed_of).toStrictEqual([FLEET_COMPONENT_TEMPLATE]); }); it('adds hidden field correctly', () => { diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts index 07d0df021c827..6aa7680395bed 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts @@ -10,13 +10,13 @@ import type { ElasticsearchClient } from 'kibana/server'; import type { Field, Fields } from '../../fields/field'; import type { RegistryDataStream, - TemplateRef, + IndexTemplateEntry, IndexTemplate, IndexTemplateMappings, } from '../../../../types'; import { appContextService } from '../../../'; import { getRegistryDataStreamAssetBaseName } from '../index'; -import { FINAL_PIPELINE_ID } from '../ingest_pipeline/final_pipeline'; +import { FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME } from '../../../../constants'; interface Properties { [key: string]: any; @@ -90,7 +90,11 @@ export function getTemplate({ if (template.template.settings.index.final_pipeline) { throw new Error(`Error template for ${templateIndexPattern} contains a final_pipeline`); } - template.template.settings.index.final_pipeline = FINAL_PIPELINE_ID; + + if (appContextService.getConfig()?.agentIdVerificationEnabled) { + // Add fleet global assets + template.composed_of = [...(template.composed_of || []), FLEET_GLOBAL_COMPONENT_TEMPLATE_NAME]; + } return template; } @@ -456,7 +460,7 @@ function getBaseTemplate( export const updateCurrentWriteIndices = async ( esClient: ElasticsearchClient, - templates: TemplateRef[] + templates: IndexTemplateEntry[] ): Promise<void> => { if (!templates.length) return; @@ -471,7 +475,7 @@ function isCurrentDataStream(item: CurrentDataStream[] | undefined): item is Cur const queryDataStreamsFromTemplates = async ( esClient: ElasticsearchClient, - templates: TemplateRef[] + templates: IndexTemplateEntry[] ): Promise<CurrentDataStream[]> => { const dataStreamPromises = templates.map((template) => { return getDataStreams(esClient, template); @@ -482,7 +486,7 @@ const queryDataStreamsFromTemplates = async ( const getDataStreams = async ( esClient: ElasticsearchClient, - template: TemplateRef + template: IndexTemplateEntry ): Promise<CurrentDataStream[] | undefined> => { const { templateName, indexTemplate } = template; const { body } = await esClient.indices.getDataStream({ name: `${templateName}-*` }); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts index 65d71ac5fdc17..1bbbb1bb9b6a2 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts @@ -10,10 +10,10 @@ import type { ElasticsearchClient, SavedObject, SavedObjectsClientContract } fro import { MAX_TIME_COMPLETE_INSTALL, ASSETS_SAVED_OBJECT_TYPE } from '../../../../common'; import type { InstallablePackage, InstallSource, PackageAssetReference } from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; -import { ElasticsearchAssetType } from '../../../types'; import type { AssetReference, Installation, InstallType } from '../../../types'; import { installTemplates } from '../elasticsearch/template/install'; import { installPipelines, deletePreviousPipelines } from '../elasticsearch/ingest_pipeline/'; +import { getAllTemplateRefs } from '../elasticsearch/template/install'; import { installILMPolicy } from '../elasticsearch/ilm/install'; import { installKibanaAssets, getKibanaAssets } from '../kibana/assets/install'; import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; @@ -170,10 +170,7 @@ export async function _installPackage({ installedPkg.attributes.install_version ); } - const installedTemplateRefs = installedTemplates.map((template) => ({ - id: template.templateName, - type: ElasticsearchAssetType.indexTemplate, - })); + const installedTemplateRefs = getAllTemplateRefs(installedTemplates); // make sure the assets are installed (or didn't error) if (installKibanaAssetsError) throw installKibanaAssetsError; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts index 28af2b563da79..6a5968441e634 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -101,6 +101,8 @@ export async function getPackageSavedObjects( }); } +export const getInstallations = getPackageSavedObjects; + export async function getPackageInfo(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/index.ts b/x-pack/plugins/fleet/server/services/epm/packages/index.ts index 608e157017e9b..1f9113590f0f7 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/index.ts @@ -17,6 +17,7 @@ export { getFile, getInstallationObject, getInstallation, + getInstallations, getPackageInfo, getPackages, getLimitedPackages, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index c6fd9a8f763ab..e00526cbb4ec4 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -257,8 +257,7 @@ async function installPackageFromRegistry({ const { paths, packageInfo } = await Registry.getRegistryPackage(pkgName, pkgVersion); // try installing the package, if there was an error, call error handler and rethrow - // TODO: without the ts-ignore, TS complains about the type of the value of the returned InstallResult.status - // @ts-ignore + // @ts-expect-error status is string instead of InstallResult.status 'installed' | 'already_installed' return _installPackage({ savedObjectsClient, esClient, @@ -334,8 +333,7 @@ async function installPackageByUpload({ version: packageInfo.version, packageInfo, }); - // TODO: without the ts-ignore, TS complains about the type of the value of the returned InstallResult.status - // @ts-ignore + // @ts-expect-error status is string instead of InstallResult.status 'installed' | 'already_installed' return _installPackage({ savedObjectsClient, esClient, @@ -484,17 +482,17 @@ export const saveInstalledEsRefs = async ( return installedAssets; }; -export const removeAssetsFromInstalledEsByType = async ( +export const removeAssetTypesFromInstalledEs = async ( savedObjectsClient: SavedObjectsClientContract, pkgName: string, - assetType: AssetType + assetTypes: AssetType[] ) => { const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); const installedAssets = installedPkg?.attributes.installed_es; if (!installedAssets?.length) return; - const installedAssetsToSave = installedAssets?.filter(({ id, type }) => { - return type !== assetType; - }); + const installedAssetsToSave = installedAssets?.filter( + (asset) => !assetTypes.includes(asset.type) + ); return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { installed_es: installedAssetsToSave, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts index 706f1bbbaaf35..70167d1156a66 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts @@ -89,13 +89,18 @@ function deleteKibanaAssets( }); } -function deleteESAssets(installedObjects: EsAssetReference[], esClient: ElasticsearchClient) { +function deleteESAssets( + installedObjects: EsAssetReference[], + esClient: ElasticsearchClient +): Array<Promise<unknown>> { return installedObjects.map(async ({ id, type }) => { const assetType = type as AssetType; if (assetType === ElasticsearchAssetType.ingestPipeline) { return deletePipeline(esClient, id); } else if (assetType === ElasticsearchAssetType.indexTemplate) { - return deleteTemplate(esClient, id); + return deleteIndexTemplate(esClient, id); + } else if (assetType === ElasticsearchAssetType.componentTemplate) { + return deleteComponentTemplate(esClient, id); } else if (assetType === ElasticsearchAssetType.transform) { return deleteTransforms(esClient, [id]); } else if (assetType === ElasticsearchAssetType.dataStreamIlmPolicy) { @@ -111,13 +116,30 @@ async function deleteAssets( ) { const logger = appContextService.getLogger(); - const deletePromises: Array<Promise<unknown>> = [ - ...deleteESAssets(installedEs, esClient), - ...deleteKibanaAssets(installedKibana, savedObjectsClient), - ]; + // must delete index templates first, or component templates which reference them cannot be deleted + // separate the assets into Index Templates and other assets + type Tuple = [EsAssetReference[], EsAssetReference[]]; + const [indexTemplates, otherAssets] = installedEs.reduce<Tuple>( + ([indexAssetTypes, otherAssetTypes], asset) => { + if (asset.type === ElasticsearchAssetType.indexTemplate) { + indexAssetTypes.push(asset); + } else { + otherAssetTypes.push(asset); + } + + return [indexAssetTypes, otherAssetTypes]; + }, + [[], []] + ); try { - await Promise.all(deletePromises); + // must delete index templates first + await Promise.all(deleteESAssets(indexTemplates, esClient)); + // then the other asset types + await Promise.all([ + ...deleteESAssets(otherAssets, esClient), + ...deleteKibanaAssets(installedKibana, savedObjectsClient), + ]); } catch (err) { // in the rollback case, partial installs are likely, so missing assets are not an error if (!savedObjectsClient.errors.isNotFoundError(err)) { @@ -126,13 +148,24 @@ async function deleteAssets( } } -async function deleteTemplate(esClient: ElasticsearchClient, name: string): Promise<void> { +async function deleteIndexTemplate(esClient: ElasticsearchClient, name: string): Promise<void> { // '*' shouldn't ever appear here, but it still would delete all templates if (name && name !== '*') { try { await esClient.indices.deleteIndexTemplate({ name }, { ignore: [404] }); } catch { - throw new Error(`error deleting template ${name}`); + throw new Error(`error deleting index template ${name}`); + } + } +} + +async function deleteComponentTemplate(esClient: ElasticsearchClient, name: string): Promise<void> { + // '*' shouldn't ever appear here, but it still would delete all templates + if (name && name !== '*') { + try { + await esClient.cluster.deleteComponentTemplate({ name }, { ignore: [404] }); + } catch (error) { + throw new Error(`error deleting component template ${name}`); } } } diff --git a/x-pack/plugins/fleet/server/services/output.ts b/x-pack/plugins/fleet/server/services/output.ts index 0c7b086f78fdf..8c6bc7eca0401 100644 --- a/x-pack/plugins/fleet/server/services/output.ts +++ b/x-pack/plugins/fleet/server/services/output.ts @@ -9,10 +9,9 @@ import type { SavedObjectsClientContract } from 'src/core/server'; import type { NewOutput, Output, OutputSOAttributes } from '../types'; import { DEFAULT_OUTPUT, OUTPUT_SAVED_OBJECT_TYPE } from '../constants'; -import { decodeCloudId } from '../../common'; +import { decodeCloudId, normalizeHostsForAgents } from '../../common'; import { appContextService } from './app_context'; -import { normalizeHostsForAgents } from './hosts_utils'; const SAVED_OBJECT_TYPE = OUTPUT_SAVED_OBJECT_TYPE; diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index a8be94ca61c0a..e016fafe5459d 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -108,7 +108,7 @@ export async function ensurePreconfiguredPackagesAndPolicies( policies.map(async (preconfiguredAgentPolicy) => { if (preconfiguredAgentPolicy.id) { // Check to see if a preconfigured policy with the same preconfiguration id was already deleted by the user - const preconfigurationId = String(preconfiguredAgentPolicy.id); + const preconfigurationId = preconfiguredAgentPolicy.id.toString(); const searchParams = { searchFields: ['id'], search: escapeSearchQueryPhrase(preconfigurationId), diff --git a/x-pack/plugins/fleet/server/services/settings.ts b/x-pack/plugins/fleet/server/services/settings.ts index 226fbb29467c2..26d581f32d9a2 100644 --- a/x-pack/plugins/fleet/server/services/settings.ts +++ b/x-pack/plugins/fleet/server/services/settings.ts @@ -8,11 +8,14 @@ import Boom from '@hapi/boom'; import type { SavedObjectsClientContract } from 'kibana/server'; -import { decodeCloudId, GLOBAL_SETTINGS_SAVED_OBJECT_TYPE } from '../../common'; +import { + decodeCloudId, + GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + normalizeHostsForAgents, +} from '../../common'; import type { SettingsSOAttributes, Settings, BaseSettings } from '../../common'; import { appContextService } from './app_context'; -import { normalizeHostsForAgents } from './hosts_utils'; export async function getSettings(soClient: SavedObjectsClientContract): Promise<Settings> { const res = await soClient.find<SettingsSOAttributes>({ diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index 45805bb066c3b..cfef04846d92e 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -24,7 +24,10 @@ import { awaitIfPending } from './setup_utils'; import { ensureAgentActionPolicyChangeExists } from './agents'; import { awaitIfFleetServerSetupPending } from './fleet_server'; import { ensureFleetFinalPipelineIsInstalled } from './epm/elasticsearch/ingest_pipeline/install'; +import { ensureDefaultComponentTemplate } from './epm/elasticsearch/template/install'; +import { getInstallations, installPackage } from './epm/packages'; import { isPackageInstalled } from './epm/packages/install'; +import { pkgToPkgKey } from './epm/registry'; export interface SetupStatus { isInitialized: boolean; @@ -47,9 +50,10 @@ async function createSetupSideEffects( settingsService.settingsSetup(soClient), ]); - await ensureFleetFinalPipelineIsInstalled(esClient); - await awaitIfFleetServerSetupPending(); + if (appContextService.getConfig()?.agentIdVerificationEnabled) { + await ensureFleetGlobalEsAssets(soClient, esClient); + } const { agentPolicies: policiesOrUndefined, packages: packagesOrUndefined } = appContextService.getConfig() ?? {}; @@ -95,6 +99,49 @@ async function createSetupSideEffects( }; } +/** + * Ensure ES assets shared by all Fleet index template are installed + */ +export async function ensureFleetGlobalEsAssets( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient +) { + const logger = appContextService.getLogger(); + // Ensure Global Fleet ES assets are installed + const globalAssetsRes = await Promise.all([ + ensureDefaultComponentTemplate(esClient), + ensureFleetFinalPipelineIsInstalled(esClient), + ]); + + if (globalAssetsRes.some((asset) => asset.isCreated)) { + // Update existing index template + const packages = await getInstallations(soClient); + + await Promise.all( + packages.saved_objects.map(async ({ attributes: installation }) => { + if (installation.install_source !== 'registry') { + logger.error( + `Package needs to be manually reinstalled ${installation.name} after installing Fleet global assets` + ); + return; + } + await installPackage({ + installSource: installation.install_source, + savedObjectsClient: soClient, + pkgkey: pkgToPkgKey({ name: installation.name, version: installation.version }), + esClient, + // Force install the pacakge will update the index template and the datastream write indices + force: true, + }).catch((err) => { + logger.error( + `Package needs to be manually reinstalled ${installation.name} after installing Fleet global assets: ${err.message}` + ); + }); + }) + ); + } +} + export async function ensureDefaultEnrollmentAPIKeysExists( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index 8927676976457..0c08a09e76f4e 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -63,7 +63,7 @@ export { IndexTemplate, RegistrySearchResults, RegistrySearchResult, - TemplateRef, + IndexTemplateEntry, IndexTemplateMappings, Settings, SettingsSOAttributes, diff --git a/x-pack/plugins/fleet/server/types/models/agent_policy.ts b/x-pack/plugins/fleet/server/types/models/agent_policy.ts index db551b25e9ebb..48aea1b5cbcc4 100644 --- a/x-pack/plugins/fleet/server/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/agent_policy.ts @@ -16,6 +16,7 @@ export const AgentPolicyBaseSchema = { namespace: NamespaceSchema, description: schema.maybe(schema.string()), is_managed: schema.maybe(schema.boolean()), + unenroll_timeout: schema.maybe(schema.number({ min: 1 })), monitoring_enabled: schema.maybe( schema.arrayOf( schema.oneOf([schema.literal(dataTypes.Logs), schema.literal(dataTypes.Metrics)]) diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap index 6254a6512efb5..9595009347259 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap +++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap @@ -314,89 +314,99 @@ exports[`extend index management ilm summary extension should return extension w </span> </div> <EuiText + color="default" size="s" > <div className="euiText euiText--small" > - illegal_argument_exception - : - setting [index.lifecycle.rollover_alias] for index [testy3] is empty or not defined - <EuiSpacer - size="s" + <EuiTextColor + color="default" + component="div" > <div - className="euiSpacer euiSpacer--s" - /> - </EuiSpacer> - <EuiPopover - anchorPosition="downCenter" - button={ - <EuiButtonEmpty - onClick={[Function]} + className="euiTextColor euiTextColor--default" + > + illegal_argument_exception + : + setting [index.lifecycle.rollover_alias] for index [testy3] is empty or not defined + <EuiSpacer + size="s" > - <FormattedMessage - defaultMessage="Stack trace" - id="xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.stackTraceButton" - values={Object {}} + <div + className="euiSpacer euiSpacer--s" /> - </EuiButtonEmpty> - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="stackPopover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - > - <div - className="euiPopover euiPopover--anchorDownCenter" - id="stackPopover" - > - <div - className="euiPopover__anchor" + </EuiSpacer> + <EuiPopover + anchorPosition="downCenter" + button={ + <EuiButtonEmpty + onClick={[Function]} + > + <FormattedMessage + defaultMessage="Stack trace" + id="xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.stackTraceButton" + values={Object {}} + /> + </EuiButtonEmpty> + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="stackPopover" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" > - <EuiButtonEmpty - onClick={[Function]} + <div + className="euiPopover euiPopover--anchorDownCenter" + id="stackPopover" > - <button - className="euiButtonEmpty euiButtonEmpty--primary" - disabled={false} - onClick={[Function]} - type="button" + <div + className="euiPopover__anchor" > - <EuiButtonContent - className="euiButtonEmpty__content" - iconSide="left" - iconSize="m" - textProps={ - Object { - "className": "euiButtonEmpty__text", - } - } + <EuiButtonEmpty + onClick={[Function]} > - <span - className="euiButtonContent euiButtonEmpty__content" + <button + className="euiButtonEmpty euiButtonEmpty--primary" + disabled={false} + onClick={[Function]} + type="button" > - <span - className="euiButtonEmpty__text" + <EuiButtonContent + className="euiButtonEmpty__content" + iconSide="left" + iconSize="m" + textProps={ + Object { + "className": "euiButtonEmpty__text", + } + } > - <FormattedMessage - defaultMessage="Stack trace" - id="xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.stackTraceButton" - values={Object {}} + <span + className="euiButtonContent euiButtonEmpty__content" > - Stack trace - </FormattedMessage> - </span> - </span> - </EuiButtonContent> - </button> - </EuiButtonEmpty> - </div> + <span + className="euiButtonEmpty__text" + > + <FormattedMessage + defaultMessage="Stack trace" + id="xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.stackTraceButton" + values={Object {}} + > + Stack trace + </FormattedMessage> + </span> + </span> + </EuiButtonContent> + </button> + </EuiButtonEmpty> + </div> + </div> + </EuiPopover> </div> - </EuiPopover> + </EuiTextColor> </div> </EuiText> </div> diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap index 556ac35d0565e..4d2b47c8a6039 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap +++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/policy_table.test.tsx.snap @@ -58,16 +58,16 @@ exports[`policy table should show empty state when there are not any policies 1` data-euiicon-type="managementApp" /> <div - class="euiSpacer euiSpacer--s" + class="euiSpacer euiSpacer--m" /> + <h1 + class="euiTitle euiTitle--medium" + > + Create your first index lifecycle policy + </h1> <span class="euiTextColor euiTextColor--subdued" > - <h1 - class="euiTitle euiTitle--medium" - > - Create your first index lifecycle policy - </h1> <div class="euiSpacer euiSpacer--m" /> @@ -82,9 +82,6 @@ exports[`policy table should show empty state when there are not any policies 1` <div class="euiSpacer euiSpacer--l" /> - <div - class="euiSpacer euiSpacer--s" - /> <button class="euiButton euiButton--primary euiButton--fill" data-test-subj="createPolicyButton" diff --git a/x-pack/plugins/index_management/__jest__/components/index_table.test.js b/x-pack/plugins/index_management/__jest__/components/index_table.test.js index 4ac94319d4711..463d0b30cad08 100644 --- a/x-pack/plugins/index_management/__jest__/components/index_table.test.js +++ b/x-pack/plugins/index_management/__jest__/components/index_table.test.js @@ -6,9 +6,12 @@ */ import React from 'react'; +import { Provider } from 'react-redux'; +import { MemoryRouter } from 'react-router-dom'; import axios from 'axios'; +import sinon from 'sinon'; +import { findTestSubject } from '@elastic/eui/lib/test'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; -import { MemoryRouter } from 'react-router-dom'; /** * The below import is required to avoid a console error warn from brace package @@ -18,9 +21,9 @@ import { MemoryRouter } from 'react-router-dom'; */ import { mountWithIntl, stubWebWorker } from '@kbn/test/jest'; // eslint-disable-line no-unused-vars +import { BASE_PATH, API_BASE_PATH } from '../../common/constants'; import { AppWithoutRouter } from '../../public/application/app'; import { AppContextProvider } from '../../public/application/app_context'; -import { Provider } from 'react-redux'; import { loadIndicesSuccess } from '../../public/application/store/actions'; import { breadcrumbService } from '../../public/application/services/breadcrumbs'; import { UiMetricService } from '../../public/application/services/ui_metric'; @@ -29,10 +32,7 @@ import { httpService } from '../../public/application/services/http'; import { setUiMetricService } from '../../public/application/services/api'; import { indexManagementStore } from '../../public/application/store'; import { setExtensionsService } from '../../public/application/store/selectors/extension_service'; -import { BASE_PATH, API_BASE_PATH } from '../../common/constants'; import { ExtensionsService } from '../../public/services'; -import sinon from 'sinon'; -import { findTestSubject } from '@elastic/eui/lib/test'; /* eslint-disable @kbn/eslint/no-restricted-paths */ import { notificationServiceMock } from '../../../../../src/core/public/notifications/notifications_service.mock'; @@ -40,9 +40,9 @@ import { notificationServiceMock } from '../../../../../src/core/public/notifica const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); let server = null; - let store = null; const indices = []; + for (let i = 0; i < 105; i++) { const baseFake = { health: i % 2 === 0 ? 'green' : 'yellow', @@ -63,8 +63,12 @@ for (let i = 0; i < 105; i++) { name: `.admin${i}`, }); } + let component = null; +// Resolve outstanding API requests. See https://www.benmvp.com/blog/asynchronous-testing-with-enzyme-react-jest/ +const runAllPromises = () => new Promise(setImmediate); + const status = (rendered, row = 0) => { rendered.update(); return findTestSubject(rendered, 'indexTableCell-status') @@ -76,39 +80,54 @@ const status = (rendered, row = 0) => { const snapshot = (rendered) => { expect(rendered).toMatchSnapshot(); }; + const openMenuAndClickButton = (rendered, rowIndex, buttonIndex) => { + // Select a row. const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox'); checkboxes.at(rowIndex).simulate('change', { target: { checked: true } }); rendered.update(); + + // Click the bulk actions button to open the context menu. const actionButton = findTestSubject(rendered, 'indexActionsContextMenuButton'); actionButton.simulate('click'); rendered.update(); + + // Click an action in the context menu. const contextMenuButtons = findTestSubject(rendered, 'indexTableContextMenuButton'); contextMenuButtons.at(buttonIndex).simulate('click'); + rendered.update(); }; -const testEditor = (buttonIndex, rowIndex = 0) => { - const rendered = mountWithIntl(component); + +const testEditor = (rendered, buttonIndex, rowIndex = 0) => { openMenuAndClickButton(rendered, rowIndex, buttonIndex); rendered.update(); snapshot(findTestSubject(rendered, 'detailPanelTabSelected').text()); }; -const testAction = (buttonIndex, done, rowIndex = 0) => { - const rendered = mountWithIntl(component); - let count = 0; + +const testAction = (rendered, buttonIndex, rowIndex = 0) => { + // This is leaking some implementation details about how Redux works. Not sure exactly what's going on + // but it looks like we're aware of how many Redux actions are dispatched in response to user interaction, + // so we "time" our assertion based on how many Redux actions we observe. This is brittle because it + // depends upon how our UI is architected, which will affect how many actions are dispatched. + // Expect this to break when we rearchitect the UI. + let dispatchedActionsCount = 0; store.subscribe(() => { - if (count > 1) { + if (dispatchedActionsCount === 1) { + // Take snapshot of final state. snapshot(status(rendered, rowIndex)); - done(); } - count++; + dispatchedActionsCount++; }); - expect.assertions(2); + openMenuAndClickButton(rendered, rowIndex, buttonIndex); + // take snapshot of initial state. snapshot(status(rendered, rowIndex)); }; + const names = (rendered) => { return findTestSubject(rendered, 'indexTableIndexNameLink'); }; + const namesText = (rendered) => { return names(rendered).map((button) => button.text()); }; @@ -142,23 +161,28 @@ describe('index table', () => { </MemoryRouter> </Provider> ); + store.dispatch(loadIndicesSuccess({ indices })); server = sinon.fakeServer.create(); + server.respondWith(`${API_BASE_PATH}/indices`, [ 200, { 'Content-Type': 'application/json' }, JSON.stringify(indices), ]); + server.respondWith([ 200, { 'Content-Type': 'application/json' }, JSON.stringify({ acknowledged: true }), ]); + server.respondWith(`${API_BASE_PATH}/indices/reload`, [ 200, { 'Content-Type': 'application/json' }, JSON.stringify(indices), ]); + server.respondImmediately = true; }); afterEach(() => { @@ -168,83 +192,124 @@ describe('index table', () => { server.restore(); }); - test('should change pages when a pagination link is clicked on', () => { + test('should change pages when a pagination link is clicked on', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + snapshot(namesText(rendered)); + const pagingButtons = rendered.find('.euiPaginationButton'); pagingButtons.at(2).simulate('click'); - rendered.update(); snapshot(namesText(rendered)); }); - test('should show more when per page value is increased', () => { + + test('should show more when per page value is increased', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const perPageButton = rendered.find('EuiTablePagination EuiPopover').find('button'); perPageButton.simulate('click'); rendered.update(); + const fiftyButton = rendered.find('.euiContextMenuItem').at(1); fiftyButton.simulate('click'); rendered.update(); expect(namesText(rendered).length).toBe(50); }); - test('should show the Actions menu button only when at least one row is selected', () => { + + test('should show the Actions menu button only when at least one row is selected', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + let button = findTestSubject(rendered, 'indexTableContextMenuButton'); expect(button.length).toEqual(0); + const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox'); checkboxes.at(0).simulate('change', { target: { checked: true } }); rendered.update(); button = findTestSubject(rendered, 'indexActionsContextMenuButton'); expect(button.length).toEqual(1); }); - test('should update the Actions menu button text when more than one row is selected', () => { + + test('should update the Actions menu button text when more than one row is selected', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + let button = findTestSubject(rendered, 'indexTableContextMenuButton'); expect(button.length).toEqual(0); + const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox'); checkboxes.at(0).simulate('change', { target: { checked: true } }); rendered.update(); button = findTestSubject(rendered, 'indexActionsContextMenuButton'); expect(button.text()).toEqual('Manage index'); + checkboxes.at(1).simulate('change', { target: { checked: true } }); rendered.update(); button = findTestSubject(rendered, 'indexActionsContextMenuButton'); expect(button.text()).toEqual('Manage 2 indices'); }); - test('should show system indices only when the switch is turned on', () => { + + test('should show system indices only when the switch is turned on', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + snapshot(rendered.find('.euiPagination li').map((item) => item.text())); const switchControl = rendered.find('.euiSwitch__button'); switchControl.simulate('click'); snapshot(rendered.find('.euiPagination li').map((item) => item.text())); }); - test('should filter based on content of search input', () => { + + test('should filter based on content of search input', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const searchInput = rendered.find('.euiFieldSearch').first(); searchInput.instance().value = 'testy0'; searchInput.simulate('keyup', { key: 'Enter', keyCode: 13, which: 13 }); rendered.update(); snapshot(namesText(rendered)); }); - test('should sort when header is clicked', () => { + + test('should sort when header is clicked', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const nameHeader = findTestSubject(rendered, 'indexTableHeaderCell-name').find('button'); nameHeader.simulate('click'); rendered.update(); snapshot(namesText(rendered)); + nameHeader.simulate('click'); rendered.update(); snapshot(namesText(rendered)); }); - test('should open the index detail slideout when the index name is clicked', () => { + + test('should open the index detail slideout when the index name is clicked', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + expect(findTestSubject(rendered, 'indexDetailFlyout').length).toBe(0); + const indexNameLink = names(rendered).at(0); indexNameLink.simulate('click'); rendered.update(); expect(findTestSubject(rendered, 'indexDetailFlyout').length).toBe(1); }); - test('should show the right context menu options when one index is selected and open', () => { + + test('should show the right context menu options when one index is selected and open', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox'); checkboxes.at(0).simulate('change', { target: { checked: true } }); rendered.update(); @@ -253,8 +318,12 @@ describe('index table', () => { rendered.update(); snapshot(findTestSubject(rendered, 'indexTableContextMenuButton').map((span) => span.text())); }); - test('should show the right context menu options when one index is selected and closed', () => { + + test('should show the right context menu options when one index is selected and closed', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox'); checkboxes.at(1).simulate('change', { target: { checked: true } }); rendered.update(); @@ -263,8 +332,12 @@ describe('index table', () => { rendered.update(); snapshot(findTestSubject(rendered, 'indexTableContextMenuButton').map((span) => span.text())); }); - test('should show the right context menu options when one open and one closed index is selected', () => { + + test('should show the right context menu options when one open and one closed index is selected', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox'); checkboxes.at(0).simulate('change', { target: { checked: true } }); checkboxes.at(1).simulate('change', { target: { checked: true } }); @@ -274,8 +347,12 @@ describe('index table', () => { rendered.update(); snapshot(findTestSubject(rendered, 'indexTableContextMenuButton').map((span) => span.text())); }); - test('should show the right context menu options when more than one open index is selected', () => { + + test('should show the right context menu options when more than one open index is selected', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox'); checkboxes.at(0).simulate('change', { target: { checked: true } }); checkboxes.at(2).simulate('change', { target: { checked: true } }); @@ -285,8 +362,12 @@ describe('index table', () => { rendered.update(); snapshot(findTestSubject(rendered, 'indexTableContextMenuButton').map((span) => span.text())); }); - test('should show the right context menu options when more than one closed index is selected', () => { + + test('should show the right context menu options when more than one closed index is selected', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const checkboxes = findTestSubject(rendered, 'indexTableRowCheckbox'); checkboxes.at(1).simulate('change', { target: { checked: true } }); checkboxes.at(3).simulate('change', { target: { checked: true } }); @@ -296,37 +377,57 @@ describe('index table', () => { rendered.update(); snapshot(findTestSubject(rendered, 'indexTableContextMenuButton').map((span) => span.text())); }); - test('flush button works from context menu', (done) => { - testAction(8, done); + + test('flush button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + testAction(rendered, 8); }); - test('clear cache button works from context menu', (done) => { - testAction(7, done); + + test('clear cache button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + testAction(rendered, 7); }); - test('refresh button works from context menu', (done) => { - testAction(6, done); + + test('refresh button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + testAction(rendered, 6); }); - test('force merge button works from context menu', (done) => { + + test('force merge button works from context menu', async () => { const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const rowIndex = 0; openMenuAndClickButton(rendered, rowIndex, 5); snapshot(status(rendered, rowIndex)); expect(rendered.find('.euiModal').length).toBe(1); + let count = 0; store.subscribe(() => { - if (count > 1) { + if (count === 1) { snapshot(status(rendered, rowIndex)); expect(rendered.find('.euiModal').length).toBe(0); - done(); } count++; }); + const confirmButton = findTestSubject(rendered, 'confirmModalConfirmButton'); confirmButton.simulate('click'); snapshot(status(rendered, rowIndex)); }); - // Commenting the following 2 tests as it works in the browser (status changes to "closed" or "open") but the - // snapshot say the contrary. Need to be investigated. - test('close index button works from context menu', (done) => { + + test('close index button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const modifiedIndices = indices.map((index) => { return { ...index, @@ -339,32 +440,56 @@ describe('index table', () => { { 'Content-Type': 'application/json' }, JSON.stringify(modifiedIndices), ]); - testAction(4, done); + + testAction(rendered, 4); }); - test('open index button works from context menu', (done) => { + + test('open index button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + const modifiedIndices = indices.map((index) => { return { ...index, status: index.name === 'testy1' ? 'open' : index.status, }; }); + server.respondWith(`${API_BASE_PATH}/indices/reload`, [ 200, { 'Content-Type': 'application/json' }, JSON.stringify(modifiedIndices), ]); - testAction(3, done, 1); + + testAction(rendered, 3, 1); }); - test('show settings button works from context menu', () => { - testEditor(0); + + test('show settings button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + testEditor(rendered, 0); }); - test('show mappings button works from context menu', () => { - testEditor(1); + + test('show mappings button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + testEditor(rendered, 1); }); - test('show stats button works from context menu', () => { - testEditor(2); + + test('show stats button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + testEditor(rendered, 2); }); - test('edit index button works from context menu', () => { - testEditor(3); + + test('edit index button works from context menu', async () => { + const rendered = mountWithIntl(component); + await runAllPromises(); + rendered.update(); + testEditor(rendered, 3); }); }); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts index 8c8f7e5789925..dee15f2ae3a45 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts @@ -165,8 +165,10 @@ describe('<ComponentTemplateList />', () => { const { exists, find } = testBed; expect(exists('componentTemplatesLoadError')).toBe(true); + // The text here looks weird because the child elements' text values (title and description) + // are concatenated when we retrive the error element's text value. expect(find('componentTemplatesLoadError').text()).toContain( - 'Unable to load component templates. Try again.' + 'Error loading component templatesInternal server error' ); }); }); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx index 2bb240e6b6ae1..77668f7d55072 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx @@ -13,8 +13,13 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { ScopedHistory } from 'kibana/public'; import { EuiLink, EuiText, EuiSpacer } from '@elastic/eui'; -import { attemptToURIDecode } from '../../../../shared_imports'; -import { SectionLoading, ComponentTemplateDeserialized, GlobalFlyout } from '../shared_imports'; +import { + APP_WRAPPER_CLASS, + PageLoading, + PageError, + attemptToURIDecode, +} from '../../../../shared_imports'; +import { ComponentTemplateDeserialized, GlobalFlyout } from '../shared_imports'; import { UIM_COMPONENT_TEMPLATE_LIST_LOAD } from '../constants'; import { useComponentTemplatesContext } from '../component_templates_context'; import { @@ -24,7 +29,6 @@ import { } from '../component_template_details'; import { EmptyPrompt } from './empty_prompt'; import { ComponentTable } from './table'; -import { LoadError } from './error'; import { ComponentTemplatesDeleteModal } from './delete_modal'; interface Props { @@ -138,18 +142,20 @@ export const ComponentTemplateList: React.FunctionComponent<Props> = ({ } }, [componentTemplateName, removeContentFromGlobalFlyout]); - let content: React.ReactNode; - if (isLoading) { - content = ( - <SectionLoading data-test-subj="sectionLoading"> + return ( + <PageLoading data-test-subj="sectionLoading"> <FormattedMessage id="xpack.idxMgmt.home.componentTemplates.list.loadingMessage" defaultMessage="Loading component templates…" /> - </SectionLoading> + </PageLoading> ); - } else if (data?.length) { + } + + let content: React.ReactNode; + + if (data?.length) { content = ( <> <EuiText color="subdued"> @@ -183,11 +189,22 @@ export const ComponentTemplateList: React.FunctionComponent<Props> = ({ } else if (data && data.length === 0) { content = <EmptyPrompt history={history} />; } else if (error) { - content = <LoadError onReloadClick={resendRequest} />; + content = ( + <PageError + title={ + <FormattedMessage + id="xpack.idxMgmt.home.componentTemplates.list.loadingErrorMessage" + defaultMessage="Error loading component templates" + /> + } + error={error} + data-test-subj="componentTemplatesLoadError" + /> + ); } return ( - <div data-test-subj="componentTemplateList"> + <div className={APP_WRAPPER_CLASS} data-test-subj="componentTemplateList"> {content} {/* delete modal */} diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/error.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/error.tsx deleted file mode 100644 index 9fd0031fe8778..0000000000000 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/error.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { FunctionComponent } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiLink, EuiCallOut } from '@elastic/eui'; - -export interface Props { - onReloadClick: () => void; -} - -export const LoadError: FunctionComponent<Props> = ({ onReloadClick }) => { - return ( - <EuiCallOut - iconType="faceSad" - color="danger" - data-test-subj="componentTemplatesLoadError" - title={ - <FormattedMessage - id="xpack.idxMgmt.home.componentTemplates.list.loadErrorTitle" - defaultMessage="Unable to load component templates. {reloadLink}" - values={{ - reloadLink: ( - <EuiLink onClick={onReloadClick}> - <FormattedMessage - id="xpack.idxMgmt.home.componentTemplates.list.loadErrorReloadLinkLabel" - defaultMessage="Try again." - /> - </EuiLink> - ), - }} - /> - } - /> - ); -}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx index a0f6dc4b59fe7..eecb56768df9a 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/with_privileges.tsx @@ -9,10 +9,10 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { FunctionComponent } from 'react'; import { - SectionError, + PageLoading, + PageError, useAuthorizationContext, WithPrivileges, - SectionLoading, NotAuthorizedSection, } from '../shared_imports'; import { APP_CLUSTER_REQUIRED_PRIVILEGES } from '../constants'; @@ -26,7 +26,7 @@ export const ComponentTemplatesWithPrivileges: FunctionComponent = ({ if (apiError) { return ( - <SectionError + <PageError title={ <FormattedMessage id="xpack.idxMgmt.home.componentTemplates.checkingPrivilegesErrorMessage" @@ -45,12 +45,12 @@ export const ComponentTemplatesWithPrivileges: FunctionComponent = ({ {({ isLoading, hasPrivileges, privilegesMissing }) => { if (isLoading) { return ( - <SectionLoading> + <PageLoading> <FormattedMessage id="xpack.idxMgmt.home.componentTemplates.checkingPrivilegesDescription" defaultMessage="Checking privileges…" /> - </SectionLoading> + </PageLoading> ); } diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx index b87b043c924a6..d19c500c3622a 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx @@ -10,7 +10,7 @@ import { RouteComponentProps } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SectionLoading, attemptToURIDecode } from '../../shared_imports'; +import { PageLoading, attemptToURIDecode } from '../../shared_imports'; import { useComponentTemplatesContext } from '../../component_templates_context'; import { ComponentTemplateCreate } from '../component_template_create'; @@ -30,7 +30,8 @@ export const ComponentTemplateClone: FunctionComponent<RouteComponentProps<Param useEffect(() => { if (error && !isLoading) { - toasts.addError(error, { + // Toasts expects a generic Error object, which is typed as having a required name property. + toasts.addError({ ...error, name: '' } as Error, { title: i18n.translate('xpack.idxMgmt.componentTemplateClone.loadComponentTemplateTitle', { defaultMessage: `Error loading component template '{sourceComponentTemplateName}'.`, values: { sourceComponentTemplateName }, @@ -42,12 +43,12 @@ export const ComponentTemplateClone: FunctionComponent<RouteComponentProps<Param if (isLoading) { return ( - <SectionLoading> + <PageLoading> <FormattedMessage id="xpack.idxMgmt.componentTemplateEdit.loadingDescription" defaultMessage="Loading component template…" /> - </SectionLoading> + </PageLoading> ); } else { // We still show the create form (unpopulated) even if we were not able to load the diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx index 5163c75bdbadd..8fe2c193daa0c 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx @@ -8,7 +8,7 @@ import React, { useState, useEffect } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { EuiPageContentBody, EuiSpacer, EuiPageHeader } from '@elastic/eui'; import { ComponentTemplateDeserialized } from '../../shared_imports'; import { useComponentTemplatesContext } from '../../component_templates_context'; @@ -59,27 +59,28 @@ export const ComponentTemplateCreate: React.FunctionComponent<RouteComponentProp }, [breadcrumbs]); return ( - <EuiPageBody> - <EuiPageContent> - <EuiTitle size="l"> - <h1 data-test-subj="pageTitle"> + <EuiPageContentBody restrictWidth style={{ width: '100%' }}> + <EuiPageHeader + pageTitle={ + <span data-test-subj="pageTitle"> <FormattedMessage id="xpack.idxMgmt.createComponentTemplate.pageTitle" defaultMessage="Create component template" /> - </h1> - </EuiTitle> - - <EuiSpacer size="l" /> - - <ComponentTemplateForm - defaultValue={sourceComponentTemplate} - onSave={onSave} - isSaving={isSaving} - saveError={saveError} - clearSaveError={clearSaveError} - /> - </EuiPageContent> - </EuiPageBody> + </span> + } + bottomBorder + /> + + <EuiSpacer size="l" /> + + <ComponentTemplateForm + defaultValue={sourceComponentTemplate} + onSave={onSave} + isSaving={isSaving} + saveError={saveError} + clearSaveError={clearSaveError} + /> + </EuiPageContentBody> ); }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx index 809fac980069f..6ac831b5dacce 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx @@ -8,13 +8,15 @@ import React, { useState, useEffect } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@elastic/eui'; +import { EuiPageContentBody, EuiPageHeader, EuiSpacer } from '@elastic/eui'; import { useComponentTemplatesContext } from '../../component_templates_context'; import { ComponentTemplateDeserialized, - SectionLoading, + PageLoading, + PageError, attemptToURIDecode, + Error, } from '../../shared_imports'; import { ComponentTemplateForm } from '../component_template_form'; @@ -65,64 +67,57 @@ export const ComponentTemplateEdit: React.FunctionComponent<RouteComponentProps< setSaveError(null); }; - let content; - if (isLoading) { - content = ( - <SectionLoading> + return ( + <PageLoading> <FormattedMessage id="xpack.idxMgmt.componentTemplateEdit.loadingDescription" defaultMessage="Loading component template…" /> - </SectionLoading> - ); - } else if (error) { - content = ( - <> - <EuiCallOut - title={ - <FormattedMessage - id="xpack.idxMgmt.componentTemplateEdit.loadComponentTemplateError" - defaultMessage="Error loading component template" - /> - } - color="danger" - iconType="alert" - data-test-subj="loadComponentTemplateError" - > - <div>{error.message}</div> - </EuiCallOut> - <EuiSpacer size="m" /> - </> + </PageLoading> ); - } else if (componentTemplate) { - content = ( - <ComponentTemplateForm - defaultValue={componentTemplate} - onSave={onSave} - isSaving={isSaving} - saveError={saveError} - clearSaveError={clearSaveError} - isEditing={true} + } + + if (error) { + return ( + <PageError + title={ + <FormattedMessage + id="xpack.idxMgmt.componentTemplateEdit.loadComponentTemplateError" + defaultMessage="Error loading component template" + /> + } + error={error as Error} + data-test-subj="loadComponentTemplateError" /> ); } return ( - <EuiPageBody> - <EuiPageContent> - <EuiTitle size="l"> - <h1 data-test-subj="pageTitle"> + <EuiPageContentBody restrictWidth style={{ width: '100%' }}> + <EuiPageHeader + pageTitle={ + <span data-test-subj="pageTitle"> <FormattedMessage id="xpack.idxMgmt.componentTemplateEdit.editPageTitle" defaultMessage="Edit component template '{name}'" values={{ name: decodedName }} /> - </h1> - </EuiTitle> - <EuiSpacer size="l" /> - {content} - </EuiPageContent> - </EuiPageBody> + </span> + } + bottomBorder + /> + + <EuiSpacer size="l" /> + + <ComponentTemplateForm + defaultValue={componentTemplate!} + onSave={onSave} + isSaving={isSaving} + saveError={saveError} + clearSaveError={clearSaveError} + isEditing={true} + /> + </EuiPageContentBody> ); }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts index 75c68e71996b8..6bf6d204fd9a5 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts @@ -10,7 +10,6 @@ import { ComponentTemplateListItem, ComponentTemplateDeserialized, ComponentTemplateSerialized, - Error, } from '../shared_imports'; import { UIM_COMPONENT_TEMPLATE_DELETE_MANY, @@ -26,7 +25,7 @@ export const getApi = ( trackMetric: (type: UiCounterMetricType, eventName: string) => void ) => { function useLoadComponentTemplates() { - return useRequest<ComponentTemplateListItem[], Error>({ + return useRequest<ComponentTemplateListItem[]>({ path: `${apiBasePath}/component_templates`, method: 'get', }); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/request.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/request.ts index 64b2e6b47e5d9..a7056e27b5cad 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/request.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/request.ts @@ -14,6 +14,7 @@ import { SendRequestResponse, sendRequest as _sendRequest, useRequest as _useRequest, + Error, } from '../shared_imports'; export type UseRequestHook = <T = any, E = Error>( diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts index afc7aed874387..15528f5b4e8e5 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts @@ -12,10 +12,12 @@ export { SendRequestResponse, sendRequest, useRequest, - SectionLoading, WithPrivileges, AuthorizationProvider, SectionError, + SectionLoading, + PageLoading, + PageError, Error, useAuthorizationContext, NotAuthorizedSection, diff --git a/x-pack/plugins/index_management/public/application/components/index.ts b/x-pack/plugins/index_management/public/application/components/index.ts index f5c58e5b45ebd..eeba6e16b543c 100644 --- a/x-pack/plugins/index_management/public/application/components/index.ts +++ b/x-pack/plugins/index_management/public/application/components/index.ts @@ -6,9 +6,7 @@ */ export { SectionError, Error } from './section_error'; -export { SectionLoading } from './section_loading'; export { NoMatch } from './no_match'; -export { PageErrorForbidden } from './page_error'; export { TemplateDeleteModal } from './template_delete_modal'; export { TemplateForm } from './template_form'; export { DataHealth } from './data_health'; diff --git a/x-pack/plugins/index_management/public/application/components/page_error/page_error_forbidden.tsx b/x-pack/plugins/index_management/public/application/components/page_error/page_error_forbidden.tsx deleted file mode 100644 index e22b180881ed5..0000000000000 --- a/x-pack/plugins/index_management/public/application/components/page_error/page_error_forbidden.tsx +++ /dev/null @@ -1,30 +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 { EuiEmptyPrompt, EuiPageContent } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export function PageErrorForbidden() { - return ( - <EuiPageContent> - <EuiEmptyPrompt - iconType="securityApp" - iconColor={undefined} - title={ - <h1> - <FormattedMessage - id="xpack.idxMgmt.pageErrorForbidden.title" - defaultMessage="You do not have permissions to use Index Management" - /> - </h1> - } - /> - </EuiPageContent> - ); -} diff --git a/x-pack/plugins/index_management/public/application/components/section_loading.tsx b/x-pack/plugins/index_management/public/application/components/section_loading.tsx deleted file mode 100644 index 3c31744dee398..0000000000000 --- a/x-pack/plugins/index_management/public/application/components/section_loading.tsx +++ /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 React from 'react'; - -import { EuiEmptyPrompt, EuiLoadingSpinner, EuiText } from '@elastic/eui'; - -interface Props { - children: React.ReactNode; -} - -export const SectionLoading: React.FunctionComponent<Props> = ({ children }) => { - return ( - <EuiEmptyPrompt - title={<EuiLoadingSpinner size="xl" />} - body={<EuiText color="subdued">{children}</EuiText>} - data-test-subj="sectionLoading" - /> - ); -}; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx index 54160141827d0..4ccd77d275a94 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx @@ -8,7 +8,7 @@ import React, { useState, useCallback, useRef } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiSpacer, EuiButton } from '@elastic/eui'; +import { EuiSpacer, EuiButton, EuiPageHeader } from '@elastic/eui'; import { ScopedHistory } from 'kibana/public'; import { TemplateDeserialized } from '../../../../common'; @@ -292,7 +292,7 @@ export const TemplateForm = ({ return ( <> {/* Form header */} - {title} + <EuiPageHeader pageTitle={<span data-test-subj="pageTitle">{title}</span>} bottomBorder /> <EuiSpacer size="m" /> diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx index a9258c6a3b10b..3d5f56c08f8e1 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx @@ -24,8 +24,8 @@ import { EuiTitle, } from '@elastic/eui'; -import { reactRouterNavigate } from '../../../../../shared_imports'; -import { SectionLoading, SectionError, Error, DataHealth } from '../../../../components'; +import { SectionLoading, reactRouterNavigate } from '../../../../../shared_imports'; +import { SectionError, Error, DataHealth } from '../../../../components'; import { useLoadDataStream } from '../../../../services/api'; import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal'; import { humanizeTimeStamp } from '../humanize_time_stamp'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx index 131dc2662bc1c..7bd7c163837d8 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx @@ -16,18 +16,22 @@ import { EuiText, EuiIconTip, EuiSpacer, + EuiPageContent, EuiEmptyPrompt, EuiLink, } from '@elastic/eui'; import { ScopedHistory } from 'kibana/public'; import { + PageLoading, + PageError, + Error, reactRouterNavigate, extractQueryParams, attemptToURIDecode, + APP_WRAPPER_CLASS, } from '../../../../shared_imports'; import { useAppContext } from '../../../app_context'; -import { SectionError, SectionLoading, Error } from '../../../components'; import { useLoadDataStreams } from '../../../services/api'; import { documentationService } from '../../../services/documentation'; import { Section } from '../home'; @@ -166,16 +170,16 @@ export const DataStreamList: React.FunctionComponent<RouteComponentProps<MatchPa if (isLoading) { content = ( - <SectionLoading> + <PageLoading> <FormattedMessage id="xpack.idxMgmt.dataStreamList.loadingDataStreamsDescription" defaultMessage="Loading data streams…" /> - </SectionLoading> + </PageLoading> ); } else if (error) { content = ( - <SectionError + <PageError title={ <FormattedMessage id="xpack.idxMgmt.dataStreamList.loadingDataStreamsErrorMessage" @@ -252,10 +256,10 @@ export const DataStreamList: React.FunctionComponent<RouteComponentProps<MatchPa data-test-subj="emptyPrompt" /> ); - } else if (Array.isArray(dataStreams) && dataStreams.length > 0) { - activateHiddenFilter(isSelectedDataStreamHidden(dataStreams, decodedDataStreamName)); + } else { + activateHiddenFilter(isSelectedDataStreamHidden(dataStreams!, decodedDataStreamName)); content = ( - <> + <EuiPageContent hasShadow={false} paddingSize="none" data-test-subj="dataStreamList"> {renderHeader()} <EuiSpacer size="l" /> @@ -270,12 +274,12 @@ export const DataStreamList: React.FunctionComponent<RouteComponentProps<MatchPa history={history as ScopedHistory} includeStats={isIncludeStatsChecked} /> - </> + </EuiPageContent> ); } return ( - <div data-test-subj="dataStreamList"> + <div className={APP_WRAPPER_CLASS}> {content} {/* diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.tsx index ac46b5dbd256b..fc68ca33e9536 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_list.tsx @@ -8,12 +8,13 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; +import { APP_WRAPPER_CLASS } from '../../../../shared_imports'; import { DetailPanel } from './detail_panel'; import { IndexTable } from './index_table'; export const IndexList: React.FunctionComponent<RouteComponentProps> = ({ history }) => { return ( - <div className="im-snapshotTestSubject" data-test-subj="indicesList"> + <div className={`${APP_WRAPPER_CLASS} im-snapshotTestSubject`} data-test-subj="indicesList"> <IndexTable history={history} /> <DetailPanel /> </div> diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js index f488290692e7e..0a407927e3466 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js @@ -19,7 +19,7 @@ import { EuiCheckbox, EuiFlexGroup, EuiFlexItem, - EuiLoadingSpinner, + EuiPageContent, EuiScreenReaderOnly, EuiSpacer, EuiSearchBar, @@ -37,13 +37,18 @@ import { } from '@elastic/eui'; import { UIM_SHOW_DETAILS_CLICK } from '../../../../../../common/constants'; -import { reactRouterNavigate, attemptToURIDecode } from '../../../../../shared_imports'; +import { + PageLoading, + PageError, + reactRouterNavigate, + attemptToURIDecode, +} from '../../../../../shared_imports'; import { REFRESH_RATE_INDEX_LIST } from '../../../../constants'; import { getDataStreamDetailsLink } from '../../../../services/routing'; import { documentationService } from '../../../../services/documentation'; import { AppContextConsumer } from '../../../../app_context'; import { renderBadges } from '../../../../lib/render_badges'; -import { NoMatch, PageErrorForbidden, DataHealth } from '../../../../components'; +import { NoMatch, DataHealth } from '../../../../components'; import { IndexActionsContextMenu } from '../index_actions_context_menu'; const HEADERS = { @@ -332,42 +337,6 @@ export class IndexTable extends Component { }); } - renderError() { - const { indicesError } = this.props; - - const data = indicesError.body ? indicesError.body : indicesError; - - const { error: errorString, cause, message } = data; - - return ( - <Fragment> - <EuiCallOut - title={ - <FormattedMessage - id="xpack.idxMgmt.indexTable.serverErrorTitle" - defaultMessage="Error loading indices" - /> - } - color="danger" - iconType="alert" - > - <div>{message || errorString}</div> - {cause && ( - <Fragment> - <EuiSpacer size="m" /> - <ul> - {cause.map((message, i) => ( - <li key={i}>{message}</li> - ))} - </ul> - </Fragment> - )} - </EuiCallOut> - <EuiSpacer size="xl" /> - </Fragment> - ); - } - renderBanners(extensionsService) { const { allIndices = [], filterChanged } = this.props; return extensionsService.banners.map((bannerExtension, i) => { @@ -470,37 +439,71 @@ export class IndexTable extends Component { } = this.props; const { includeHiddenIndices } = this.readURLParams(); + const hasContent = !indicesLoading && !indicesError; - let emptyState; + if (!hasContent) { + const renderNoContent = () => { + if (indicesLoading) { + return ( + <PageLoading> + <FormattedMessage + id="xpack.idxMgmt.indexTable.loadingIndicesDescription" + defaultMessage="Loading indices…" + /> + </PageLoading> + ); + } + + if (indicesError) { + if (indicesError.status === 403) { + return ( + <PageError + title={ + <FormattedMessage + id="xpack.idxMgmt.pageErrorForbidden.title" + defaultMessage="You do not have permissions to use Index Management" + /> + } + /> + ); + } - if (indicesLoading) { - emptyState = ( - <EuiFlexGroup justifyContent="spaceAround"> - <EuiFlexItem grow={false}> - <EuiLoadingSpinner size="xl" /> - </EuiFlexItem> - </EuiFlexGroup> - ); - } + return ( + <PageError + title={ + <FormattedMessage + id="xpack.idxMgmt.indexTable.serverErrorTitle" + defaultMessage="Error loading indices" + /> + } + error={indicesError.body} + /> + ); + } + }; - if (!indicesLoading && !indicesError) { - emptyState = <NoMatch />; + return ( + <EuiPageContent + hasShadow={false} + paddingSize="none" + verticalPosition="center" + horizontalPosition="center" + > + {renderNoContent()} + </EuiPageContent> + ); } const { selectedIndicesMap } = this.state; const atLeastOneItemSelected = Object.keys(selectedIndicesMap).length > 0; - if (indicesError && indicesError.status === 403) { - return <PageErrorForbidden />; - } - return ( <AppContextConsumer> {({ services }) => { const { extensionsService } = services; return ( - <Fragment> + <EuiPageContent hasShadow={false} paddingSize="none"> <EuiFlexGroup alignItems="center"> <EuiFlexItem grow={true}> <EuiText color="subdued"> @@ -557,8 +560,6 @@ export class IndexTable extends Component { {this.renderBanners(extensionsService)} - {indicesError && this.renderError()} - <EuiFlexGroup gutterSize="l" alignItems="center"> {atLeastOneItemSelected ? ( <EuiFlexItem grow={false}> @@ -665,13 +666,13 @@ export class IndexTable extends Component { </EuiTable> </div> ) : ( - emptyState + <NoMatch /> )} <EuiSpacer size="m" /> {indices.length > 0 ? this.renderPager() : null} - </Fragment> + </EuiPageContent> ); }} </AppContextConsumer> diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx index e61362efb8c99..1a82cb3bfbdd1 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx @@ -33,8 +33,8 @@ import { UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, UIM_TEMPLATE_DETAIL_PANEL_PREVIEW_TAB, } from '../../../../../../common/constants'; -import { UseRequestResponse } from '../../../../../shared_imports'; -import { TemplateDeleteModal, SectionLoading, SectionError, Error } from '../../../../components'; +import { SectionLoading, UseRequestResponse } from '../../../../../shared_imports'; +import { TemplateDeleteModal, SectionError, Error } from '../../../../components'; import { useLoadIndexTemplate } from '../../../../services/api'; import { useServices } from '../../../../app_context'; import { TabAliases, TabMappings, TabSettings } from '../../../../components/shared'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx index b8b5a8e3c7d1a..57f18134be5d6 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -24,13 +24,14 @@ import { import { UIM_TEMPLATE_LIST_LOAD } from '../../../../../common/constants'; import { TemplateListItem } from '../../../../../common'; -import { attemptToURIDecode } from '../../../../shared_imports'; import { - SectionError, - SectionLoading, - Error, - LegacyIndexTemplatesDeprecation, -} from '../../../components'; + APP_WRAPPER_CLASS, + PageLoading, + PageError, + attemptToURIDecode, + reactRouterNavigate, +} from '../../../../shared_imports'; +import { LegacyIndexTemplatesDeprecation } from '../../../components'; import { useLoadIndexTemplates } from '../../../services/api'; import { documentationService } from '../../../services/documentation'; import { useServices } from '../../../app_context'; @@ -130,7 +131,8 @@ export const TemplateList: React.FunctionComponent<RouteComponentProps<MatchPara }; const renderHeader = () => ( - <EuiFlexGroup alignItems="center" gutterSize="s"> + // flex-grow: 0 is needed here because the parent element is a flex column and the header would otherwise expand. + <EuiFlexGroup alignItems="center" gutterSize="s" style={{ flexGrow: 0 }}> <EuiFlexItem grow={true}> <EuiText color="subdued"> <FormattedMessage @@ -218,77 +220,99 @@ export const TemplateList: React.FunctionComponent<RouteComponentProps<MatchPara </> ); - const renderContent = () => { - if (isLoading) { - return ( - <SectionLoading> + // Track this component mounted. + useEffect(() => { + uiMetricService.trackMetric(METRIC_TYPE.LOADED, UIM_TEMPLATE_LIST_LOAD); + }, [uiMetricService]); + + let content; + + if (isLoading) { + content = ( + <PageLoading> + <FormattedMessage + id="xpack.idxMgmt.indexTemplatesList.loadingIndexTemplatesDescription" + defaultMessage="Loading templates…" + /> + </PageLoading> + ); + } else if (error) { + content = ( + <PageError + title={ <FormattedMessage - id="xpack.idxMgmt.indexTemplatesList.loadingIndexTemplatesDescription" - defaultMessage="Loading templates…" + id="xpack.idxMgmt.indexTemplatesList.loadingIndexTemplatesErrorMessage" + defaultMessage="Error loading templates" /> - </SectionLoading> - ); - } else if (error) { - return ( - <SectionError - title={ + } + error={error} + /> + ); + } else if (!hasTemplates) { + content = ( + <EuiEmptyPrompt + iconType="managementApp" + title={ + <h1 data-test-subj="title"> <FormattedMessage - id="xpack.idxMgmt.indexTemplatesList.loadingIndexTemplatesErrorMessage" - defaultMessage="Error loading templates" + id="xpack.idxMgmt.indexTemplatesList.emptyPrompt.noIndexTemplatesTitle" + defaultMessage="Create your first index template" /> - } - error={error as Error} - /> - ); - } else if (!hasTemplates) { - return ( - <EuiEmptyPrompt - iconType="managementApp" - title={ - <h1 data-test-subj="title"> + </h1> + } + body={ + <> + <p> <FormattedMessage - id="xpack.idxMgmt.indexTemplatesList.emptyPrompt.noIndexTemplatesTitle" - defaultMessage="You don't have any templates yet" + id="xpack.idxMgmt.indexTemplatesList.emptyPrompt.noIndexTemplatesDescription" + defaultMessage="An index template automatically applies settings, mappings, and aliases to new indices." /> - </h1> - } - data-test-subj="emptyPrompt" - /> - ); - } else { - return ( - <Fragment> - {/* Header */} - {renderHeader()} + </p> + </> + } + actions={ + <EuiButton + {...reactRouterNavigate(history, '/create_template')} + fill + iconType="plusInCircle" + > + <FormattedMessage + id="xpack.idxMgmt.indexTemplatesList.emptyPrompt.createTemplatesButtonLabel" + defaultMessage="Create template" + /> + </EuiButton> + } + data-test-subj="emptyPrompt" + /> + ); + } else { + content = ( + <> + {/* Header */} + {renderHeader()} - {/* Composable index templates table */} - {renderTemplatesTable()} + {/* Composable index templates table */} + {renderTemplatesTable()} - {/* Legacy index templates table. We discourage their adoption if the user isn't already using them. */} - {filteredTemplates.legacyTemplates.length > 0 && renderLegacyTemplatesTable()} - </Fragment> - ); - } - }; + {/* Legacy index templates table. We discourage their adoption if the user isn't already using them. */} + {filteredTemplates.legacyTemplates.length > 0 && renderLegacyTemplatesTable()} - // Track component loaded - useEffect(() => { - uiMetricService.trackMetric(METRIC_TYPE.LOADED, UIM_TEMPLATE_LIST_LOAD); - }, [uiMetricService]); + {isTemplateDetailsVisible && ( + <TemplateDetails + template={selectedTemplate!} + onClose={closeTemplateDetails} + editTemplate={editTemplate} + cloneTemplate={cloneTemplate} + reload={reload} + /> + )} + </> + ); + } return ( - <div data-test-subj="templateList"> - {renderContent()} - - {isTemplateDetailsVisible && ( - <TemplateDetails - template={selectedTemplate!} - onClose={closeTemplateDetails} - editTemplate={editTemplate} - cloneTemplate={cloneTemplate} - reload={reload} - /> - )} + <div data-test-subj="templateList" className={APP_WRAPPER_CLASS}> + {content} </div> ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx index 36bff298e345b..32c84bc3b15f1 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx @@ -8,11 +8,12 @@ import React, { useEffect, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPageBody, EuiPageContent, EuiTitle } from '@elastic/eui'; +import { EuiPageContentBody } from '@elastic/eui'; import { ScopedHistory } from 'kibana/public'; +import { PageLoading, PageError, Error } from '../../../shared_imports'; import { TemplateDeserialized } from '../../../../common'; -import { TemplateForm, SectionLoading, SectionError, Error } from '../../components'; +import { TemplateForm } from '../../components'; import { breadcrumbService } from '../../services/breadcrumbs'; import { getTemplateDetailsLink } from '../../services/routing'; import { saveTemplate, useLoadIndexTemplate } from '../../services/api'; @@ -62,24 +63,22 @@ export const TemplateClone: React.FunctionComponent<RouteComponentProps<MatchPar setSaveError(null); }; - let content; - useEffect(() => { breadcrumbService.setBreadcrumbs('templateClone'); }, []); if (isLoading) { - content = ( - <SectionLoading> + return ( + <PageLoading> <FormattedMessage id="xpack.idxMgmt.templateCreate.loadingTemplateToCloneDescription" defaultMessage="Loading template to clone…" /> - </SectionLoading> + </PageLoading> ); } else if (templateToCloneError) { - content = ( - <SectionError + return ( + <PageError title={ <FormattedMessage id="xpack.idxMgmt.templateCreate.loadingTemplateToCloneErrorMessage" @@ -90,24 +89,22 @@ export const TemplateClone: React.FunctionComponent<RouteComponentProps<MatchPar data-test-subj="sectionError" /> ); - } else if (templateToClone) { - const templateData = { - ...templateToClone, - name: `${decodedTemplateName}-copy`, - } as TemplateDeserialized; + } + + const templateData = { + ...templateToClone, + name: `${decodedTemplateName}-copy`, + } as TemplateDeserialized; - content = ( + return ( + <EuiPageContentBody restrictWidth style={{ width: '100%' }}> <TemplateForm title={ - <EuiTitle size="l"> - <h1 data-test-subj="pageTitle"> - <FormattedMessage - id="xpack.idxMgmt.createTemplate.cloneTemplatePageTitle" - defaultMessage="Clone template '{name}'" - values={{ name: decodedTemplateName }} - /> - </h1> - </EuiTitle> + <FormattedMessage + id="xpack.idxMgmt.createTemplate.cloneTemplatePageTitle" + defaultMessage="Clone template '{name}'" + values={{ name: decodedTemplateName }} + /> } defaultValue={templateData} onSave={onSave} @@ -117,12 +114,6 @@ export const TemplateClone: React.FunctionComponent<RouteComponentProps<MatchPar isLegacy={isLegacy} history={history as ScopedHistory} /> - ); - } - - return ( - <EuiPageBody> - <EuiPageContent>{content}</EuiPageContent> - </EuiPageBody> + </EuiPageContentBody> ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx index 310807aeef38f..6eba112b11939 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx @@ -8,7 +8,7 @@ import React, { useEffect, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPageBody, EuiPageContent, EuiTitle } from '@elastic/eui'; +import { EuiPageContentBody } from '@elastic/eui'; import { useLocation } from 'react-router-dom'; import { parse } from 'query-string'; import { ScopedHistory } from 'kibana/public'; @@ -52,34 +52,28 @@ export const TemplateCreate: React.FunctionComponent<RouteComponentProps> = ({ h }, []); return ( - <EuiPageBody> - <EuiPageContent> - <TemplateForm - title={ - <EuiTitle size="l"> - <h1 data-test-subj="pageTitle"> - {isLegacy ? ( - <FormattedMessage - id="xpack.idxMgmt.createTemplate.createLegacyTemplatePageTitle" - defaultMessage="Create legacy template" - /> - ) : ( - <FormattedMessage - id="xpack.idxMgmt.createTemplate.createTemplatePageTitle" - defaultMessage="Create template" - /> - )} - </h1> - </EuiTitle> - } - onSave={onSave} - isSaving={isSaving} - saveError={saveError} - clearSaveError={clearSaveError} - isLegacy={isLegacy} - history={history as ScopedHistory} - /> - </EuiPageContent> - </EuiPageBody> + <EuiPageContentBody restrictWidth style={{ width: '100%' }}> + <TemplateForm + title={ + isLegacy ? ( + <FormattedMessage + id="xpack.idxMgmt.createTemplate.createLegacyTemplatePageTitle" + defaultMessage="Create legacy template" + /> + ) : ( + <FormattedMessage + id="xpack.idxMgmt.createTemplate.createTemplatePageTitle" + defaultMessage="Create template" + /> + ) + } + onSave={onSave} + isSaving={isSaving} + saveError={saveError} + clearSaveError={clearSaveError} + isLegacy={isLegacy} + history={history as ScopedHistory} + /> + </EuiPageContentBody> ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx index f4ffe97931a24..ff6909d4666f8 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx @@ -7,16 +7,17 @@ import React, { useEffect, useState, Fragment } from 'react'; import { RouteComponentProps } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@elastic/eui'; +import { EuiPageContentBody, EuiSpacer, EuiCallOut } from '@elastic/eui'; import { ScopedHistory } from 'kibana/public'; import { TemplateDeserialized } from '../../../../common'; -import { attemptToURIDecode } from '../../../shared_imports'; +import { PageError, PageLoading, attemptToURIDecode, Error } from '../../../shared_imports'; import { breadcrumbService } from '../../services/breadcrumbs'; import { useLoadIndexTemplate, updateTemplate } from '../../services/api'; import { getTemplateDetailsLink } from '../../services/routing'; -import { SectionLoading, SectionError, TemplateForm, Error } from '../../components'; +import { TemplateForm } from '../../components'; import { getIsLegacyFromQueryParams } from '../../lib/index_templates'; interface MatchParams { @@ -62,27 +63,27 @@ export const TemplateEdit: React.FunctionComponent<RouteComponentProps<MatchPara setSaveError(null); }; - let content; + let isSystemTemplate; if (isLoading) { - content = ( - <SectionLoading> + return ( + <PageLoading> <FormattedMessage id="xpack.idxMgmt.templateEdit.loadingIndexTemplateDescription" defaultMessage="Loading template…" /> - </SectionLoading> + </PageLoading> ); } else if (error) { - content = ( - <SectionError + return ( + <PageError title={ <FormattedMessage id="xpack.idxMgmt.templateEdit.loadingIndexTemplateErrorMessage" defaultMessage="Error loading template" /> } - error={error as Error} + error={error} data-test-subj="sectionError" /> ); @@ -91,80 +92,75 @@ export const TemplateEdit: React.FunctionComponent<RouteComponentProps<MatchPara name: templateName, _kbnMeta: { type }, } = template; - const isSystemTemplate = templateName && templateName.startsWith('.'); + + isSystemTemplate = templateName && templateName.startsWith('.'); if (type === 'cloudManaged') { - content = ( - <EuiCallOut + return ( + <PageError title={ <FormattedMessage id="xpack.idxMgmt.templateEdit.managedTemplateWarningTitle" defaultMessage="Editing a managed template is not permitted" /> } - color="danger" - iconType="alert" + error={ + { + message: i18n.translate( + 'xpack.idxMgmt.templateEdit.managedTemplateWarningDescription', + { + defaultMessage: 'Managed templates are critical for internal operations.', + } + ), + } as Error + } data-test-subj="systemTemplateEditCallout" - > - <FormattedMessage - id="xpack.idxMgmt.templateEdit.managedTemplateWarningDescription" - defaultMessage="Managed templates are critical for internal operations." - /> - </EuiCallOut> + /> ); - } else { - content = ( + } + } + + return ( + <EuiPageContentBody restrictWidth style={{ width: '100%' }}> + {isSystemTemplate && ( <Fragment> - {isSystemTemplate && ( - <Fragment> - <EuiCallOut - title={ - <FormattedMessage - id="xpack.idxMgmt.templateEdit.systemTemplateWarningTitle" - defaultMessage="Editing a system template can break Kibana" - /> - } - color="danger" - iconType="alert" - data-test-subj="systemTemplateEditCallout" - > - <FormattedMessage - id="xpack.idxMgmt.templateEdit.systemTemplateWarningDescription" - defaultMessage="System templates are critical for internal operations." - /> - </EuiCallOut> - <EuiSpacer size="l" /> - </Fragment> - )} - <TemplateForm + <EuiCallOut title={ - <EuiTitle size="l"> - <h1 data-test-subj="pageTitle"> - <FormattedMessage - id="xpack.idxMgmt.editTemplate.editTemplatePageTitle" - defaultMessage="Edit template '{name}'" - values={{ name: decodedTemplateName }} - /> - </h1> - </EuiTitle> + <FormattedMessage + id="xpack.idxMgmt.templateEdit.systemTemplateWarningTitle" + defaultMessage="Editing a system template can break Kibana" + /> } - defaultValue={template} - onSave={onSave} - isSaving={isSaving} - saveError={saveError} - clearSaveError={clearSaveError} - isEditing={true} - isLegacy={isLegacy} - history={history as ScopedHistory} - /> + color="danger" + iconType="alert" + data-test-subj="systemTemplateEditCallout" + > + <FormattedMessage + id="xpack.idxMgmt.templateEdit.systemTemplateWarningDescription" + defaultMessage="System templates are critical for internal operations." + /> + </EuiCallOut> + <EuiSpacer size="l" /> </Fragment> - ); - } - } + )} - return ( - <EuiPageBody> - <EuiPageContent>{content}</EuiPageContent> - </EuiPageBody> + <TemplateForm + title={ + <FormattedMessage + id="xpack.idxMgmt.editTemplate.editTemplatePageTitle" + defaultMessage="Edit template '{name}'" + values={{ name: decodedTemplateName }} + /> + } + defaultValue={template!} + onSave={onSave} + isSaving={isSaving} + saveError={saveError} + clearSaveError={clearSaveError} + isEditing={true} + isLegacy={isLegacy} + history={history as ScopedHistory} + /> + </EuiPageContentBody> ); }; diff --git a/x-pack/plugins/index_management/public/application/services/use_request.ts b/x-pack/plugins/index_management/public/application/services/use_request.ts index f4d3426439562..3b1d5cf22452d 100644 --- a/x-pack/plugins/index_management/public/application/services/use_request.ts +++ b/x-pack/plugins/index_management/public/application/services/use_request.ts @@ -11,6 +11,7 @@ import { UseRequestConfig, sendRequest as _sendRequest, useRequest as _useRequest, + Error, } from '../../shared_imports'; import { httpService } from './http'; @@ -19,6 +20,6 @@ export const sendRequest = (config: SendRequestConfig): Promise<SendRequestRespo return _sendRequest(httpService.httpClient, config); }; -export const useRequest = <T = any>(config: UseRequestConfig) => { - return _useRequest<T>(httpService.httpClient, config); +export const useRequest = <T = any, E = Error>(config: UseRequestConfig) => { + return _useRequest<T, E>(httpService.httpClient, config); }; diff --git a/x-pack/plugins/index_management/public/shared_imports.ts b/x-pack/plugins/index_management/public/shared_imports.ts index eddac8e4b8a86..fa27b22e502fa 100644 --- a/x-pack/plugins/index_management/public/shared_imports.ts +++ b/x-pack/plugins/index_management/public/shared_imports.ts @@ -5,6 +5,8 @@ * 2.0. */ +export { APP_WRAPPER_CLASS } from '../../../../src/core/public'; + export { SendRequestConfig, SendRequestResponse, @@ -16,6 +18,10 @@ export { extractQueryParams, GlobalFlyout, attemptToURIDecode, + PageLoading, + PageError, + Error, + SectionLoading, } from '../../../../src/plugins/es_ui_shared/public'; export { diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts index bd000186d91c4..231a2764d2710 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts @@ -17,28 +17,40 @@ import { getCloudManagedTemplatePrefix } from '../../../lib/get_managed_template import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; -export function registerGetAllRoute({ router }: RouteDependencies) { +export function registerGetAllRoute({ router, lib: { isEsError } }: RouteDependencies) { router.get({ path: addBasePath('/index_templates'), validate: false }, async (ctx, req, res) => { const { callAsCurrentUser } = ctx.dataManagement!.client; - const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(callAsCurrentUser); - const legacyTemplatesEs = await callAsCurrentUser('indices.getTemplate'); - const { index_templates: templatesEs } = await callAsCurrentUser( - 'dataManagement.getComposableIndexTemplates' - ); - - const legacyTemplates = deserializeLegacyTemplateList( - legacyTemplatesEs, - cloudManagedTemplatePrefix - ); - const templates = deserializeTemplateList(templatesEs, cloudManagedTemplatePrefix); - - const body = { - templates, - legacyTemplates, - }; - - return res.ok({ body }); + try { + const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(callAsCurrentUser); + + const legacyTemplatesEs = await callAsCurrentUser('indices.getTemplate'); + const { index_templates: templatesEs } = await callAsCurrentUser( + 'dataManagement.getComposableIndexTemplates' + ); + + const legacyTemplates = deserializeLegacyTemplateList( + legacyTemplatesEs, + cloudManagedTemplatePrefix + ); + const templates = deserializeTemplateList(templatesEs, cloudManagedTemplatePrefix); + + const body = { + templates, + legacyTemplates, + }; + + return res.ok({ body }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + // Case: default + throw error; + } }); } 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 0087d559a42e6..ff9b749911c84 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 @@ -112,7 +112,7 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re const { derivedIndexPattern, - isLoadingSourceConfiguration, + isLoading: isLoadingSource, loadSource, sourceConfiguration, } = useLogSource({ @@ -138,7 +138,7 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re hasMoreAfter, hasMoreBefore, isLoadingMore, - isReloading, + isReloading: isLoadingEntries, } = useLogStream({ sourceId, startTimestamp, @@ -198,7 +198,7 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re items={streamItems} scale="medium" wrap={true} - isReloading={isLoadingSourceConfiguration || isReloading} + isReloading={isLoadingSource || isLoadingEntries} isLoadingMore={isLoadingMore} hasMoreBeforeStart={hasMoreBefore} hasMoreAfterEnd={hasMoreAfter} diff --git a/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts b/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts index 021aa8f79fe59..4cdeb678c432b 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts @@ -5,8 +5,9 @@ * 2.0. */ +import { isEqual } from 'lodash'; import createContainer from 'constate'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import usePrevious from 'react-use/lib/usePrevious'; import useSetState from 'react-use/lib/useSetState'; import { esQuery } from '../../../../../../../src/plugins/data/public'; @@ -65,6 +66,12 @@ export function useLogStream({ const prevStartTimestamp = usePrevious(startTimestamp); const prevEndTimestamp = usePrevious(endTimestamp); + const cachedQuery = useRef(query); + + if (!isEqual(query, cachedQuery)) { + cachedQuery.current = query; + } + useEffect(() => { if (prevStartTimestamp && prevStartTimestamp > startTimestamp) { setState({ hasMoreBefore: true }); @@ -82,10 +89,10 @@ export function useLogStream({ sourceId, startTimestamp, endTimestamp, - query, + query: cachedQuery.current, columnOverrides: columns, }), - [columns, endTimestamp, query, sourceId, startTimestamp] + [columns, endTimestamp, cachedQuery, sourceId, startTimestamp] ); const { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomalies_table/anomalies_table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomalies_table/anomalies_table.tsx index 8c8a5ae56c3ba..98f3c82818dd2 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomalies_table/anomalies_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomalies_table/anomalies_table.tsx @@ -26,7 +26,7 @@ import { EuiText, OnTimeChangeProps, } from '@elastic/eui'; -import { FormattedDate, FormattedMessage } from 'react-intl'; +import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; import { datemathToEpochMillis } from '../../../../../../../utils/datemath'; import { SnapshotMetricType } from '../../../../../../../../common/inventory_models/types'; import { withTheme } from '../../../../../../../../../../../src/plugins/kibana_react/common'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/table.tsx index 1d465698dcb45..053e50ff87049 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/table.tsx @@ -18,7 +18,7 @@ import { import { i18n } from '@kbn/i18n'; import { first } from 'lodash'; import React, { useCallback, useMemo, useState } from 'react'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage } from '@kbn/i18n/react'; interface Row { name: string; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx index 15e8c323b1308..5f6ace2069410 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx @@ -156,6 +156,9 @@ type TestSubject = | 'separatorValueField.input' | 'quoteValueField.input' | 'emptyValueField.input' + | 'extractDeviceTypeSwitch.input' + | 'propertiesValueField' + | 'regexFileField.input' | 'valueFieldInput' | 'mediaTypeSelectorField' | 'ignoreEmptyField.input' diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/user_agent.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/user_agent.test.tsx new file mode 100644 index 0000000000000..fa1c24c9dfb39 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/user_agent.test.tsx @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { setup, SetupResult, getProcessorValue } from './processor.helpers'; + +// Default parameter values automatically added to the user agent processor when saved +const defaultUserAgentParameters = { + if: undefined, + regex_file: undefined, + properties: undefined, + description: undefined, + ignore_missing: undefined, + ignore_failure: undefined, + extract_device_type: undefined, +}; + +const USER_AGENT_TYPE = 'user_agent'; + +describe('Processor: User Agent', () => { + let onUpdate: jest.Mock; + let testBed: SetupResult; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(async () => { + onUpdate = jest.fn(); + + await act(async () => { + testBed = await setup({ + value: { + processors: [], + }, + onFlyoutOpen: jest.fn(), + onUpdate, + }); + }); + + testBed.component.update(); + + // Open flyout to add new processor + testBed.actions.addProcessor(); + // Add type (the other fields are not visible until a type is selected) + await testBed.actions.addProcessorType(USER_AGENT_TYPE); + }); + + test('prevents form submission if required fields are not provided', async () => { + const { + actions: { saveNewProcessor }, + form, + } = testBed; + + // Click submit button with only the processor type defined + await saveNewProcessor(); + + // Expect form error as "field" is required parameter + expect(form.getErrorsMessages()).toEqual(['A field value is required.']); + }); + + test('saves with just the default parameter value', async () => { + const { + actions: { saveNewProcessor }, + form, + } = testBed; + + // Add "field" value (required) + form.setInputValue('fieldNameField.input', 'field_1'); + // Save the field + await saveNewProcessor(); + + const processors = getProcessorValue(onUpdate, USER_AGENT_TYPE); + expect(processors[0][USER_AGENT_TYPE]).toEqual({ + ...defaultUserAgentParameters, + field: 'field_1', + }); + }); + + test('allows optional parameters to be set', async () => { + const { + actions: { saveNewProcessor }, + form, + find, + component, + } = testBed; + + // Add "field" value (required) + form.setInputValue('fieldNameField.input', 'field_1'); + + // Set optional parameteres + form.setInputValue('targetField.input', 'target_field'); + form.setInputValue('regexFileField.input', 'hello*'); + form.toggleEuiSwitch('ignoreMissingSwitch.input'); + form.toggleEuiSwitch('ignoreFailureSwitch.input'); + form.toggleEuiSwitch('extractDeviceTypeSwitch.input'); + await act(async () => { + find('propertiesValueField').simulate('change', [{ label: 'os' }]); + }); + component.update(); + + // Save the field with new changes + await saveNewProcessor(); + + const processors = getProcessorValue(onUpdate, USER_AGENT_TYPE); + expect(processors[0][USER_AGENT_TYPE]).toEqual({ + ...defaultUserAgentParameters, + field: 'field_1', + target_field: 'target_field', + properties: ['os'], + regex_file: 'hello*', + extract_device_type: true, + ignore_missing: true, + ignore_failure: true, + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/properties_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/properties_field.tsx index dd52375a19436..c8a50cf64484e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/properties_field.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/properties_field.tsx @@ -6,9 +6,9 @@ */ import React, { FunctionComponent } from 'react'; +import { EuiComboBoxProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { EuiComboBoxOptionOption } from '@elastic/eui'; import { ComboBoxField, FIELD_TYPES, UseField } from '../../../../../../../shared_imports'; import { FieldsConfig, to } from '../shared'; @@ -29,10 +29,10 @@ const fieldsConfig: FieldsConfig = { interface Props { helpText?: React.ReactNode; - propertyOptions?: EuiComboBoxOptionOption[]; + euiFieldProps?: EuiComboBoxProps<string>; } -export const PropertiesField: FunctionComponent<Props> = ({ helpText, propertyOptions }) => { +export const PropertiesField: FunctionComponent<Props> = ({ helpText, euiFieldProps }) => { return ( <UseField config={{ @@ -41,12 +41,7 @@ export const PropertiesField: FunctionComponent<Props> = ({ helpText, propertyOp }} component={ComboBoxField} path="fields.properties" - componentProps={{ - euiFieldProps: { - options: propertyOptions || [], - noSuggestions: !propertyOptions, - }, - }} + componentProps={{ euiFieldProps }} /> ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/user_agent.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/user_agent.tsx index 893e52bcc0073..2b5a68f799b7e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/user_agent.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/user_agent.tsx @@ -6,20 +6,20 @@ */ import React, { FunctionComponent } from 'react'; -import { EuiCode } from '@elastic/eui'; +import { EuiCode, EuiBetaBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { EuiComboBoxOptionOption } from '@elastic/eui'; -import { FIELD_TYPES, UseField, Field } from '../../../../../../shared_imports'; +import { FIELD_TYPES, ToggleField, UseField, Field } from '../../../../../../shared_imports'; -import { FieldsConfig, from } from './shared'; +import { FieldsConfig, from, to } from './shared'; import { IgnoreMissingField } from './common_fields/ignore_missing_field'; import { FieldNameField } from './common_fields/field_name_field'; import { TargetField } from './common_fields/target_field'; import { PropertiesField } from './common_fields/properties_field'; -const propertyOptions: EuiComboBoxOptionOption[] = [ +const propertyOptions: Array<EuiComboBoxOptionOption<string>> = [ { label: 'name' }, { label: 'os' }, { label: 'device' }, @@ -47,6 +47,18 @@ const fieldsConfig: FieldsConfig = { } ), }, + extract_device_type: { + type: FIELD_TYPES.TOGGLE, + defaultValue: false, + deserializer: to.booleanOrUndef, + serializer: from.undefinedIfValue(false), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.userAgentForm.extractDeviceTypeFieldHelpText', + { + defaultMessage: 'Extracts device type from the user agent string.', + } + ), + }, }; export const UserAgent: FunctionComponent = () => { @@ -59,7 +71,12 @@ export const UserAgent: FunctionComponent = () => { )} /> - <UseField config={fieldsConfig.regex_file} component={Field} path="fields.regex_file" /> + <UseField + config={fieldsConfig.regex_file} + component={Field} + path="fields.regex_file" + data-test-subj="regexFileField" + /> <TargetField helpText={ @@ -78,7 +95,40 @@ export const UserAgent: FunctionComponent = () => { 'xpack.ingestPipelines.pipelineEditor.userAgentForm.propertiesFieldHelpText', { defaultMessage: 'Properties added to the target field.' } )} - propertyOptions={propertyOptions} + euiFieldProps={{ + options: propertyOptions, + noSuggestions: false, + 'data-test-subj': 'propertiesValueField', + }} + /> + + <UseField + config={fieldsConfig.extract_device_type} + component={ToggleField} + path="fields.extract_device_type" + data-test-subj="extractDeviceTypeSwitch" + euiFieldProps={{ + label: ( + <EuiFlexGroup gutterSize="xs"> + <EuiFlexItem grow={false}> + <FormattedMessage + id="xpack.ingestPipelines.pipelineEditor.userAgentForm.extractDeviceNameFieldText" + defaultMessage="Extract device type" + /> + </EuiFlexItem> + <EuiFlexItem> + <EuiBetaBadge + size="s" + label="Beta" + tooltipContent={i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.userAgentForm.extractDeviceNameTooltipText', + { defaultMessage: 'This functionality is in beta and is subject to change.' } + )} + /> + </EuiFlexItem> + </EuiFlexGroup> + ), + }} /> <IgnoreMissingField /> diff --git a/x-pack/plugins/ingest_pipelines/public/index.ts b/x-pack/plugins/ingest_pipelines/public/index.ts index 8948a3e8d56be..d120f60ef8a2d 100644 --- a/x-pack/plugins/ingest_pipelines/public/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/index.ts @@ -10,10 +10,3 @@ import { IngestPipelinesPlugin } from './plugin'; export function plugin() { return new IngestPipelinesPlugin(); } - -export { - INGEST_PIPELINES_APP_ULR_GENERATOR, - IngestPipelinesUrlGenerator, - IngestPipelinesUrlGeneratorState, - INGEST_PIPELINES_PAGES, -} from './url_generator'; diff --git a/x-pack/plugins/ingest_pipelines/public/locator.test.ts b/x-pack/plugins/ingest_pipelines/public/locator.test.ts new file mode 100644 index 0000000000000..0b1246b2bed59 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/locator.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 { ManagementAppLocatorDefinition } from 'src/plugins/management/common/locator'; +import { IngestPipelinesLocatorDefinition, INGEST_PIPELINES_PAGES } from './locator'; + +describe('Ingest pipeline locator', () => { + const setup = () => { + const managementDefinition = new ManagementAppLocatorDefinition(); + const definition = new IngestPipelinesLocatorDefinition({ + managementAppLocator: { + getLocation: (params) => managementDefinition.getLocation(params), + getUrl: async () => { + throw new Error('not implemented'); + }, + navigate: async () => { + throw new Error('not implemented'); + }, + useUrl: () => '', + }, + }); + return { definition }; + }; + + describe('Pipelines List', () => { + it('generates relative url for list without pipelineId', async () => { + const { definition } = setup(); + const location = await definition.getLocation({ + page: INGEST_PIPELINES_PAGES.LIST, + }); + + expect(location).toMatchObject({ + app: 'management', + path: '/ingest/ingest_pipelines', + }); + }); + + it('generates relative url for list with a pipelineId', async () => { + const { definition } = setup(); + const location = await definition.getLocation({ + page: INGEST_PIPELINES_PAGES.LIST, + pipelineId: 'pipeline_name', + }); + + expect(location).toMatchObject({ + app: 'management', + path: '/ingest/ingest_pipelines/?pipeline=pipeline_name', + }); + }); + }); + + describe('Pipeline Edit', () => { + it('generates relative url for pipeline edit', async () => { + const { definition } = setup(); + const location = await definition.getLocation({ + page: INGEST_PIPELINES_PAGES.EDIT, + pipelineId: 'pipeline_name', + }); + + expect(location).toMatchObject({ + app: 'management', + path: '/ingest/ingest_pipelines/edit/pipeline_name', + }); + }); + }); + + describe('Pipeline Clone', () => { + it('generates relative url for pipeline clone', async () => { + const { definition } = setup(); + const location = await definition.getLocation({ + page: INGEST_PIPELINES_PAGES.CLONE, + pipelineId: 'pipeline_name', + }); + + expect(location).toMatchObject({ + app: 'management', + path: '/ingest/ingest_pipelines/create/pipeline_name', + }); + }); + }); + + describe('Pipeline Create', () => { + it('generates relative url for pipeline create', async () => { + const { definition } = setup(); + const location = await definition.getLocation({ + page: INGEST_PIPELINES_PAGES.CREATE, + pipelineId: 'pipeline_name', + }); + + expect(location).toMatchObject({ + app: 'management', + path: '/ingest/ingest_pipelines/create', + }); + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/locator.ts b/x-pack/plugins/ingest_pipelines/public/locator.ts new file mode 100644 index 0000000000000..d819011f14f47 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/locator.ts @@ -0,0 +1,102 @@ +/* + * 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 { SerializableState } from 'src/plugins/kibana_utils/common'; +import { ManagementAppLocator } from 'src/plugins/management/common'; +import { + LocatorPublic, + LocatorDefinition, + KibanaLocation, +} from '../../../../src/plugins/share/public'; +import { + getClonePath, + getCreatePath, + getEditPath, + getListPath, +} from './application/services/navigation'; +import { PLUGIN_ID } from '../common/constants'; + +export enum INGEST_PIPELINES_PAGES { + LIST = 'pipelines_list', + EDIT = 'pipeline_edit', + CREATE = 'pipeline_create', + CLONE = 'pipeline_clone', +} + +interface IngestPipelinesBaseParams extends SerializableState { + pipelineId: string; +} +export interface IngestPipelinesListParams extends Partial<IngestPipelinesBaseParams> { + page: INGEST_PIPELINES_PAGES.LIST; +} + +export interface IngestPipelinesEditParams extends IngestPipelinesBaseParams { + page: INGEST_PIPELINES_PAGES.EDIT; +} + +export interface IngestPipelinesCloneParams extends IngestPipelinesBaseParams { + page: INGEST_PIPELINES_PAGES.CLONE; +} + +export interface IngestPipelinesCreateParams extends IngestPipelinesBaseParams { + page: INGEST_PIPELINES_PAGES.CREATE; +} + +export type IngestPipelinesParams = + | IngestPipelinesListParams + | IngestPipelinesEditParams + | IngestPipelinesCloneParams + | IngestPipelinesCreateParams; + +export type IngestPipelinesLocator = LocatorPublic<void>; + +export const INGEST_PIPELINES_APP_LOCATOR = 'INGEST_PIPELINES_APP_LOCATOR'; + +export interface IngestPipelinesLocatorDependencies { + managementAppLocator: ManagementAppLocator; +} + +export class IngestPipelinesLocatorDefinition implements LocatorDefinition<IngestPipelinesParams> { + public readonly id = INGEST_PIPELINES_APP_LOCATOR; + + constructor(protected readonly deps: IngestPipelinesLocatorDependencies) {} + + public readonly getLocation = async (params: IngestPipelinesParams): Promise<KibanaLocation> => { + const location = await this.deps.managementAppLocator.getLocation({ + sectionId: 'ingest', + appId: PLUGIN_ID, + }); + + let path: string = ''; + + switch (params.page) { + case INGEST_PIPELINES_PAGES.EDIT: + path = getEditPath({ + pipelineName: params.pipelineId, + }); + break; + case INGEST_PIPELINES_PAGES.CREATE: + path = getCreatePath(); + break; + case INGEST_PIPELINES_PAGES.LIST: + path = getListPath({ + inspectedPipelineName: params.pipelineId, + }); + break; + case INGEST_PIPELINES_PAGES.CLONE: + path = getClonePath({ + clonedPipelineName: params.pipelineId, + }); + break; + } + + return { + ...location, + path: path === '/' ? location.path : location.path + path, + }; + }; +} diff --git a/x-pack/plugins/ingest_pipelines/public/plugin.ts b/x-pack/plugins/ingest_pipelines/public/plugin.ts index 4a138a12d6819..b4eb33162a1f4 100644 --- a/x-pack/plugins/ingest_pipelines/public/plugin.ts +++ b/x-pack/plugins/ingest_pipelines/public/plugin.ts @@ -11,7 +11,7 @@ import { CoreSetup, Plugin } from 'src/core/public'; import { PLUGIN_ID } from '../common/constants'; import { uiMetricService, apiService } from './application/services'; import { SetupDependencies, StartDependencies } from './types'; -import { registerUrlGenerator } from './url_generator'; +import { IngestPipelinesLocatorDefinition } from './locator'; export class IngestPipelinesPlugin implements Plugin<void, void, SetupDependencies, StartDependencies> { @@ -50,7 +50,11 @@ export class IngestPipelinesPlugin }, }); - registerUrlGenerator(coreSetup, management, share); + share.url.locators.create( + new IngestPipelinesLocatorDefinition({ + managementAppLocator: management.locator, + }) + ); } public start() {} diff --git a/x-pack/plugins/ingest_pipelines/public/url_generator.test.ts b/x-pack/plugins/ingest_pipelines/public/url_generator.test.ts deleted file mode 100644 index dc45f9bc39088..0000000000000 --- a/x-pack/plugins/ingest_pipelines/public/url_generator.test.ts +++ /dev/null @@ -1,108 +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 { IngestPipelinesUrlGenerator, INGEST_PIPELINES_PAGES } from './url_generator'; - -describe('IngestPipelinesUrlGenerator', () => { - const getAppBasePath = (absolute: boolean = false) => { - if (absolute) { - return Promise.resolve('http://localhost/app/test_app'); - } - return Promise.resolve('/app/test_app'); - }; - const urlGenerator = new IngestPipelinesUrlGenerator(getAppBasePath); - - describe('Pipelines List', () => { - it('generates relative url for list without pipelineId', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.LIST, - }); - expect(url).toBe('/app/test_app/'); - }); - - it('generates absolute url for list without pipelineId', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.LIST, - absolute: true, - }); - expect(url).toBe('http://localhost/app/test_app/'); - }); - it('generates relative url for list with a pipelineId', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.LIST, - pipelineId: 'pipeline_name', - }); - expect(url).toBe('/app/test_app/?pipeline=pipeline_name'); - }); - - it('generates absolute url for list with a pipelineId', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.LIST, - pipelineId: 'pipeline_name', - absolute: true, - }); - expect(url).toBe('http://localhost/app/test_app/?pipeline=pipeline_name'); - }); - }); - - describe('Pipeline Edit', () => { - it('generates relative url for pipeline edit', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.EDIT, - pipelineId: 'pipeline_name', - }); - expect(url).toBe('/app/test_app/edit/pipeline_name'); - }); - - it('generates absolute url for pipeline edit', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.EDIT, - pipelineId: 'pipeline_name', - absolute: true, - }); - expect(url).toBe('http://localhost/app/test_app/edit/pipeline_name'); - }); - }); - - describe('Pipeline Clone', () => { - it('generates relative url for pipeline clone', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.CLONE, - pipelineId: 'pipeline_name', - }); - expect(url).toBe('/app/test_app/create/pipeline_name'); - }); - - it('generates absolute url for pipeline clone', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.CLONE, - pipelineId: 'pipeline_name', - absolute: true, - }); - expect(url).toBe('http://localhost/app/test_app/create/pipeline_name'); - }); - }); - - describe('Pipeline Create', () => { - it('generates relative url for pipeline create', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.CREATE, - pipelineId: 'pipeline_name', - }); - expect(url).toBe('/app/test_app/create'); - }); - - it('generates absolute url for pipeline create', async () => { - const url = await urlGenerator.createUrl({ - page: INGEST_PIPELINES_PAGES.CREATE, - pipelineId: 'pipeline_name', - absolute: true, - }); - expect(url).toBe('http://localhost/app/test_app/create'); - }); - }); -}); diff --git a/x-pack/plugins/ingest_pipelines/public/url_generator.ts b/x-pack/plugins/ingest_pipelines/public/url_generator.ts deleted file mode 100644 index d9a77addcd5fd..0000000000000 --- a/x-pack/plugins/ingest_pipelines/public/url_generator.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CoreSetup } from 'src/core/public'; -import { MANAGEMENT_APP_ID } from '../../../../src/plugins/management/public'; -import { UrlGeneratorsDefinition } from '../../../../src/plugins/share/public'; -import { - getClonePath, - getCreatePath, - getEditPath, - getListPath, -} from './application/services/navigation'; -import { SetupDependencies } from './types'; -import { PLUGIN_ID } from '../common/constants'; - -export const INGEST_PIPELINES_APP_ULR_GENERATOR = 'INGEST_PIPELINES_APP_URL_GENERATOR'; - -export enum INGEST_PIPELINES_PAGES { - LIST = 'pipelines_list', - EDIT = 'pipeline_edit', - CREATE = 'pipeline_create', - CLONE = 'pipeline_clone', -} - -interface UrlGeneratorState { - pipelineId: string; - absolute?: boolean; -} -export interface PipelinesListUrlGeneratorState extends Partial<UrlGeneratorState> { - page: INGEST_PIPELINES_PAGES.LIST; -} - -export interface PipelineEditUrlGeneratorState extends UrlGeneratorState { - page: INGEST_PIPELINES_PAGES.EDIT; -} - -export interface PipelineCloneUrlGeneratorState extends UrlGeneratorState { - page: INGEST_PIPELINES_PAGES.CLONE; -} - -export interface PipelineCreateUrlGeneratorState extends UrlGeneratorState { - page: INGEST_PIPELINES_PAGES.CREATE; -} - -export type IngestPipelinesUrlGeneratorState = - | PipelinesListUrlGeneratorState - | PipelineEditUrlGeneratorState - | PipelineCloneUrlGeneratorState - | PipelineCreateUrlGeneratorState; - -export class IngestPipelinesUrlGenerator - implements UrlGeneratorsDefinition<typeof INGEST_PIPELINES_APP_ULR_GENERATOR> { - constructor(private readonly getAppBasePath: (absolute: boolean) => Promise<string>) {} - - public readonly id = INGEST_PIPELINES_APP_ULR_GENERATOR; - - public readonly createUrl = async (state: IngestPipelinesUrlGeneratorState): Promise<string> => { - switch (state.page) { - case INGEST_PIPELINES_PAGES.EDIT: { - return `${await this.getAppBasePath(!!state.absolute)}${getEditPath({ - pipelineName: state.pipelineId, - })}`; - } - case INGEST_PIPELINES_PAGES.CREATE: { - return `${await this.getAppBasePath(!!state.absolute)}${getCreatePath()}`; - } - case INGEST_PIPELINES_PAGES.LIST: { - return `${await this.getAppBasePath(!!state.absolute)}${getListPath({ - inspectedPipelineName: state.pipelineId, - })}`; - } - case INGEST_PIPELINES_PAGES.CLONE: { - return `${await this.getAppBasePath(!!state.absolute)}${getClonePath({ - clonedPipelineName: state.pipelineId, - })}`; - } - } - }; -} - -export const registerUrlGenerator = ( - coreSetup: CoreSetup, - management: SetupDependencies['management'], - share: SetupDependencies['share'] -) => { - const getAppBasePath = async (absolute = false) => { - const [coreStart] = await coreSetup.getStartServices(); - return coreStart.application.getUrlForApp(MANAGEMENT_APP_ID, { - path: management.sections.section.ingest.getApp(PLUGIN_ID)!.basePath, - absolute: !!absolute, - }); - }; - - share.urlGenerators.registerUrlGenerator(new IngestPipelinesUrlGenerator(getAppBasePath)); -}; diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts index 0d44ae3aa6dec..8615ed6536316 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts @@ -15,8 +15,6 @@ import type { LensToggleAction, } from './types'; import { ColumnConfig } from './table_basic'; - -import { desanitizeFilterContext } from '../../utils'; import { getOriginalId } from '../transpose_helpers'; export const createGridResizeHandler = ( @@ -92,7 +90,7 @@ export const createGridFilterHandler = ( timeFieldName, }; - onClickValue(desanitizeFilterContext(data)); + onClickValue(data); }; export const createTransposeColumnFilterHandler = ( @@ -125,7 +123,7 @@ export const createTransposeColumnFilterHandler = ( timeFieldName, }; - onClickValue(desanitizeFilterContext(data)); + onClickValue(data); }; export const createGridSortingConfig = ( diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx index 972ef99d7d7f6..4a34bd030429e 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx @@ -6,7 +6,8 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { ReactWrapper, shallow } from 'enzyme'; +import { act } from 'react-dom/test-utils'; import { mountWithIntl } from '@kbn/test/jest'; import { EuiDataGrid } from '@elastic/eui'; import { IAggType, IFieldFormat } from 'src/plugins/data/public'; @@ -83,6 +84,13 @@ function copyData(data: LensMultiTable): LensMultiTable { return JSON.parse(JSON.stringify(data)); } +async function waitForWrapperUpdate(wrapper: ReactWrapper) { + await act(async () => { + await new Promise((r) => setTimeout(r, 0)); + }); + wrapper.update(); +} + describe('DatatableComponent', () => { let onDispatchEvent: jest.Mock; @@ -149,7 +157,7 @@ describe('DatatableComponent', () => { ).toMatchSnapshot(); }); - test('it invokes executeTriggerActions with correct context on click on top value', () => { + test('it invokes executeTriggerActions with correct context on click on top value', async () => { const { args, data } = sampleArgs(); const wrapper = mountWithIntl( @@ -173,6 +181,8 @@ describe('DatatableComponent', () => { wrapper.find('[data-test-subj="dataGridRowCell"]').first().simulate('focus'); + await waitForWrapperUpdate(wrapper); + wrapper.find('[data-test-subj="lensDatatableFilterOut"]').first().simulate('click'); expect(onDispatchEvent).toHaveBeenCalledWith({ @@ -192,7 +202,7 @@ describe('DatatableComponent', () => { }); }); - test('it invokes executeTriggerActions with correct context on click on timefield', () => { + test('it invokes executeTriggerActions with correct context on click on timefield', async () => { const { args, data } = sampleArgs(); const wrapper = mountWithIntl( @@ -216,6 +226,8 @@ describe('DatatableComponent', () => { wrapper.find('[data-test-subj="dataGridRowCell"]').at(1).simulate('focus'); + await waitForWrapperUpdate(wrapper); + wrapper.find('[data-test-subj="lensDatatableFilterFor"]').first().simulate('click'); expect(onDispatchEvent).toHaveBeenCalledWith({ @@ -235,7 +247,7 @@ describe('DatatableComponent', () => { }); }); - test('it invokes executeTriggerActions with correct context on click on timefield from range', () => { + test('it invokes executeTriggerActions with correct context on click on timefield from range', async () => { const data: LensMultiTable = { type: 'lens_multitable', tables: { @@ -298,6 +310,8 @@ describe('DatatableComponent', () => { wrapper.find('[data-test-subj="dataGridRowCell"]').at(0).simulate('focus'); + await waitForWrapperUpdate(wrapper); + wrapper.find('[data-test-subj="lensDatatableFilterFor"]').first().simulate('click'); expect(onDispatchEvent).toHaveBeenCalledWith({ diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index 52488cb32ae83..0e2ba5ce8ad59 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -1370,6 +1370,57 @@ describe('editor_frame', () => { }) ); }); + + it('should avoid completely to compute suggestion when in fullscreen mode', async () => { + const props = { + ...getDefaultProps(), + initialContext: { + indexPatternId: '1', + fieldName: 'test', + }, + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + testDatasource2: mockDatasource2, + }, + + ExpressionRenderer: expressionRendererMock, + }; + + const { instance: el } = await mountWithProvider( + <EditorFrame {...props} />, + props.plugins.data + ); + instance = el; + + expect( + instance.find(FrameLayout).prop('suggestionsPanel') as ReactElement + ).not.toBeUndefined(); + + await act(async () => { + (instance.find(FrameLayout).prop('dataPanel') as ReactElement)!.props.dispatch({ + type: 'TOGGLE_FULLSCREEN', + }); + }); + + instance.update(); + + expect(instance.find(FrameLayout).prop('suggestionsPanel') as ReactElement).toBe(false); + + await act(async () => { + (instance.find(FrameLayout).prop('dataPanel') as ReactElement)!.props.dispatch({ + type: 'TOGGLE_FULLSCREEN', + }); + }); + + instance.update(); + + expect( + instance.find(FrameLayout).prop('suggestionsPanel') as ReactElement + ).not.toBeUndefined(); + }); }); describe('passing state back to the caller', () => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index cc65bb126d2d9..bd96682f427fa 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -452,7 +452,8 @@ export function EditorFrame(props: EditorFrameProps) { ) } suggestionsPanel={ - allLoaded && ( + allLoaded && + !state.isFullscreenDatasource && ( <SuggestionPanel frame={framePublicAPI} activeDatasourceId={state.activeDatasourceId} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss index 282e69cd7636c..2d86b37669ed0 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.scss @@ -88,9 +88,8 @@ a tilemap in an iframe: https://github.com/elastic/kibana/issues/16457 */ position: relative; } -.lnsFrameLayout-isFullscreen .lnsFrameLayout__sidebar--left, -.lnsFrameLayout-isFullscreen .lnsFrameLayout__suggestionPanel { - // Hide the datapanel and suggestions in fullscreen mode. Using display: none does trigger +.lnsFrameLayout-isFullscreen .lnsFrameLayout__sidebar--left { + // Hide the datapanel in fullscreen mode. Using display: none does trigger // a rerender when the container becomes visible again, maybe pushing offscreen is better display: none; } diff --git a/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx b/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx index 3048f3b3db580..8214d5ba129d4 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx +++ b/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx @@ -21,7 +21,6 @@ import { VisualizationContainer } from '../visualization_container'; import { HeatmapRenderProps } from './types'; import './index.scss'; import { LensBrushEvent, LensFilterEvent } from '../types'; -import { desanitizeFilterContext } from '../utils'; import { EmptyPlaceholder } from '../shared_components'; import { LensIconChartHeatmap } from '../assets/chart_heatmap'; @@ -117,7 +116,7 @@ export const HeatmapComponent: FC<HeatmapRenderProps> = ({ })), timeFieldName, }; - onClickValue(desanitizeFilterContext(context)); + onClickValue(context); }) as ElementClickListener; const onBrushEnd = (e: HeatmapBrushEvent) => { @@ -164,7 +163,7 @@ export const HeatmapComponent: FC<HeatmapRenderProps> = ({ })), timeFieldName, }; - onClickValue(desanitizeFilterContext(context)); + onClickValue(context); } }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index b35986c42054d..05100567c1b03 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -117,21 +117,14 @@ export function DimensionEditor(props: DimensionEditorProps) { const setStateWrapper = ( setter: IndexPatternLayer | ((prevLayer: IndexPatternLayer) => IndexPatternLayer) ) => { - const prevOperationType = - operationDefinitionMap[state.layers[layerId].columns[columnId]?.operationType]?.input; - const hypotheticalLayer = typeof setter === 'function' ? setter(state.layers[layerId]) : setter; - const hasIncompleteColumns = Boolean(hypotheticalLayer.incompleteColumns?.[columnId]); setState( (prevState) => { const layer = typeof setter === 'function' ? setter(prevState.layers[layerId]) : setter; return mergeLayer({ state: prevState, layerId, newLayer: layer }); }, { - isDimensionComplete: - prevOperationType === 'fullReference' - ? !hasIncompleteColumns - : Boolean(hypotheticalLayer.columns[columnId]), + isDimensionComplete: Boolean(hypotheticalLayer.columns[columnId]), } ); }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index afcecdf5be9b8..d757d8573f25a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -908,20 +908,21 @@ describe('IndexPatternDimensionEditorPanel', () => { }); }); - it('should clean up when transitioning from incomplete reference-based operations to field operation', () => { + it('should keep current state and write incomplete column when transitioning from incomplete reference-based operations to field operation', () => { + const baseState = getStateWithColumns({ + ...defaultProps.state.layers.first.columns, + col2: { + label: 'Counter rate', + dataType: 'number', + isBucketed: false, + operationType: 'counter_rate', + references: ['ref'], + }, + }); wrapper = mount( <IndexPatternDimensionEditorComponent {...defaultProps} - state={getStateWithColumns({ - ...defaultProps.state.layers.first.columns, - col2: { - label: 'Counter rate', - dataType: 'number', - isBucketed: false, - operationType: 'counter_rate', - references: ['ref'], - }, - })} + state={baseState} columnId={'col2'} /> ); @@ -932,15 +933,12 @@ describe('IndexPatternDimensionEditorPanel', () => { .simulate('click'); // Now check that the dimension gets cleaned up on state update - expect(setState.mock.calls[0]).toEqual([ - expect.any(Function), - { isDimensionComplete: false }, - ]); + expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](state)).toEqual({ - ...state, + ...baseState, layers: { first: { - ...state.layers.first, + ...baseState.layers.first, incompleteColumns: { col2: { operationType: 'average' }, }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx index ba9525ac53fc5..c2415c9c9a75a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_shift.tsx @@ -157,7 +157,7 @@ export function TimeShift({ isClearable={false} data-test-subj="indexPattern-dimension-time-shift" placeholder={i18n.translate('xpack.lens.indexPattern.timeShiftPlaceholder', { - defaultMessage: 'Time shift (e.g. 1d)', + defaultMessage: 'Type custom values (e.g. 8w)', })} options={timeShiftOptions.filter(({ value }) => { const parsedValue = parseTimeShift(value); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts index 03b9d6c07709c..87116f71919b5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts @@ -7,11 +7,12 @@ import { i18n } from '@kbn/i18n'; import type { ExpressionFunctionAST } from '@kbn/interpreter/common'; +import memoizeOne from 'memoize-one'; import type { TimeScaleUnit } from '../../../time_scale'; import type { IndexPattern, IndexPatternLayer } from '../../../types'; import { adjustTimeScaleLabelSuffix } from '../../time_scale_utils'; import type { ReferenceBasedIndexPatternColumn } from '../column_types'; -import { isColumnValidAsReference } from '../../layer_helpers'; +import { getManagedColumnsFrom, isColumnValidAsReference } from '../../layer_helpers'; import { operationDefinitionMap } from '..'; export const buildLabelFunction = (ofName: (name?: string) => string) => ( @@ -45,6 +46,23 @@ export function checkForDateHistogram(layer: IndexPatternLayer, name: string) { ]; } +const getFullyManagedColumnIds = memoizeOne((layer: IndexPatternLayer) => { + const managedColumnIds = new Set<string>(); + Object.entries(layer.columns).forEach(([id, column]) => { + if ( + 'references' in column && + operationDefinitionMap[column.operationType].input === 'managedReference' + ) { + managedColumnIds.add(id); + const managedColumns = getManagedColumnsFrom(id, layer.columns); + managedColumns.map(([managedId]) => { + managedColumnIds.add(managedId); + }); + } + }); + return managedColumnIds; +}); + export function checkReferences(layer: IndexPatternLayer, columnId: string) { const column = layer.columns[columnId] as ReferenceBasedIndexPatternColumn; @@ -72,7 +90,8 @@ export function checkReferences(layer: IndexPatternLayer, columnId: string) { column: referenceColumn, }); - if (!isValid) { + // do not enforce column validity if current column is part of managed subtree + if (!isValid && !getFullyManagedColumnIds(layer).has(columnId)) { errors.push( i18n.translate('xpack.lens.indexPattern.invalidReferenceConfiguration', { defaultMessage: 'Dimension "{dimensionLabel}" is configured incorrectly', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx index 654a93374703d..d1b0ec8876feb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx @@ -29,7 +29,7 @@ import { ParamEditorProps } from '../../index'; import { getManagedColumnsFrom } from '../../../layer_helpers'; import { ErrorWrapper, runASTValidation, tryToParse } from '../validation'; import { - LensMathSuggestion, + LensMathSuggestions, SUGGESTION_TYPE, suggest, getSuggestion, @@ -329,7 +329,7 @@ export function FormulaEditor({ context: monaco.languages.CompletionContext ) => { const innerText = model.getValue(); - let aSuggestions: { list: LensMathSuggestion[]; type: SUGGESTION_TYPE } = { + let aSuggestions: LensMathSuggestions = { list: [], type: SUGGESTION_TYPE.FIELD, }; @@ -367,7 +367,13 @@ export function FormulaEditor({ return { suggestions: aSuggestions.list.map((s) => - getSuggestion(s, aSuggestions.type, visibleOperationsMap, context.triggerCharacter) + getSuggestion( + s, + aSuggestions.type, + visibleOperationsMap, + context.triggerCharacter, + aSuggestions.range + ) ), }; }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts index 9cd748f5759c9..c55f22dd682d0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts @@ -18,6 +18,7 @@ import { getHover, suggest, monacoPositionToOffset, + offsetToRowColumn, getInfoAtZeroIndexedPosition, } from './math_completion'; @@ -363,6 +364,36 @@ describe('math completion', () => { }); }); + describe('offsetToRowColumn', () => { + it('should work with single-line strings', () => { + const input = `0123456`; + expect(offsetToRowColumn(input, 5)).toEqual( + expect.objectContaining({ + lineNumber: 1, + column: 6, + }) + ); + }); + + it('should work with multi-line strings accounting for newline characters', () => { + const input = `012 +456 +89')`; + expect(offsetToRowColumn(input, 0)).toEqual( + expect.objectContaining({ + lineNumber: 1, + column: 1, + }) + ); + expect(offsetToRowColumn(input, 9)).toEqual( + expect.objectContaining({ + lineNumber: 3, + column: 2, + }) + ); + }); + }); + describe('monacoPositionToOffset', () => { it('should work with multi-line strings accounting for newline characters', () => { const input = `012 diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts index 815df943cdba3..28e762e7dff0f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts @@ -13,6 +13,7 @@ import { TinymathLocation, TinymathAST, TinymathFunction, + TinymathVariable, TinymathNamedArgument, } from '@kbn/tinymath'; import type { @@ -21,7 +22,7 @@ import type { } from '../../../../../../../../../src/plugins/data/public'; import { IndexPattern } from '../../../../types'; import { memoizedGetAvailableOperationsByMetadata } from '../../../operations'; -import { tinymathFunctions, groupArgsByType } from '../util'; +import { tinymathFunctions, groupArgsByType, unquotedStringRegex } from '../util'; import type { GenericOperationDefinition } from '../..'; import { getFunctionSignatureLabel, getHelpTextContent } from './formula_help'; import { hasFunctionFieldArgument } from '../validation'; @@ -47,6 +48,7 @@ export type LensMathSuggestion = export interface LensMathSuggestions { list: LensMathSuggestion[]; type: SUGGESTION_TYPE; + range?: monaco.IRange; } function inLocation(cursorPosition: number, location: TinymathLocation) { @@ -92,7 +94,7 @@ export function offsetToRowColumn(expression: string, offset: number): monaco.Po let lineNumber = 1; for (const line of lines) { if (line.length >= remainingChars) { - return new monaco.Position(lineNumber, remainingChars); + return new monaco.Position(lineNumber, remainingChars + 1); } remainingChars -= line.length + 1; lineNumber++; @@ -128,7 +130,7 @@ export async function suggest({ operationDefinitionMap: Record<string, GenericOperationDefinition>; data: DataPublicPluginStart; dateHistogramInterval?: number; -}): Promise<{ list: LensMathSuggestion[]; type: SUGGESTION_TYPE }> { +}): Promise<LensMathSuggestions> { const text = expression.substr(0, zeroIndexedOffset) + MARKER + expression.substr(zeroIndexedOffset); try { @@ -154,6 +156,7 @@ export async function suggest({ return getArgumentSuggestions( tokenInfo.parent, tokenInfo.parent.args.findIndex((a) => a === tokenAst), + text, indexPattern, operationDefinitionMap ); @@ -210,6 +213,7 @@ function getFunctionSuggestions( function getArgumentSuggestions( ast: TinymathFunction, position: number, + expression: string, indexPattern: IndexPattern, operationDefinitionMap: Record<string, GenericOperationDefinition> ) { @@ -280,7 +284,16 @@ function getArgumentSuggestions( .filter((op) => op.operationType === operation.type) .map((op) => ('field' in op ? op.field : undefined)) .filter((field) => field); - return { list: fields as string[], type: SUGGESTION_TYPE.FIELD }; + const fieldArg = ast.args[0]; + const location = typeof fieldArg !== 'string' && (fieldArg as TinymathVariable).location; + let range: monaco.IRange | undefined; + if (location) { + const start = offsetToRowColumn(expression, location.min); + // This accounts for any characters that the user has already typed + const end = offsetToRowColumn(expression, location.max - MARKER.length); + range = monaco.Range.fromPositions(start, end); + } + return { list: fields as string[], type: SUGGESTION_TYPE.FIELD, range }; } else { return { list: [], type: SUGGESTION_TYPE.FIELD }; } @@ -375,7 +388,8 @@ export function getSuggestion( suggestion: LensMathSuggestion, type: SUGGESTION_TYPE, operationDefinitionMap: Record<string, GenericOperationDefinition>, - triggerChar: string | undefined + triggerChar: string | undefined, + range?: monaco.IRange ): monaco.languages.CompletionItem { let kind: monaco.languages.CompletionItemKind = monaco.languages.CompletionItemKind.Method; let label: string = @@ -397,6 +411,10 @@ export function getSuggestion( break; case SUGGESTION_TYPE.FIELD: kind = monaco.languages.CompletionItemKind.Value; + // Look for unsafe characters + if (unquotedStringRegex.test(label)) { + insertText = `'${label.replaceAll(`'`, "\\'")}'`; + } break; case SUGGESTION_TYPE.FUNCTIONS: insertText = `${label}($0)`; @@ -450,7 +468,7 @@ export function getSuggestion( command, additionalTextEdits: [], // @ts-expect-error Monaco says this type is required, but provides a default value - range: undefined, + range, sortText, filterText, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx index e6aa29ea4d763..279e76b839548 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx @@ -413,13 +413,13 @@ describe('formula', () => { ).newLayer ).toEqual({ ...layer, - columnOrder: ['col1X0', 'col1X1', 'col1'], + columnOrder: ['col1X0', 'col1'], columns: { ...layer.columns, col1: { ...currentColumn, label: 'average(bytes)', - references: ['col1X1'], + references: ['col1X0'], params: { ...currentColumn.params, formula: 'average(bytes)', @@ -436,18 +436,6 @@ describe('formula', () => { sourceField: 'bytes', timeScale: false, }, - col1X1: { - customLabel: true, - dataType: 'number', - isBucketed: false, - label: 'Part of average(bytes)', - operationType: 'math', - params: { - tinymathAst: 'col1X0', - }, - references: ['col1X0'], - scale: 'ratio', - }, }, }); }); @@ -568,8 +556,8 @@ describe('formula', () => { ).locations ).toEqual({ col1X0: { min: 15, max: 29 }, - col1X2: { min: 0, max: 41 }, - col1X3: { min: 42, max: 50 }, + col1X1: { min: 0, max: 41 }, + col1X2: { min: 42, max: 50 }, }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts index a5c19c537acee..589f547434b91 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/generate.ts @@ -13,6 +13,7 @@ import { } from '../index'; import { ReferenceBasedIndexPatternColumn } from '../column_types'; import { IndexPatternLayer } from '../../../types'; +import { unquotedStringRegex } from './util'; // Just handle two levels for now type OperationParams = Record<string, string | number | Record<string, string | number>>; @@ -25,6 +26,9 @@ export function getSafeFieldName({ if (!fieldName || operationType === 'count') { return ''; } + if (unquotedStringRegex.test(fieldName)) { + return `'${fieldName.replaceAll(`'`, "\\'")}'`; + } return fieldName; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts index 8b726d06f4602..cb1d0dc143efc 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/parse.ts @@ -123,17 +123,20 @@ function extractColumns( if (nodeOperation.input === 'fullReference') { const [referencedOp] = functions; const consumedParam = parseNode(referencedOp); + const hasActualMathContent = typeof consumedParam !== 'string'; - const subNodeVariables = consumedParam ? findVariables(consumedParam) : []; - const mathColumn = mathOperation.buildColumn({ - layer, - indexPattern, - }); - mathColumn.references = subNodeVariables.map(({ value }) => value); - mathColumn.params.tinymathAst = consumedParam!; - columns.push({ column: mathColumn }); - mathColumn.customLabel = true; - mathColumn.label = label; + if (hasActualMathContent) { + const subNodeVariables = consumedParam ? findVariables(consumedParam) : []; + const mathColumn = mathOperation.buildColumn({ + layer, + indexPattern, + }); + mathColumn.references = subNodeVariables.map(({ value }) => value); + mathColumn.params.tinymathAst = consumedParam!; + columns.push({ column: mathColumn }); + mathColumn.customLabel = true; + mathColumn.label = label; + } const mappedParams = getOperationParams(nodeOperation, namedArguments || []); const newCol = (nodeOperation as OperationDefinition< @@ -143,7 +146,11 @@ function extractColumns( { layer, indexPattern, - referenceIds: [getManagedId(idPrefix, columns.length - 1)], + referenceIds: [ + hasActualMathContent + ? getManagedId(idPrefix, columns.length - 1) + : (consumedParam as string), + ], }, mappedParams ); @@ -160,16 +167,19 @@ function extractColumns( if (root === undefined) { return []; } - const variables = findVariables(root); - const mathColumn = mathOperation.buildColumn({ - layer, - indexPattern, - }); - mathColumn.references = variables.map(({ value }) => value); - mathColumn.params.tinymathAst = root!; - mathColumn.customLabel = true; - mathColumn.label = label; - columns.push({ column: mathColumn }); + const topLevelMath = typeof root !== 'string'; + if (topLevelMath) { + const variables = findVariables(root); + const mathColumn = mathOperation.buildColumn({ + layer, + indexPattern, + }); + mathColumn.references = variables.map(({ value }) => value); + mathColumn.params.tinymathAst = root!; + mathColumn.customLabel = true; + mathColumn.label = label; + columns.push({ column: mathColumn }); + } return columns; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts index d29682eafa329..9806cdaad637e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts @@ -16,6 +16,8 @@ import type { import type { OperationDefinition, IndexPatternColumn, GenericOperationDefinition } from '../index'; import type { GroupedNodes } from './types'; +export const unquotedStringRegex = /[^0-9A-Za-z._@\[\]/]/; + export function groupArgsByType(args: TinymathAST[]) { const { namedArgument, variable, function: functions } = groupBy<TinymathAST>( args, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index 7551b88039182..a458a1edcfa16 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -424,25 +424,9 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field /> </EuiFormRow> <EuiFormRow - label={ - <> - {i18n.translate('xpack.lens.indexPattern.terms.orderDirection', { - defaultMessage: 'Rank direction', - })}{' '} - <EuiIconTip - color="subdued" - content={i18n.translate('xpack.lens.indexPattern.terms.orderDirectionHelp', { - defaultMessage: `Specifies the ranking order of the top values.`, - })} - iconProps={{ - className: 'eui-alignTop', - }} - position="top" - size="s" - type="questionInCircle" - /> - </> - } + label={i18n.translate('xpack.lens.indexPattern.terms.orderDirection', { + defaultMessage: 'Rank direction', + })} display="columnCompressed" fullWidth > @@ -513,7 +497,10 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field defaultMessage: 'Include documents without this field', })} compressed - disabled={!currentColumn.params.otherBucket} + disabled={ + !currentColumn.params.otherBucket || + indexPattern.getFieldByName(currentColumn.sourceField)?.type !== 'string' + } data-test-subj="indexPattern-terms-missing-bucket" checked={Boolean(currentColumn.params.missingBucket)} onChange={(e: EuiSwitchEvent) => diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index 3b557461546ca..f326f3e3ed5f6 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -60,7 +60,7 @@ describe('terms', () => { size: 3, orderDirection: 'asc', }, - sourceField: 'category', + sourceField: 'source', }, col2: { label: 'Count', @@ -88,7 +88,7 @@ describe('terms', () => { expect.objectContaining({ arguments: expect.objectContaining({ orderBy: ['_key'], - field: ['category'], + field: ['source'], size: [3], otherBucket: [true], }), @@ -770,6 +770,34 @@ describe('terms', () => { expect(select.prop('disabled')).toEqual(false); }); + it('should disable missing bucket setting if field is not a string', () => { + const updateLayerSpy = jest.fn(); + const instance = shallow( + <InlineOptions + {...defaultProps} + layer={layer} + updateLayer={updateLayerSpy} + columnId="col1" + currentColumn={ + { + ...layer.columns.col1, + sourceField: 'bytes', + params: { + ...layer.columns.col1.params, + otherBucket: true, + }, + } as TermsIndexPatternColumn + } + /> + ); + + const select = instance + .find('[data-test-subj="indexPattern-terms-missing-bucket"]') + .find(EuiSwitch); + + expect(select.prop('disabled')).toEqual(true); + }); + it('should update state when clicking other bucket toggle', () => { const updateLayerSpy = jest.fn(); const instance = shallow( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index 387a61ff79264..9eedae6d82d43 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -25,6 +25,7 @@ import { documentField } from '../document_field'; import { getFieldByNameFactory } from '../pure_helpers'; import { generateId } from '../../id_generator'; import { createMockedFullReference, createMockedManagedReference } from './mocks'; +import { TinymathAST } from 'packages/kbn-tinymath'; jest.mock('../operations'); jest.mock('../../id_generator'); @@ -105,28 +106,34 @@ describe('state_helpers', () => { const source = { dataType: 'number' as const, isBucketed: false, - label: 'moving_average(sum(bytes), window=5)', + label: '5 + moving_average(sum(bytes), window=5)', operationType: 'formula' as const, params: { - formula: 'moving_average(sum(bytes), window=5)', + formula: '5 + moving_average(sum(bytes), window=5)', isFormulaBroken: false, }, - references: ['formulaX1'], + references: ['formulaX2'], }; const math = { customLabel: true, dataType: 'number' as const, isBucketed: false, - label: 'Part of moving_average(sum(bytes), window=5)', operationType: 'math' as const, - params: { tinymathAst: 'formulaX2' }, - references: ['formulaX2'], + label: 'Part of 5 + moving_average(sum(bytes), window=5)', + references: ['formulaX1'], + params: { + tinymathAst: { + type: 'function', + name: 'add', + args: [5, 'formulaX1'], + } as TinymathAST, + }, }; const sum = { customLabel: true, dataType: 'number' as const, isBucketed: false, - label: 'Part of moving_average(sum(bytes), window=5)', + label: 'Part of 5 + moving_average(sum(bytes), window=5)', operationType: 'sum' as const, scale: 'ratio' as const, sourceField: 'bytes', @@ -135,7 +142,7 @@ describe('state_helpers', () => { customLabel: true, dataType: 'number' as const, isBucketed: false, - label: 'Part of moving_average(sum(bytes), window=5)', + label: 'Part of 5 + moving_average(sum(bytes), window=5)', operationType: 'moving_average' as const, params: { window: 5 }, references: ['formulaX0'], @@ -148,14 +155,8 @@ describe('state_helpers', () => { columns: { source, formulaX0: sum, - formulaX1: math, - formulaX2: movingAvg, - formulaX3: { - ...math, - label: 'Part of moving_average(sum(bytes), window=5)', - references: ['formulaX2'], - params: { tinymathAst: 'formulaX2' }, - }, + formulaX1: movingAvg, + formulaX2: math, }, }, targetId: 'copy', @@ -171,40 +172,34 @@ describe('state_helpers', () => { 'formulaX0', 'formulaX1', 'formulaX2', - 'formulaX3', 'copyX0', 'copyX1', 'copyX2', - 'copyX3', 'copy', ], columns: { source, formulaX0: sum, - formulaX1: math, - formulaX2: movingAvg, - formulaX3: { - ...math, - references: ['formulaX2'], - params: { tinymathAst: 'formulaX2' }, - }, - copy: expect.objectContaining({ ...source, references: ['copyX3'] }), + formulaX1: movingAvg, + formulaX2: math, + copy: expect.objectContaining({ ...source, references: ['copyX2'] }), copyX0: expect.objectContaining({ ...sum, }), copyX1: expect.objectContaining({ - ...math, + ...movingAvg, references: ['copyX0'], - params: { tinymathAst: 'copyX0' }, }), copyX2: expect.objectContaining({ - ...movingAvg, - references: ['copyX1'], - }), - copyX3: expect.objectContaining({ ...math, - references: ['copyX2'], - params: { tinymathAst: 'copyX2' }, + references: ['copyX1'], + params: { + tinymathAst: expect.objectContaining({ + type: 'function', + name: 'add', + args: [5, 'copyX1'], + } as TinymathAST), + }, }), }, }); @@ -1922,6 +1917,54 @@ describe('state_helpers', () => { }) ); }); + + it('should keep state and set incomplete column on incompatible switch', () => { + const layer: IndexPatternLayer = { + indexPatternId: '1', + columnOrder: ['metric', 'ref'], + columns: { + metric: { + dataType: 'number' as const, + isBucketed: false, + sourceField: 'source', + operationType: 'unique_count' as const, + filter: { language: 'kuery', query: 'bytes > 4000' }, + timeShift: '3h', + label: 'Cardinality', + customLabel: true, + }, + ref: { + label: 'Reference', + dataType: 'number', + isBucketed: false, + operationType: 'differences', + references: ['metric'], + filter: { language: 'kuery', query: 'bytes > 4000' }, + timeShift: '3h', + }, + }, + }; + const result = replaceColumn({ + layer, + indexPattern, + columnId: 'ref', + op: 'sum', + visualizationGroups: [], + }); + expect(result.columnOrder).toEqual(layer.columnOrder); + expect(result.columns).toEqual(layer.columns); + expect(result.incompleteColumns).toEqual({ + ref: { + operationType: 'sum', + filter: { + language: 'kuery', + query: 'bytes > 4000', + }, + timeScale: undefined, + timeShift: '3h', + }, + }); + }); }); it('should allow making a replacement on an operation that is being referenced, even if it ends up invalid', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index fd3df9f97cecf..b5b1960b7b769 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -19,6 +19,7 @@ import { OperationType, IndexPatternColumn, RequiredReference, + OperationDefinition, GenericOperationDefinition, } from './definitions'; import type { @@ -532,20 +533,15 @@ export function replaceColumn({ ); } - // This logic comes after the transitions because they need to look at previous columns - if (previousDefinition.input === 'fullReference') { - (previousColumn as ReferenceBasedIndexPatternColumn).references.forEach((id: string) => { - tempLayer = deleteColumn({ - layer: tempLayer, - columnId: id, - indexPattern, - }); - }); - } - if (operationDefinition.input === 'none') { let newColumn = operationDefinition.buildColumn({ ...baseOptions, layer: tempLayer }); newColumn = copyCustomLabel(newColumn, previousColumn); + tempLayer = removeOrphanedColumns( + previousDefinition, + previousColumn, + tempLayer, + indexPattern + ); const newLayer = { ...tempLayer, columns: { ...tempLayer.columns, [columnId]: newColumn } }; return updateDefaultLabels( @@ -564,7 +560,6 @@ export function replaceColumn({ } & ColumnAdvancedParams = { operationType: op }; // if no field is available perform a full clean of the column from the layer if (previousDefinition.input === 'fullReference') { - tempLayer = deleteColumn({ layer: tempLayer, columnId, indexPattern }); const previousReferenceId = (previousColumn as ReferenceBasedIndexPatternColumn) .references[0]; const referenceColumn = layer.columns[previousReferenceId]; @@ -598,6 +593,8 @@ export function replaceColumn({ }; } + tempLayer = removeOrphanedColumns(previousDefinition, previousColumn, tempLayer, indexPattern); + let newColumn = operationDefinition.buildColumn({ ...baseOptions, layer: tempLayer, field }); if (!shouldResetLabel) { newColumn = copyCustomLabel(newColumn, previousColumn); @@ -637,6 +634,27 @@ export function replaceColumn({ } } +function removeOrphanedColumns( + previousDefinition: + | OperationDefinition<IndexPatternColumn, 'field'> + | OperationDefinition<IndexPatternColumn, 'none'> + | OperationDefinition<IndexPatternColumn, 'fullReference'>, + previousColumn: IndexPatternColumn, + tempLayer: IndexPatternLayer, + indexPattern: IndexPattern +) { + if (previousDefinition.input === 'fullReference') { + (previousColumn as ReferenceBasedIndexPatternColumn).references.forEach((id: string) => { + tempLayer = deleteColumn({ + layer: tempLayer, + columnId: id, + indexPattern, + }); + }); + } + return tempLayer; +} + export function canTransition({ layer, columnId, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/query_input.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/query_input.tsx index a67199a9d3432..1b418ee3b408f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/query_input.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/query_input.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { isEqual } from 'lodash'; import { QueryStringInput, Query } from '../../../../../src/plugins/data/public'; import { useDebouncedValue } from '../shared_components'; @@ -36,7 +37,11 @@ export const QueryInput = ({ bubbleSubmitEvent={false} indexPatterns={[indexPatternTitle]} query={inputValue} - onChange={handleInputChange} + onChange={(newQuery) => { + if (!isEqual(newQuery, inputValue)) { + handleInputChange(newQuery); + } + }} onSubmit={() => { if (inputValue.query) { onSubmit(); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts index 0750b99db5f67..5654a599c5e27 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts @@ -83,29 +83,6 @@ describe('rename_columns', () => { `); }); - it('should replace "" with a visible value', () => { - const input: Datatable = { - type: 'datatable', - columns: [{ id: 'a', name: 'A', meta: { type: 'string' } }], - rows: [{ a: '' }], - }; - - const idMap = { - a: { - id: 'a', - label: 'Austrailia', - }, - }; - - const result = renameColumns.fn( - input, - { idMap: JSON.stringify(idMap) }, - createMockExecutionContext() - ); - - expect(result.rows[0].a).toEqual('(empty)'); - }); - it('should keep columns which are not mapped', () => { const input: Datatable = { type: 'datatable', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.ts b/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.ts index 89c63880248d0..a16756126c030 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/rename_columns.ts @@ -49,9 +49,9 @@ export const renameColumns: ExpressionFunctionDefinition< Object.entries(row).forEach(([id, value]) => { if (id in idMap) { - mappedRow[idMap[id].id] = sanitizeValue(value); + mappedRow[idMap[id].id] = value; } else { - mappedRow[id] = sanitizeValue(value); + mappedRow[id] = value; } }); @@ -86,13 +86,3 @@ function getColumnName(originalColumn: OriginalColumn, newColumn: DatatableColum return originalColumn.label; } - -function sanitizeValue(value: unknown) { - if (value === '') { - return i18n.translate('xpack.lens.indexpattern.emptyTextColumnValue', { - defaultMessage: '(empty)', - }); - } - - return value; -} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx index 14ba6b9189e6b..a1bc643c3bd93 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/time_shift_utils.tsx @@ -23,67 +23,67 @@ import { FramePublicAPI } from '../types'; export const timeShiftOptions = [ { label: i18n.translate('xpack.lens.indexPattern.timeShift.hour', { - defaultMessage: '1 hour (1h)', + defaultMessage: '1 hour ago (1h)', }), value: '1h', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.3hours', { - defaultMessage: '3 hours (3h)', + defaultMessage: '3 hours ago (3h)', }), value: '3h', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.6hours', { - defaultMessage: '6 hours (6h)', + defaultMessage: '6 hours ago (6h)', }), value: '6h', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.12hours', { - defaultMessage: '12 hours (12h)', + defaultMessage: '12 hours ago (12h)', }), value: '12h', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.day', { - defaultMessage: '1 day (1d)', + defaultMessage: '1 day ago (1d)', }), value: '1d', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.week', { - defaultMessage: '1 week (1w)', + defaultMessage: '1 week ago (1w)', }), value: '1w', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.month', { - defaultMessage: '1 month (1M)', + defaultMessage: '1 month ago (1M)', }), value: '1M', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.3months', { - defaultMessage: '3 months (3M)', + defaultMessage: '3 months ago (3M)', }), value: '3M', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.6months', { - defaultMessage: '6 months (6M)', + defaultMessage: '6 months ago (6M)', }), value: '6M', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.year', { - defaultMessage: '1 year (1y)', + defaultMessage: '1 year ago (1y)', }), value: '1y', }, { label: i18n.translate('xpack.lens.indexPattern.timeShift.previous', { - defaultMessage: 'Previous', + defaultMessage: 'Previous time range', }), value: 'previous', }, diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index f329cfe1bb8b9..2e5a06b4f705f 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -31,7 +31,6 @@ import { PieExpressionProps } from './types'; import { getSliceValue, getFilterContext } from './render_helpers'; import { EmptyPlaceholder } from '../shared_components'; import './visualization.scss'; -import { desanitizeFilterContext } from '../utils'; import { ChartsPluginSetup, PaletteRegistry, @@ -254,7 +253,7 @@ export function PieComponent( const onElementClickHandler: ElementClickListener = (args) => { const context = getFilterContext(args[0][0] as LayerValue[], groups, firstTable); - onClickValue(desanitizeFilterContext(context)); + onClickValue(context); }; return ( diff --git a/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx b/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx index f71bda986a8bb..ae7204d9f93e7 100644 --- a/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx +++ b/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { FC } from 'react'; import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; import { EuiFormRow, @@ -39,6 +39,17 @@ import { } from './utils'; const idPrefix = htmlIdGenerator()(); +const ContinuityOption: FC<{ iconType: string }> = ({ children, iconType }) => { + return ( + <EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}> + <EuiFlexItem grow={false}> + <EuiIcon type={iconType} /> + </EuiFlexItem> + <EuiFlexItem grow={false}>{children}</EuiFlexItem> + </EuiFlexGroup> + ); +}; + /** * Some name conventions here: * * `displayStops` => It's an additional transformation of `stops` into a [0, N] domain for the EUIPaletteDisplay component. @@ -141,41 +152,45 @@ export function CustomizablePalette({ options={[ { value: 'above', - inputDisplay: i18n.translate( - 'xpack.lens.table.dynamicColoring.continuity.aboveLabel', - { - defaultMessage: 'Above range', - } + inputDisplay: ( + <ContinuityOption iconType="continuityAbove"> + {i18n.translate('xpack.lens.table.dynamicColoring.continuity.aboveLabel', { + defaultMessage: 'Above range', + })} + </ContinuityOption> ), 'data-test-subj': 'continuity-above', }, { value: 'below', - inputDisplay: i18n.translate( - 'xpack.lens.table.dynamicColoring.continuity.belowLabel', - { - defaultMessage: 'Below range', - } + inputDisplay: ( + <ContinuityOption iconType="continuityBelow"> + {i18n.translate('xpack.lens.table.dynamicColoring.continuity.belowLabel', { + defaultMessage: 'Below range', + })} + </ContinuityOption> ), 'data-test-subj': 'continuity-below', }, { value: 'all', - inputDisplay: i18n.translate( - 'xpack.lens.table.dynamicColoring.continuity.allLabel', - { - defaultMessage: 'Above and below range', - } + inputDisplay: ( + <ContinuityOption iconType="continuityAboveBelow"> + {i18n.translate('xpack.lens.table.dynamicColoring.continuity.allLabel', { + defaultMessage: 'Above and below range', + })} + </ContinuityOption> ), 'data-test-subj': 'continuity-all', }, { value: 'none', - inputDisplay: i18n.translate( - 'xpack.lens.table.dynamicColoring.continuity.noneLabel', - { - defaultMessage: 'Within range', - } + inputDisplay: ( + <ContinuityOption iconType="continuityWithin"> + {i18n.translate('xpack.lens.table.dynamicColoring.continuity.noneLabel', { + defaultMessage: 'Within range', + })} + </ContinuityOption> ), 'data-test-subj': 'continuity-none', }, diff --git a/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx b/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx index e344cb5289f51..5027629ef6ae5 100644 --- a/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx +++ b/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx @@ -9,7 +9,6 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover, EuiContextMenu } from '@elastic/eui'; import type { LensFilterEvent } from '../types'; -import { desanitizeFilterContext } from '../utils'; export interface LegendActionPopoverProps { /** @@ -45,7 +44,7 @@ export const LegendActionPopover: React.FunctionComponent<LegendActionPopoverPro icon: <EuiIcon type="plusInCircle" size="m" />, onClick: () => { setPopoverOpen(false); - onFilter(desanitizeFilterContext(context)); + onFilter(context); }, }, { @@ -56,7 +55,7 @@ export const LegendActionPopover: React.FunctionComponent<LegendActionPopoverPro icon: <EuiIcon type="minusInCircle" size="m" />, onClick: () => { setPopoverOpen(false); - onFilter(desanitizeFilterContext({ ...context, negate: true })); + onFilter({ ...context, negate: true }); }, }, ], diff --git a/x-pack/plugins/lens/public/utils.test.ts b/x-pack/plugins/lens/public/utils.test.ts deleted file mode 100644 index 76597870b3beb..0000000000000 --- a/x-pack/plugins/lens/public/utils.test.ts +++ /dev/null @@ -1,118 +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 { LensFilterEvent } from './types'; -import { desanitizeFilterContext } from './utils'; -import { Datatable } from '../../../../src/plugins/expressions/common'; - -describe('desanitizeFilterContext', () => { - it(`When filtered value equals '(empty)' replaces it with '' in table and in value.`, () => { - const table: Datatable = { - type: 'datatable', - rows: [ - { - 'f903668f-1175-4705-a5bd-713259d10326': 1589414640000, - '5d5446b2-72e8-4f86-91e0-88380f0fa14c': '(empty)', - 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 1, - }, - { - 'f903668f-1175-4705-a5bd-713259d10326': 1589414670000, - 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 0, - }, - { - 'f903668f-1175-4705-a5bd-713259d10326': 1589414880000, - '5d5446b2-72e8-4f86-91e0-88380f0fa14c': '123123123', - 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 1, - }, - { - 'f903668f-1175-4705-a5bd-713259d10326': 1589414910000, - '5d5446b2-72e8-4f86-91e0-88380f0fa14c': '(empty)', - 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 1, - }, - ], - columns: [ - { - id: 'f903668f-1175-4705-a5bd-713259d10326', - name: 'order_date per 30 seconds', - meta: { type: 'date' }, - }, - { - id: '5d5446b2-72e8-4f86-91e0-88380f0fa14c', - name: 'Top values of customer_phone', - meta: { type: 'string' }, - }, - { - id: '9f0b6f88-c399-43a0-a993-0ad943c9af25', - name: 'Count of records', - meta: { type: 'number' }, - }, - ], - }; - - const contextWithEmptyValue: LensFilterEvent['data'] = { - data: [ - { - row: 3, - column: 0, - value: 1589414910000, - table, - }, - { - row: 0, - column: 1, - value: '(empty)', - table, - }, - ], - timeFieldName: 'order_date', - }; - - const desanitizedFilterContext = desanitizeFilterContext(contextWithEmptyValue); - - expect(desanitizedFilterContext).toEqual({ - data: [ - { - row: 3, - column: 0, - value: 1589414910000, - table, - }, - { - value: '', - row: 0, - column: 1, - table: { - rows: [ - { - 'f903668f-1175-4705-a5bd-713259d10326': 1589414640000, - '5d5446b2-72e8-4f86-91e0-88380f0fa14c': '', - 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 1, - }, - { - 'f903668f-1175-4705-a5bd-713259d10326': 1589414670000, - 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 0, - }, - { - 'f903668f-1175-4705-a5bd-713259d10326': 1589414880000, - '5d5446b2-72e8-4f86-91e0-88380f0fa14c': '123123123', - 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 1, - }, - { - 'f903668f-1175-4705-a5bd-713259d10326': 1589414910000, - '5d5446b2-72e8-4f86-91e0-88380f0fa14c': '(empty)', - 'col-1-9f0b6f88-c399-43a0-a993-0ad943c9af25': 1, - }, - ], - columns: table.columns, - type: 'datatable', - }, - }, - ], - timeFieldName: 'order_date', - }); - }); -}); diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index 2706fe977c68e..1c4b2c67f96fc 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -9,42 +9,6 @@ import { i18n } from '@kbn/i18n'; import { IndexPattern, IndexPatternsContract, TimefilterContract } from 'src/plugins/data/public'; import { IUiSettingsClient } from 'kibana/public'; import moment from 'moment-timezone'; -import { LensFilterEvent } from './types'; - -/** replaces the value `(empty) to empty string for proper filtering` */ -export const desanitizeFilterContext = ( - context: LensFilterEvent['data'] -): LensFilterEvent['data'] => { - const emptyTextValue = i18n.translate('xpack.lens.indexpattern.emptyTextColumnValue', { - defaultMessage: '(empty)', - }); - const result: LensFilterEvent['data'] = { - ...context, - data: context.data.map((point) => - point.value === emptyTextValue - ? { - ...point, - value: '', - table: { - ...point.table, - rows: point.table.rows.map((row, index) => - index === point.row - ? { - ...row, - [point.table.columns[point.column].id]: '', - } - : row - ), - }, - } - : point - ), - }; - if (context.timeFieldName) { - result.timeFieldName = context.timeFieldName; - } - return result; -}; export function getVisualizeGeoFieldMessage(fieldType: string) { return i18n.translate('xpack.lens.visualizeGeoFieldMessage', { diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 1de5cf6b30533..3fe98282a18b0 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -53,7 +53,6 @@ import { SeriesLayer, } from '../../../../../src/plugins/charts/public'; import { EmptyPlaceholder } from '../shared_components'; -import { desanitizeFilterContext } from '../utils'; import { fittingFunctionDefinitions, getFitOptions } from './fitting_functions'; import { getAxesConfiguration, GroupsConfiguration, validateExtent } from './axes_configuration'; import { getColorAssignments } from './color_assignment'; @@ -575,7 +574,7 @@ export function XYChart({ })), timeFieldName: xDomain && isDateField ? xAxisFieldName : undefined, }; - onClickValue(desanitizeFilterContext(context)); + onClickValue(context); }; const brushHandler: BrushEndListener = ({ x }) => { diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index f2840b6d3844b..dee0e5763dee4 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -872,6 +872,59 @@ describe('xy_visualization', () => { }, ]); }); + + it('should return an error if string and date histogram xAccessors (multiple layers) are used together', () => { + // current incompatibility is only for date and numeric histograms as xAccessors + const datasourceLayers = { + first: mockDatasource.publicAPIMock, + second: createMockDatasource('testDatasource').publicAPIMock, + }; + datasourceLayers.first.getOperationForColumnId = jest.fn((id: string) => + id === 'a' + ? (({ + dataType: 'date', + scale: 'interval', + } as unknown) as Operation) + : null + ); + datasourceLayers.second.getOperationForColumnId = jest.fn((id: string) => + id === 'e' + ? (({ + dataType: 'string', + scale: 'ordinal', + } as unknown) as Operation) + : null + ); + expect( + xyVisualization.getErrorMessages( + { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + splitAccessor: 'd', + xAccessor: 'a', + accessors: ['b'], + }, + { + layerId: 'second', + seriesType: 'area', + splitAccessor: 'd', + xAccessor: 'e', + accessors: ['b'], + }, + ], + }, + datasourceLayers + ) + ).toEqual([ + { + shortMessage: 'Wrong data type for Horizontal axis.', + longMessage: 'Data type mismatch for the Horizontal axis, use a different function.', + }, + ]); + }); }); describe('#getWarningMessages', () => { diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index ad2c9fd713985..bd20ed300bf61 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -542,8 +542,15 @@ function checkXAccessorCompatibility( datasourceLayers: Record<string, DatasourcePublicAPI> ) { const errors = []; - const hasDateHistogramSet = state.layers.some(checkIntervalOperation('date', datasourceLayers)); - const hasNumberHistogram = state.layers.some(checkIntervalOperation('number', datasourceLayers)); + const hasDateHistogramSet = state.layers.some( + checkScaleOperation('interval', 'date', datasourceLayers) + ); + const hasNumberHistogram = state.layers.some( + checkScaleOperation('interval', 'number', datasourceLayers) + ); + const hasOrdinalAxis = state.layers.some( + checkScaleOperation('ordinal', undefined, datasourceLayers) + ); if (state.layers.length > 1 && hasDateHistogramSet && hasNumberHistogram) { errors.push({ shortMessage: i18n.translate('xpack.lens.xyVisualization.dataTypeFailureXShort', { @@ -560,11 +567,28 @@ function checkXAccessorCompatibility( }), }); } + if (state.layers.length > 1 && (hasDateHistogramSet || hasNumberHistogram) && hasOrdinalAxis) { + errors.push({ + shortMessage: i18n.translate('xpack.lens.xyVisualization.dataTypeFailureXShort', { + defaultMessage: `Wrong data type for {axis}.`, + values: { + axis: getAxisName('x', { isHorizontal: isHorizontalChart(state.layers) }), + }, + }), + longMessage: i18n.translate('xpack.lens.xyVisualization.dataTypeFailureXOrdinalLong', { + defaultMessage: `Data type mismatch for the {axis}, use a different function.`, + values: { + axis: getAxisName('x', { isHorizontal: isHorizontalChart(state.layers) }), + }, + }), + }); + } return errors; } -function checkIntervalOperation( - dataType: 'date' | 'number', +function checkScaleOperation( + scaleType: 'ordinal' | 'interval' | 'ratio', + dataType: 'date' | 'number' | 'string' | undefined, datasourceLayers: Record<string, DatasourcePublicAPI> ) { return (layer: XYLayerConfig) => { @@ -573,6 +597,8 @@ function checkIntervalOperation( return false; } const operation = datasourceAPI?.getOperationForColumnId(layer.xAccessor); - return Boolean(operation?.dataType === dataType && operation.scale === 'interval'); + return Boolean( + operation && (!dataType || operation.dataType === dataType) && operation.scale === scaleType + ); }; } diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap index 29ec3ddbfdc02..45e7055f4db2b 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap @@ -1417,19 +1417,29 @@ exports[`UploadLicense should display an error when ES says license is expired 1 </span> </div> <EuiText + color="default" size="s" > <div className="euiText euiText--small" > - <ul> - <li - className="euiForm__error" - key="0" + <EuiTextColor + color="default" + component="div" + > + <div + className="euiTextColor euiTextColor--default" > - The supplied license has expired. - </li> - </ul> + <ul> + <li + className="euiForm__error" + key="0" + > + The supplied license has expired. + </li> + </ul> + </div> + </EuiTextColor> </div> </EuiText> </div> @@ -2149,19 +2159,29 @@ exports[`UploadLicense should display an error when ES says license is invalid 1 </span> </div> <EuiText + color="default" size="s" > <div className="euiText euiText--small" > - <ul> - <li - className="euiForm__error" - key="0" + <EuiTextColor + color="default" + component="div" + > + <div + className="euiTextColor euiTextColor--default" > - The supplied license is not valid for this product. - </li> - </ul> + <ul> + <li + className="euiForm__error" + key="0" + > + The supplied license is not valid for this product. + </li> + </ul> + </div> + </EuiTextColor> </div> </EuiText> </div> @@ -2881,19 +2901,29 @@ exports[`UploadLicense should display an error when submitting invalid JSON 1`] </span> </div> <EuiText + color="default" size="s" > <div className="euiText euiText--small" > - <ul> - <li - className="euiForm__error" - key="0" + <EuiTextColor + color="default" + component="div" + > + <div + className="euiTextColor euiTextColor--default" > - Error encountered uploading license: Check your license file. - </li> - </ul> + <ul> + <li + className="euiForm__error" + key="0" + > + Error encountered uploading license: Check your license file. + </li> + </ul> + </div> + </EuiTextColor> </div> </EuiText> </div> @@ -3613,19 +3643,29 @@ exports[`UploadLicense should display error when ES returns error 1`] = ` </span> </div> <EuiText + color="default" size="s" > <div className="euiText euiText--small" > - <ul> - <li - className="euiForm__error" - key="0" + <EuiTextColor + color="default" + component="div" + > + <div + className="euiTextColor euiTextColor--default" > - Error encountered uploading license: Can not upgrade to a production license unless TLS is configured or security is disabled - </li> - </ul> + <ul> + <li + className="euiForm__error" + key="0" + > + Error encountered uploading license: Can not upgrade to a production license unless TLS is configured or security is disabled + </li> + </ul> + </div> + </EuiTextColor> </div> </EuiText> </div> diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts index bdcb4224eed9c..4987de321c556 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.test.ts @@ -48,6 +48,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: false, }) ); @@ -83,6 +84,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: false, }) ); @@ -122,6 +124,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: true, }) ); @@ -132,7 +135,7 @@ describe('useExceptionLists', () => { expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({ filters: - '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)', + '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)', http: mockKibanaHttpService, namespaceTypes: 'single,agnostic', pagination: { page: 1, perPage: 20 }, @@ -157,6 +160,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: false, }) ); @@ -167,7 +171,79 @@ describe('useExceptionLists', () => { expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({ filters: - '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)', + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)', + http: mockKibanaHttpService, + namespaceTypes: 'single,agnostic', + pagination: { page: 1, perPage: 20 }, + signal: new AbortController().signal, + }); + }); + }); + + test('fetches event filters lists if "showEventFilters" is true', async () => { + const spyOnfetchExceptionLists = jest.spyOn(api, 'fetchExceptionLists'); + + await act(async () => { + const { waitForNextUpdate } = renderHook<UseExceptionListsProps, ReturnExceptionLists>(() => + useExceptionLists({ + errorMessage: 'Uh oh', + filterOptions: {}, + http: mockKibanaHttpService, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + showEventFilters: true, + showTrustedApps: false, + }) + ); + // NOTE: First `waitForNextUpdate` is initialization + // Second call applies the params + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({ + filters: + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*)', + http: mockKibanaHttpService, + namespaceTypes: 'single,agnostic', + pagination: { page: 1, perPage: 20 }, + signal: new AbortController().signal, + }); + }); + }); + + test('does not fetch event filters lists if "showEventFilters" is false', async () => { + const spyOnfetchExceptionLists = jest.spyOn(api, 'fetchExceptionLists'); + + await act(async () => { + const { waitForNextUpdate } = renderHook<UseExceptionListsProps, ReturnExceptionLists>(() => + useExceptionLists({ + errorMessage: 'Uh oh', + filterOptions: {}, + http: mockKibanaHttpService, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + showEventFilters: false, + showTrustedApps: false, + }) + ); + // NOTE: First `waitForNextUpdate` is initialization + // Second call applies the params + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({ + filters: + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)', http: mockKibanaHttpService, namespaceTypes: 'single,agnostic', pagination: { page: 1, perPage: 20 }, @@ -195,6 +271,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: false, }) ); @@ -205,7 +282,7 @@ describe('useExceptionLists', () => { expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({ filters: - '(exception-list.attributes.created_by:Moi OR exception-list-agnostic.attributes.created_by:Moi) AND (exception-list.attributes.name.text:Sample Endpoint OR exception-list-agnostic.attributes.name.text:Sample Endpoint) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)', + '(exception-list.attributes.created_by:Moi OR exception-list-agnostic.attributes.created_by:Moi) AND (exception-list.attributes.name.text:Sample Endpoint OR exception-list-agnostic.attributes.name.text:Sample Endpoint) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)', http: mockKibanaHttpService, namespaceTypes: 'single,agnostic', pagination: { page: 1, perPage: 20 }, @@ -228,6 +305,7 @@ describe('useExceptionLists', () => { namespaceTypes, notifications, pagination, + showEventFilters, showTrustedApps, }) => useExceptionLists({ @@ -237,6 +315,7 @@ describe('useExceptionLists', () => { namespaceTypes, notifications, pagination, + showEventFilters, showTrustedApps, }), { @@ -251,6 +330,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: false, }, } @@ -271,6 +351,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: false, }); // NOTE: Only need one call here because hook already initilaized @@ -298,6 +379,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: false, }) ); @@ -336,6 +418,7 @@ describe('useExceptionLists', () => { perPage: 20, total: 0, }, + showEventFilters: false, showTrustedApps: false, }) ); diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 37a8e8063c4ed..fa065e701184e 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -58,15 +58,14 @@ export const KBN_IS_CENTROID_FEATURE = '__kbn_is_centroid_feature__'; export const MVT_TOKEN_PARAM_NAME = 'token'; -const MAP_BASE_URL = `/${MAPS_APP_PATH}/${MAP_PATH}`; export function getNewMapPath() { - return MAP_BASE_URL; + return `/${MAPS_APP_PATH}/${MAP_PATH}`; } -export function getExistingMapPath(id: string) { - return `${MAP_BASE_URL}/${id}`; +export function getFullPath(id: string | undefined) { + return `/${MAPS_APP_PATH}${getEditPath(id)}`; } -export function getEditPath(id: string) { - return `/${MAP_PATH}/${id}`; +export function getEditPath(id: string | undefined) { + return id ? `/${MAP_PATH}/${id}` : `/${MAP_PATH}`; } export enum LAYER_TYPE { diff --git a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts index 07de57d0ac832..d1690ddfff43d 100644 --- a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts @@ -66,6 +66,7 @@ export type VectorSourceRequestMeta = MapFilters & { applyGlobalTime: boolean; fieldNames: string[]; geogridPrecision?: number; + timesiceMaskField?: string; sourceQuery?: MapQuery; sourceMeta: VectorSourceSyncMeta; }; @@ -84,6 +85,9 @@ export type VectorStyleRequestMeta = MapFilters & { export type ESSearchSourceResponseMeta = { areResultsTrimmed?: boolean; resultsCount?: number; + // results time extent, either Kibana time range or timeslider time slice + timeExtent?: Timeslice; + isTimeExtentForTimeslice?: boolean; // top hits meta areEntitiesTrimmed?: boolean; diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts index 6dd454137be7d..9bfa74825c338 100644 --- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts @@ -22,7 +22,6 @@ import { LAYER_STYLE_TYPE, FIELD_ORIGIN, } from '../../../../common/constants'; -import { isTotalHitsGreaterThan, TotalHits } from '../../../../common/elasticsearch_util'; import { ESGeoGridSource } from '../../sources/es_geo_grid_source/es_geo_grid_source'; import { canSkipSourceUpdate } from '../../util/can_skip_fetch'; import { IESSource } from '../../sources/es_source'; @@ -35,6 +34,7 @@ import { DynamicStylePropertyOptions, StylePropertyOptions, LayerDescriptor, + Timeslice, VectorLayerDescriptor, VectorSourceRequestMeta, VectorStylePropertiesDescriptor, @@ -46,10 +46,6 @@ import { isSearchSourceAbortError } from '../../sources/es_source/es_source'; const ACTIVE_COUNT_DATA_ID = 'ACTIVE_COUNT_DATA_ID'; -interface CountData { - isSyncClustered: boolean; -} - function getAggType( dynamicProperty: IDynamicStyleProperty<DynamicStylePropertyOptions> ): AGG_TYPE.AVG | AGG_TYPE.TERMS { @@ -216,7 +212,7 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { let isClustered = false; const countDataRequest = this.getDataRequest(ACTIVE_COUNT_DATA_ID); if (countDataRequest) { - const requestData = countDataRequest.getData() as CountData; + const requestData = countDataRequest.getData() as { isSyncClustered: boolean }; if (requestData && requestData.isSyncClustered) { isClustered = true; } @@ -294,7 +290,7 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { async syncData(syncContext: DataRequestContext) { const dataRequestId = ACTIVE_COUNT_DATA_ID; const requestToken = Symbol(`layer-active-count:${this.getId()}`); - const searchFilters: VectorSourceRequestMeta = this._getSearchFilters( + const searchFilters: VectorSourceRequestMeta = await this._getSearchFilters( syncContext.dataFilters, this.getSource(), this.getCurrentStyle() @@ -305,6 +301,9 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { prevDataRequest: this.getDataRequest(dataRequestId), nextMeta: searchFilters, extentAware: source.isFilterByMapBounds(), + getUpdateDueToTimeslice: (timeslice?: Timeslice) => { + return this._getUpdateDueToTimesliceFromSourceRequestMeta(source, timeslice); + }, }); let activeSource; @@ -322,22 +321,11 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { let isSyncClustered; try { syncContext.startLoading(dataRequestId, requestToken, searchFilters); - const abortController = new AbortController(); - syncContext.registerCancelCallback(requestToken, () => abortController.abort()); - const maxResultWindow = await this._documentSource.getMaxResultWindow(); - const searchSource = await this._documentSource.makeSearchSource(searchFilters, 0); - searchSource.setField('trackTotalHits', maxResultWindow + 1); - const resp = await searchSource.fetch({ - abortSignal: abortController.signal, - sessionId: syncContext.dataFilters.searchSessionId, - legacyHitsTotal: false, - }); - isSyncClustered = isTotalHitsGreaterThan( - (resp.hits.total as unknown) as TotalHits, - maxResultWindow - ); - const countData = { isSyncClustered } as CountData; - syncContext.stopLoading(dataRequestId, requestToken, countData, searchFilters); + isSyncClustered = !(await this._documentSource.canLoadAllDocuments( + searchFilters, + syncContext.registerCancelCallback.bind(null, requestToken) + )); + syncContext.stopLoading(dataRequestId, requestToken, { isSyncClustered }, searchFilters); } catch (error) { if (!(error instanceof DataRequestAbortError) || !isSearchSourceAbortError(error)) { syncContext.onLoadError(dataRequestId, requestToken, error.message); diff --git a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts index 368ff8bebcdd1..d12c8432a4191 100644 --- a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts @@ -111,6 +111,9 @@ export class HeatmapLayer extends AbstractLayer { }, syncContext, source: this.getSource(), + getUpdateDueToTimeslice: () => { + return true; + }, }); } catch (error) { if (!(error instanceof DataRequestAbortError)) { diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index be113ab4cc2c9..ef41c157a2b17 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -36,6 +36,7 @@ import { LayerDescriptor, MapExtent, StyleDescriptor, + Timeslice, } from '../../../common/descriptor_types'; import { ImmutableSourceProperty, ISource, SourceEditorArgs } from '../sources/source'; import { DataRequestContext } from '../../actions'; @@ -78,7 +79,7 @@ export interface ILayer { getMbLayerIds(): string[]; ownsMbLayerId(mbLayerId: string): boolean; ownsMbSourceId(mbSourceId: string): boolean; - syncLayerWithMB(mbMap: MbMap): void; + syncLayerWithMB(mbMap: MbMap, timeslice?: Timeslice): void; getLayerTypeIconName(): string; isInitialDataLoadComplete(): boolean; getIndexPatternIds(): string[]; diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx index 6dba935ccc87d..2ad6a5ef73c6d 100644 --- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx @@ -21,6 +21,7 @@ import { VectorLayer, VectorLayerArguments } from '../vector_layer'; import { ITiledSingleLayerVectorSource } from '../../sources/tiled_single_layer_vector_source'; import { DataRequestContext } from '../../../actions'; import { + Timeslice, VectorLayerDescriptor, VectorSourceRequestMeta, } from '../../../../common/descriptor_types'; @@ -66,7 +67,7 @@ export class TiledVectorLayer extends VectorLayer { dataFilters, }: DataRequestContext) { const requestToken: symbol = Symbol(`layer-${this.getId()}-${SOURCE_DATA_REQUEST_ID}`); - const searchFilters: VectorSourceRequestMeta = this._getSearchFilters( + const searchFilters: VectorSourceRequestMeta = await this._getSearchFilters( dataFilters, this.getSource(), this._style as IVectorStyle @@ -84,6 +85,10 @@ export class TiledVectorLayer extends VectorLayer { source: this.getSource(), prevDataRequest, nextMeta: searchFilters, + getUpdateDueToTimeslice: (timeslice?: Timeslice) => { + // TODO use meta features to determine if tiles already contain features for timeslice. + return true; + }, }); const canSkip = noChangesInSourceState && noChangesInSearchState; if (canSkip) { diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx index d305bb920b2ad..346e59f60af32 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx @@ -13,7 +13,13 @@ import { SOURCE_DATA_REQUEST_ID, VECTOR_SHAPE_TYPE, } from '../../../../common/constants'; -import { MapExtent, MapQuery, VectorSourceRequestMeta } from '../../../../common/descriptor_types'; +import { + DataMeta, + MapExtent, + MapQuery, + Timeslice, + VectorSourceRequestMeta, +} from '../../../../common/descriptor_types'; import { DataRequestContext } from '../../../actions'; import { IVectorSource } from '../../sources/vector_source'; import { DataRequestAbortError } from '../../util/data_request'; @@ -52,6 +58,7 @@ export async function syncVectorSource({ requestMeta, syncContext, source, + getUpdateDueToTimeslice, }: { layerId: string; layerName: string; @@ -59,6 +66,7 @@ export async function syncVectorSource({ requestMeta: VectorSourceRequestMeta; syncContext: DataRequestContext; source: IVectorSource; + getUpdateDueToTimeslice: (timeslice?: Timeslice) => boolean; }): Promise<{ refreshed: boolean; featureCollection: FeatureCollection }> { const { startLoading, @@ -76,6 +84,7 @@ export async function syncVectorSource({ prevDataRequest, nextMeta: requestMeta, extentAware: source.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); if (canSkipFetch) { return { @@ -104,7 +113,14 @@ export async function syncVectorSource({ ) { layerFeatureCollection.features.push(...getCentroidFeatures(layerFeatureCollection)); } - stopLoading(dataRequestId, requestToken, layerFeatureCollection, meta); + const responseMeta: DataMeta = meta ? { ...meta } : {}; + if (requestMeta.applyGlobalTime && (await source.isTimeAware())) { + const timesiceMaskField = await source.getTimesliceMaskFieldName(); + if (timesiceMaskField) { + responseMeta.timesiceMaskField = timesiceMaskField; + } + } + stopLoading(dataRequestId, requestToken, layerFeatureCollection, responseMeta); return { refreshed: true, featureCollection: layerFeatureCollection, diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index 8b4d25f4612cc..49a0878ef80b2 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -43,16 +43,19 @@ import { getFillFilterExpression, getLineFilterExpression, getPointFilterExpression, + TimesliceMaskConfig, } from '../../util/mb_filter_expressions'; import { DynamicStylePropertyOptions, MapFilters, MapQuery, + Timeslice, VectorJoinSourceRequestMeta, VectorLayerDescriptor, VectorSourceRequestMeta, VectorStyleRequestMeta, } from '../../../../common/descriptor_types'; +import { ISource } from '../../sources/source'; import { IVectorSource } from '../../sources/vector_source'; import { CustomIconAndTooltipContent, ILayer } from '../layer'; import { InnerJoin } from '../../joins/inner_join'; @@ -347,6 +350,9 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { prevDataRequest, nextMeta: searchFilters, extentAware: false, // join-sources are term-aggs that are spatially unaware (e.g. ESTermSource/TableSource). + getUpdateDueToTimeslice: () => { + return true; + }, }); if (canSkipFetch) { return { @@ -389,17 +395,22 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { return await Promise.all(joinSyncs); } - _getSearchFilters( + async _getSearchFilters( dataFilters: MapFilters, source: IVectorSource, style: IVectorStyle - ): VectorSourceRequestMeta { + ): Promise<VectorSourceRequestMeta> { const fieldNames = [ ...source.getFieldNames(), ...style.getSourceFieldNames(), ...this.getValidJoins().map((join) => join.getLeftField().getName()), ]; + const timesliceMaskFieldName = await source.getTimesliceMaskFieldName(); + if (timesliceMaskFieldName) { + fieldNames.push(timesliceMaskFieldName); + } + const sourceQuery = this.getQuery() as MapQuery; return { ...dataFilters, @@ -674,9 +685,12 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { layerId: this.getId(), layerName: await this.getDisplayName(source), prevDataRequest: this.getSourceDataRequest(), - requestMeta: this._getSearchFilters(syncContext.dataFilters, source, style), + requestMeta: await this._getSearchFilters(syncContext.dataFilters, source, style), syncContext, source, + getUpdateDueToTimeslice: (timeslice?: Timeslice) => { + return this._getUpdateDueToTimesliceFromSourceRequestMeta(source, timeslice); + }, }); await this._syncSupportsFeatureEditing({ syncContext, source }); if ( @@ -754,7 +768,11 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { } } - _setMbPointsProperties(mbMap: MbMap, mvtSourceLayer?: string) { + _setMbPointsProperties( + mbMap: MbMap, + mvtSourceLayer?: string, + timesliceMaskConfig?: TimesliceMaskConfig + ) { const pointLayerId = this._getMbPointLayerId(); const symbolLayerId = this._getMbSymbolLayerId(); const pointLayer = mbMap.getLayer(pointLayerId); @@ -771,7 +789,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { if (symbolLayer) { mbMap.setLayoutProperty(symbolLayerId, 'visibility', 'none'); } - this._setMbCircleProperties(mbMap, mvtSourceLayer); + this._setMbCircleProperties(mbMap, mvtSourceLayer, timesliceMaskConfig); } else { markerLayerId = symbolLayerId; textLayerId = symbolLayerId; @@ -779,7 +797,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { mbMap.setLayoutProperty(pointLayerId, 'visibility', 'none'); mbMap.setLayoutProperty(this._getMbTextLayerId(), 'visibility', 'none'); } - this._setMbSymbolProperties(mbMap, mvtSourceLayer); + this._setMbSymbolProperties(mbMap, mvtSourceLayer, timesliceMaskConfig); } this.syncVisibilityWithMb(mbMap, markerLayerId); @@ -790,7 +808,11 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { } } - _setMbCircleProperties(mbMap: MbMap, mvtSourceLayer?: string) { + _setMbCircleProperties( + mbMap: MbMap, + mvtSourceLayer?: string, + timesliceMaskConfig?: TimesliceMaskConfig + ) { const sourceId = this.getId(); const pointLayerId = this._getMbPointLayerId(); const pointLayer = mbMap.getLayer(pointLayerId); @@ -822,7 +844,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { mbMap.addLayer(mbLayer); } - const filterExpr = getPointFilterExpression(this.hasJoins()); + const filterExpr = getPointFilterExpression(this.hasJoins(), timesliceMaskConfig); if (!_.isEqual(filterExpr, mbMap.getFilter(pointLayerId))) { mbMap.setFilter(pointLayerId, filterExpr); mbMap.setFilter(textLayerId, filterExpr); @@ -841,7 +863,11 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { }); } - _setMbSymbolProperties(mbMap: MbMap, mvtSourceLayer?: string) { + _setMbSymbolProperties( + mbMap: MbMap, + mvtSourceLayer?: string, + timesliceMaskConfig?: TimesliceMaskConfig + ) { const sourceId = this.getId(); const symbolLayerId = this._getMbSymbolLayerId(); const symbolLayer = mbMap.getLayer(symbolLayerId); @@ -858,7 +884,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { mbMap.addLayer(mbLayer); } - const filterExpr = getPointFilterExpression(this.hasJoins()); + const filterExpr = getPointFilterExpression(this.hasJoins(), timesliceMaskConfig); if (!_.isEqual(filterExpr, mbMap.getFilter(symbolLayerId))) { mbMap.setFilter(symbolLayerId, filterExpr); } @@ -876,7 +902,11 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { }); } - _setMbLinePolygonProperties(mbMap: MbMap, mvtSourceLayer?: string) { + _setMbLinePolygonProperties( + mbMap: MbMap, + mvtSourceLayer?: string, + timesliceMaskConfig?: TimesliceMaskConfig + ) { const sourceId = this.getId(); const fillLayerId = this._getMbPolygonLayerId(); const lineLayerId = this._getMbLineLayerId(); @@ -940,14 +970,14 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { this.syncVisibilityWithMb(mbMap, fillLayerId); mbMap.setLayerZoomRange(fillLayerId, this.getMinZoom(), this.getMaxZoom()); - const fillFilterExpr = getFillFilterExpression(hasJoins); + const fillFilterExpr = getFillFilterExpression(hasJoins, timesliceMaskConfig); if (!_.isEqual(fillFilterExpr, mbMap.getFilter(fillLayerId))) { mbMap.setFilter(fillLayerId, fillFilterExpr); } this.syncVisibilityWithMb(mbMap, lineLayerId); mbMap.setLayerZoomRange(lineLayerId, this.getMinZoom(), this.getMaxZoom()); - const lineFilterExpr = getLineFilterExpression(hasJoins); + const lineFilterExpr = getLineFilterExpression(hasJoins, timesliceMaskConfig); if (!_.isEqual(lineFilterExpr, mbMap.getFilter(lineLayerId))) { mbMap.setFilter(lineLayerId, lineFilterExpr); } @@ -956,7 +986,11 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { mbMap.setLayerZoomRange(tooManyFeaturesLayerId, this.getMinZoom(), this.getMaxZoom()); } - _setMbCentroidProperties(mbMap: MbMap, mvtSourceLayer?: string) { + _setMbCentroidProperties( + mbMap: MbMap, + mvtSourceLayer?: string, + timesliceMaskConfig?: TimesliceMaskConfig + ) { const centroidLayerId = this._getMbCentroidLayerId(); const centroidLayer = mbMap.getLayer(centroidLayerId); if (!centroidLayer) { @@ -971,7 +1005,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { mbMap.addLayer(mbLayer); } - const filterExpr = getCentroidFilterExpression(this.hasJoins()); + const filterExpr = getCentroidFilterExpression(this.hasJoins(), timesliceMaskConfig); if (!_.isEqual(filterExpr, mbMap.getFilter(centroidLayerId))) { mbMap.setFilter(centroidLayerId, filterExpr); } @@ -986,17 +1020,32 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { mbMap.setLayerZoomRange(centroidLayerId, this.getMinZoom(), this.getMaxZoom()); } - _syncStylePropertiesWithMb(mbMap: MbMap) { - this._setMbPointsProperties(mbMap); - this._setMbLinePolygonProperties(mbMap); + _syncStylePropertiesWithMb(mbMap: MbMap, timeslice?: Timeslice) { + const timesliceMaskConfig = this._getTimesliceMaskConfig(timeslice); + this._setMbPointsProperties(mbMap, undefined, timesliceMaskConfig); + this._setMbLinePolygonProperties(mbMap, undefined, timesliceMaskConfig); // centroid layers added after polygon layers to ensure they are on top of polygon layers - this._setMbCentroidProperties(mbMap); + this._setMbCentroidProperties(mbMap, undefined, timesliceMaskConfig); } - syncLayerWithMB(mbMap: MbMap) { + _getTimesliceMaskConfig(timeslice?: Timeslice): TimesliceMaskConfig | undefined { + if (!timeslice || this.hasJoins()) { + return; + } + + const prevMeta = this.getSourceDataRequest()?.getMeta(); + return prevMeta !== undefined && prevMeta.timesiceMaskField !== undefined + ? { + timesiceMaskField: prevMeta.timesiceMaskField, + timeslice, + } + : undefined; + } + + syncLayerWithMB(mbMap: MbMap, timeslice?: Timeslice) { addGeoJsonMbSource(this._getMbSourceId(), this.getMbLayerIds(), mbMap); this._syncFeatureCollectionWithMb(mbMap); - this._syncStylePropertiesWithMb(mbMap); + this._syncStylePropertiesWithMb(mbMap, timeslice); } _getMbPointLayerId() { @@ -1094,6 +1143,15 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { return await this._source.getLicensedFeatures(); } + _getUpdateDueToTimesliceFromSourceRequestMeta(source: ISource, timeslice?: Timeslice) { + const prevDataRequest = this.getSourceDataRequest(); + const prevMeta = prevDataRequest?.getMeta(); + if (!prevMeta) { + return true; + } + return source.getUpdateDueToTimeslice(prevMeta, timeslice); + } + async addFeature(geometry: Geometry | Position[]) { const layerSource = this.getSource(); await layerSource.addFeature(geometry); diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index a51e291574b70..9f7bd1260ca22 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -12,13 +12,19 @@ import { i18n } from '@kbn/i18n'; import { IFieldType, IndexPattern } from 'src/plugins/data/public'; import { GeoJsonProperties, Geometry, Position } from 'geojson'; import { AbstractESSource } from '../es_source'; -import { getHttp, getMapAppConfig, getSearchService } from '../../../kibana_services'; +import { + getHttp, + getMapAppConfig, + getSearchService, + getTimeFilter, +} from '../../../kibana_services'; import { addFieldToDSL, getField, hitsToGeoJson, isTotalHitsGreaterThan, PreIndexedShape, + TotalHits, } from '../../../../common/elasticsearch_util'; // @ts-expect-error import { UpdateSourceEditor } from './update_source_editor'; @@ -41,11 +47,14 @@ import { DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants'; import { ESDocField } from '../../fields/es_doc_field'; import { registerSource } from '../source_registry'; import { + DataMeta, ESSearchSourceDescriptor, + Timeslice, VectorSourceRequestMeta, VectorSourceSyncMeta, } from '../../../../common/descriptor_types'; import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters'; +import { TimeRange } from '../../../../../../../src/plugins/data/common'; import { ImmutableSourceProperty, SourceEditorArgs } from '../source'; import { IField } from '../../fields/field'; import { GeoJsonWithMeta, SourceTooltipConfig } from '../vector_source'; @@ -59,6 +68,16 @@ import { getDocValueAndSourceFields, ScriptField } from './util/get_docvalue_sou import { ITiledSingleLayerMvtParams } from '../tiled_single_layer_vector_source/tiled_single_layer_vector_source'; import { addFeatureToIndex, getMatchingIndexes } from './util/feature_edit'; +export function timerangeToTimeextent(timerange: TimeRange): Timeslice | undefined { + const timeRangeBounds = getTimeFilter().calculateBounds(timerange); + return timeRangeBounds.min !== undefined && timeRangeBounds.max !== undefined + ? { + from: timeRangeBounds.min.valueOf(), + to: timeRangeBounds.max.valueOf(), + } + : undefined; +} + export const sourceTitle = i18n.translate('xpack.maps.source.esSearchTitle', { defaultMessage: 'Documents', }); @@ -338,7 +357,6 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye async _getSearchHits( layerName: string, searchFilters: VectorSourceRequestMeta, - maxResultWindow: number, registerCancelCallback: (callback: () => void) => void ) { const indexPattern = await this.getIndexPattern(); @@ -350,8 +368,18 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye ); const initialSearchContext = { docvalue_fields: docValueFields }; // Request fields in docvalue_fields insted of _source + + // Use Kibana global time extent instead of timeslice extent when all documents for global time extent can be loaded + // to allow for client-side masking of timeslice + const searchFiltersWithoutTimeslice = { ...searchFilters }; + delete searchFiltersWithoutTimeslice.timeslice; + const useSearchFiltersWithoutTimeslice = + searchFilters.timeslice !== undefined && + (await this.canLoadAllDocuments(searchFiltersWithoutTimeslice, registerCancelCallback)); + + const maxResultWindow = await this.getMaxResultWindow(); const searchSource = await this.makeSearchSource( - searchFilters, + useSearchFiltersWithoutTimeslice ? searchFiltersWithoutTimeslice : searchFilters, maxResultWindow, initialSearchContext ); @@ -375,11 +403,17 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye searchSessionId: searchFilters.searchSessionId, }); + const isTimeExtentForTimeslice = + searchFilters.timeslice !== undefined && !useSearchFiltersWithoutTimeslice; return { hits: resp.hits.hits.reverse(), // Reverse hits so top documents by sort are drawn on top meta: { resultsCount: resp.hits.hits.length, areResultsTrimmed: isTotalHitsGreaterThan(resp.hits.total, resp.hits.hits.length), + timeExtent: isTimeExtentForTimeslice + ? searchFilters.timeslice + : timerangeToTimeextent(searchFilters.timeFilters), + isTimeExtentForTimeslice, }, }; } @@ -424,16 +458,9 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye ): Promise<GeoJsonWithMeta> { const indexPattern = await this.getIndexPattern(); - const indexSettings = await loadIndexSettings(indexPattern.title); - const { hits, meta } = this._isTopHits() ? await this._getTopHits(layerName, searchFilters, registerCancelCallback) - : await this._getSearchHits( - layerName, - searchFilters, - indexSettings.maxResultWindow, - registerCancelCallback - ); + : await this._getSearchHits(layerName, searchFilters, registerCancelCallback); const unusedMetaFields = indexPattern.metaFields.filter((metaField) => { return !['_id', '_index'].includes(metaField); @@ -743,6 +770,62 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye : urlTemplate, }; } + + async getTimesliceMaskFieldName(): Promise<string | null> { + if (this._isTopHits() || this._descriptor.scalingType === SCALING_TYPES.MVT) { + return null; + } + + const indexPattern = await this.getIndexPattern(); + return indexPattern.timeFieldName ? indexPattern.timeFieldName : null; + } + + getUpdateDueToTimeslice(prevMeta: DataMeta, timeslice?: Timeslice): boolean { + if (this._isTopHits() || this._descriptor.scalingType === SCALING_TYPES.MVT) { + return true; + } + + if ( + prevMeta.timeExtent === undefined || + prevMeta.areResultsTrimmed === undefined || + prevMeta.areResultsTrimmed + ) { + return true; + } + + const isTimeExtentForTimeslice = + prevMeta.isTimeExtentForTimeslice !== undefined ? prevMeta.isTimeExtentForTimeslice : false; + if (!timeslice) { + return isTimeExtentForTimeslice + ? // Previous request only covers timeslice extent. Will need to re-fetch data to cover global time extent + true + : // Previous request covers global time extent. + // No need to re-fetch data since previous request already has data for the entire global time extent. + false; + } + + const isWithin = isTimeExtentForTimeslice + ? timeslice.from >= prevMeta.timeExtent.from && timeslice.to <= prevMeta.timeExtent.to + : true; + return !isWithin; + } + + async canLoadAllDocuments( + searchFilters: VectorSourceRequestMeta, + registerCancelCallback: (callback: () => void) => void + ) { + const abortController = new AbortController(); + registerCancelCallback(() => abortController.abort()); + const maxResultWindow = await this.getMaxResultWindow(); + const searchSource = await this.makeSearchSource(searchFilters, 0); + searchSource.setField('trackTotalHits', maxResultWindow + 1); + const resp = await searchSource.fetch({ + abortSignal: abortController.signal, + sessionId: searchFilters.searchSessionId, + legacyHitsTotal: false, + }); + return !isTotalHitsGreaterThan((resp.hits.total as unknown) as TotalHits, maxResultWindow); + } } registerSource({ diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx index d58e71db2a9ab..5bf7a2e47cc66 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx @@ -228,6 +228,10 @@ export class MVTSingleLayerVectorSource return tooltips; } + async getTimesliceMaskFieldName() { + return null; + } + async supportsFeatureEditing(): Promise<boolean> { return false; } diff --git a/x-pack/plugins/maps/public/classes/sources/source.ts b/x-pack/plugins/maps/public/classes/sources/source.ts index 7a8fca337fd2e..0ecbde06cf3e2 100644 --- a/x-pack/plugins/maps/public/classes/sources/source.ts +++ b/x-pack/plugins/maps/public/classes/sources/source.ts @@ -13,7 +13,12 @@ import { GeoJsonProperties } from 'geojson'; import { copyPersistentState } from '../../reducers/copy_persistent_state'; import { IField } from '../fields/field'; import { FieldFormatter, LAYER_TYPE, MAX_ZOOM, MIN_ZOOM } from '../../../common/constants'; -import { AbstractSourceDescriptor, Attribution } from '../../../common/descriptor_types'; +import { + AbstractSourceDescriptor, + Attribution, + DataMeta, + Timeslice, +} from '../../../common/descriptor_types'; import { LICENSED_FEATURES } from '../../licensed_features'; import { PreIndexedShape } from '../../../common/elasticsearch_util'; @@ -64,6 +69,7 @@ export interface ISource { getMinZoom(): number; getMaxZoom(): number; getLicensedFeatures(): Promise<LICENSED_FEATURES[]>; + getUpdateDueToTimeslice(prevMeta: DataMeta, timeslice?: Timeslice): boolean; } export class AbstractSource implements ISource { @@ -194,4 +200,8 @@ export class AbstractSource implements ISource { async getLicensedFeatures(): Promise<LICENSED_FEATURES[]> { return []; } + + getUpdateDueToTimeslice(prevMeta: DataMeta, timeslice?: Timeslice): boolean { + return true; + } } diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx index 1194d571e344b..8f93de705e365 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx @@ -66,6 +66,7 @@ export interface IVectorSource extends ISource { getSupportedShapeTypes(): Promise<VECTOR_SHAPE_TYPE[]>; isBoundsAware(): boolean; getSourceTooltipContent(sourceDataRequest?: DataRequest): SourceTooltipConfig; + getTimesliceMaskFieldName(): Promise<string | null>; supportsFeatureEditing(): Promise<boolean>; addFeature(geometry: Geometry | Position[]): Promise<void>; } @@ -156,6 +157,10 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc return null; } + async getTimesliceMaskFieldName(): Promise<string | null> { + return null; + } + async addFeature(geometry: Geometry | Position[]) { throw new Error('Should implement VectorSource#addFeature'); } diff --git a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.js b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.js index c13b2fd441cad..da3cbb9055d43 100644 --- a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.js +++ b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.js @@ -82,6 +82,9 @@ describe('updateDueToExtent', () => { describe('canSkipSourceUpdate', () => { const SOURCE_DATA_REQUEST_ID = 'foo'; + const getUpdateDueToTimeslice = () => { + return true; + }; describe('isQueryAware', () => { const queryAwareSourceMock = { @@ -136,6 +139,7 @@ describe('canSkipSourceUpdate', () => { prevDataRequest, nextMeta, extentAware: queryAwareSourceMock.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(true); @@ -156,6 +160,7 @@ describe('canSkipSourceUpdate', () => { prevDataRequest, nextMeta, extentAware: queryAwareSourceMock.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(true); @@ -176,6 +181,7 @@ describe('canSkipSourceUpdate', () => { prevDataRequest, nextMeta, extentAware: queryAwareSourceMock.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -193,6 +199,7 @@ describe('canSkipSourceUpdate', () => { prevDataRequest, nextMeta, extentAware: queryAwareSourceMock.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -224,6 +231,7 @@ describe('canSkipSourceUpdate', () => { prevDataRequest, nextMeta, extentAware: queryAwareSourceMock.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -244,6 +252,7 @@ describe('canSkipSourceUpdate', () => { prevDataRequest, nextMeta, extentAware: queryAwareSourceMock.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -264,6 +273,7 @@ describe('canSkipSourceUpdate', () => { prevDataRequest, nextMeta, extentAware: queryAwareSourceMock.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -281,6 +291,7 @@ describe('canSkipSourceUpdate', () => { prevDataRequest, nextMeta, extentAware: queryAwareSourceMock.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -327,6 +338,7 @@ describe('canSkipSourceUpdate', () => { applyGlobalTime: false, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -346,6 +358,7 @@ describe('canSkipSourceUpdate', () => { applyGlobalTime: true, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(true); @@ -375,6 +388,7 @@ describe('canSkipSourceUpdate', () => { }, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -402,6 +416,7 @@ describe('canSkipSourceUpdate', () => { }, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(true); @@ -429,6 +444,7 @@ describe('canSkipSourceUpdate', () => { }, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(true); @@ -463,6 +479,7 @@ describe('canSkipSourceUpdate', () => { }, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -498,6 +515,7 @@ describe('canSkipSourceUpdate', () => { }, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -529,6 +547,7 @@ describe('canSkipSourceUpdate', () => { }, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -564,6 +583,7 @@ describe('canSkipSourceUpdate', () => { }, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(true); @@ -599,6 +619,7 @@ describe('canSkipSourceUpdate', () => { }, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(true); diff --git a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts index 1f2678f40eecd..b6f03ef3d1c63 100644 --- a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts +++ b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts @@ -10,7 +10,7 @@ import turfBboxPolygon from '@turf/bbox-polygon'; import turfBooleanContains from '@turf/boolean-contains'; import { isRefreshOnlyQuery } from './is_refresh_only_query'; import { ISource } from '../sources/source'; -import { DataMeta } from '../../../common/descriptor_types'; +import { DataMeta, Timeslice } from '../../../common/descriptor_types'; import { DataRequest } from './data_request'; const SOURCE_UPDATE_REQUIRED = true; @@ -56,11 +56,13 @@ export async function canSkipSourceUpdate({ prevDataRequest, nextMeta, extentAware, + getUpdateDueToTimeslice, }: { source: ISource; prevDataRequest: DataRequest | undefined; nextMeta: DataMeta; extentAware: boolean; + getUpdateDueToTimeslice: (timeslice?: Timeslice) => boolean; }): Promise<boolean> { const timeAware = await source.isTimeAware(); const refreshTimerAware = await source.isRefreshTimerAware(); @@ -94,7 +96,9 @@ export async function canSkipSourceUpdate({ updateDueToApplyGlobalTime = prevMeta.applyGlobalTime !== nextMeta.applyGlobalTime; if (nextMeta.applyGlobalTime) { updateDueToTime = !_.isEqual(prevMeta.timeFilters, nextMeta.timeFilters); - updateDueToTimeslice = !_.isEqual(prevMeta.timeslice, nextMeta.timeslice); + if (!_.isEqual(prevMeta.timeslice, nextMeta.timeslice)) { + updateDueToTimeslice = getUpdateDueToTimeslice(nextMeta.timeslice); + } } } diff --git a/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts index f5df741759cb3..6a193216c7c1e 100644 --- a/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts +++ b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts @@ -12,67 +12,110 @@ import { KBN_TOO_MANY_FEATURES_PROPERTY, } from '../../../common/constants'; +import { Timeslice } from '../../../common/descriptor_types'; + +export interface TimesliceMaskConfig { + timesiceMaskField: string; + timeslice: Timeslice; +} + export const EXCLUDE_TOO_MANY_FEATURES_BOX = ['!=', ['get', KBN_TOO_MANY_FEATURES_PROPERTY], true]; const EXCLUDE_CENTROID_FEATURES = ['!=', ['get', KBN_IS_CENTROID_FEATURE], true]; -function getFilterExpression(geometryFilter: unknown[], hasJoins: boolean) { - const filters: unknown[] = [ - EXCLUDE_TOO_MANY_FEATURES_BOX, - EXCLUDE_CENTROID_FEATURES, - geometryFilter, - ]; +function getFilterExpression( + filters: unknown[], + hasJoins: boolean, + timesliceMaskConfig?: TimesliceMaskConfig +) { + const allFilters: unknown[] = [...filters]; if (hasJoins) { - filters.push(['==', ['get', FEATURE_VISIBLE_PROPERTY_NAME], true]); + allFilters.push(['==', ['get', FEATURE_VISIBLE_PROPERTY_NAME], true]); } - return ['all', ...filters]; + if (timesliceMaskConfig) { + allFilters.push(['has', timesliceMaskConfig.timesiceMaskField]); + allFilters.push([ + '>=', + ['get', timesliceMaskConfig.timesiceMaskField], + timesliceMaskConfig.timeslice.from, + ]); + allFilters.push([ + '<', + ['get', timesliceMaskConfig.timesiceMaskField], + timesliceMaskConfig.timeslice.to, + ]); + } + + return ['all', ...allFilters]; } -export function getFillFilterExpression(hasJoins: boolean): unknown[] { +export function getFillFilterExpression( + hasJoins: boolean, + timesliceMaskConfig?: TimesliceMaskConfig +): unknown[] { return getFilterExpression( [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], + EXCLUDE_TOO_MANY_FEATURES_BOX, + EXCLUDE_CENTROID_FEATURES, + [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], + ], ], - hasJoins + hasJoins, + timesliceMaskConfig ); } -export function getLineFilterExpression(hasJoins: boolean): unknown[] { +export function getLineFilterExpression( + hasJoins: boolean, + timesliceMaskConfig?: TimesliceMaskConfig +): unknown[] { return getFilterExpression( [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING], + EXCLUDE_TOO_MANY_FEATURES_BOX, + EXCLUDE_CENTROID_FEATURES, + [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING], + ], ], - hasJoins + hasJoins, + timesliceMaskConfig ); } -export function getPointFilterExpression(hasJoins: boolean): unknown[] { +export function getPointFilterExpression( + hasJoins: boolean, + timesliceMaskConfig?: TimesliceMaskConfig +): unknown[] { return getFilterExpression( [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POINT], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT], + EXCLUDE_TOO_MANY_FEATURES_BOX, + EXCLUDE_CENTROID_FEATURES, + [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POINT], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT], + ], ], - hasJoins + hasJoins, + timesliceMaskConfig ); } -export function getCentroidFilterExpression(hasJoins: boolean): unknown[] { - const filters: unknown[] = [ - EXCLUDE_TOO_MANY_FEATURES_BOX, - ['==', ['get', KBN_IS_CENTROID_FEATURE], true], - ]; - - if (hasJoins) { - filters.push(['==', ['get', FEATURE_VISIBLE_PROPERTY_NAME], true]); - } - - return ['all', ...filters]; +export function getCentroidFilterExpression( + hasJoins: boolean, + timesliceMaskConfig?: TimesliceMaskConfig +): unknown[] { + return getFilterExpression( + [EXCLUDE_TOO_MANY_FEATURES_BOX, ['==', ['get', KBN_IS_CENTROID_FEATURE], true]], + hasJoins, + timesliceMaskConfig + ); } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_circle.ts b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_circle.ts index f0df797582bef..998329a78bfbb 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_circle.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_circle.ts @@ -11,7 +11,11 @@ import turfDistance from '@turf/distance'; // @ts-expect-error import turfCircle from '@turf/circle'; -import { Position } from 'geojson'; +import { Feature, GeoJSON, Position } from 'geojson'; + +const DRAW_CIRCLE_RADIUS = 'draw-circle-radius'; + +export const DRAW_CIRCLE_RADIUS_MB_FILTER = ['==', 'meta', DRAW_CIRCLE_RADIUS]; export interface DrawCircleProperties { center: Position; @@ -22,10 +26,12 @@ type DrawCircleState = { circle: { properties: Omit<DrawCircleProperties, 'center'> & { center: Position | null; + edge: Position | null; + radiusKm: number; }; id: string | number; incomingCoords: (coords: unknown[]) => void; - toGeoJSON: () => unknown; + toGeoJSON: () => GeoJSON; }; }; @@ -43,6 +49,7 @@ export const DrawCircle = { type: 'Feature', properties: { center: null, + edge: null, radiusKm: 0, }, geometry: { @@ -96,6 +103,7 @@ export const DrawCircle = { } const mouseLocation = [e.lngLat.lng, e.lngLat.lat]; + state.circle.properties.edge = mouseLocation; state.circle.properties.radiusKm = turfDistance(state.circle.properties.center, mouseLocation); const newCircleFeature = turfCircle( state.circle.properties.center, @@ -124,15 +132,53 @@ export const DrawCircle = { this.changeMode('simple_select', {}, { silent: true }); } }, - toDisplayFeatures( - state: DrawCircleState, - geojson: { properties: { active: string } }, - display: (geojson: unknown) => unknown - ) { - if (state.circle.properties.center) { - geojson.properties.active = 'true'; - return display(geojson); + toDisplayFeatures(state: DrawCircleState, geojson: Feature, display: (geojson: Feature) => void) { + if (!state.circle.properties.center || !state.circle.properties.edge) { + return null; + } + + geojson.properties!.active = 'true'; + + let radiusLabel = ''; + if (state.circle.properties.radiusKm <= 1) { + radiusLabel = `${Math.round(state.circle.properties.radiusKm * 1000)} m`; + } else if (state.circle.properties.radiusKm <= 10) { + radiusLabel = `${state.circle.properties.radiusKm.toFixed(1)} km`; + } else { + radiusLabel = `${Math.round(state.circle.properties.radiusKm)} km`; } + + // display radius label, requires custom 'symbol' style with DRAW_CIRCLE_RADIUS_MB_FILTER filter + display({ + type: 'Feature', + properties: { + meta: DRAW_CIRCLE_RADIUS, + parent: state.circle.id, + radiusLabel, + active: 'false', + }, + geometry: { + type: 'Point', + coordinates: state.circle.properties.edge, + }, + }); + + // display line from center vertex to edge + display({ + type: 'Feature', + properties: { + meta: 'draw-circle-radius-line', + parent: state.circle.id, + active: 'true', + }, + geometry: { + type: 'LineString', + coordinates: [state.circle.properties.center, state.circle.properties.edge], + }, + }); + + // display circle + display(geojson); }, onTrash(state: DrawCircleState) { // @ts-ignore diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx index 879bd85dd6019..5d9cb59bbe522 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/draw_control/draw_control.tsx @@ -14,9 +14,11 @@ import DrawRectangle from 'mapbox-gl-draw-rectangle-mode'; import type { Map as MbMap } from '@kbn/mapbox-gl'; import { Feature } from 'geojson'; import { DRAW_SHAPE } from '../../../../common/constants'; -import { DrawCircle } from './draw_circle'; +import { DrawCircle, DRAW_CIRCLE_RADIUS_MB_FILTER } from './draw_circle'; import { DrawTooltip } from './draw_tooltip'; +const GL_DRAW_RADIUS_LABEL_LAYER_ID = 'gl-draw-radius-label'; + const mbModeEquivalencies = new Map<string, DRAW_SHAPE>([ ['simple_select', DRAW_SHAPE.SIMPLE_SELECT], ['draw_rectangle', DRAW_SHAPE.BOUNDS], @@ -94,6 +96,7 @@ export class DrawControl extends Component<Props> { this.props.mbMap.getCanvas().style.cursor = ''; this.props.mbMap.off('draw.modechange', this._onModeChange); this.props.mbMap.off('draw.create', this._onDraw); + this.props.mbMap.removeLayer(GL_DRAW_RADIUS_LABEL_LAYER_ID); this.props.mbMap.removeControl(this._mbDrawControl); this._mbDrawControlAdded = false; } @@ -105,6 +108,25 @@ export class DrawControl extends Component<Props> { if (!this._mbDrawControlAdded) { this.props.mbMap.addControl(this._mbDrawControl); + this.props.mbMap.addLayer({ + id: GL_DRAW_RADIUS_LABEL_LAYER_ID, + type: 'symbol', + source: 'mapbox-gl-draw-hot', + filter: DRAW_CIRCLE_RADIUS_MB_FILTER, + layout: { + 'text-anchor': 'right', + 'text-field': '{radiusLabel}', + 'text-size': 16, + 'text-offset': [-1, 0], + 'text-ignore-placement': true, + 'text-allow-overlap': true, + }, + paint: { + 'text-color': '#fbb03b', + 'text-halo-color': 'rgba(255, 255, 255, 1)', + 'text-halo-width': 2, + }, + }); this._mbDrawControlAdded = true; this.props.mbMap.getCanvas().style.cursor = 'crosshair'; this.props.mbMap.on('draw.modechange', this._onModeChange); diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/index.ts index 4f94cbc7b7458..b9b4b184318f5 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/index.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/index.ts @@ -27,6 +27,7 @@ import { getMapSettings, getScrollZoom, getSpatialFiltersLayer, + getTimeslice, } from '../../selectors/map_selectors'; import { getDrawMode, getIsFullScreen } from '../../selectors/ui_selectors'; import { getInspectorAdapters } from '../../reducers/non_serializable_instances'; @@ -43,6 +44,7 @@ function mapStateToProps(state: MapStoreState) { inspectorAdapters: getInspectorAdapters(state), scrollZoom: getScrollZoom(state), isFullScreen: getIsFullScreen(state), + timeslice: getTimeslice(state), featureModeActive: getDrawMode(state) === DRAW_MODE.DRAW_SHAPES || getDrawMode(state) === DRAW_MODE.DRAW_POINTS, filterModeActive: getDrawMode(state) === DRAW_MODE.DRAW_FILTERS, diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx index 96ff7b7dcf882..2ce4e2d98ce5f 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx @@ -25,7 +25,7 @@ import { getInitialView } from './get_initial_view'; import { getPreserveDrawingBuffer } from '../../kibana_services'; import { ILayer } from '../../classes/layers/layer'; import { MapSettings } from '../../reducers/map'; -import { Goto, MapCenterAndZoom } from '../../../common/descriptor_types'; +import { Goto, MapCenterAndZoom, Timeslice } from '../../../common/descriptor_types'; import { DECIMAL_DEGREES_PRECISION, KBN_TOO_MANY_FEATURES_IMAGE_ID, @@ -68,13 +68,12 @@ export interface Props { onSingleValueTrigger?: (actionId: string, key: string, value: RawValue) => void; renderTooltipContent?: RenderToolTipContent; setAreTilesLoaded: (layerId: string, areTilesLoaded: boolean) => void; + timeslice?: Timeslice; featureModeActive: boolean; filterModeActive: boolean; } interface State { - prevLayerList: ILayer[] | undefined; - hasSyncedLayerList: boolean; mbMap: MapboxMap | undefined; } @@ -83,38 +82,23 @@ export class MBMap extends Component<Props, State> { private _isMounted: boolean = false; private _containerRef: HTMLDivElement | null = null; private _prevDisableInteractive?: boolean; + private _prevLayerList?: ILayer[]; + private _prevTimeslice?: Timeslice; private _navigationControl = new mapboxgl.NavigationControl({ showCompass: false }); private _tileStatusTracker?: TileStatusTracker; state: State = { - prevLayerList: undefined, - hasSyncedLayerList: false, mbMap: undefined, }; - static getDerivedStateFromProps(nextProps: Props, prevState: State) { - const nextLayerList = nextProps.layerList; - if (nextLayerList !== prevState.prevLayerList) { - return { - prevLayerList: nextLayerList, - hasSyncedLayerList: false, - }; - } - - return null; - } - componentDidMount() { this._initializeMap(); this._isMounted = true; } componentDidUpdate() { - if (this.state.mbMap) { - // do not debounce syncing of map-state - this._syncMbMapWithMapState(); - this._debouncedSync(); - } + this._syncMbMapWithMapState(); // do not debounce syncing of map-state + this._debouncedSync(); } componentWillUnmount() { @@ -134,16 +118,13 @@ export class MBMap extends Component<Props, State> { _debouncedSync = _.debounce(() => { if (this._isMounted && this.props.isMapReady && this.state.mbMap) { - if (!this.state.hasSyncedLayerList) { - this.setState( - { - hasSyncedLayerList: true, - }, - () => { - this._syncMbMapWithLayerList(); - this._syncMbMapWithInspector(); - } - ); + const hasLayerListChanged = this._prevLayerList !== this.props.layerList; // Comparing re-select memoized instance so no deep equals needed + const hasTimesliceChanged = !_.isEqual(this._prevTimeslice, this.props.timeslice); + if (hasLayerListChanged || hasTimesliceChanged) { + this._prevLayerList = this.props.layerList; + this._prevTimeslice = this.props.timeslice; + this._syncMbMapWithLayerList(); + this._syncMbMapWithInspector(); } this.props.spatialFiltersLayer.syncLayerWithMB(this.state.mbMap); this._syncSettings(); @@ -346,7 +327,9 @@ export class MBMap extends Component<Props, State> { this.props.layerList, this.props.spatialFiltersLayer ); - this.props.layerList.forEach((layer) => layer.syncLayerWithMB(this.state.mbMap!)); + this.props.layerList.forEach((layer) => + layer.syncLayerWithMB(this.state.mbMap!, this.props.timeslice) + ); syncLayerOrder(this.state.mbMap, this.props.spatialFiltersLayer, this.props.layerList); }; diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index 5a477754683e6..509cece671dd6 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -54,9 +54,9 @@ import { } from '../selectors/map_selectors'; import { APP_ID, - getExistingMapPath, + getEditPath, + getFullPath, MAP_SAVED_OBJECT_TYPE, - MAP_PATH, RawValue, } from '../../common/constants'; import { RenderToolTipContent } from '../classes/tooltips/tooltip_property'; @@ -180,13 +180,13 @@ export class MapEmbeddable : ''; const input = this.getInput(); const title = input.hidePanelTitles ? '' : input.title || savedMapTitle; - const savedObjectId = (input as MapByReferenceInput).savedObjectId; + const savedObjectId = 'savedObjectId' in input ? input.savedObjectId : undefined; this.updateOutput({ ...this.getOutput(), defaultTitle: savedMapTitle, title, - editPath: `/${MAP_PATH}/${savedObjectId}`, - editUrl: getHttp().basePath.prepend(getExistingMapPath(savedObjectId)), + editPath: getEditPath(savedObjectId), + editUrl: getHttp().basePath.prepend(getFullPath(savedObjectId)), indexPatterns: await this._getIndexPatterns(), }); } diff --git a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts index eff49c1b1242e..cc0ed19db0b40 100644 --- a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts +++ b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts @@ -6,40 +6,22 @@ */ import { suggestEMSTermJoinConfig } from './ems_autosuggest'; -import { FeatureCollection } from 'geojson'; class MockFileLayer { - private readonly _url: string; private readonly _id: string; private readonly _fields: Array<{ id: string }>; - constructor(url: string, fields: Array<{ id: string }>) { - this._url = url; - this._id = url; + constructor(id: string, fields: Array<{ id: string; alias?: string[]; values?: string[] }>) { + this._id = id; this._fields = fields; } - getFields() { - return this._fields; + getId() { + return this._id; } - getGeoJson() { - if (this._url === 'world_countries') { - return ({ - type: 'FeatureCollection', - features: [ - { properties: { iso2: 'CA', iso3: 'CAN' } }, - { properties: { iso2: 'US', iso3: 'USA' } }, - ], - } as unknown) as FeatureCollection; - } else if (this._url === 'zips') { - return ({ - type: 'FeatureCollection', - features: [{ properties: { zip: '40204' } }, { properties: { zip: '40205' } }], - } as unknown) as FeatureCollection; - } else { - throw new Error(`unrecognized mock url ${this._url}`); - } + getFields() { + return this._fields; } hasId(id: string) { @@ -51,31 +33,31 @@ jest.mock('../util', () => { return { async getEmsFileLayers() { return [ - new MockFileLayer('world_countries', [{ id: 'iso2' }, { id: 'iso3' }]), - new MockFileLayer('zips', [{ id: 'zip' }]), + new MockFileLayer('world_countries', [ + { + id: 'iso2', + alias: ['(geo\\.){0,}country_iso_code$', '(country|countries)'], + values: ['CA', 'US'], + }, + { id: 'iso3', values: ['CAN', 'USA'] }, + { id: 'name', alias: ['(country|countries)'] }, + ]), + new MockFileLayer('usa_zip_codes', [ + { id: 'zip', alias: ['zip'], values: ['40204', '40205'] }, + ]), ]; }, }; }); describe('suggestEMSTermJoinConfig', () => { - test('no info provided', async () => { + test('Should not validate when no info provided', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({}); expect(termJoinConfig).toBe(null); }); - describe('validate common column names', () => { - test('ecs region', async () => { - const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValuesColumnName: 'destination.geo.region_iso_code', - }); - expect(termJoinConfig).toEqual({ - layerId: 'administrative_regions_lvl2', - field: 'region_iso_code', - }); - }); - - test('ecs country', async () => { + describe('With common column names', () => { + test('should match first match', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({ sampleValuesColumnName: 'country_iso_code', }); @@ -85,78 +67,61 @@ describe('suggestEMSTermJoinConfig', () => { }); }); - test('country', async () => { + test('When sampleValues are provided, should reject match if no sampleValues for a layer, even though the name matches', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValuesColumnName: 'Country_name', - }); - expect(termJoinConfig).toEqual({ - layerId: 'world_countries', - field: 'name', + sampleValuesColumnName: 'country_iso_code', + sampleValues: ['FO', 'US', 'CA'], }); + expect(termJoinConfig).toEqual(null); }); - test('unknown name', async () => { + test('should reject match if sampleValues not in id-list', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValuesColumnName: 'cntry', + sampleValuesColumnName: 'zip', + sampleValues: ['90201', '40205'], }); expect(termJoinConfig).toEqual(null); }); - }); - describe('validate well known formats', () => { - test('5-digit zip code', async () => { + test('should return first match (regex matches both iso2 and name)', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValues: ['90201', 40204], + sampleValuesColumnName: 'Country_name', }); expect(termJoinConfig).toEqual({ - layerId: 'usa_zip_codes', - field: 'zip', + layerId: 'world_countries', + field: 'iso2', }); }); - test('mismatch', async () => { + test('unknown name', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValues: ['90201', 'foobar'], + sampleValuesColumnName: 'cntry', }); expect(termJoinConfig).toEqual(null); }); }); - describe('validate based on EMS data', () => { - test('Should validate with zip codes layer', async () => { + describe('validate well known formats (using id-values in manifest)', () => { + test('Should validate known zipcodes', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValues: ['40204', 40205], - emsLayerIds: ['world_countries', 'zips'], + sampleValues: ['40205', 40204], }); expect(termJoinConfig).toEqual({ - layerId: 'zips', + layerId: 'usa_zip_codes', field: 'zip', }); }); - test('Should not validate with faulty zip codes', async () => { + test('Should not validate unknown zipcode (in this case, 90201)', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValues: ['40204', '00000'], - emsLayerIds: ['world_countries', 'zips'], + sampleValues: ['90201', 40204], }); expect(termJoinConfig).toEqual(null); }); - test('Should validate against countries', async () => { + test('Should not validate mismatches', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValues: ['USA', 'USA', 'CAN'], - emsLayerIds: ['world_countries', 'zips'], - }); - expect(termJoinConfig).toEqual({ - layerId: 'world_countries', - field: 'iso3', - }); - }); - - test('Should not validate against missing countries', async () => { - const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValues: ['USA', 'BEL', 'CAN'], - emsLayerIds: ['world_countries', 'zips'], + sampleValues: ['90201', 'foobar'], }); expect(termJoinConfig).toEqual(null); }); diff --git a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts index 952e48a71a9dc..66fcbd805f53e 100644 --- a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts +++ b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts @@ -7,10 +7,8 @@ import type { FileLayer } from '@elastic/ems-client'; import { getEmsFileLayers } from '../util'; -import { emsWorldLayerId, emsRegionLayerId, emsUsaZipLayerId } from '../../common'; export interface SampleValuesConfig { - emsLayerIds?: string[]; sampleValues?: Array<string | number>; sampleValuesColumnName?: string; } @@ -20,44 +18,16 @@ export interface EMSTermJoinConfig { field: string; } -const wellKnownColumnNames = [ - { - regex: /(geo\.){0,}country_iso_code$/i, // ECS postfix for country - emsConfig: { - layerId: emsWorldLayerId, - field: 'iso2', - }, - }, - { - regex: /(geo\.){0,}region_iso_code$/i, // ECS postfixn for region - emsConfig: { - layerId: emsRegionLayerId, - field: 'region_iso_code', - }, - }, - { - regex: /^country/i, // anything starting with country - emsConfig: { - layerId: emsWorldLayerId, - field: 'name', - }, - }, -]; - -const wellKnownColumnFormats = [ - { - regex: /(^\d{5}$)/i, // 5-digit zipcode - emsConfig: { - layerId: emsUsaZipLayerId, - field: 'zip', - }, - }, -]; - interface UniqueMatch { - config: { layerId: string; field: string }; + config: EMSTermJoinConfig; count: number; } +interface FileLayerFieldShim { + id: string; + values?: string[]; + regex?: string; + alias?: string[]; +} export async function suggestEMSTermJoinConfig( sampleValuesConfig: SampleValuesConfig @@ -65,20 +35,17 @@ export async function suggestEMSTermJoinConfig( const matches: EMSTermJoinConfig[] = []; if (sampleValuesConfig.sampleValuesColumnName) { - matches.push(...suggestByName(sampleValuesConfig.sampleValuesColumnName)); + const matchesBasedOnColumnName = await suggestByName( + sampleValuesConfig.sampleValuesColumnName, + sampleValuesConfig.sampleValues + ); + matches.push(...matchesBasedOnColumnName); } if (sampleValuesConfig.sampleValues && sampleValuesConfig.sampleValues.length) { - if (sampleValuesConfig.emsLayerIds && sampleValuesConfig.emsLayerIds.length) { - matches.push( - ...(await suggestByEMSLayerIds( - sampleValuesConfig.emsLayerIds, - sampleValuesConfig.sampleValues - )) - ); - } else { - matches.push(...suggestByValues(sampleValuesConfig.sampleValues)); - } + // Only looks at id-values in main manifest + const matchesBasedOnIds = await suggestByIdValues(sampleValuesConfig.sampleValues); + matches.push(...matchesBasedOnIds); } const uniqMatches: UniqueMatch[] = matches.reduce((accum: UniqueMatch[], match) => { @@ -105,92 +72,80 @@ export async function suggestEMSTermJoinConfig( return uniqMatches.length ? uniqMatches[0].config : null; } -function suggestByName(columnName: string): EMSTermJoinConfig[] { - const matches = wellKnownColumnNames.filter((wellknown) => { - return columnName.match(wellknown.regex); - }); - - return matches.map((m) => { - return m.emsConfig; - }); -} +async function suggestByName( + columnName: string, + sampleValues?: Array<string | number> +): Promise<EMSTermJoinConfig[]> { + const fileLayers = await getEmsFileLayers(); -function suggestByValues(values: Array<string | number>): EMSTermJoinConfig[] { - const matches = wellKnownColumnFormats.filter((wellknown) => { - for (let i = 0; i < values.length; i++) { - const value = values[i].toString(); - if (!value.match(wellknown.regex)) { - return false; + const matches: EMSTermJoinConfig[] = []; + fileLayers.forEach((fileLayer) => { + const emsFields: FileLayerFieldShim[] = fileLayer.getFields(); + emsFields.forEach((emsField: FileLayerFieldShim) => { + if (!emsField.alias || !emsField.alias.length) { + return; } - } - return true; - }); - return matches.map((m) => { - return m.emsConfig; + const emsConfig = { + layerId: fileLayer.getId(), + field: emsField.id, + }; + emsField.alias.forEach((alias: string) => { + const regex = new RegExp(alias, 'i'); + const nameMatchesAlias = !!columnName.match(regex); + // Check if this violates any known id-values. + + let isMatch: boolean; + if (sampleValues) { + if (emsField.values && emsField.values.length) { + isMatch = nameMatchesAlias && allSamplesMatch(sampleValues, emsField.values); + } else { + // requires validation against sample-values but EMS provides no meta to do so. + isMatch = false; + } + } else { + isMatch = nameMatchesAlias; + } + + if (isMatch) { + matches.push(emsConfig); + } + }); + }); }); -} -function existsInEMS(emsJson: any, emsFieldId: string, sampleValue: string): boolean { - for (let i = 0; i < emsJson.features.length; i++) { - const emsFieldValue = emsJson.features[i].properties[emsFieldId].toString(); - if (emsFieldValue.toString() === sampleValue) { - return true; - } - } - return false; + return matches; } -function matchesEmsField(emsJson: any, emsFieldId: string, sampleValues: Array<string | number>) { +function allSamplesMatch(sampleValues: Array<string | number>, ids: string[]) { for (let j = 0; j < sampleValues.length; j++) { const sampleValue = sampleValues[j].toString(); - if (!existsInEMS(emsJson, emsFieldId, sampleValue)) { + if (!ids.includes(sampleValue)) { return false; } } return true; } -async function getMatchesForEMSLayer( - emsLayerId: string, +async function suggestByIdValues( sampleValues: Array<string | number> ): Promise<EMSTermJoinConfig[]> { + const matches: EMSTermJoinConfig[] = []; const fileLayers: FileLayer[] = await getEmsFileLayers(); - const emsFileLayer: FileLayer | undefined = fileLayers.find((fl: FileLayer) => - fl.hasId(emsLayerId) - ); - - if (!emsFileLayer) { - return []; - } - - const emsFields = emsFileLayer.getFields(); - - try { - const emsJson = await emsFileLayer.getGeoJson(); - const matches: EMSTermJoinConfig[] = []; - for (let f = 0; f < emsFields.length; f++) { - if (matchesEmsField(emsJson, emsFields[f].id, sampleValues)) { - matches.push({ - layerId: emsLayerId, - field: emsFields[f].id, - }); + fileLayers.forEach((fileLayer) => { + const emsFields: FileLayerFieldShim[] = fileLayer.getFields(); + emsFields.forEach((emsField: FileLayerFieldShim) => { + if (!emsField.values || !emsField.values.length) { + return; } - } - return matches; - } catch (e) { - return []; - } -} - -async function suggestByEMSLayerIds( - emsLayerIds: string[], - values: Array<string | number> -): Promise<EMSTermJoinConfig[]> { - const matches = []; - for (const emsLayerId of emsLayerIds) { - const layerIdMathes = await getMatchesForEMSLayer(emsLayerId, values); - matches.push(...layerIdMathes); - } + const emsConfig = { + layerId: fileLayer.getId(), + field: emsField.id, + }; + if (allSamplesMatch(sampleValues, emsField.values)) { + matches.push(emsConfig); + } + }); + }); return matches; } diff --git a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx index 0dfff5a2c221e..92459ed28ab91 100644 --- a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx @@ -44,7 +44,7 @@ import { getTopNavConfig } from '../top_nav_config'; import { MapQuery } from '../../../../common/descriptor_types'; import { goToSpecifiedPath } from '../../../render_app'; import { MapSavedObjectAttributes } from '../../../../common/map_saved_object_type'; -import { getExistingMapPath, APP_ID } from '../../../../common/constants'; +import { getFullPath, APP_ID } from '../../../../common/constants'; import { getInitialQuery, getInitialRefreshConfig, @@ -356,7 +356,7 @@ export class MapApp extends React.Component<Props, State> { const savedObjectId = this.props.savedMap.getSavedObjectId(); if (savedObjectId) { getCoreChrome().recentlyAccessed.add( - getExistingMapPath(savedObjectId), + getFullPath(savedObjectId), this.props.savedMap.getTitle(), savedObjectId ); diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts index f7e0012fdd9c2..45d3e0352acf6 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts @@ -241,6 +241,10 @@ export class SavedMap { return this._originatingApp; } + public getOriginatingAppName(): string | undefined { + return this._originatingApp ? this.getAppNameFromId(this._originatingApp) : undefined; + } + public getAppNameFromId = (appId: string): string | undefined => { return this._getStateTransfer().getAppNameFromId(appId); }; diff --git a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx index 7ac8c3070eb9d..79bc820d67b46 100644 --- a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx @@ -151,9 +151,8 @@ export function getTopNavConfig({ const saveModalProps = { onSave: async ( props: OnSaveProps & { - returnToOrigin?: boolean; dashboardId?: string | null; - addToLibrary?: boolean; + addToLibrary: boolean; } ) => { try { @@ -181,7 +180,7 @@ export function getTopNavConfig({ await savedMap.save({ ...props, newTags: selectedTags, - saveByReference: Boolean(props.addToLibrary), + saveByReference: props.addToLibrary, }); // showSaveModal wrapper requires onSave to return an object with an id to close the modal after successful save return { id: 'id' }; @@ -204,8 +203,19 @@ export function getTopNavConfig({ saveModal = ( <SavedObjectSaveModalOrigin {...saveModalProps} + onSave={async (props: OnSaveProps) => { + return saveModalProps.onSave({ ...props, addToLibrary: true }); + }} originatingApp={savedMap.getOriginatingApp()} getAppNameFromId={savedMap.getAppNameFromId} + returnToOriginSwitchLabel={ + savedMap.isByValue() + ? i18n.translate('xpack.maps.topNav.updatePanel', { + defaultMessage: 'Update panel on {originatingAppName}', + values: { originatingAppName: savedMap.getOriginatingAppName() }, + }) + : undefined + } options={tagSelector} /> ); diff --git a/x-pack/plugins/maps/public/routes/map_page/url_state/app_sync.ts b/x-pack/plugins/maps/public/routes/map_page/url_state/app_sync.ts index 268e5fa600b46..f05836dff2bd9 100644 --- a/x-pack/plugins/maps/public/routes/map_page/url_state/app_sync.ts +++ b/x-pack/plugins/maps/public/routes/map_page/url_state/app_sync.ts @@ -60,7 +60,9 @@ export function startAppStateSyncing(appStateManager: AppStateManager) { stateContainer.set(initialAppState); // set current url to whatever is in app state container - kbnUrlStateStorage.set('_a', initialAppState); + kbnUrlStateStorage.set('_a', initialAppState, { + replace: true, + }); // finally start syncing state containers with url startSyncingAppStateWithUrl(); diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index c753297932037..b8676559a4e2b 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -22,7 +22,7 @@ import { getFlightsSavedObjects } from './sample_data/flights_saved_objects.js'; // @ts-ignore import { getWebLogsSavedObjects } from './sample_data/web_logs_saved_objects.js'; import { registerMapsUsageCollector } from './maps_telemetry/collectors/register'; -import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE, getExistingMapPath } from '../common/constants'; +import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE, getFullPath } from '../common/constants'; import { mapSavedObjects, mapsTelemetrySavedObjects } from './saved_objects'; import { MapsXPackConfig } from '../config'; // @ts-ignore @@ -77,7 +77,7 @@ export class MapsPlugin implements Plugin { home.sampleData.addAppLinksToSampleDataset('ecommerce', [ { - path: getExistingMapPath('2c9c1f60-1909-11e9-919b-ffe5949a18d2'), + path: getFullPath('2c9c1f60-1909-11e9-919b-ffe5949a18d2'), label: sampleDataLinkLabel, icon: APP_ICON, }, @@ -99,7 +99,7 @@ export class MapsPlugin implements Plugin { home.sampleData.addAppLinksToSampleDataset('flights', [ { - path: getExistingMapPath('5dd88580-1906-11e9-919b-ffe5949a18d2'), + path: getFullPath('5dd88580-1906-11e9-919b-ffe5949a18d2'), label: sampleDataLinkLabel, icon: APP_ICON, }, @@ -120,7 +120,7 @@ export class MapsPlugin implements Plugin { home.sampleData.addSavedObjectsToSampleDataset('logs', getWebLogsSavedObjects()); home.sampleData.addAppLinksToSampleDataset('logs', [ { - path: getExistingMapPath('de71f4f0-1902-11e9-919b-ffe5949a18d2'), + path: getFullPath('de71f4f0-1902-11e9-919b-ffe5949a18d2'), label: sampleDataLinkLabel, icon: APP_ICON, }, diff --git a/x-pack/plugins/maps/server/saved_objects/map.ts b/x-pack/plugins/maps/server/saved_objects/map.ts index 78f70e27b2b7b..24effd651a31b 100644 --- a/x-pack/plugins/maps/server/saved_objects/map.ts +++ b/x-pack/plugins/maps/server/saved_objects/map.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsType } from 'src/core/server'; -import { APP_ICON, getExistingMapPath } from '../../common/constants'; +import { APP_ICON, getFullPath } from '../../common/constants'; // @ts-ignore import { savedObjectMigrations } from './saved_object_migrations'; @@ -34,7 +34,7 @@ export const mapSavedObjects: SavedObjectsType = { }, getInAppUrl(obj) { return { - path: getExistingMapPath(obj.id), + path: getFullPath(obj.id), uiCapabilitiesPath: 'maps.show', }; }, diff --git a/x-pack/plugins/maps/server/tutorials/ems/index.ts b/x-pack/plugins/maps/server/tutorials/ems/index.ts index 410c833b8ac77..3c63850f87291 100644 --- a/x-pack/plugins/maps/server/tutorials/ems/index.ts +++ b/x-pack/plugins/maps/server/tutorials/ems/index.ts @@ -16,6 +16,48 @@ export function emsBoundariesSpecProvider({ emsLandingPageUrl: string; prependBasePath: (path: string) => string; }) { + const instructions = { + instructionSets: [ + { + instructionVariants: [ + { + id: 'EMS', + instructions: [ + { + title: i18n.translate('xpack.maps.tutorials.ems.downloadStepTitle', { + defaultMessage: 'Download Elastic Maps Service boundaries', + }), + textPre: i18n.translate('xpack.maps.tutorials.ems.downloadStepText', { + defaultMessage: + '1. Navigate to Elastic Maps Service [landing page]({emsLandingPageUrl}/).\n\ +2. In the left sidebar, select an administrative boundary.\n\ +3. Click `Download GeoJSON` button.', + values: { + emsLandingPageUrl, + }, + }), + }, + { + title: i18n.translate('xpack.maps.tutorials.ems.uploadStepTitle', { + defaultMessage: 'Index Elastic Maps Service boundaries', + }), + textPre: i18n.translate('xpack.maps.tutorials.ems.uploadStepText', { + defaultMessage: + '1. Open [Maps]({newMapUrl}).\n\ +2. Click `Add layer`, then select `Upload GeoJSON`.\n\ +3. Upload the GeoJSON file and click `Import file`.', + values: { + newMapUrl: prependBasePath(getNewMapPath()), + }, + }), + }, + ], + }, + ], + }, + ], + }; + return () => ({ id: 'emsBoundaries', name: i18n.translate('xpack.maps.tutorials.ems.nameTitle', { @@ -34,46 +76,7 @@ Indexing EMS administrative boundaries in Elasticsearch allows for search on bou euiIconType: 'emsApp', completionTimeMinutes: 1, previewImagePath: '/plugins/maps/assets/boundaries_screenshot.png', - onPrem: { - instructionSets: [ - { - instructionVariants: [ - { - id: 'EMS', - instructions: [ - { - title: i18n.translate('xpack.maps.tutorials.ems.downloadStepTitle', { - defaultMessage: 'Download Elastic Maps Service boundaries', - }), - textPre: i18n.translate('xpack.maps.tutorials.ems.downloadStepText', { - defaultMessage: - '1. Navigate to Elastic Maps Service [landing page]({emsLandingPageUrl}).\n\ -2. In the left sidebar, select an administrative boundary.\n\ -3. Click `Download GeoJSON` button.', - values: { - emsLandingPageUrl, - }, - }), - }, - { - title: i18n.translate('xpack.maps.tutorials.ems.uploadStepTitle', { - defaultMessage: 'Index Elastic Maps Service boundaries', - }), - textPre: i18n.translate('xpack.maps.tutorials.ems.uploadStepText', { - defaultMessage: - '1. Open [Maps]({newMapUrl}).\n\ -2. Click `Add layer`, then select `Upload GeoJSON`.\n\ -3. Upload the GeoJSON file and click `Import file`.', - values: { - newMapUrl: prependBasePath(getNewMapPath()), - }, - }), - }, - ], - }, - ], - }, - ], - }, + onPrem: instructions, + elasticCloud: instructions, }); } diff --git a/x-pack/plugins/ml/common/types/results.ts b/x-pack/plugins/ml/common/types/results.ts index fa40cefcaed48..74d3286438588 100644 --- a/x-pack/plugins/ml/common/types/results.ts +++ b/x-pack/plugins/ml/common/types/results.ts @@ -6,6 +6,7 @@ */ import { estypes } from '@elastic/elasticsearch'; +import { LineAnnotationDatum, RectAnnotationDatum } from '@elastic/charts'; export interface GetStoppedPartitionResult { jobs: string[] | Record<string, string[]>; @@ -13,6 +14,9 @@ export interface GetStoppedPartitionResult { export interface GetDatafeedResultsChartDataResult { bucketResults: number[][]; datafeedResults: number[][]; + annotationResultsRect: RectAnnotationDatum[]; + annotationResultsLine: LineAnnotationDatum[]; + modelSnapshotResultsLine: LineAnnotationDatum[]; } export interface DatafeedResultsChartDataParams { diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index e3bcf307e6f00..7b3f457106033 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -27,7 +27,6 @@ "management", "licenseManagement", "maps", - "lens", "usageCollection" ], "server": true, diff --git a/x-pack/plugins/ml/public/__mocks__/ml_start_deps.ts b/x-pack/plugins/ml/public/__mocks__/ml_start_deps.ts index 0907cce832bf8..f16ba27524670 100644 --- a/x-pack/plugins/ml/public/__mocks__/ml_start_deps.ts +++ b/x-pack/plugins/ml/public/__mocks__/ml_start_deps.ts @@ -9,7 +9,6 @@ import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/publi import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; import { kibanaLegacyPluginMock } from '../../../../../src/plugins/kibana_legacy/public/mocks'; import { embeddablePluginMock } from '../../../../../src/plugins/embeddable/public/mocks'; -import { lensPluginMock } from '../../../lens/public/mocks'; import { triggersActionsUiMock } from '../../../triggers_actions_ui/public/mocks'; export const createMlStartDepsMock = () => ({ @@ -22,7 +21,6 @@ export const createMlStartDepsMock = () => ({ spaces: jest.fn(), embeddable: embeddablePluginMock.createStartContract(), maps: jest.fn(), - lens: lensPluginMock.createStartContract(), triggersActionsUi: triggersActionsUiMock.createStart(), dataVisualizer: jest.fn(), }); diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index 8be513f372e56..222d23acb40a7 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -77,7 +77,6 @@ const App: FC<AppProps> = ({ coreStart, deps, appMountParams }) => { data: deps.data, security: deps.security, licenseManagement: deps.licenseManagement, - lens: deps.lens, storage: localStorage, embeddable: deps.embeddable, maps: deps.maps, diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.test.tsx b/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.test.tsx index 05d400c5bb0ad..bf4b33350b382 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.test.tsx +++ b/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.test.tsx @@ -9,7 +9,7 @@ import useObservable from 'react-use/lib/useObservable'; import mockAnnotations from '../annotations_table/__mocks__/mock_annotations.json'; import React from 'react'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { Annotation } from '../../../../../common/types/annotations'; import { AnnotationUpdatesService } from '../../../services/annotations_service'; diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index afed7e79ff757..b68e64a5d9f6a 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -494,13 +494,13 @@ class AnnotationsTableUI extends Component { render: (annotation) => { const viewDataFeedText = ( <FormattedMessage - id="xpack.ml.annotationsTable.viewDatafeedTooltip" - defaultMessage="View datafeed" + id="xpack.ml.annotationsTable.datafeedChartTooltip" + defaultMessage="Datafeed chart" /> ); const viewDataFeedTooltipAriaLabelText = i18n.translate( - 'xpack.ml.annotationsTable.viewDatafeedTooltipAriaLabel', - { defaultMessage: 'View datafeed' } + 'xpack.ml.annotationsTable.datafeedChartTooltipAriaLabel', + { defaultMessage: 'Datafeed chart' } ); return ( <EuiButtonEmpty @@ -735,9 +735,7 @@ class AnnotationsTableUI extends Component { }); }} end={this.state.datafeedEnd} - timefield={this.props.jobs[0].data_description.time_field} jobId={this.state.jobId} - bucketSpan={this.props.jobs[0].analysis_config.bucket_span} /> ) : null} </Fragment> diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx index 10deaa1c2d489..d0e70c38c23b4 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { render, waitFor, screen } from '@testing-library/react'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts index 841f0d03fa21c..1ade617fa60a5 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts @@ -19,7 +19,6 @@ import { IStorageWrapper } from '../../../../../../../src/plugins/kibana_utils/p import type { EmbeddableStart } from '../../../../../../../src/plugins/embeddable/public'; import type { MapsStartApi } from '../../../../../maps/public'; import type { DataVisualizerPluginStart } from '../../../../../data_visualizer/public'; -import type { LensPublicStart } from '../../../../../lens/public'; import { TriggersAndActionsUIPublicPluginStart } from '../../../../../triggers_actions_ui/public'; interface StartPlugins { @@ -29,7 +28,6 @@ interface StartPlugins { share: SharePluginStart; embeddable: EmbeddableStart; maps?: MapsStartApi; - lens?: LensPublicStart; triggersActionsUi?: TriggersAndActionsUIPublicPluginStart; dataVisualizer?: DataVisualizerPluginStart; } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_action_flyout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_action_flyout.tsx index d24ec2126aee8..766f1bda64d5e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_action_flyout.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_action_flyout.tsx @@ -22,7 +22,6 @@ import { EuiFlyoutHeader, EuiForm, EuiFormRow, - EuiOverlayMask, EuiSelect, EuiTitle, } from '@elastic/eui'; @@ -129,188 +128,180 @@ export const EditActionFlyout: FC<Required<EditAction>> = ({ closeFlyout, item } }; return ( - <EuiOverlayMask> - <EuiFlyout - onClose={closeFlyout} - hideCloseButton - aria-labelledby="analyticsEditFlyoutTitle" - data-test-subj="mlAnalyticsEditFlyout" - > - <EuiFlyoutHeader hasBorder> - <EuiTitle size="m"> - <h2 id="analyticsEditFlyoutTitle"> - {i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutTitle', { - defaultMessage: 'Edit {jobId}', - values: { - jobId, - }, - })} - </h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <EuiForm> - <EuiFormRow - label={i18n.translate( - 'xpack.ml.dataframe.analyticsList.editFlyout.allowLazyStartLabel', + <EuiFlyout + onClose={closeFlyout} + hideCloseButton + aria-labelledby="analyticsEditFlyoutTitle" + data-test-subj="mlAnalyticsEditFlyout" + > + <EuiFlyoutHeader hasBorder> + <EuiTitle size="m"> + <h2 id="analyticsEditFlyoutTitle"> + {i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutTitle', { + defaultMessage: 'Edit {jobId}', + values: { + jobId, + }, + })} + </h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiForm> + <EuiFormRow + label={i18n.translate( + 'xpack.ml.dataframe.analyticsList.editFlyout.allowLazyStartLabel', + { + defaultMessage: 'Allow lazy start', + } + )} + > + <EuiSelect + aria-label={i18n.translate( + 'xpack.ml.dataframe.analyticsList.editFlyout.allowLazyStartAriaLabel', { - defaultMessage: 'Allow lazy start', + defaultMessage: 'Update allow lazy start.', } )} - > - <EuiSelect - aria-label={i18n.translate( - 'xpack.ml.dataframe.analyticsList.editFlyout.allowLazyStartAriaLabel', - { - defaultMessage: 'Update allow lazy start.', - } - )} - data-test-subj="mlAnalyticsEditFlyoutAllowLazyStartInput" - options={[ - { - value: 'true', - text: i18n.translate( - 'xpack.ml.dataframe.analyticsList.editFlyout.allowLazyStartTrueValue', - { - defaultMessage: 'True', - } - ), - }, - { - value: 'false', - text: i18n.translate( - 'xpack.ml.dataframe.analyticsList.editFlyout.allowLazyStartFalseValue', - { - defaultMessage: 'False', - } - ), - }, - ]} - value={allowLazyStart} - onChange={(e: React.ChangeEvent<HTMLSelectElement>) => - setAllowLazyStart(e.target.value) - } - /> - </EuiFormRow> - <EuiFormRow - label={i18n.translate( - 'xpack.ml.dataframe.analyticsList.editFlyout.descriptionLabel', + data-test-subj="mlAnalyticsEditFlyoutAllowLazyStartInput" + options={[ { - defaultMessage: 'Description', + value: 'true', + text: i18n.translate( + 'xpack.ml.dataframe.analyticsList.editFlyout.allowLazyStartTrueValue', + { + defaultMessage: 'True', + } + ), + }, + { + value: 'false', + text: i18n.translate( + 'xpack.ml.dataframe.analyticsList.editFlyout.allowLazyStartFalseValue', + { + defaultMessage: 'False', + } + ), + }, + ]} + value={allowLazyStart} + onChange={(e: React.ChangeEvent<HTMLSelectElement>) => + setAllowLazyStart(e.target.value) + } + /> + </EuiFormRow> + <EuiFormRow + label={i18n.translate('xpack.ml.dataframe.analyticsList.editFlyout.descriptionLabel', { + defaultMessage: 'Description', + })} + > + <EuiFieldText + data-test-subj="mlAnalyticsEditFlyoutDescriptionInput" + value={description} + onChange={(e) => setDescription(e.target.value)} + aria-label={i18n.translate( + 'xpack.ml.dataframe.analyticsList.editFlyout.descriptionAriaLabel', + { + defaultMessage: 'Update the job description.', } )} - > - <EuiFieldText - data-test-subj="mlAnalyticsEditFlyoutDescriptionInput" - value={description} - onChange={(e) => setDescription(e.target.value)} - aria-label={i18n.translate( - 'xpack.ml.dataframe.analyticsList.editFlyout.descriptionAriaLabel', - { - defaultMessage: 'Update the job description.', - } - )} - /> - </EuiFormRow> - <EuiFormRow - helpText={ - state !== DATA_FRAME_TASK_STATE.STOPPED && - i18n.translate('xpack.ml.dataframe.analyticsList.editFlyout.modelMemoryHelpText', { - defaultMessage: 'Model memory limit cannot be edited until the job has stopped.', - }) + /> + </EuiFormRow> + <EuiFormRow + helpText={ + state !== DATA_FRAME_TASK_STATE.STOPPED && + i18n.translate('xpack.ml.dataframe.analyticsList.editFlyout.modelMemoryHelpText', { + defaultMessage: 'Model memory limit cannot be edited until the job has stopped.', + }) + } + label={i18n.translate( + 'xpack.ml.dataframe.analyticsList.editFlyout.modelMemoryLimitLabel', + { + defaultMessage: 'Model memory limit', } - label={i18n.translate( - 'xpack.ml.dataframe.analyticsList.editFlyout.modelMemoryLimitLabel', + )} + isInvalid={mmlValidationError !== undefined} + error={mmlValidationError} + > + <EuiFieldText + data-test-subj="mlAnalyticsEditFlyoutmodelMemoryLimitInput" + isInvalid={mmlValidationError !== undefined} + readOnly={state !== DATA_FRAME_TASK_STATE.STOPPED} + value={modelMemoryLimit} + onChange={(e) => setModelMemoryLimit(e.target.value)} + aria-label={i18n.translate( + 'xpack.ml.dataframe.analyticsList.editFlyout.modelMemoryLimitAriaLabel', { - defaultMessage: 'Model memory limit', + defaultMessage: 'Update the model memory limit.', } )} - isInvalid={mmlValidationError !== undefined} - error={mmlValidationError} - > - <EuiFieldText - data-test-subj="mlAnalyticsEditFlyoutmodelMemoryLimitInput" - isInvalid={mmlValidationError !== undefined} - readOnly={state !== DATA_FRAME_TASK_STATE.STOPPED} - value={modelMemoryLimit} - onChange={(e) => setModelMemoryLimit(e.target.value)} - aria-label={i18n.translate( - 'xpack.ml.dataframe.analyticsList.editFlyout.modelMemoryLimitAriaLabel', - { - defaultMessage: 'Update the model memory limit.', - } - )} - /> - </EuiFormRow> - <EuiFormRow - helpText={ - state !== DATA_FRAME_TASK_STATE.STOPPED && - i18n.translate( - 'xpack.ml.dataframe.analyticsList.editFlyout.maxNumThreadsHelpText', - { - defaultMessage: - 'Maximum number of threads cannot be edited until the job has stopped.', - } - ) + /> + </EuiFormRow> + <EuiFormRow + helpText={ + state !== DATA_FRAME_TASK_STATE.STOPPED && + i18n.translate('xpack.ml.dataframe.analyticsList.editFlyout.maxNumThreadsHelpText', { + defaultMessage: + 'Maximum number of threads cannot be edited until the job has stopped.', + }) + } + label={i18n.translate( + 'xpack.ml.dataframe.analyticsList.editFlyout.maxNumThreadsLabel', + { + defaultMessage: 'Maximum number of threads', } - label={i18n.translate( - 'xpack.ml.dataframe.analyticsList.editFlyout.maxNumThreadsLabel', + )} + isInvalid={maxNumThreads === 0} + error={ + maxNumThreads === 0 && + i18n.translate('xpack.ml.dataframe.analyticsList.editFlyout.maxNumThreadsError', { + defaultMessage: 'The minimum value is 1.', + }) + } + > + <EuiFieldNumber + aria-label={i18n.translate( + 'xpack.ml.dataframe.analyticsList.editFlyout.maxNumThreadsAriaLabel', { - defaultMessage: 'Maximum number of threads', + defaultMessage: + 'Update the maximum number of threads to be used by the analysis.', } )} - isInvalid={maxNumThreads === 0} - error={ - maxNumThreads === 0 && - i18n.translate('xpack.ml.dataframe.analyticsList.editFlyout.maxNumThreadsError', { - defaultMessage: 'The minimum value is 1.', - }) + data-test-subj="mlAnalyticsEditFlyoutMaxNumThreadsLimitInput" + onChange={(e) => + setMaxNumThreads(e.target.value === '' ? undefined : +e.target.value) } + step={1} + min={1} + readOnly={state !== DATA_FRAME_TASK_STATE.STOPPED} + value={maxNumThreads} + /> + </EuiFormRow> + </EuiForm> + </EuiFlyoutBody> + <EuiFlyoutFooter> + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty iconType="cross" onClick={closeFlyout} flush="left"> + {i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutCancelButtonText', { + defaultMessage: 'Cancel', + })} + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + data-test-subj="mlAnalyticsEditFlyoutUpdateButton" + onClick={onSubmit} + fill + isDisabled={updateButtonDisabled} > - <EuiFieldNumber - aria-label={i18n.translate( - 'xpack.ml.dataframe.analyticsList.editFlyout.maxNumThreadsAriaLabel', - { - defaultMessage: - 'Update the maximum number of threads to be used by the analysis.', - } - )} - data-test-subj="mlAnalyticsEditFlyoutMaxNumThreadsLimitInput" - onChange={(e) => - setMaxNumThreads(e.target.value === '' ? undefined : +e.target.value) - } - step={1} - min={1} - readOnly={state !== DATA_FRAME_TASK_STATE.STOPPED} - value={maxNumThreads} - /> - </EuiFormRow> - </EuiForm> - </EuiFlyoutBody> - <EuiFlyoutFooter> - <EuiFlexGroup justifyContent="spaceBetween"> - <EuiFlexItem grow={false}> - <EuiButtonEmpty iconType="cross" onClick={closeFlyout} flush="left"> - {i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutCancelButtonText', { - defaultMessage: 'Cancel', - })} - </EuiButtonEmpty> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButton - data-test-subj="mlAnalyticsEditFlyoutUpdateButton" - onClick={onSubmit} - fill - isDisabled={updateButtonDisabled} - > - {i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutUpdateButtonText', { - defaultMessage: 'Update', - })} - </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlyoutFooter> - </EuiFlyout> - </EuiOverlayMask> + {i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutUpdateButtonText', { + defaultMessage: 'Update', + })} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlyoutFooter> + </EuiFlyout> ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx index 88ffaa0da7fdc..93be45bbdaf97 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/expanded_row.tsx @@ -114,10 +114,7 @@ export const ExpandedRow: FC<ExpandedRowProps> = ({ item }) => { } const { - services: { - share, - application: { navigateToUrl }, - }, + services: { share }, } = useMlKibana(); const tabs = [ @@ -402,17 +399,16 @@ export const ExpandedRow: FC<ExpandedRowProps> = ({ item }) => { </EuiFlexItem> <EuiFlexItem grow={false}> <EuiButtonEmpty - onClick={async () => { - const ingestPipelinesAppUrlGenerator = share.urlGenerators.getUrlGenerator( - 'INGEST_PIPELINES_APP_URL_GENERATOR' - ); - await navigateToUrl( - await ingestPipelinesAppUrlGenerator.createUrl({ - page: 'pipeline_edit', - pipelineId: pipelineName, - absolute: true, - }) + onClick={() => { + const locator = share.url.locators.get( + 'INGEST_PIPELINES_APP_LOCATOR' ); + if (!locator) return; + locator.navigate({ + page: 'pipeline_edit', + pipelineId: pipelineName, + absolute: true, + }); }} > <FormattedMessage diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.test.tsx index 8e7aecf429ad0..7e90a4e3ed44a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.test.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { render, fireEvent, waitFor, screen } from '@testing-library/react'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { getIndexPatternAndSavedSearch, diff --git a/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx b/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx index 73a6a9d64b60e..fe43bd659131f 100644 --- a/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx +++ b/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx @@ -28,7 +28,6 @@ import { isDefined } from '../../../common/types/guards'; import { MlEmbeddedMapComponent } from '../components/ml_embedded_map'; import { EMSTermJoinConfig } from '../../../../maps/public'; import { AnomaliesTableRecord } from '../../../common/types/anomalies'; -import { COMMON_EMS_LAYER_IDS } from '../../../common/constants/embeddable_map'; const MAX_ENTITY_VALUES = 3; @@ -177,7 +176,6 @@ export const AnomaliesMap: FC<Props> = ({ anomalies, jobIds }) => { } const suggestion: EMSTermJoinConfig | null = await mapsPlugin.suggestEMSTermJoinConfig({ - emsLayerIds: COMMON_EMS_LAYER_IDS, sampleValues: Array.from(entityValues), sampleValuesColumnName: entityName || '', }); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/constants.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/constants.ts index 71f3795518bc9..b3b9487523196 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/constants.ts +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/constants.ts @@ -15,7 +15,7 @@ export const CHART_DIRECTION = { export type ChartDirectionType = typeof CHART_DIRECTION[keyof typeof CHART_DIRECTION]; // [width, height] -export const CHART_SIZE: ChartSizeArray = ['100%', 300]; +export const CHART_SIZE: ChartSizeArray = ['100%', 380]; export const TAB_IDS = { CHART: 'chart', diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/datafeed_modal.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/datafeed_modal.tsx index cf547a49cac4c..2dece82e6f5c7 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/datafeed_modal.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/datafeed_modal.tsx @@ -11,25 +11,35 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { EuiButtonEmpty, + EuiCheckbox, EuiDatePicker, EuiFlexGroup, EuiFlexItem, + EuiIcon, + EuiIconTip, EuiLoadingChart, EuiModal, EuiModalHeader, EuiModalBody, - EuiSelect, EuiSpacer, EuiTabs, EuiTab, + EuiText, + EuiTitle, EuiToolTip, + htmlIdGenerator, } from '@elastic/eui'; import { + AnnotationDomainType, Axis, Chart, CurveType, + LineAnnotation, LineSeries, + LineAnnotationDatum, Position, + RectAnnotation, + RectAnnotationDatum, ScaleType, Settings, timeFormatter, @@ -42,7 +52,6 @@ import { useMlApiContext } from '../../../../contexts/kibana'; import { useCurrentEuiTheme } from '../../../../components/color_range_legend'; import { JobMessagesPane } from '../job_details/job_messages_pane'; import { EditQueryDelay } from './edit_query_delay'; -import { getIntervalOptions } from './get_interval_options'; import { CHART_DIRECTION, ChartDirectionType, @@ -53,12 +62,18 @@ import { } from './constants'; import { loadFullJob } from '../utils'; -const dateFormatter = timeFormatter('MM-DD HH:mm'); +const dateFormatter = timeFormatter('MM-DD HH:mm:ss'); +const MAX_CHART_POINTS = 480; interface DatafeedModalProps { jobId: string; end: number; - onClose: (deletionApproved?: boolean) => void; + onClose: () => void; +} + +function setLineAnnotationHeader(lineDatum: LineAnnotationDatum) { + lineDatum.header = dateFormatter(lineDatum.dataValue); + return lineDatum; } export const DatafeedModal: FC<DatafeedModalProps> = ({ jobId, end, onClose }) => { @@ -68,11 +83,17 @@ export const DatafeedModal: FC<DatafeedModalProps> = ({ jobId, end, onClose }) = isInitialized: boolean; }>({ datafeedConfig: undefined, bucketSpan: undefined, isInitialized: false }); const [endDate, setEndDate] = useState<any>(moment(end)); - const [interval, setInterval] = useState<string | undefined>(); const [selectedTabId, setSelectedTabId] = useState<TabIdsType>(TAB_IDS.CHART); const [isLoadingChartData, setIsLoadingChartData] = useState<boolean>(false); const [bucketData, setBucketData] = useState<number[][]>([]); + const [annotationData, setAnnotationData] = useState<{ + rect: RectAnnotationDatum[]; + line: LineAnnotationDatum[]; + }>({ rect: [], line: [] }); + const [modelSnapshotData, setModelSnapshotData] = useState<LineAnnotationDatum[]>([]); const [sourceData, setSourceData] = useState<number[][]>([]); + const [showAnnotations, setShowAnnotations] = useState<boolean>(true); + const [showModelSnapshots, setShowModelSnapshots] = useState<boolean>(true); const { results: { getDatafeedResultChartData }, @@ -102,25 +123,30 @@ export const DatafeedModal: FC<DatafeedModalProps> = ({ jobId, end, onClose }) = const handleChange = (date: moment.Moment) => setEndDate(date); const handleEndDateChange = (direction: ChartDirectionType) => { - if (interval === undefined) return; + if (data.bucketSpan === undefined) return; const newEndDate = endDate.clone(); - const [count, type] = interval.split(' '); + const unitMatch = data.bucketSpan.match(/[d | h| m | s]/g)!; + const unit = unitMatch[0]; + const count = Number(data.bucketSpan.replace(/[^0-9]/g, '')); if (direction === CHART_DIRECTION.FORWARD) { - newEndDate.add(Number(count), type); + newEndDate.add(MAX_CHART_POINTS * count, unit); } else { - newEndDate.subtract(Number(count), type); + newEndDate.subtract(MAX_CHART_POINTS * count, unit); } setEndDate(newEndDate); }; const getChartData = useCallback(async () => { - if (interval === undefined) return; + if (data.bucketSpan === undefined) return; const endTimestamp = moment(endDate).valueOf(); - const [count, type] = interval.split(' '); - const startMoment = endDate.clone().subtract(Number(count), type); + const unitMatch = data.bucketSpan.match(/[d | h| m | s]/g)!; + const unit = unitMatch[0]; + const count = Number(data.bucketSpan.replace(/[^0-9]/g, '')); + // STARTTIME = ENDTIME - (BucketSpan * MAX_CHART_POINTS) + const startMoment = endDate.clone().subtract(MAX_CHART_POINTS * count, unit); const startTimestamp = moment(startMoment).valueOf(); try { @@ -128,6 +154,11 @@ export const DatafeedModal: FC<DatafeedModalProps> = ({ jobId, end, onClose }) = setSourceData(chartData.datafeedResults); setBucketData(chartData.bucketResults); + setAnnotationData({ + rect: chartData.annotationResultsRect, + line: chartData.annotationResultsLine.map(setLineAnnotationHeader), + }); + setModelSnapshotData(chartData.modelSnapshotResultsLine.map(setLineAnnotationHeader)); } catch (error) { const title = i18n.translate('xpack.ml.jobsList.datafeedModal.errorToastTitle', { defaultMessage: 'Error fetching data', @@ -135,7 +166,7 @@ export const DatafeedModal: FC<DatafeedModalProps> = ({ jobId, end, onClose }) = displayErrorToast(error, title); } setIsLoadingChartData(false); - }, [endDate, interval]); + }, [endDate, data.bucketSpan]); const getJobData = async () => { try { @@ -145,11 +176,6 @@ export const DatafeedModal: FC<DatafeedModalProps> = ({ jobId, end, onClose }) = bucketSpan: job.analysis_config.bucket_span, isInitialized: true, }); - const intervalOptions = getIntervalOptions(job.analysis_config.bucket_span); - const initialInterval = intervalOptions.length - ? intervalOptions[intervalOptions.length - 1] - : undefined; - setInterval(initialInterval?.value || '72 hours'); } catch (error) { displayErrorToast(error); } @@ -161,20 +187,17 @@ export const DatafeedModal: FC<DatafeedModalProps> = ({ jobId, end, onClose }) = useEffect( function loadChartData() { - if (interval !== undefined) { + if (data.bucketSpan !== undefined) { setIsLoadingChartData(true); getChartData(); } }, - [endDate, interval] + [endDate, data.bucketSpan] ); const { datafeedConfig, bucketSpan, isInitialized } = data; - - const intervalOptions = useMemo(() => { - if (bucketSpan === undefined) return []; - return getIntervalOptions(bucketSpan); - }, [bucketSpan]); + const checkboxIdAnnotation = useMemo(() => htmlIdGenerator()(), []); + const checkboxIdModelSnapshot = useMemo(() => htmlIdGenerator()(), []); return ( <EuiModal @@ -185,13 +208,33 @@ export const DatafeedModal: FC<DatafeedModalProps> = ({ jobId, end, onClose }) = <EuiModalHeader> <EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="xl"> <EuiFlexItem grow={false}> - <FormattedMessage - id="xpack.ml.jobsList.datafeedModal.header" - defaultMessage="{jobId}" - values={{ - jobId, - }} - /> + <EuiFlexGroup alignItems="center" gutterSize="s"> + <EuiFlexItem grow={false}> + <EuiIconTip + color="primary" + type="help" + content={ + <FormattedMessage + id="xpack.ml.jobsList.datafeedModal.headerTooltipContent" + defaultMessage="Charts the event counts of the job and the source data to identify where missing data has occurred." + /> + } + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiTitle size="xs"> + <h4> + <FormattedMessage + id="xpack.ml.jobsList.datafeedModal.header" + defaultMessage="Datafeed chart for {jobId}" + values={{ + jobId, + }} + /> + </h4> + </EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> </EuiFlexItem> <EuiFlexItem grow={false}> <EuiDatePicker @@ -219,19 +262,6 @@ export const DatafeedModal: FC<DatafeedModalProps> = ({ jobId, end, onClose }) = <EuiFlexGroup direction="column"> <EuiFlexItem grow={false}> <EuiFlexGroup justifyContent="spaceBetween"> - <EuiFlexItem grow={false}> - <EuiSelect - options={intervalOptions} - value={interval} - onChange={(e) => setInterval(e.target.value)} - aria-label={i18n.translate( - 'xpack.ml.jobsList.datafeedModal.intervalSelection', - { - defaultMessage: 'Datafeed modal chart interval selection', - } - )} - /> - </EuiFlexItem> <EuiFlexItem grow={false}> <EditQueryDelay datafeedId={datafeedConfig.datafeed_id} @@ -239,6 +269,40 @@ export const DatafeedModal: FC<DatafeedModalProps> = ({ jobId, end, onClose }) = isEnabled={datafeedConfig.state === DATAFEED_STATE.STOPPED} /> </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiCheckbox + id={checkboxIdAnnotation} + label={ + <EuiText size={'xs'}> + <FormattedMessage + id="xpack.ml.jobsList.datafeedModal.showAnnotationsCheckboxLabel" + defaultMessage="Show annotations" + /> + </EuiText> + } + checked={showAnnotations} + onChange={() => setShowAnnotations(!showAnnotations)} + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiCheckbox + id={checkboxIdModelSnapshot} + label={ + <EuiText size={'xs'}> + <FormattedMessage + id="xpack.ml.jobsList.datafeedModal.showModelSnapshotsCheckboxLabel" + defaultMessage="Show model snapshots" + /> + </EuiText> + } + checked={showModelSnapshots} + onChange={() => setShowModelSnapshots(!showModelSnapshots)} + /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> </EuiFlexGroup> </EuiFlexItem> <EuiFlexItem> @@ -298,7 +362,65 @@ export const DatafeedModal: FC<DatafeedModalProps> = ({ jobId, end, onClose }) = })} position={Position.Left} /> + {showModelSnapshots ? ( + <LineAnnotation + id={i18n.translate( + 'xpack.ml.jobsList.datafeedModal.modelSnapshotsLineSeriesId', + { + defaultMessage: 'Model snapshots', + } + )} + key="model-snapshots-results-line" + domainType={AnnotationDomainType.XDomain} + dataValues={modelSnapshotData} + marker={<EuiIcon type="asterisk" />} + markerPosition={Position.Top} + style={{ + line: { + strokeWidth: 3, + stroke: euiTheme.euiColorVis1, + opacity: 0.5, + }, + }} + /> + ) : null} + {showAnnotations ? ( + <> + <LineAnnotation + id={i18n.translate( + 'xpack.ml.jobsList.datafeedModal.annotationLineSeriesId', + { + defaultMessage: 'Annotations line result', + } + )} + key="annotation-results-line" + domainType={AnnotationDomainType.XDomain} + dataValues={annotationData.line} + marker={<EuiIcon type="annotation" />} + markerPosition={Position.Top} + style={{ + line: { + strokeWidth: 3, + stroke: euiTheme.euiColorDangerText, + opacity: 0.5, + }, + }} + /> + <RectAnnotation + key="annotation-results-rect" + dataValues={annotationData.rect} + id={i18n.translate( + 'xpack.ml.jobsList.datafeedModal.annotationRectSeriesId', + { + defaultMessage: 'Annotations rectangle result', + } + )} + style={{ fill: euiTheme.euiColorDangerText }} + /> + </> + ) : null} <LineSeries + key={'source-results'} color={euiTheme.euiColorPrimary} id={i18n.translate('xpack.ml.jobsList.datafeedModal.sourceSeriesId', { defaultMessage: 'Source indices', @@ -311,6 +433,7 @@ export const DatafeedModal: FC<DatafeedModalProps> = ({ jobId, end, onClose }) = curve={CurveType.LINEAR} /> <LineSeries + key={'job-results'} color={euiTheme.euiColorAccentText} id={i18n.translate('xpack.ml.jobsList.datafeedModal.bucketSeriesId', { defaultMessage: 'Job results', diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/get_interval_options.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/get_interval_options.ts deleted file mode 100644 index cc9431549c79c..0000000000000 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/datafeed_modal/get_interval_options.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const getIntervalOptions = (bucketSpan: string) => { - const unitMatch = bucketSpan.match(/[d | h| m | s]/g)!; - const unit = unitMatch[0]; - const count = Number(bucketSpan.replace(/[^0-9]/g, '')); - - const intervalOptions = []; - - if (['s', 'ms', 'micros', 'nanos'].includes(unit)) { - intervalOptions.push( - { - value: '1 hour', - text: i18n.translate('xpack.ml.jobsList.datafeedModal.1hourOption', { - defaultMessage: '{count} hour', - values: { count: 1 }, - }), - }, - { - value: '2 hours', - text: i18n.translate('xpack.ml.jobsList.datafeedModal.2hourOption', { - defaultMessage: '{count} hours', - values: { count: 2 }, - }), - } - ); - } - - if ((unit === 'm' && count <= 4) || unit === 'h') { - intervalOptions.push( - { - value: '3 hours', - text: i18n.translate('xpack.ml.jobsList.datafeedModal.3hourOption', { - defaultMessage: '{count} hours', - values: { count: 3 }, - }), - }, - { - value: '8 hours', - text: i18n.translate('xpack.ml.jobsList.datafeedModal.8hourOption', { - defaultMessage: '{count} hours', - values: { count: 8 }, - }), - }, - { - value: '12 hours', - text: i18n.translate('xpack.ml.jobsList.datafeedModal.12hourOption', { - defaultMessage: '{count} hours', - values: { count: 12 }, - }), - }, - { - value: '24 hours', - text: i18n.translate('xpack.ml.jobsList.datafeedModal.24hourOption', { - defaultMessage: '{count} hours', - values: { count: 24 }, - }), - } - ); - } - - if ((unit === 'm' && count >= 5 && count <= 15) || unit === 'h') { - intervalOptions.push( - { - value: '48 hours', - text: i18n.translate('xpack.ml.jobsList.datafeedModal.48hourOption', { - defaultMessage: '{count} hours', - values: { count: 48 }, - }), - }, - { - value: '72 hours', - text: i18n.translate('xpack.ml.jobsList.datafeedModal.72hourOption', { - defaultMessage: '{count} hours', - values: { count: 72 }, - }), - } - ); - } - - if ((unit === 'm' && count >= 10 && count <= 15) || unit === 'h' || unit === 'd') { - intervalOptions.push( - { - value: '5 days', - text: i18n.translate('xpack.ml.jobsList.datafeedModal.5daysOption', { - defaultMessage: '{count} days', - values: { count: 5 }, - }), - }, - { - value: '7 days', - text: i18n.translate('xpack.ml.jobsList.datafeedModal.7daysOption', { - defaultMessage: '{count} days', - values: { count: 7 }, - }), - } - ); - } - - if (unit === 'h' || unit === 'd') { - intervalOptions.push({ - value: '14 days', - text: i18n.translate('xpack.ml.jobsList.datafeedModal.14DaysOption', { - defaultMessage: '{count} days', - values: { count: 14 }, - }), - }); - } - - return intervalOptions; -}; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js index b514c8433daf4..d3856e6afa398 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js @@ -7,26 +7,29 @@ import PropTypes from 'prop-types'; import React, { Component, Fragment } from 'react'; - -import { EuiTabbedContent, EuiLoadingSpinner } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonIcon, EuiTabbedContent, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui'; import { extractJobDetails } from './extract_job_details'; import { JsonPane } from './json_tab'; import { DatafeedPreviewPane } from './datafeed_preview_tab'; import { AnnotationsTable } from '../../../../components/annotations/annotations_table'; +import { DatafeedModal } from '../datafeed_modal'; import { AnnotationFlyout } from '../../../../components/annotations/annotation_flyout'; import { ModelSnapshotTable } from '../../../../components/model_snapshots'; import { ForecastsTable } from './forecasts_table'; import { JobDetailsPane } from './job_details_pane'; import { JobMessagesPane } from './job_messages_pane'; -import { i18n } from '@kbn/i18n'; import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; export class JobDetailsUI extends Component { constructor(props) { super(props); - this.state = {}; + this.state = { + datafeedModalVisible: false, + }; if (this.props.addYourself) { this.props.addYourself(props.jobId, (j) => this.updateJob(j)); } @@ -77,6 +80,30 @@ export class JobDetailsUI extends Component { alertRules, } = extractJobDetails(job, basePath, refreshJobList); + datafeed.titleAction = ( + <EuiToolTip + content={ + <FormattedMessage + id="xpack.ml.jobDetails.datafeedChartTooltipText" + defaultMessage="Datafeed chart" + /> + } + > + <EuiButtonIcon + size="xs" + aria-label={i18n.translate('xpack.ml.jobDetails.datafeedChartAriaLabel', { + defaultMessage: 'Datafeed chart', + })} + iconType="visAreaStacked" + onClick={() => + this.setState({ + datafeedModalVisible: true, + }) + } + /> + </EuiToolTip> + ); + const tabs = [ { id: 'job-settings', @@ -105,6 +132,32 @@ export class JobDetailsUI extends Component { /> ), }, + { + id: 'datafeed', + 'data-test-subj': 'mlJobListTab-datafeed', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.datafeedLabel', { + defaultMessage: 'Datafeed', + }), + content: ( + <> + <JobDetailsPane + data-test-subj="mlJobDetails-datafeed" + sections={[datafeed, datafeedTimingStats]} + /> + {this.props.jobId && this.state.datafeedModalVisible ? ( + <DatafeedModal + onClose={() => { + this.setState({ + datafeedModalVisible: false, + }); + }} + end={job.data_counts.latest_bucket_timestamp} + jobId={this.props.jobId} + /> + ) : null} + </> + ), + }, { id: 'counts', 'data-test-subj': 'mlJobListTab-counts', @@ -137,21 +190,6 @@ export class JobDetailsUI extends Component { ]; if (showFullDetails && datafeed.items.length) { - // Datafeed should be at index 2 in tabs array for full details - tabs.splice(2, 0, { - id: 'datafeed', - 'data-test-subj': 'mlJobListTab-datafeed', - name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.datafeedLabel', { - defaultMessage: 'Datafeed', - }), - content: ( - <JobDetailsPane - data-test-subj="mlJobDetails-datafeed" - sections={[datafeed, datafeedTimingStats]} - /> - ), - }); - tabs.push( { id: 'datafeed-preview', diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js index 49d9bcde49052..4046f4d5d8071 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details_pane.js @@ -9,6 +9,8 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { + EuiFlexGroup, + EuiFlexItem, EuiTitle, EuiTable, EuiTableBody, @@ -42,9 +44,14 @@ function Section({ section }) { return ( <React.Fragment> - <EuiTitle size="xs"> - <h4>{section.title}</h4> - </EuiTitle> + <EuiFlexGroup gutterSize="xs"> + <EuiFlexItem grow={false}> + <EuiTitle size="xs"> + <h4>{section.title}</h4> + </EuiTitle> + </EuiFlexItem> + <EuiFlexItem grow={false}>{section.titleAction}</EuiFlexItem> + </EuiFlexGroup> <div className="job-section" data-test-subj={`mlJobRowDetailsSection-${section.id}`}> <EuiTable compressed={true}> <EuiTableBody> diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts index 19ba5aa304bf0..25ef36782207f 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts @@ -6,7 +6,10 @@ */ // Service for obtaining data for the ML Results dashboards. -import { GetStoppedPartitionResult } from '../../../../common/types/results'; +import { + GetStoppedPartitionResult, + GetDatafeedResultsChartDataResult, +} from '../../../../common/types/results'; import { HttpService } from '../http_service'; import { basePath } from './index'; import { JobId } from '../../../../common/types/anomaly_detection_jobs'; @@ -148,7 +151,7 @@ export const resultsApiProvider = (httpService: HttpService) => ({ start, end, }); - return httpService.http<any>({ + return httpService.http<GetDatafeedResultsChartDataResult>({ path: `${basePath()}/results/datafeed_results_chart`, method: 'POST', body, diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index e3a4a8348ebc1..917619a67fea9 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -44,7 +44,6 @@ import { registerFeature } from './register_feature'; // Not importing from `ml_url_generator/index` here to avoid importing unnecessary code import { registerUrlGenerator } from './ml_url_generator/ml_url_generator'; import type { MapsStartApi } from '../../maps/public'; -import { LensPublicStart } from '../../lens/public'; import { TriggersAndActionsUIPublicPluginSetup, TriggersAndActionsUIPublicPluginStart, @@ -62,7 +61,6 @@ export interface MlStartDependencies { spaces?: SpacesPluginStart; embeddable: EmbeddableStart; maps?: MapsStartApi; - lens?: LensPublicStart; triggersActionsUi?: TriggersAndActionsUIPublicPluginStart; dataVisualizer: DataVisualizerPluginStart; } @@ -119,7 +117,6 @@ export class MlPlugin implements Plugin<MlPluginSetup, MlPluginStart> { embeddable: { ...pluginsSetup.embeddable, ...pluginsStart.embeddable }, maps: pluginsStart.maps, uiActions: pluginsStart.uiActions, - lens: pluginsStart.lens, kibanaVersion, triggersActionsUi: pluginsStart.triggersActionsUi, dataVisualizer: pluginsStart.dataVisualizer, diff --git a/x-pack/plugins/ml/server/models/results_service/results_service.ts b/x-pack/plugins/ml/server/models/results_service/results_service.ts index 9413ee00184d2..81ee394b99704 100644 --- a/x-pack/plugins/ml/server/models/results_service/results_service.ts +++ b/x-pack/plugins/ml/server/models/results_service/results_service.ts @@ -27,6 +27,7 @@ import { import { MlJobsResponse } from '../../../common/types/job_service'; import type { MlClient } from '../../lib/ml_client'; import { datafeedsProvider } from '../job_service/datafeeds'; +import { annotationServiceProvider } from '../annotation_service'; // Service for carrying out Elasticsearch queries to obtain data for the // ML Results dashboards. @@ -620,13 +621,19 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust const finalResults: GetDatafeedResultsChartDataResult = { bucketResults: [], datafeedResults: [], + annotationResultsRect: [], + annotationResultsLine: [], + modelSnapshotResultsLine: [], }; const { getDatafeedByJobId } = datafeedsProvider(client!, mlClient); - const datafeedConfig = await getDatafeedByJobId(jobId); - const { body: jobsResponse } = await mlClient.getJobs({ job_id: jobId }); - if (jobsResponse.count === 0 || jobsResponse.jobs === undefined) { + const [datafeedConfig, { body: jobsResponse }] = await Promise.all([ + getDatafeedByJobId(jobId), + mlClient.getJobs({ job_id: jobId }), + ]); + + if (jobsResponse && (jobsResponse.count === 0 || jobsResponse.jobs === undefined)) { throw Boom.notFound(`Job with the id "${jobId}" not found`); } @@ -696,10 +703,25 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust ]) || []; } - const bucketResp = await mlClient.getBuckets({ - job_id: jobId, - body: { desc: true, start: String(start), end: String(end), page: { from: 0, size: 1000 } }, - }); + const { getAnnotations } = annotationServiceProvider(client!); + + const [bucketResp, annotationResp, { body: modelSnapshotsResp }] = await Promise.all([ + mlClient.getBuckets({ + job_id: jobId, + body: { desc: true, start: String(start), end: String(end), page: { from: 0, size: 1000 } }, + }), + getAnnotations({ + jobIds: [jobId], + earliestMs: start, + latestMs: end, + maxAnnotations: 1000, + }), + mlClient.getModelSnapshots({ + job_id: jobId, + start: String(start), + end: String(end), + }), + ]); const bucketResults = bucketResp?.body?.buckets ?? []; bucketResults.forEach((dataForTime) => { @@ -708,6 +730,36 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust finalResults.bucketResults.push([timestamp, eventCount]); }); + const annotationResults = annotationResp.annotations[jobId] || []; + annotationResults.forEach((annotation) => { + const timestamp = Number(annotation?.timestamp); + const endTimestamp = Number(annotation?.end_timestamp); + if (timestamp === endTimestamp) { + finalResults.annotationResultsLine.push({ + dataValue: timestamp, + details: annotation.annotation, + }); + } else { + finalResults.annotationResultsRect.push({ + coordinates: { + x0: timestamp, + x1: endTimestamp, + }, + details: annotation.annotation, + }); + } + }); + + const modelSnapshots = modelSnapshotsResp?.model_snapshots ?? []; + modelSnapshots.forEach((modelSnapshot) => { + const timestamp = Number(modelSnapshot?.timestamp); + + finalResults.modelSnapshotResultsLine.push({ + dataValue: timestamp, + details: modelSnapshot.description, + }); + }); + return finalResults; } diff --git a/x-pack/plugins/ml/tsconfig.json b/x-pack/plugins/ml/tsconfig.json index 221718d423383..8e859c35e3f85 100644 --- a/x-pack/plugins/ml/tsconfig.json +++ b/x-pack/plugins/ml/tsconfig.json @@ -16,7 +16,7 @@ "../../../typings/**/*", // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 "public/**/*.json", - "server/**/*.json", + "server/**/*.json" ], "references": [ { "path": "../../../src/core/tsconfig.json" }, @@ -28,7 +28,6 @@ { "path": "../license_management/tsconfig.json" }, { "path": "../licensing/tsconfig.json" }, { "path": "../maps/tsconfig.json" }, - { "path": "../lens/tsconfig.json" }, { "path": "../security/tsconfig.json" }, { "path": "../spaces/tsconfig.json" }, { "path": "../alerting/tsconfig.json" }, diff --git a/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/checker_errors.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/checker_errors.test.js.snap index 5018bad317708..e3fa9da6639b3 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/checker_errors.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/checker_errors.test.js.snap @@ -22,33 +22,37 @@ Array [ <div class="euiText euiText--small" > - <p> - There were some errors encountered in trying to check Elasticsearch settings. You need administrator rights to check the settings and, if needed, to enable the monitoring collection setting. - </p> - <dl - class="euiDescriptionList euiDescriptionList--row" + <div + class="euiTextColor euiTextColor--default" > - <dt - class="euiDescriptionList__title" + <p> + There were some errors encountered in trying to check Elasticsearch settings. You need administrator rights to check the settings and, if needed, to enable the monitoring collection setting. + </p> + <dl + class="euiDescriptionList euiDescriptionList--row" > - 403 Forbidden - </dt> - <dd - class="euiDescriptionList__description" - > - no access for you - </dd> - <dt - class="euiDescriptionList__title" - > - 500 Internal Server Error - </dt> - <dd - class="euiDescriptionList__description" - > - An internal server error occurred - </dd> - </dl> + <dt + class="euiDescriptionList__title" + > + 403 Forbidden + </dt> + <dd + class="euiDescriptionList__description" + > + no access for you + </dd> + <dt + class="euiDescriptionList__title" + > + 500 Internal Server Error + </dt> + <dd + class="euiDescriptionList__description" + > + An internal server error occurred + </dd> + </dl> + </div> </div> </div>, ] diff --git a/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap index fe277062bc95a..34a4c049dddcc 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap @@ -9,7 +9,7 @@ exports[`NoData should show a default message if reason is unknown 1`] = ` > No monitoring data found. </h1> - <main + <div class="euiPageBody euiPageBody--borderRadiusNone euiPageBody--restrictWidth-custom" style="max-width:600px" > @@ -87,7 +87,7 @@ exports[`NoData should show a default message if reason is unknown 1`] = ` </span> </button> </div> - </main> + </div> </div> `; @@ -100,7 +100,7 @@ exports[`NoData should show text next to the spinner while checking a setting 1` > No monitoring data found. </h1> - <main + <div class="euiPageBody euiPageBody--borderRadiusNone euiPageBody--restrictWidth-custom" style="max-width:600px" > @@ -178,6 +178,6 @@ exports[`NoData should show text next to the spinner while checking a setting 1` </span> </button> </div> - </main> + </div> </div> `; diff --git a/x-pack/plugins/monitoring/public/components/page_loading/__snapshots__/page_loading.test.js.snap b/x-pack/plugins/monitoring/public/components/page_loading/__snapshots__/page_loading.test.js.snap index 7f38a92beae8f..7b04e6410d996 100644 --- a/x-pack/plugins/monitoring/public/components/page_loading/__snapshots__/page_loading.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/page_loading/__snapshots__/page_loading.test.js.snap @@ -5,7 +5,7 @@ exports[`PageLoading should show a simple page loading component 1`] = ` class="euiPage euiPage--paddingMedium euiPage--grow" style="height:calc(100vh - 50px)" > - <main + <div class="euiPageBody euiPageBody--borderRadiusNone" > <div @@ -37,6 +37,6 @@ exports[`PageLoading should show a simple page loading component 1`] = ` </div> </div> </div> - </main> + </div> </div> `; diff --git a/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx b/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx index c358067123747..f92f12c79a56d 100644 --- a/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx @@ -49,7 +49,7 @@ describe('CreateCaseFlyout', () => { </EuiThemeProvider> ); - wrapper.find('.euiFlyout__closeButton').first().simulate('click'); + wrapper.find(`[data-test-subj='euiFlyoutCloseButton']`).first().simulate('click'); expect(onCloseFlyout).toBeCalled(); }); }); diff --git a/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx b/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx index df29d02e8d830..b6cdcf3111672 100644 --- a/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import React, { memo } from 'react'; -import styled from 'styled-components'; +import React, { memo, ReactNode } from 'react'; +import styled, { StyledComponent } from 'styled-components'; import { EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui'; import * as i18n from '../translations'; @@ -20,7 +20,11 @@ export interface CreateCaseModalProps { onSuccess: (theCase: Case) => Promise<void>; } -const StyledFlyout = styled(EuiFlyout)` +// TODO: EUI team follow up on complex types and styled-components `styled` +// https://github.com/elastic/eui/issues/4855 +const StyledFlyout: StyledComponent<typeof EuiFlyout, {}, { children?: ReactNode }> = styled( + EuiFlyout +)` ${({ theme }) => ` z-index: ${theme.eui.euiZModal}; `} diff --git a/x-pack/plugins/observability/public/components/shared/core_web_vitals/__stories__/core_vitals.stories.tsx b/x-pack/plugins/observability/public/components/shared/core_web_vitals/__stories__/core_vitals.stories.tsx index d84e637da087e..5f5cf2cb4da21 100644 --- a/x-pack/plugins/observability/public/components/shared/core_web_vitals/__stories__/core_vitals.stories.tsx +++ b/x-pack/plugins/observability/public/components/shared/core_web_vitals/__stories__/core_vitals.stories.tsx @@ -6,7 +6,7 @@ */ import React, { ComponentType } from 'react'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { Observable } from 'rxjs'; import { CoreStart } from 'src/core/public'; import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx index ea69a371cedae..3566835b1701c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx @@ -38,6 +38,12 @@ export function EmptyView({ emptyMessage = SELECTED_DATA_TYPE_FOR_REPORT; } + if (!series) { + emptyMessage = i18n.translate('xpack.observability.expView.seriesEditor.notFound', { + defaultMessage: 'No series found. Please add a series.', + }); + } + return ( <Wrapper height={height}> {loading && ( @@ -77,7 +83,7 @@ export const EMPTY_LABEL = i18n.translate('xpack.observability.expView.seriesBui export const CHOOSE_REPORT_DEFINITION = i18n.translate( 'xpack.observability.expView.seriesBuilder.emptyReportDefinition', { - defaultMessage: 'Select a report type to create a visualization.', + defaultMessage: 'Select a report definition to create a visualization.', } ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx index af64e74bca89c..fe2953edd36d6 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx @@ -29,6 +29,7 @@ describe('FilterLabel', function () { negate={false} seriesId={'kpi-over-time'} removeFilter={jest.fn()} + indexPattern={mockIndexPattern} /> ); @@ -52,6 +53,7 @@ describe('FilterLabel', function () { negate={false} seriesId={'kpi-over-time'} removeFilter={removeFilter} + indexPattern={mockIndexPattern} /> ); @@ -74,6 +76,7 @@ describe('FilterLabel', function () { negate={false} seriesId={'kpi-over-time'} removeFilter={removeFilter} + indexPattern={mockIndexPattern} /> ); @@ -99,6 +102,7 @@ describe('FilterLabel', function () { negate={true} seriesId={'kpi-over-time'} removeFilter={jest.fn()} + indexPattern={mockIndexPattern} /> ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx index 3d4ba6dc08c37..a08e777c5ea71 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; +import { IndexPattern } from '../../../../../../../../src/plugins/data/public'; import { useSeriesFilters } from '../hooks/use_series_filters'; import { FilterValueLabel } from '../../filter_value_label/filter_value_label'; @@ -17,6 +17,7 @@ interface Props { seriesId: string; negate: boolean; definitionFilter?: boolean; + indexPattern: IndexPattern; removeFilter: (field: string, value: string, notVal: boolean) => void; } @@ -26,11 +27,10 @@ export function FilterLabel({ field, value, negate, + indexPattern, removeFilter, definitionFilter, }: Props) { - const { indexPattern } = useAppIndexPatternContext(); - const { invertFilter } = useSeriesFilters({ seriesId }); return indexPattern ? ( 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 e119507860c5c..01e8d023ae96b 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 @@ -5,8 +5,15 @@ * 2.0. */ -import { ReportViewTypeId } from '../../types'; -import { CLS_FIELD, FCP_FIELD, FID_FIELD, LCP_FIELD, TBT_FIELD } from './elasticsearch_fieldnames'; +import { ReportViewType } from '../../types'; +import { + CLS_FIELD, + FCP_FIELD, + FID_FIELD, + LCP_FIELD, + TBT_FIELD, + TRANSACTION_TIME_TO_FIRST_BYTE, +} from './elasticsearch_fieldnames'; import { AGENT_HOST_LABEL, BROWSER_FAMILY_LABEL, @@ -58,6 +65,7 @@ export const FieldLabels: Record<string, string> = { [TBT_FIELD]: TBT_LABEL, [FID_FIELD]: FID_LABEL, [CLS_FIELD]: CLS_LABEL, + [TRANSACTION_TIME_TO_FIRST_BYTE]: 'Page load time', 'monitor.id': MONITOR_ID_LABEL, 'monitor.status': MONITOR_STATUS_LABEL, @@ -77,11 +85,11 @@ export const FieldLabels: Record<string, string> = { 'http.request.method': REQUEST_METHOD, }; -export const DataViewLabels: Record<ReportViewTypeId, string> = { - dist: PERF_DIST_LABEL, - kpi: KPI_OVER_TIME_LABEL, - cwv: CORE_WEB_VITALS_LABEL, - mdd: DEVICE_DISTRIBUTION_LABEL, +export const DataViewLabels: Record<ReportViewType, string> = { + 'data-distribution': PERF_DIST_LABEL, + 'kpi-over-time': KPI_OVER_TIME_LABEL, + 'core-web-vitals': CORE_WEB_VITALS_LABEL, + 'device-data-distribution': DEVICE_DISTRIBUTION_LABEL, }; export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN'; 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 index 73739b7db12ef..eb8af4f26c01a 100644 --- 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 @@ -272,7 +272,7 @@ export const CARRIER_LOCATION = i18n.translate( export const RESPONSE_LATENCY = i18n.translate( 'xpack.observability.expView.fieldLabels.responseLatency', { - defaultMessage: 'Response latency', + defaultMessage: 'Latency', } ); @@ -294,7 +294,7 @@ export const CPU_USAGE = i18n.translate('xpack.observability.expView.fieldLabels export const TRANSACTIONS_PER_MINUTE = i18n.translate( 'xpack.observability.expView.fieldLabels.transactionPerMinute', { - defaultMessage: 'Transactions per minute', + defaultMessage: 'Throughput', } ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts index 07342d976cbea..574a9f6a2bc10 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { AppDataType, ReportViewTypes } from '../types'; +import { AppDataType, ReportViewType } from '../types'; import { getRumDistributionConfig } from './rum/data_distribution_config'; import { getSyntheticsDistributionConfig } from './synthetics/data_distribution_config'; import { getSyntheticsKPIConfig } from './synthetics/kpi_over_time_config'; @@ -17,7 +17,7 @@ import { getMobileKPIDistributionConfig } from './mobile/distribution_config'; import { getMobileDeviceDistributionConfig } from './mobile/device_distribution_config'; interface Props { - reportType: keyof typeof ReportViewTypes; + reportType: ReportViewType; indexPattern: IndexPattern; dataType: AppDataType; } @@ -25,23 +25,23 @@ interface Props { export const getDefaultConfigs = ({ reportType, dataType, indexPattern }: Props) => { switch (dataType) { case 'ux': - if (reportType === 'dist') { + if (reportType === 'data-distribution') { return getRumDistributionConfig({ indexPattern }); } - if (reportType === 'cwv') { + if (reportType === 'core-web-vitals') { return getCoreWebVitalsConfig({ indexPattern }); } return getKPITrendsLensConfig({ indexPattern }); case 'synthetics': - if (reportType === 'dist') { + if (reportType === 'data-distribution') { return getSyntheticsDistributionConfig({ indexPattern }); } return getSyntheticsKPIConfig({ indexPattern }); case 'mobile': - if (reportType === 'dist') { + if (reportType === 'data-distribution') { return getMobileKPIDistributionConfig({ indexPattern }); } - if (reportType === 'mdd') { + if (reportType === 'device-data-distribution') { return getMobileDeviceDistributionConfig({ indexPattern }); } return getMobileKPIConfig({ indexPattern }); 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 8b21df64a3c91..5189a529bda8f 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 @@ -5,25 +5,37 @@ * 2.0. */ -import { LensAttributes } from './lens_attributes'; +import { LayerConfig, LensAttributes } from './lens_attributes'; import { mockAppIndexPattern, mockIndexPattern } from '../rtl_helpers'; import { getDefaultConfigs } from './default_configs'; import { sampleAttribute } from './test_data/sample_attribute'; -import { LCP_FIELD, SERVICE_NAME, USER_AGENT_NAME } from './constants/elasticsearch_fieldnames'; +import { LCP_FIELD, USER_AGENT_NAME } from './constants/elasticsearch_fieldnames'; +import { buildExistsFilter, buildPhrasesFilter } from './utils'; describe('Lens Attribute', () => { mockAppIndexPattern(); const reportViewConfig = getDefaultConfigs({ - reportType: 'dist', + reportType: 'data-distribution', dataType: 'ux', indexPattern: mockIndexPattern, }); + reportViewConfig.filters?.push(...buildExistsFilter('transaction.type', mockIndexPattern)); + let lnsAttr: LensAttributes; + const layerConfig: LayerConfig = { + reportConfig: reportViewConfig, + seriesType: 'line', + operationType: 'count', + indexPattern: mockIndexPattern, + reportDefinitions: {}, + time: { from: 'now-15m', to: 'now' }, + }; + beforeEach(() => { - lnsAttr = new LensAttributes(mockIndexPattern, reportViewConfig, 'line', [], 'count', {}); + lnsAttr = new LensAttributes([layerConfig]); }); it('should return expected json', function () { @@ -31,7 +43,7 @@ describe('Lens Attribute', () => { }); it('should return main y axis', function () { - expect(lnsAttr.getMainYAxis()).toEqual({ + expect(lnsAttr.getMainYAxis(layerConfig)).toEqual({ dataType: 'number', isBucketed: false, label: 'Pages loaded', @@ -42,7 +54,7 @@ describe('Lens Attribute', () => { }); it('should return expected field type', function () { - expect(JSON.stringify(lnsAttr.getFieldMeta('transaction.type'))).toEqual( + expect(JSON.stringify(lnsAttr.getFieldMeta('transaction.type', layerConfig))).toEqual( JSON.stringify({ fieldMeta: { count: 0, @@ -60,7 +72,7 @@ describe('Lens Attribute', () => { }); it('should return expected field type for custom field with default value', function () { - expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric'))).toEqual( + expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric', layerConfig))).toEqual( JSON.stringify({ fieldMeta: { count: 0, @@ -79,11 +91,18 @@ describe('Lens Attribute', () => { }); it('should return expected field type for custom field with passed value', function () { - lnsAttr = new LensAttributes(mockIndexPattern, reportViewConfig, 'line', [], 'count', { - 'performance.metric': [LCP_FIELD], - }); + const layerConfig1: LayerConfig = { + reportConfig: reportViewConfig, + seriesType: 'line', + operationType: 'count', + indexPattern: mockIndexPattern, + reportDefinitions: { 'performance.metric': [LCP_FIELD] }, + time: { from: 'now-15m', to: 'now' }, + }; - expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric'))).toEqual( + lnsAttr = new LensAttributes([layerConfig1]); + + expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric', layerConfig1))).toEqual( JSON.stringify({ fieldMeta: { count: 0, @@ -102,7 +121,7 @@ describe('Lens Attribute', () => { }); it('should return expected number range column', function () { - expect(lnsAttr.getNumberRangeColumn('transaction.duration.us')).toEqual({ + expect(lnsAttr.getNumberRangeColumn('transaction.duration.us', reportViewConfig)).toEqual({ dataType: 'number', isBucketed: true, label: 'Page load time', @@ -124,7 +143,7 @@ describe('Lens Attribute', () => { }); it('should return expected number operation column', function () { - expect(lnsAttr.getNumberRangeColumn('transaction.duration.us')).toEqual({ + expect(lnsAttr.getNumberRangeColumn('transaction.duration.us', reportViewConfig)).toEqual({ dataType: 'number', isBucketed: true, label: 'Page load time', @@ -160,7 +179,7 @@ describe('Lens Attribute', () => { }); it('should return main x axis', function () { - expect(lnsAttr.getXAxis()).toEqual({ + expect(lnsAttr.getXAxis(layerConfig, 'layer0')).toEqual({ dataType: 'number', isBucketed: true, label: 'Page load time', @@ -182,38 +201,45 @@ describe('Lens Attribute', () => { }); it('should return first layer', function () { - expect(lnsAttr.getLayer()).toEqual({ - columnOrder: ['x-axis-column', 'y-axis-column'], - columns: { - 'x-axis-column': { - dataType: 'number', - isBucketed: true, - label: 'Page load time', - operationType: 'range', - params: { - maxBars: 'auto', - ranges: [ - { - from: 0, - label: '', - to: 1000, - }, - ], - type: 'histogram', + expect(lnsAttr.getLayers()).toEqual({ + layer0: { + columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'], + columns: { + 'x-axis-column-layer0': { + dataType: 'number', + isBucketed: true, + label: 'Page load time', + operationType: 'range', + params: { + maxBars: 'auto', + ranges: [ + { + from: 0, + label: '', + to: 1000, + }, + ], + type: 'histogram', + }, + scale: 'interval', + sourceField: 'transaction.duration.us', + }, + 'y-axis-column-layer0': { + dataType: 'number', + isBucketed: false, + label: 'Pages loaded', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + filter: { + language: 'kuery', + query: + 'transaction.type: page-load and processor.event: transaction and transaction.type : *', + }, }, - scale: 'interval', - sourceField: 'transaction.duration.us', - }, - 'y-axis-column': { - dataType: 'number', - isBucketed: false, - label: 'Pages loaded', - operationType: 'count', - scale: 'ratio', - sourceField: 'Records', }, + incompleteColumns: {}, }, - incompleteColumns: {}, }); }); @@ -225,12 +251,12 @@ describe('Lens Attribute', () => { gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, layers: [ { - accessors: ['y-axis-column'], - layerId: 'layer1', + accessors: ['y-axis-column-layer0'], + layerId: 'layer0', palette: undefined, seriesType: 'line', - xAccessor: 'x-axis-column', - yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }], + xAccessor: 'x-axis-column-layer0', + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], }, ], legend: { isVisible: true, position: 'right' }, @@ -240,108 +266,52 @@ describe('Lens Attribute', () => { }); }); - describe('ParseFilters function', function () { - it('should parse default filters', function () { - expect(lnsAttr.parseFilters()).toEqual([ - { meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } }, - { meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } }, - ]); - }); - - it('should parse default and ui filters', function () { - lnsAttr = new LensAttributes( - mockIndexPattern, - reportViewConfig, - 'line', - [ - { field: SERVICE_NAME, values: ['elastic-co', 'kibana-front'] }, - { field: USER_AGENT_NAME, values: ['Firefox'], notValues: ['Chrome'] }, - ], - 'count', - {} - ); + describe('Layer breakdowns', function () { + it('should return breakdown column', function () { + const layerConfig1: LayerConfig = { + reportConfig: reportViewConfig, + seriesType: 'line', + operationType: 'count', + indexPattern: mockIndexPattern, + reportDefinitions: { 'performance.metric': [LCP_FIELD] }, + breakdown: USER_AGENT_NAME, + time: { from: 'now-15m', to: 'now' }, + }; - expect(lnsAttr.parseFilters()).toEqual([ - { meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } }, - { meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } }, - { - meta: { - index: 'apm-*', - key: 'service.name', - params: ['elastic-co', 'kibana-front'], - type: 'phrases', - value: 'elastic-co, kibana-front', - }, - query: { - bool: { - minimum_should_match: 1, - should: [ - { - match_phrase: { - 'service.name': 'elastic-co', - }, - }, - { - match_phrase: { - 'service.name': 'kibana-front', - }, - }, - ], - }, - }, - }, - { - meta: { - index: 'apm-*', - }, - query: { - match_phrase: { - 'user_agent.name': 'Firefox', - }, - }, - }, - { - meta: { - index: 'apm-*', - negate: true, - }, - query: { - match_phrase: { - 'user_agent.name': 'Chrome', - }, - }, - }, - ]); - }); - }); + lnsAttr = new LensAttributes([layerConfig1]); - describe('Layer breakdowns', function () { - it('should add breakdown column', function () { - lnsAttr.addBreakdown(USER_AGENT_NAME); + lnsAttr.getBreakdownColumn({ + sourceField: USER_AGENT_NAME, + layerId: 'layer0', + indexPattern: mockIndexPattern, + }); expect(lnsAttr.visualization.layers).toEqual([ { - accessors: ['y-axis-column'], - layerId: 'layer1', + accessors: ['y-axis-column-layer0'], + layerId: 'layer0', palette: undefined, seriesType: 'line', - splitAccessor: 'break-down-column', - xAccessor: 'x-axis-column', - yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }], + splitAccessor: 'breakdown-column-layer0', + xAccessor: 'x-axis-column-layer0', + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], }, ]); - expect(lnsAttr.layers.layer1).toEqual({ - columnOrder: ['x-axis-column', 'break-down-column', 'y-axis-column'], + expect(lnsAttr.layers.layer0).toEqual({ + columnOrder: ['x-axis-column-layer0', 'breakdown-column-layer0', 'y-axis-column-layer0'], columns: { - 'break-down-column': { + 'breakdown-column-layer0': { dataType: 'string', isBucketed: true, label: 'Top values of Browser family', operationType: 'terms', params: { missingBucket: false, - orderBy: { columnId: 'y-axis-column', type: 'column' }, + orderBy: { + columnId: 'y-axis-column-layer0', + type: 'column', + }, orderDirection: 'desc', otherBucket: true, size: 10, @@ -349,10 +319,10 @@ describe('Lens Attribute', () => { scale: 'ordinal', sourceField: 'user_agent.name', }, - 'x-axis-column': { + 'x-axis-column-layer0': { dataType: 'number', isBucketed: true, - label: 'Page load time', + label: 'Largest contentful paint', operationType: 'range', params: { maxBars: 'auto', @@ -360,62 +330,47 @@ describe('Lens Attribute', () => { type: 'histogram', }, scale: 'interval', - sourceField: 'transaction.duration.us', + sourceField: 'transaction.marks.agent.largestContentfulPaint', }, - 'y-axis-column': { + 'y-axis-column-layer0': { dataType: 'number', isBucketed: false, label: 'Pages loaded', operationType: 'count', scale: 'ratio', sourceField: 'Records', + filter: { + language: 'kuery', + query: + 'transaction.type: page-load and processor.event: transaction and transaction.type : *', + }, }, }, incompleteColumns: {}, }); }); + }); - it('should remove breakdown column', function () { - lnsAttr.addBreakdown(USER_AGENT_NAME); - - lnsAttr.removeBreakdown(); + describe('Layer Filters', function () { + it('should return expected filters', function () { + reportViewConfig.filters?.push( + ...buildPhrasesFilter('service.name', ['elastic', 'kibana'], mockIndexPattern) + ); - expect(lnsAttr.visualization.layers).toEqual([ - { - accessors: ['y-axis-column'], - layerId: 'layer1', - palette: undefined, - seriesType: 'line', - xAccessor: 'x-axis-column', - yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }], - }, - ]); + const layerConfig1: LayerConfig = { + reportConfig: reportViewConfig, + seriesType: 'line', + operationType: 'count', + indexPattern: mockIndexPattern, + reportDefinitions: { 'performance.metric': [LCP_FIELD] }, + time: { from: 'now-15m', to: 'now' }, + }; - expect(lnsAttr.layers.layer1.columnOrder).toEqual(['x-axis-column', 'y-axis-column']); + const filters = lnsAttr.getLayerFilters(layerConfig1, 2); - expect(lnsAttr.layers.layer1.columns).toEqual({ - 'x-axis-column': { - dataType: 'number', - isBucketed: true, - label: 'Page load time', - operationType: 'range', - params: { - maxBars: 'auto', - ranges: [{ from: 0, label: '', to: 1000 }], - type: 'histogram', - }, - scale: 'interval', - sourceField: 'transaction.duration.us', - }, - 'y-axis-column': { - dataType: 'number', - isBucketed: false, - label: 'Pages loaded', - operationType: 'count', - scale: 'ratio', - sourceField: 'Records', - }, - }); + expect(filters).toEqual( + '@timestamp >= now-15m and @timestamp <= now and transaction.type: page-load and processor.event: transaction and transaction.type : * and service.name: (elastic or kibana)' + ); }); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index 22ad18c663b32..208e8d8ba43c2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -27,13 +27,12 @@ import { TermsIndexPatternColumn, CardinalityIndexPatternColumn, } from '../../../../../../lens/public'; -import { - buildPhraseFilter, - buildPhrasesFilter, - IndexPattern, -} from '../../../../../../../../src/plugins/data/common'; +import { urlFiltersToKueryString } from '../utils/stringify_kueries'; +import { ExistsFilter, IndexPattern } from '../../../../../../../../src/plugins/data/common'; import { FieldLabels, FILTER_RECORDS, USE_BREAK_DOWN_COLUMN, TERMS_COLUMN } from './constants'; import { ColumnFilter, DataSeries, UrlFilter, URLReportDefinition } from '../types'; +import { PersistableFilter } from '../../../../../../lens/common'; +import { parseAbsoluteDate } from '../series_date_picker/date_range_picker'; function getLayerReferenceName(layerId: string) { return `indexpattern-datasource-layer-${layerId}`; @@ -87,46 +86,50 @@ export const parseCustomFieldName = ( return { fieldName, columnType, columnFilters, timeScale, columnLabel }; }; -export class LensAttributes { +export interface LayerConfig { + filters?: UrlFilter[]; + reportConfig: DataSeries; + breakdown?: string; + seriesType?: SeriesType; + operationType?: OperationType; + reportDefinitions: URLReportDefinition; + time: { to: string; from: string }; indexPattern: IndexPattern; +} + +export class LensAttributes { layers: Record<string, PersistedIndexPatternLayer>; visualization: XYState; - filters: UrlFilter[]; - seriesType: SeriesType; - reportViewConfig: DataSeries; - reportDefinitions: URLReportDefinition; - breakdownSource?: string; + layerConfigs: LayerConfig[]; - constructor( - indexPattern: IndexPattern, - reportViewConfig: DataSeries, - seriesType?: SeriesType, - filters?: UrlFilter[], - operationType?: OperationType, - reportDefinitions?: URLReportDefinition, - breakdownSource?: string - ) { - this.indexPattern = indexPattern; + constructor(layerConfigs: LayerConfig[]) { this.layers = {}; - this.filters = filters ?? []; - this.reportDefinitions = reportDefinitions ?? {}; - this.breakdownSource = breakdownSource; - - if (operationType) { - reportViewConfig.yAxisColumns.forEach((yAxisColumn) => { - if (typeof yAxisColumn.operationType !== undefined) { - yAxisColumn.operationType = operationType as FieldBasedIndexPatternColumn['operationType']; - } - }); - } - this.seriesType = seriesType ?? reportViewConfig.defaultSeriesType; - this.reportViewConfig = reportViewConfig; - this.layers.layer1 = this.getLayer(); + + layerConfigs.forEach(({ reportConfig, operationType }) => { + if (operationType) { + reportConfig.yAxisColumns.forEach((yAxisColumn) => { + if (typeof yAxisColumn.operationType !== undefined) { + yAxisColumn.operationType = operationType as FieldBasedIndexPatternColumn['operationType']; + } + }); + } + }); + + this.layerConfigs = layerConfigs; + this.layers = this.getLayers(); this.visualization = this.getXyState(); } - getBreakdownColumn(sourceField: string): TermsIndexPatternColumn { - const fieldMeta = this.indexPattern.getFieldByName(sourceField); + getBreakdownColumn({ + sourceField, + layerId, + indexPattern, + }: { + sourceField: string; + layerId: string; + indexPattern: IndexPattern; + }): TermsIndexPatternColumn { + const fieldMeta = indexPattern.getFieldByName(sourceField); return { sourceField, @@ -136,8 +139,8 @@ export class LensAttributes { scale: 'ordinal', isBucketed: true, params: { + orderBy: { type: 'column', columnId: `y-axis-column-${layerId}` }, size: 10, - orderBy: { type: 'column', columnId: 'y-axis-column' }, orderDirection: 'desc', otherBucket: true, missingBucket: false, @@ -145,36 +148,14 @@ export class LensAttributes { }; } - addBreakdown(sourceField: string) { - const { xAxisColumn } = this.reportViewConfig; - if (xAxisColumn?.sourceField === USE_BREAK_DOWN_COLUMN) { - // do nothing since this will be used a x axis source - return; - } - this.layers.layer1.columns['break-down-column'] = this.getBreakdownColumn(sourceField); - - this.layers.layer1.columnOrder = [ - 'x-axis-column', - 'break-down-column', - 'y-axis-column', - ...Object.keys(this.getChildYAxises()), - ]; - - this.visualization.layers[0].splitAccessor = 'break-down-column'; - } - - removeBreakdown() { - delete this.layers.layer1.columns['break-down-column']; - - this.layers.layer1.columnOrder = ['x-axis-column', 'y-axis-column']; - - this.visualization.layers[0].splitAccessor = undefined; - } - - getNumberRangeColumn(sourceField: string, label?: string): RangeIndexPatternColumn { + getNumberRangeColumn( + sourceField: string, + reportViewConfig: DataSeries, + label?: string + ): RangeIndexPatternColumn { return { sourceField, - label: this.reportViewConfig.labels[sourceField] ?? label, + label: reportViewConfig.labels[sourceField] ?? label, dataType: 'number', operationType: 'range', isBucketed: true, @@ -187,16 +168,36 @@ export class LensAttributes { }; } - getCardinalityColumn(sourceField: string, label?: string) { - return this.getNumberOperationColumn(sourceField, 'unique_count', label); + getCardinalityColumn({ + sourceField, + label, + reportViewConfig, + }: { + sourceField: string; + label?: string; + reportViewConfig: DataSeries; + }) { + return this.getNumberOperationColumn({ + sourceField, + operationType: 'unique_count', + label, + reportViewConfig, + }); } - getNumberColumn( - sourceField: string, - columnType?: string, - operationType?: string, - label?: string - ) { + getNumberColumn({ + reportViewConfig, + label, + sourceField, + columnType, + operationType, + }: { + sourceField: string; + columnType?: string; + operationType?: string; + label?: string; + reportViewConfig: DataSeries; + }) { if (columnType === 'operation' || operationType) { if ( operationType === 'median' || @@ -204,48 +205,58 @@ export class LensAttributes { operationType === 'sum' || operationType === 'unique_count' ) { - return this.getNumberOperationColumn(sourceField, operationType, label); + return this.getNumberOperationColumn({ + sourceField, + operationType, + label, + reportViewConfig, + }); } if (operationType?.includes('th')) { - return this.getPercentileNumberColumn(sourceField, operationType); + return this.getPercentileNumberColumn(sourceField, operationType, reportViewConfig!); } } - return this.getNumberRangeColumn(sourceField, label); + return this.getNumberRangeColumn(sourceField, reportViewConfig!, label); } - getNumberOperationColumn( - sourceField: string, - operationType: 'average' | 'median' | 'sum' | 'unique_count', - label?: string - ): + getNumberOperationColumn({ + sourceField, + label, + reportViewConfig, + operationType, + }: { + sourceField: string; + operationType: 'average' | 'median' | 'sum' | 'unique_count'; + label?: string; + reportViewConfig: DataSeries; + }): | AvgIndexPatternColumn | MedianIndexPatternColumn | SumIndexPatternColumn | CardinalityIndexPatternColumn { return { ...buildNumberColumn(sourceField), - label: - label || - i18n.translate('xpack.observability.expView.columns.operation.label', { - defaultMessage: '{operationType} of {sourceField}', - values: { - sourceField: this.reportViewConfig.labels[sourceField], - operationType: capitalize(operationType), - }, - }), + label: i18n.translate('xpack.observability.expView.columns.operation.label', { + defaultMessage: '{operationType} of {sourceField}', + values: { + sourceField: label || reportViewConfig.labels[sourceField], + operationType: capitalize(operationType), + }, + }), operationType, }; } getPercentileNumberColumn( sourceField: string, - percentileValue: string + percentileValue: string, + reportViewConfig: DataSeries ): PercentileIndexPatternColumn { return { ...buildNumberColumn(sourceField), label: i18n.translate('xpack.observability.expView.columns.label', { defaultMessage: '{percentileValue} percentile of {sourceField}', - values: { sourceField: this.reportViewConfig.labels[sourceField], percentileValue }, + values: { sourceField: reportViewConfig.labels[sourceField], percentileValue }, }), operationType: 'percentile', params: { percentile: Number(percentileValue.split('th')[0]) }, @@ -268,7 +279,7 @@ export class LensAttributes { return { operationType: 'terms', sourceField, - label: label || 'Top values of ' + sourceField, + label: 'Top values of ' + label || sourceField, dataType: 'string', isBucketed: true, scale: 'ordinal', @@ -283,30 +294,45 @@ export class LensAttributes { }; } - getXAxis() { - const { xAxisColumn } = this.reportViewConfig; + getXAxis(layerConfig: LayerConfig, layerId: string) { + const { xAxisColumn } = layerConfig.reportConfig; if (xAxisColumn?.sourceField === USE_BREAK_DOWN_COLUMN) { - return this.getBreakdownColumn(this.breakdownSource || this.reportViewConfig.breakdowns[0]); + return this.getBreakdownColumn({ + layerId, + indexPattern: layerConfig.indexPattern, + sourceField: layerConfig.breakdown || layerConfig.reportConfig.breakdowns[0], + }); } - return this.getColumnBasedOnType(xAxisColumn.sourceField!, undefined, xAxisColumn.label); + return this.getColumnBasedOnType({ + layerConfig, + label: xAxisColumn.label, + sourceField: xAxisColumn.sourceField!, + }); } - getColumnBasedOnType( - sourceField: string, - operationType?: OperationType, - label?: string, - colIndex?: number - ) { + getColumnBasedOnType({ + sourceField, + label, + layerConfig, + operationType, + colIndex, + }: { + sourceField: string; + operationType?: OperationType; + label?: string; + layerConfig: LayerConfig; + colIndex?: number; + }) { const { fieldMeta, columnType, fieldName, - columnFilters, - timeScale, columnLabel, - } = this.getFieldMeta(sourceField); + timeScale, + columnFilters, + } = this.getFieldMeta(sourceField, layerConfig); const { type: fieldType } = fieldMeta ?? {}; if (columnType === TERMS_COLUMN) { @@ -325,47 +351,76 @@ export class LensAttributes { return this.getDateHistogramColumn(fieldName); } if (fieldType === 'number') { - return this.getNumberColumn(fieldName, columnType, operationType, columnLabel || label); + return this.getNumberColumn({ + sourceField: fieldName, + columnType, + operationType, + label: columnLabel || label, + reportViewConfig: layerConfig.reportConfig, + }); } if (operationType === 'unique_count') { - return this.getCardinalityColumn(fieldName, columnLabel || label); + return this.getCardinalityColumn({ + sourceField: fieldName, + label: columnLabel || label, + reportViewConfig: layerConfig.reportConfig, + }); } // FIXME review my approach again return this.getDateHistogramColumn(fieldName); } - getCustomFieldName(sourceField: string) { - return parseCustomFieldName(sourceField, this.reportViewConfig, this.reportDefinitions); + getCustomFieldName({ + sourceField, + layerConfig, + }: { + sourceField: string; + layerConfig: LayerConfig; + }) { + return parseCustomFieldName( + sourceField, + layerConfig.reportConfig, + layerConfig.reportDefinitions + ); } - getFieldMeta(sourceField: string) { + getFieldMeta(sourceField: string, layerConfig: LayerConfig) { const { fieldName, columnType, + columnLabel, columnFilters, timeScale, - columnLabel, - } = this.getCustomFieldName(sourceField); + } = this.getCustomFieldName({ + sourceField, + layerConfig, + }); - const fieldMeta = this.indexPattern.getFieldByName(fieldName); + const fieldMeta = layerConfig.indexPattern.getFieldByName(fieldName); - return { fieldMeta, fieldName, columnType, columnFilters, timeScale, columnLabel }; + return { fieldMeta, fieldName, columnType, columnLabel, columnFilters, timeScale }; } - getMainYAxis() { - const { sourceField, operationType, label } = this.reportViewConfig.yAxisColumns[0]; + getMainYAxis(layerConfig: LayerConfig) { + const { sourceField, operationType, label } = layerConfig.reportConfig.yAxisColumns[0]; if (sourceField === 'Records' || !sourceField) { return this.getRecordsColumn(label); } - return this.getColumnBasedOnType(sourceField!, operationType, label, 0); + return this.getColumnBasedOnType({ + sourceField, + operationType, + label, + layerConfig, + colIndex: 0, + }); } - getChildYAxises() { + getChildYAxises(layerConfig: LayerConfig) { const lensColumns: Record<string, FieldBasedIndexPatternColumn | SumIndexPatternColumn> = {}; - const yAxisColumns = this.reportViewConfig.yAxisColumns; + const yAxisColumns = layerConfig.reportConfig.yAxisColumns; // 1 means there is only main y axis if (yAxisColumns.length === 1) { return lensColumns; @@ -373,12 +428,13 @@ export class LensAttributes { for (let i = 1; i < yAxisColumns.length; i++) { const { sourceField, operationType, label } = yAxisColumns[i]; - lensColumns[`y-axis-column-${i}`] = this.getColumnBasedOnType( - sourceField!, + lensColumns[`y-axis-column-${i}`] = this.getColumnBasedOnType({ + sourceField: sourceField!, operationType, label, - i - ); + layerConfig, + colIndex: i, + }); } return lensColumns; } @@ -396,20 +452,139 @@ export class LensAttributes { scale: 'ratio', sourceField: 'Records', filter: columnFilter, - timeScale, + ...(timeScale ? { timeScale } : {}), } as CountIndexPatternColumn; } - getLayer() { - return { - columnOrder: ['x-axis-column', 'y-axis-column', ...Object.keys(this.getChildYAxises())], - columns: { - 'x-axis-column': this.getXAxis(), - 'y-axis-column': this.getMainYAxis(), - ...this.getChildYAxises(), - }, - incompleteColumns: {}, - }; + getLayerFilters(layerConfig: LayerConfig, totalLayers: number) { + const { + filters, + time: { from, to }, + reportConfig: { filters: layerFilters, reportType }, + } = layerConfig; + let baseFilters = ''; + if (reportType !== 'kpi-over-time' && totalLayers > 1) { + // for kpi over time, we don't need to add time range filters + // since those are essentially plotted along the x-axis + baseFilters += `@timestamp >= ${from} and @timestamp <= ${to}`; + } + + layerFilters?.forEach((filter: PersistableFilter | ExistsFilter) => { + const qFilter = filter as PersistableFilter; + if (qFilter.query?.match_phrase) { + const fieldName = Object.keys(qFilter.query.match_phrase)[0]; + const kql = `${fieldName}: ${qFilter.query.match_phrase[fieldName]}`; + if (baseFilters.length > 0) { + baseFilters += ` and ${kql}`; + } else { + baseFilters += kql; + } + } + if (qFilter.query?.bool?.should) { + const values: string[] = []; + let fieldName = ''; + qFilter.query?.bool.should.forEach((ft: PersistableFilter['query']['match_phrase']) => { + if (ft.match_phrase) { + fieldName = Object.keys(ft.match_phrase)[0]; + values.push(ft.match_phrase[fieldName]); + } + }); + + const kueryString = `${fieldName}: (${values.join(' or ')})`; + + if (baseFilters.length > 0) { + baseFilters += ` and ${kueryString}`; + } else { + baseFilters += kueryString; + } + } + const existFilter = filter as ExistsFilter; + + if (existFilter.exists) { + const fieldName = existFilter.exists.field; + const kql = `${fieldName} : *`; + if (baseFilters.length > 0) { + baseFilters += ` and ${kql}`; + } else { + baseFilters += kql; + } + } + }); + + const rFilters = urlFiltersToKueryString(filters ?? []); + if (!baseFilters) { + return rFilters; + } + if (!rFilters) { + return baseFilters; + } + return `${rFilters} and ${baseFilters}`; + } + + getTimeShift(mainLayerConfig: LayerConfig, layerConfig: LayerConfig, index: number) { + if (index === 0 || mainLayerConfig.reportConfig.reportType !== 'kpi-over-time') { + return null; + } + + const { + time: { from: mainFrom }, + } = mainLayerConfig; + + const { + time: { from }, + } = layerConfig; + + const inDays = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'days'); + if (inDays > 1) { + return inDays + 'd'; + } + const inHours = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'hours'); + return inHours + 'h'; + } + + getLayers() { + const layers: Record<string, PersistedIndexPatternLayer> = {}; + const layerConfigs = this.layerConfigs; + + layerConfigs.forEach((layerConfig, index) => { + const { breakdown } = layerConfig; + + const layerId = `layer${index}`; + const columnFilter = this.getLayerFilters(layerConfig, layerConfigs.length); + const timeShift = this.getTimeShift(this.layerConfigs[0], layerConfig, index); + const mainYAxis = this.getMainYAxis(layerConfig); + layers[layerId] = { + columnOrder: [ + `x-axis-column-${layerId}`, + ...(breakdown ? [`breakdown-column-${layerId}`] : []), + `y-axis-column-${layerId}`, + ...Object.keys(this.getChildYAxises(layerConfig)), + ], + columns: { + [`x-axis-column-${layerId}`]: this.getXAxis(layerConfig, layerId), + [`y-axis-column-${layerId}`]: { + ...mainYAxis, + label: timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label, + filter: { query: columnFilter, language: 'kuery' }, + ...(timeShift ? { timeShift } : {}), + }, + ...(breakdown && breakdown !== USE_BREAK_DOWN_COLUMN + ? // do nothing since this will be used a x axis source + { + [`breakdown-column-${layerId}`]: this.getBreakdownColumn({ + layerId, + sourceField: breakdown, + indexPattern: layerConfig.indexPattern, + }), + } + : {}), + ...this.getChildYAxises(layerConfig), + }, + incompleteColumns: {}, + }; + }); + + return layers; } getXyState(): XYState { @@ -422,71 +597,48 @@ export class LensAttributes { tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, preferredSeriesType: 'line', - layers: [ - { - accessors: ['y-axis-column', ...Object.keys(this.getChildYAxises())], - layerId: 'layer1', - seriesType: this.seriesType ?? 'line', - palette: this.reportViewConfig.palette, - yConfig: this.reportViewConfig.yConfig || [ - { forAccessor: 'y-axis-column', color: 'green' }, - ], - xAccessor: 'x-axis-column', - }, - ], - ...(this.reportViewConfig.yTitle ? { yTitle: this.reportViewConfig.yTitle } : {}), + layers: this.layerConfigs.map((layerConfig, index) => ({ + accessors: [ + `y-axis-column-layer${index}`, + ...Object.keys(this.getChildYAxises(layerConfig)), + ], + layerId: `layer${index}`, + seriesType: layerConfig.seriesType || layerConfig.reportConfig.defaultSeriesType, + palette: layerConfig.reportConfig.palette, + yConfig: layerConfig.reportConfig.yConfig || [ + { forAccessor: `y-axis-column-layer${index}` }, + ], + xAccessor: `x-axis-column-layer${index}`, + ...(layerConfig.breakdown ? { splitAccessor: `breakdown-column-layer${index}` } : {}), + })), + ...(this.layerConfigs[0].reportConfig.yTitle + ? { yTitle: this.layerConfigs[0].reportConfig.yTitle } + : {}), }; } - parseFilters() { - const defaultFilters = this.reportViewConfig.filters ?? []; - const parsedFilters = this.reportViewConfig.filters ? [...defaultFilters] : []; - - this.filters.forEach(({ field, values = [], notValues = [] }) => { - const fieldMeta = this.indexPattern.fields.find((fieldT) => fieldT.name === field)!; - - if (values?.length > 0) { - if (values?.length > 1) { - const multiFilter = buildPhrasesFilter(fieldMeta, values, this.indexPattern); - parsedFilters.push(multiFilter); - } else { - const filter = buildPhraseFilter(fieldMeta, values[0], this.indexPattern); - parsedFilters.push(filter); - } - } - - if (notValues?.length > 0) { - if (notValues?.length > 1) { - const multiFilter = buildPhrasesFilter(fieldMeta, notValues, this.indexPattern); - multiFilter.meta.negate = true; - parsedFilters.push(multiFilter); - } else { - const filter = buildPhraseFilter(fieldMeta, notValues[0], this.indexPattern); - filter.meta.negate = true; - parsedFilters.push(filter); - } - } - }); - - return parsedFilters; - } + parseFilters() {} getJSON(): TypedLensByValueInput['attributes'] { + const uniqueIndexPatternsIds = Array.from( + new Set([...this.layerConfigs.map(({ indexPattern }) => indexPattern.id)]) + ); + return { title: 'Prefilled from exploratory view app', description: '', visualizationType: 'lnsXY', references: [ - { - id: this.indexPattern.id!, + ...uniqueIndexPatternsIds.map((patternId) => ({ + id: patternId!, name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', - }, - { - id: this.indexPattern.id!, - name: getLayerReferenceName('layer1'), + })), + ...this.layerConfigs.map(({ indexPattern }, index) => ({ + id: indexPattern.id!, + name: getLayerReferenceName(`layer${index}`), type: 'index-pattern', - }, + })), ], state: { datasourceStates: { @@ -496,7 +648,7 @@ export class LensAttributes { }, visualization: this.visualization, query: { query: '', language: 'kuery' }, - filters: this.parseFilters(), + filters: [], }, }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts index 6f9806660e489..e1cb5a0370fb2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts @@ -14,7 +14,7 @@ import { MobileFields } from './mobile_fields'; export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps): DataSeries { return { - reportType: 'mobile-device-distribution', + reportType: 'device-data-distribution', defaultSeriesType: 'bar', seriesTypes: ['bar', 'bar_horizontal'], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts index 2ed4d95760db7..9a2e86a8f7969 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts @@ -71,18 +71,6 @@ export function getMobileKPIConfig({ indexPattern }: ConfigProps): DataSeries { id: TRANSACTION_DURATION, columnType: OPERATION_COLUMN, }, - { - label: MEMORY_USAGE, - field: METRIC_SYSTEM_MEMORY_USAGE, - id: METRIC_SYSTEM_MEMORY_USAGE, - columnType: OPERATION_COLUMN, - }, - { - label: CPU_USAGE, - field: METRIC_SYSTEM_CPU_USAGE, - id: METRIC_SYSTEM_CPU_USAGE, - columnType: OPERATION_COLUMN, - }, { field: RECORDS_FIELD, id: RECORDS_FIELD, @@ -95,6 +83,18 @@ export function getMobileKPIConfig({ indexPattern }: ConfigProps): DataSeries { ], timeScale: 'm', }, + { + label: MEMORY_USAGE, + field: METRIC_SYSTEM_MEMORY_USAGE, + id: METRIC_SYSTEM_MEMORY_USAGE, + columnType: OPERATION_COLUMN, + }, + { + label: CPU_USAGE, + field: METRIC_SYSTEM_CPU_USAGE, + id: METRIC_SYSTEM_CPU_USAGE, + columnType: OPERATION_COLUMN, + }, ], }, ], diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts index 854f844db047d..b958c0dd71528 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts @@ -10,10 +10,10 @@ import { FieldLabels, RECORDS_FIELD } from '../constants'; import { buildExistsFilter } from '../utils'; import { MONITORS_DURATION_LABEL, PINGS_LABEL } from '../constants/labels'; -export function getSyntheticsDistributionConfig({ indexPattern }: ConfigProps): DataSeries { +export function getSyntheticsDistributionConfig({ series, indexPattern }: ConfigProps): DataSeries { return { reportType: 'data-distribution', - defaultSeriesType: 'line', + defaultSeriesType: series?.seriesType || 'line', seriesTypes: [], xAxisColumn: { sourceField: 'performance.metric', 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 9b299e7d70bcc..edf2a42415820 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 @@ -10,16 +10,16 @@ export const sampleAttribute = { visualizationType: 'lnsXY', references: [ { id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' }, - { id: 'apm-*', name: 'indexpattern-datasource-layer-layer1', type: 'index-pattern' }, + { id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' }, ], state: { datasourceStates: { indexpattern: { layers: { - layer1: { - columnOrder: ['x-axis-column', 'y-axis-column'], + layer0: { + columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'], columns: { - 'x-axis-column': { + 'x-axis-column-layer0': { sourceField: 'transaction.duration.us', label: 'Page load time', dataType: 'number', @@ -32,13 +32,18 @@ export const sampleAttribute = { maxBars: 'auto', }, }, - 'y-axis-column': { + 'y-axis-column-layer0': { dataType: 'number', isBucketed: false, label: 'Pages loaded', operationType: 'count', scale: 'ratio', sourceField: 'Records', + filter: { + language: 'kuery', + query: + 'transaction.type: page-load and processor.event: transaction and transaction.type : *', + }, }, }, incompleteColumns: {}, @@ -57,18 +62,15 @@ export const sampleAttribute = { preferredSeriesType: 'line', layers: [ { - accessors: ['y-axis-column'], - layerId: 'layer1', + accessors: ['y-axis-column-layer0'], + layerId: 'layer0', seriesType: 'line', - yConfig: [{ forAccessor: 'y-axis-column', color: 'green' }], - xAccessor: 'x-axis-column', + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], + xAccessor: 'x-axis-column-layer0', }, ], }, query: { query: '', language: 'kuery' }, - filters: [ - { meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } }, - { meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } }, - ], + filters: [], }, }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts index fc60800bc4403..9b1e7ec141ca2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts @@ -5,11 +5,12 @@ * 2.0. */ import rison, { RisonValue } from 'rison-node'; +import type { SeriesUrl, UrlFilter } from '../types'; import type { AllSeries, AllShortSeries } from '../hooks/use_series_storage'; -import type { SeriesUrl } from '../types'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; -import { esFilters } from '../../../../../../../../src/plugins/data/public'; +import { esFilters, ExistsFilter } from '../../../../../../../../src/plugins/data/public'; import { URL_KEYS } from './constants/url_constants'; +import { PersistableFilter } from '../../../../../../lens/common'; export function convertToShortUrl(series: SeriesUrl) { const { @@ -51,7 +52,7 @@ export function createExploratoryViewUrl(allSeries: AllSeries, baseHref = '') { } export function buildPhraseFilter(field: string, value: string, indexPattern: IIndexPattern) { - const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field); + const fieldMeta = indexPattern?.fields.find((fieldT) => fieldT.name === field); if (fieldMeta) { return [esFilters.buildPhraseFilter(fieldMeta, value, indexPattern)]; } @@ -59,7 +60,7 @@ export function buildPhraseFilter(field: string, value: string, indexPattern: II } export function buildPhrasesFilter(field: string, value: string[], indexPattern: IIndexPattern) { - const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field); + const fieldMeta = indexPattern?.fields.find((fieldT) => fieldT.name === field); if (fieldMeta) { return [esFilters.buildPhrasesFilter(fieldMeta, value, indexPattern)]; } @@ -67,9 +68,38 @@ export function buildPhrasesFilter(field: string, value: string[], indexPattern: } export function buildExistsFilter(field: string, indexPattern: IIndexPattern) { - const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field); + const fieldMeta = indexPattern?.fields.find((fieldT) => fieldT.name === field); if (fieldMeta) { return [esFilters.buildExistsFilter(fieldMeta, indexPattern)]; } return []; } + +type FiltersType = PersistableFilter[] | ExistsFilter[]; + +export function urlFilterToPersistedFilter({ + urlFilters, + initFilters, + indexPattern, +}: { + urlFilters: UrlFilter[]; + initFilters: FiltersType; + indexPattern: IIndexPattern; +}) { + const parsedFilters: FiltersType = initFilters ? [...initFilters] : []; + + urlFilters.forEach(({ field, values = [], notValues = [] }) => { + if (values?.length > 0) { + const filter = buildPhrasesFilter(field, values, indexPattern); + parsedFilters.push(...filter); + } + + if (notValues?.length > 0) { + const filter = buildPhrasesFilter(field, notValues, indexPattern)[0]; + filter.meta.negate = true; + parsedFilters.push(filter); + } + }); + + return parsedFilters; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx index 779049601bd6d..989ebf17c2062 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx @@ -51,8 +51,9 @@ describe('ExploratoryView', () => { const initSeries = { data: { 'ux-series': { + isNew: true, dataType: 'ux' as const, - reportType: 'dist' as const, + reportType: 'data-distribution' as const, breakdown: 'user_agent .name', reportDefinitions: { 'service.name': ['elastic-co'] }, time: { from: 'now-15m', to: 'now' }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx index 329ed20ffed3d..ad85ecab968b2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx @@ -5,9 +5,10 @@ * 2.0. */ import { i18n } from '@kbn/i18n'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState, useCallback } from 'react'; import { EuiPanel, EuiTitle } from '@elastic/eui'; import styled from 'styled-components'; +import { isEmpty } from 'lodash'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { ExploratoryViewHeader } from './header/header'; @@ -17,10 +18,37 @@ import { EmptyView } from './components/empty_view'; import { TypedLensByValueInput } from '../../../../../lens/public'; import { useAppIndexPatternContext } from './hooks/use_app_index_pattern'; import { SeriesBuilder } from './series_builder/series_builder'; +import { SeriesUrl } from './types'; + +export const combineTimeRanges = ( + allSeries: Record<string, SeriesUrl>, + firstSeries?: SeriesUrl +) => { + let to: string = ''; + let from: string = ''; + if (firstSeries?.reportType === 'kpi-over-time') { + return firstSeries.time; + } + Object.values(allSeries ?? {}).forEach((series) => { + if (series.dataType && series.reportType && !isEmpty(series.reportDefinitions)) { + const seriesTo = new Date(series.time.to); + const seriesFrom = new Date(series.time.from); + if (!to || seriesTo > new Date(to)) { + to = series.time.to; + } + if (!from || seriesFrom < new Date(from)) { + from = series.time.from; + } + } + }); + return { to, from }; +}; export function ExploratoryView({ saveAttributes, + multiSeries, }: { + multiSeries?: boolean; saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void; }) { const { @@ -33,6 +61,8 @@ export function ExploratoryView({ const [height, setHeight] = useState<string>('100vh'); const [seriesId, setSeriesId] = useState<string>(''); + const [lastUpdated, setLastUpdated] = useState<number | undefined>(); + const [lensAttributes, setLensAttributes] = useState<TypedLensByValueInput['attributes'] | null>( null ); @@ -47,9 +77,7 @@ export function ExploratoryView({ setSeriesId(firstSeriesId); }, [allSeries, firstSeriesId]); - const lensAttributesT = useLensAttributes({ - seriesId, - }); + const lensAttributesT = useLensAttributes(); const setHeightOffset = () => { if (seriesBuilderRef?.current && wrapperRef.current) { @@ -60,10 +88,12 @@ export function ExploratoryView({ }; useEffect(() => { - if (series?.dataType) { - loadIndexPattern({ dataType: series?.dataType }); - } - }, [series?.dataType, loadIndexPattern]); + Object.values(allSeries).forEach((seriesT) => { + loadIndexPattern({ + dataType: seriesT.dataType, + }); + }); + }, [allSeries, loadIndexPattern]); useEffect(() => { setLensAttributes(lensAttributesT); @@ -72,47 +102,62 @@ export function ExploratoryView({ } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify(lensAttributesT ?? {}), series?.reportType, series?.time?.from]); + }, [JSON.stringify(lensAttributesT ?? {})]); useEffect(() => { setHeightOffset(); }); + const timeRange = combineTimeRanges(allSeries, series); + + const onLensLoad = useCallback(() => { + setLastUpdated(Date.now()); + }, []); + + const onBrushEnd = useCallback( + ({ range }: { range: number[] }) => { + if (series?.reportType !== 'data-distribution') { + setSeries(seriesId, { + ...series, + time: { + from: new Date(range[0]).toISOString(), + to: new Date(range[1]).toISOString(), + }, + }); + } else { + notifications?.toasts.add( + i18n.translate('xpack.observability.exploratoryView.noBrusing', { + defaultMessage: 'Zoom by brush selection is only available on time series charts.', + }) + ); + } + }, + [notifications?.toasts, series, seriesId, setSeries] + ); + return ( <Wrapper> {lens ? ( <> <ExploratoryViewHeader lensAttributes={lensAttributes} seriesId={seriesId} /> <LensWrapper ref={wrapperRef} height={height}> - {lensAttributes && seriesId && series?.reportType && series?.time ? ( + {lensAttributes && timeRange.to && timeRange.from ? ( <LensComponent id="exploratoryView" - timeRange={series?.time} + timeRange={timeRange} attributes={lensAttributes} - onBrushEnd={({ range }) => { - if (series?.reportType !== 'dist') { - setSeries(seriesId, { - ...series, - time: { - from: new Date(range[0]).toISOString(), - to: new Date(range[1]).toISOString(), - }, - }); - } else { - notifications?.toasts.add( - i18n.translate('xpack.observability.exploratoryView.noBrusing', { - defaultMessage: - 'Zoom by brush selection is only available on time series charts.', - }) - ); - } - }} + onLoad={onLensLoad} + onBrushEnd={onBrushEnd} /> ) : ( <EmptyView series={series} loading={loading} height={height} /> )} </LensWrapper> - <SeriesBuilder seriesId={seriesId} seriesBuilderRef={seriesBuilderRef} /> + <SeriesBuilder + seriesBuilderRef={seriesBuilderRef} + lastUpdated={lastUpdated} + multiSeries={multiSeries} + /> </> ) : ( <EuiTitle> diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx index 1dedc4142f174..8cd8977fcf741 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx @@ -26,7 +26,7 @@ describe('ExploratoryViewHeader', function () { data: { 'uptime-pings-histogram': { dataType: 'synthetics' as const, - reportType: 'kpi' as const, + reportType: 'kpi-over-time' as const, breakdown: 'monitor.status', time: { from: 'now-15m', to: 'now' }, }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx index 3e02207e26272..dbe9cd163451d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx @@ -13,6 +13,7 @@ import { useKibana } from '../../../../../../../../src/plugins/kibana_react/publ import { DataViewLabels } from '../configurations/constants'; import { ObservabilityAppServices } from '../../../../application/types'; import { useSeriesStorage } from '../hooks/use_series_storage'; +import { combineTimeRanges } from '../exploratory_view'; interface Props { seriesId: string; @@ -24,7 +25,7 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) { const { lens } = kServices; - const { getSeries } = useSeriesStorage(); + const { getSeries, allSeries } = useSeriesStorage(); const series = getSeries(seriesId); @@ -32,6 +33,8 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) { const LensSaveModalComponent = lens.SaveModalComponent; + const timeRange = combineTimeRanges(allSeries, series); + return ( <> <EuiFlexGroup justifyContent="spaceBetween" alignItems="center"> @@ -63,7 +66,7 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) { lens.navigateToPrefilledEditor( { id: '', - timeRange: series.time, + timeRange, attributes: lensAttributes, }, true diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx index 4259bb778e511..7a5f12a72b1f0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx @@ -15,7 +15,6 @@ import { getDataHandler } from '../../../../data_handler'; export interface IIndexPatternContext { loading: boolean; - selectedApp: AppDataType; indexPatterns: IndexPatternState; hasAppData: HasAppDataState; loadIndexPattern: (params: { dataType: AppDataType }) => void; @@ -29,10 +28,10 @@ interface ProviderProps { type HasAppDataState = Record<AppDataType, boolean | null>; type IndexPatternState = Record<AppDataType, IndexPattern>; +type LoadingState = Record<AppDataType, boolean>; export function IndexPatternContextProvider({ children }: ProviderProps) { - const [loading, setLoading] = useState(false); - const [selectedApp, setSelectedApp] = useState<AppDataType>(); + const [loading, setLoading] = useState<LoadingState>({} as LoadingState); const [indexPatterns, setIndexPatterns] = useState<IndexPatternState>({} as IndexPatternState); const [hasAppData, setHasAppData] = useState<HasAppDataState>({ infra_metrics: null, @@ -49,10 +48,9 @@ export function IndexPatternContextProvider({ children }: ProviderProps) { const loadIndexPattern: IIndexPatternContext['loadIndexPattern'] = useCallback( async ({ dataType }) => { - setSelectedApp(dataType); + if (hasAppData[dataType] === null && !loading[dataType]) { + setLoading((prevState) => ({ ...prevState, [dataType]: true })); - if (hasAppData[dataType] === null) { - setLoading(true); try { let hasDataT = false; let indices: string | undefined = ''; @@ -78,23 +76,22 @@ export function IndexPatternContextProvider({ children }: ProviderProps) { setIndexPatterns((prevState) => ({ ...prevState, [dataType]: indPattern })); } - setLoading(false); + setLoading((prevState) => ({ ...prevState, [dataType]: false })); } catch (e) { - setLoading(false); + setLoading((prevState) => ({ ...prevState, [dataType]: false })); } } }, - [data, hasAppData] + [data, hasAppData, loading] ); return ( <IndexPatternContext.Provider value={{ - loading, hasAppData, - selectedApp, indexPatterns, loadIndexPattern, + loading: !!Object.values(loading).find((loadingT) => loadingT), }} > {children} @@ -102,19 +99,23 @@ export function IndexPatternContextProvider({ children }: ProviderProps) { ); } -export const useAppIndexPatternContext = () => { - const { selectedApp, loading, hasAppData, loadIndexPattern, indexPatterns } = useContext( +export const useAppIndexPatternContext = (dataType?: AppDataType) => { + const { loading, hasAppData, loadIndexPattern, indexPatterns } = useContext( (IndexPatternContext as unknown) as Context<IIndexPatternContext> ); + if (dataType && !indexPatterns?.[dataType] && !loading) { + loadIndexPattern({ dataType }); + } + return useMemo(() => { return { hasAppData, - selectedApp, loading, - indexPattern: indexPatterns?.[selectedApp], - hasData: hasAppData?.[selectedApp], + indexPatterns, + indexPattern: dataType ? indexPatterns?.[dataType] : undefined, + hasData: dataType ? hasAppData?.[dataType] : undefined, loadIndexPattern, }; - }, [hasAppData, indexPatterns, loadIndexPattern, loading, selectedApp]); + }, [dataType, hasAppData, indexPatterns, loadIndexPattern, loading]); }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts index 1c85bc5089b2a..11487afe28e96 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts @@ -8,17 +8,13 @@ import { useMemo } from 'react'; import { isEmpty } from 'lodash'; import { TypedLensByValueInput } from '../../../../../../lens/public'; -import { LensAttributes } from '../configurations/lens_attributes'; +import { LayerConfig, LensAttributes } from '../configurations/lens_attributes'; import { useSeriesStorage } from './use_series_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; import { DataSeries, SeriesUrl, UrlFilter } from '../types'; import { useAppIndexPatternContext } from './use_app_index_pattern'; -interface Props { - seriesId: string; -} - export const getFiltersFromDefs = ( reportDefinitions: SeriesUrl['reportDefinitions'], dataViewConfig: DataSeries @@ -37,54 +33,51 @@ export const getFiltersFromDefs = ( }); }; -export const useLensAttributes = ({ - seriesId, -}: Props): TypedLensByValueInput['attributes'] | null => { - const { getSeries } = useSeriesStorage(); - const series = getSeries(seriesId); - const { breakdown, seriesType, operationType, reportType, dataType, reportDefinitions = {} } = - series ?? {}; +export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null => { + const { allSeriesIds, allSeries } = useSeriesStorage(); - const { indexPattern } = useAppIndexPatternContext(); + const { indexPatterns } = useAppIndexPatternContext(); return useMemo(() => { - if (!indexPattern || !reportType || isEmpty(reportDefinitions)) { + if (isEmpty(indexPatterns) || isEmpty(allSeriesIds)) { return null; } - const dataViewConfig = getDefaultConfigs({ - reportType, - dataType, - indexPattern, - }); + const layerConfigs: LayerConfig[] = []; + + allSeriesIds.forEach((seriesIdT) => { + const seriesT = allSeries[seriesIdT]; + const indexPattern = indexPatterns?.[seriesT?.dataType]; + if (indexPattern && seriesT.reportType && !isEmpty(seriesT.reportDefinitions)) { + const reportViewConfig = getDefaultConfigs({ + reportType: seriesT.reportType, + dataType: seriesT.dataType, + indexPattern, + }); - const filters: UrlFilter[] = (series.filters ?? []).concat( - getFiltersFromDefs(reportDefinitions, dataViewConfig) - ); + const filters: UrlFilter[] = (seriesT.filters ?? []).concat( + getFiltersFromDefs(seriesT.reportDefinitions, reportViewConfig) + ); - const lensAttributes = new LensAttributes( - indexPattern, - dataViewConfig, - seriesType, - filters, - operationType, - reportDefinitions, - breakdown - ); + layerConfigs.push({ + filters, + indexPattern, + reportConfig: reportViewConfig, + breakdown: seriesT.breakdown, + operationType: seriesT.operationType, + seriesType: seriesT.seriesType, + reportDefinitions: seriesT.reportDefinitions ?? {}, + time: seriesT.time, + }); + } + }); - if (breakdown) { - lensAttributes.addBreakdown(breakdown); + if (layerConfigs.length < 1) { + return null; } + const lensAttributes = new LensAttributes(layerConfigs); + return lensAttributes.getJSON(); - }, [ - indexPattern, - reportType, - reportDefinitions, - dataType, - series.filters, - seriesType, - operationType, - breakdown, - ]); + }, [indexPatterns, allSeriesIds, allSeries]); }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx index fac75f910a93f..e9ae43950d47d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx @@ -12,7 +12,7 @@ import { } from '../../../../../../../../src/plugins/kibana_utils/public'; import type { AppDataType, - ReportViewTypeId, + ReportViewType, SeriesUrl, UrlFilter, URLReportDefinition, @@ -36,6 +36,16 @@ interface ProviderProps { storage: IKbnUrlStateStorage | ISessionStorageStateStorage; } +function convertAllShortSeries(allShortSeries: AllShortSeries) { + const allSeriesIds = Object.keys(allShortSeries); + const allSeriesN: AllSeries = {}; + allSeriesIds.forEach((seriesKey) => { + allSeriesN[seriesKey] = convertFromShortUrl(allShortSeries[seriesKey]); + }); + + return allSeriesN; +} + export function UrlStorageContextProvider({ children, storage, @@ -45,15 +55,14 @@ export function UrlStorageContextProvider({ const [allShortSeries, setAllShortSeries] = useState<AllShortSeries>( () => storage.get(allSeriesKey) ?? {} ); - const [allSeries, setAllSeries] = useState<AllSeries>({}); + const [allSeries, setAllSeries] = useState<AllSeries>(() => + convertAllShortSeries(storage.get(allSeriesKey) ?? {}) + ); const [firstSeriesId, setFirstSeriesId] = useState(''); useEffect(() => { const allSeriesIds = Object.keys(allShortSeries); - const allSeriesN: AllSeries = {}; - allSeriesIds.forEach((seriesKey) => { - allSeriesN[seriesKey] = convertFromShortUrl(allShortSeries[seriesKey]); - }); + const allSeriesN: AllSeries = convertAllShortSeries(allShortSeries ?? {}); setAllSeries(allSeriesN); setFirstSeriesId(allSeriesIds?.[0]); @@ -68,8 +77,10 @@ export function UrlStorageContextProvider({ }; const removeSeries = (seriesIdN: string) => { - delete allShortSeries[seriesIdN]; - delete allSeries[seriesIdN]; + setAllShortSeries((prevState) => { + delete prevState[seriesIdN]; + return { ...prevState }; + }); }; const allSeriesIds = Object.keys(allShortSeries); @@ -115,7 +126,7 @@ function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl { interface ShortUrlSeries { [URL_KEYS.OPERATION_TYPE]?: OperationType; - [URL_KEYS.REPORT_TYPE]?: ReportViewTypeId; + [URL_KEYS.REPORT_TYPE]?: ReportViewType; [URL_KEYS.DATA_TYPE]?: AppDataType; [URL_KEYS.SERIES_TYPE]?: SeriesType; [URL_KEYS.BREAK_DOWN]?: string; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx index 3de29b02853e8..e55752ceb62ba 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx @@ -25,9 +25,11 @@ import { TypedLensByValueInput } from '../../../../../lens/public'; export function ExploratoryViewPage({ saveAttributes, + multiSeries = false, useSessionStorage = false, }: { useSessionStorage?: boolean; + multiSeries?: boolean; saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void; }) { useTrackPageview({ app: 'observability-overview', path: 'exploratory-view' }); @@ -59,7 +61,7 @@ export function ExploratoryViewPage({ <Wrapper> <IndexPatternContextProvider> <UrlStorageContextProvider storage={kbnUrlStateStorage}> - <ExploratoryView saveAttributes={saveAttributes} /> + <ExploratoryView saveAttributes={saveAttributes} multiSeries={multiSeries} /> </UrlStorageContextProvider> </IndexPatternContextProvider> </Wrapper> diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx index 8e54ab7629d26..972e3beb4b722 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx @@ -35,8 +35,11 @@ import { getStubIndexPattern } from '../../../../../../../src/plugins/data/publi import indexPatternData from './configurations/test_data/test_index_pattern.json'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { setIndexPatterns } from '../../../../../../../src/plugins/data/public/services'; -import { IndexPatternsContract } from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; -import { UrlFilter } from './types'; +import { + IndexPattern, + IndexPatternsContract, +} from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; +import { AppDataType, UrlFilter } from './types'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { ListItem } from '../../../hooks/use_values_list'; @@ -232,11 +235,11 @@ export const mockAppIndexPattern = () => { const loadIndexPattern = jest.fn(); const spy = jest.spyOn(useAppIndexPatternHook, 'useAppIndexPatternContext').mockReturnValue({ indexPattern: mockIndexPattern, - selectedApp: 'ux', hasData: true, loading: false, hasAppData: { ux: true } as any, loadIndexPattern, + indexPatterns: ({ ux: mockIndexPattern } as unknown) as Record<AppDataType, IndexPattern>, }); return { spy, loadIndexPattern }; }; @@ -260,7 +263,7 @@ function mockSeriesStorageContext({ }) { const mockDataSeries = data || { 'performance-distribution': { - reportType: 'dist', + reportType: 'data-distribution', dataType: 'ux', breakdown: breakdown || 'user_agent.name', time: { from: 'now-15m', to: 'now' }, 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 9ae8b68bf3e8c..50c2f91e6067d 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 @@ -27,18 +27,14 @@ export function SeriesChartTypesSelect({ seriesTypes?: SeriesType[]; defaultChartType: SeriesType; }) { - const { getSeries, setSeries, allSeries } = useSeriesStorage(); + const { getSeries, setSeries } = useSeriesStorage(); const series = getSeries(seriesId); const seriesType = series?.seriesType ?? defaultChartType; const onChange = (value: SeriesType) => { - Object.keys(allSeries).forEach((seriesKey) => { - const seriesN = allSeries[seriesKey]; - - setSeries(seriesKey, { ...seriesN, seriesType: value }); - }); + setSeries(seriesId, { ...series, seriesType: value }); }; return ( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx index e3c1666c533ef..b10702ebded57 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx @@ -29,7 +29,14 @@ describe('DataTypesCol', function () { fireEvent.click(screen.getByText(/user experience \(rum\)/i)); expect(setSeries).toHaveBeenCalledTimes(1); - expect(setSeries).toHaveBeenCalledWith(seriesId, { dataType: 'ux' }); + expect(setSeries).toHaveBeenCalledWith(seriesId, { + dataType: 'ux', + isNew: true, + time: { + from: 'now-15m', + to: 'now', + }, + }); }); it('should set series on change on already selected', function () { @@ -37,7 +44,7 @@ describe('DataTypesCol', function () { data: { [seriesId]: { dataType: 'synthetics' as const, - reportType: 'kpi' as const, + reportType: 'kpi-over-time' as const, breakdown: 'monitor.status', time: { from: 'now-15m', to: 'now' }, }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx index 985afdf888868..f386f62d9ed73 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx @@ -31,7 +31,11 @@ export function DataTypesCol({ seriesId }: { seriesId: string }) { if (!dataType) { removeSeries(seriesId); } else { - setSeries(seriesId || `${dataType}-series`, { dataType } as any); + setSeries(seriesId || `${dataType}-series`, { + dataType, + isNew: true, + time: series.time, + } as any); } }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx index 175fbea9445c1..6be78084ae195 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx @@ -8,14 +8,23 @@ import React from 'react'; import styled from 'styled-components'; import { SeriesDatePicker } from '../../series_date_picker'; +import { DateRangePicker } from '../../series_date_picker/date_range_picker'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; interface Props { seriesId: string; } export function DatePickerCol({ seriesId }: Props) { + const { firstSeriesId, getSeries } = useSeriesStorage(); + const { reportType } = getSeries(firstSeriesId); + return ( <Wrapper> - <SeriesDatePicker seriesId={seriesId} /> + {firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? ( + <SeriesDatePicker seriesId={seriesId} /> + ) : ( + <DateRangePicker seriesId={seriesId} /> + )} </Wrapper> ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx index c262a94f968be..516f04e3812ba 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx @@ -22,7 +22,7 @@ describe('OperationTypeSelect', function () { data: { 'performance-distribution': { dataType: 'ux' as const, - reportType: 'kpi' as const, + reportType: 'kpi-over-time' as const, operationType: 'median' as const, time: { from: 'now-15m', to: 'now' }, }, @@ -39,7 +39,7 @@ describe('OperationTypeSelect', function () { data: { 'series-id': { dataType: 'ux' as const, - reportType: 'kpi' as const, + reportType: 'kpi-over-time' as const, operationType: 'median' as const, time: { from: 'now-15m', to: 'now' }, }, @@ -53,7 +53,7 @@ describe('OperationTypeSelect', function () { expect(setSeries).toHaveBeenCalledWith('series-id', { operationType: 'median', dataType: 'ux', - reportType: 'kpi', + reportType: 'kpi-over-time', time: { from: 'now-15m', to: 'now' }, }); @@ -61,7 +61,7 @@ describe('OperationTypeSelect', function () { expect(setSeries).toHaveBeenCalledWith('series-id', { operationType: '95th', dataType: 'ux', - reportType: 'kpi', + reportType: 'kpi-over-time', time: { from: 'now-15m', to: 'now' }, }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx index 805186e877d57..203382afc1624 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx @@ -15,7 +15,7 @@ import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fiel describe('Series Builder ReportBreakdowns', function () { const seriesId = 'test-series-id'; const dataViewSeries = getDefaultConfigs({ - reportType: 'dist', + reportType: 'data-distribution', dataType: 'ux', indexPattern: mockIndexPattern, }); @@ -45,7 +45,7 @@ describe('Series Builder ReportBreakdowns', function () { expect(setSeries).toHaveBeenCalledWith(seriesId, { breakdown: USER_AGENT_OS, dataType: 'ux', - reportType: 'dist', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }); }); @@ -67,7 +67,7 @@ describe('Series Builder ReportBreakdowns', function () { expect(setSeries).toHaveBeenCalledWith(seriesId, { breakdown: undefined, dataType: 'ux', - reportType: 'dist', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx index e947961fb4300..2e5c674b9fad8 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx @@ -22,7 +22,7 @@ describe('Series Builder ReportDefinitionCol', function () { const seriesId = 'test-series-id'; const dataViewSeries = getDefaultConfigs({ - reportType: 'dist', + reportType: 'data-distribution', indexPattern: mockIndexPattern, dataType: 'ux', }); @@ -31,7 +31,7 @@ describe('Series Builder ReportDefinitionCol', function () { data: { [seriesId]: { dataType: 'ux' as const, - reportType: 'dist' as const, + reportType: 'data-distribution' as const, time: { from: 'now-30d', to: 'now' }, reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] }, }, @@ -81,7 +81,7 @@ describe('Series Builder ReportDefinitionCol', function () { expect(setSeries).toHaveBeenCalledWith(seriesId, { dataType: 'ux', reportDefinitions: {}, - reportType: 'dist', + reportType: 'data-distribution', time: { from: 'now-30d', to: 'now' }, }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx index 338f5d52c26fa..47962af0d4bc4 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import styled from 'styled-components'; -import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { CustomReportField } from '../custom_report_field'; import { DataSeries, URLReportDefinition } from '../../types'; @@ -36,8 +35,6 @@ export function ReportDefinitionCol({ dataViewSeries: DataSeries; seriesId: string; }) { - const { indexPattern } = useAppIndexPatternContext(); - const { getSeries, setSeries } = useSeriesStorage(); const series = getSeries(seriesId); @@ -69,21 +66,20 @@ export function ReportDefinitionCol({ <DatePickerCol seriesId={seriesId} /> </EuiFlexItem> <EuiHorizontalRule margin="xs" /> - {indexPattern && - reportDefinitions.map(({ field, custom, options }) => ( - <EuiFlexItem key={field}> - {!custom ? ( - <ReportDefinitionField - seriesId={seriesId} - dataSeries={dataViewSeries} - field={field} - onChange={onChange} - /> - ) : ( - <CustomReportField field={field} options={options} seriesId={seriesId} /> - )} - </EuiFlexItem> - ))} + {reportDefinitions.map(({ field, custom, options }) => ( + <EuiFlexItem key={field}> + {!custom ? ( + <ReportDefinitionField + seriesId={seriesId} + dataSeries={dataViewSeries} + field={field} + onChange={onChange} + /> + ) : ( + <CustomReportField field={field} options={options} seriesId={seriesId} /> + )} + </EuiFlexItem> + ))} {(hasOperationType || columnType === 'operation') && ( <EuiFlexItem> <OperationTypeSelect diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx index d36e33f16424c..51f4edaae93da 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx @@ -29,7 +29,7 @@ export function ReportDefinitionField({ seriesId, field, dataSeries, onChange }: const series = getSeries(seriesId); - const { indexPattern } = useAppIndexPatternContext(); + const { indexPattern } = useAppIndexPatternContext(series.dataType); const { reportDefinitions: selectedReportDefinitions = {} } = series; @@ -49,7 +49,7 @@ export function ReportDefinitionField({ seriesId, field, dataSeries, onChange }: if (!isEmpty(selectedReportDefinitions)) { reportDefinitions.forEach(({ field: fieldT, custom }) => { - if (!custom && selectedReportDefinitions?.[fieldT] && fieldT !== field) { + if (!custom && indexPattern && selectedReportDefinitions?.[fieldT] && fieldT !== field) { const values = selectedReportDefinitions?.[fieldT]; const valueFilter = buildPhrasesFilter(fieldT, values, indexPattern)[0]; filtersN.push(valueFilter.query); @@ -64,16 +64,18 @@ export function ReportDefinitionField({ seriesId, field, dataSeries, onChange }: return ( <EuiFlexGroup justifyContent="flexStart" gutterSize="s" alignItems="center" wrap> <EuiFlexItem> - <FieldValueSuggestions - label={labels[field]} - sourceField={field} - indexPatternTitle={indexPattern.title} - selectedValue={selectedReportDefinitions?.[field]} - onChange={(val?: string[]) => onChange(field, val)} - filters={queryFilters} - time={series.time} - fullWidth={true} - /> + {indexPattern && ( + <FieldValueSuggestions + label={labels[field]} + sourceField={field} + indexPatternTitle={indexPattern.title} + selectedValue={selectedReportDefinitions?.[field]} + onChange={(val?: string[]) => onChange(field, val)} + filters={queryFilters} + time={series.time} + fullWidth={true} + /> + )} </EuiFlexItem> </EuiFlexGroup> ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx index 7ca947fed0bc9..f35639388aac5 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx @@ -15,7 +15,7 @@ describe('Series Builder ReportFilters', function () { const seriesId = 'test-series-id'; const dataViewSeries = getDefaultConfigs({ - reportType: 'dist', + reportType: 'data-distribution', indexPattern: mockIndexPattern, dataType: 'ux', }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx index f36d64ca5bbbd..f7cfe06c0d928 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx @@ -11,10 +11,9 @@ import { mockAppIndexPattern, render } from '../../rtl_helpers'; import { ReportTypesCol, SELECTED_DATA_TYPE_FOR_REPORT } from './report_types_col'; import { ReportTypes } from '../series_builder'; import { DEFAULT_TIME } from '../../configurations/constants'; -import { NEW_SERIES_KEY } from '../../hooks/use_series_storage'; describe('ReportTypesCol', function () { - const seriesId = 'test-series-id'; + const seriesId = 'performance-distribution'; mockAppIndexPattern(); @@ -40,7 +39,7 @@ describe('ReportTypesCol', function () { breakdown: 'user_agent.name', dataType: 'ux', reportDefinitions: {}, - reportType: 'kpi', + reportType: 'kpi-over-time', time: { from: 'now-15m', to: 'now' }, }); expect(setSeries).toHaveBeenCalledTimes(1); @@ -49,11 +48,12 @@ describe('ReportTypesCol', function () { it('should set selected as filled', function () { const initSeries = { data: { - [NEW_SERIES_KEY]: { + [seriesId]: { dataType: 'synthetics' as const, - reportType: 'kpi' as const, + reportType: 'kpi-over-time' as const, breakdown: 'monitor.status', time: { from: 'now-15m', to: 'now' }, + isNew: true, }, }, }; @@ -74,6 +74,7 @@ describe('ReportTypesCol', function () { expect(setSeries).toHaveBeenCalledWith(seriesId, { dataType: 'synthetics', time: DEFAULT_TIME, + isNew: true, }); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx index 9fff8dae14a47..64c7b48c668b8 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx @@ -7,27 +7,33 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { map } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import styled from 'styled-components'; -import { ReportViewTypeId, SeriesUrl } from '../../types'; +import { ReportViewType, SeriesUrl } from '../../types'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { DEFAULT_TIME } from '../../configurations/constants'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; +import { ReportTypeItem, SELECT_DATA_TYPE } from '../series_builder'; interface Props { seriesId: string; - reportTypes: Array<{ id: ReportViewTypeId; label: string }>; + reportTypes: ReportTypeItem[]; } export function ReportTypesCol({ seriesId, reportTypes }: Props) { - const { setSeries, getSeries } = useSeriesStorage(); + const { setSeries, getSeries, firstSeries, firstSeriesId } = useSeriesStorage(); const { reportType: selectedReportType, ...restSeries } = getSeries(seriesId); - const { loading, hasData, selectedApp } = useAppIndexPatternContext(); + const { loading, hasData } = useAppIndexPatternContext(restSeries.dataType); - if (!loading && !hasData && selectedApp) { + if (!restSeries.dataType) { + return <span>{SELECT_DATA_TYPE}</span>; + } + + if (!loading && !hasData) { return ( <FormattedMessage id="xpack.observability.reportTypeCol.nodata" @@ -36,9 +42,16 @@ export function ReportTypesCol({ seriesId, reportTypes }: Props) { ); } + const disabledReportTypes: ReportViewType[] = map( + reportTypes.filter( + ({ reportType }) => firstSeriesId !== seriesId && reportType !== firstSeries.reportType + ), + 'reportType' + ); + return reportTypes?.length > 0 ? ( <FlexGroup direction="column" gutterSize="xs"> - {reportTypes.map(({ id: reportType, label }) => ( + {reportTypes.map(({ reportType, label }) => ( <EuiFlexItem key={reportType}> <Button fullWidth @@ -47,12 +60,13 @@ export function ReportTypesCol({ seriesId, reportTypes }: Props) { iconType="arrowRight" color={selectedReportType === reportType ? 'primary' : 'text'} fill={selectedReportType === reportType} - isDisabled={loading} + isDisabled={loading || disabledReportTypes.includes(reportType)} onClick={() => { if (reportType === selectedReportType) { setSeries(seriesId, { dataType: restSeries.dataType, time: DEFAULT_TIME, + isNew: true, } as SeriesUrl); } else { setSeries(seriesId, { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx new file mode 100644 index 0000000000000..874171de123d2 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; +import { EuiIcon, EuiText } from '@elastic/eui'; +import moment from 'moment'; + +interface Props { + lastUpdated?: number; +} +export function LastUpdated({ lastUpdated }: Props) { + const [refresh, setRefresh] = useState(() => Date.now()); + + useEffect(() => { + const interVal = setInterval(() => { + setRefresh(Date.now()); + }, 1000); + + return () => { + clearInterval(interVal); + }; + }, []); + + if (!lastUpdated) { + return null; + } + + return ( + <EuiText color="subdued" size="s"> + <EuiIcon type="clock" /> Last Updated: {moment(lastUpdated).from(refresh)} + </EuiText> + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx index 9aef16931d7ec..e596eb6be354a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx @@ -5,11 +5,19 @@ * 2.0. */ -import React, { RefObject } from 'react'; - +import React, { RefObject, useEffect, useState } from 'react'; +import { isEmpty } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { EuiBasicTable } from '@elastic/eui'; -import { AppDataType, ReportViewTypeId, ReportViewTypes, SeriesUrl } from '../types'; +import { + EuiBasicTable, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiSwitch, +} from '@elastic/eui'; +import { rgba } from 'polished'; +import { AppDataType, DataSeries, ReportViewType, SeriesUrl } from '../types'; import { DataTypesCol } from './columns/data_types_col'; import { ReportTypesCol } from './columns/report_types_col'; import { ReportDefinitionCol } from './columns/report_definition_col'; @@ -18,6 +26,10 @@ import { ReportBreakdowns } from './columns/report_breakdowns'; import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage'; import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; import { getDefaultConfigs } from '../configurations/default_configs'; +import { SeriesEditor } from '../series_editor/series_editor'; +import { SeriesActions } from '../series_editor/columns/series_actions'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; +import { LastUpdated } from './last_updated'; import { CORE_WEB_VITALS_LABEL, DEVICE_DISTRIBUTION_LABEL, @@ -25,72 +37,94 @@ import { PERF_DIST_LABEL, } from '../configurations/constants/labels'; -export const ReportTypes: Record<AppDataType, Array<{ id: ReportViewTypeId; label: string }>> = { +export interface ReportTypeItem { + id: string; + reportType: ReportViewType; + label: string; +} + +export const ReportTypes: Record<AppDataType, ReportTypeItem[]> = { synthetics: [ - { id: 'kpi', label: KPI_OVER_TIME_LABEL }, - { id: 'dist', label: PERF_DIST_LABEL }, + { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, + { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL }, ], ux: [ - { id: 'kpi', label: KPI_OVER_TIME_LABEL }, - { id: 'dist', label: PERF_DIST_LABEL }, - { id: 'cwv', label: CORE_WEB_VITALS_LABEL }, + { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, + { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL }, + { id: 'cwv', reportType: 'core-web-vitals', label: CORE_WEB_VITALS_LABEL }, ], mobile: [ - { id: 'kpi', label: KPI_OVER_TIME_LABEL }, - { id: 'dist', label: PERF_DIST_LABEL }, - { id: 'mdd', label: DEVICE_DISTRIBUTION_LABEL }, + { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, + { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL }, + { id: 'mdd', reportType: 'device-data-distribution', label: DEVICE_DISTRIBUTION_LABEL }, ], apm: [], infra_logs: [], infra_metrics: [], }; +interface BuilderItem { + id: string; + series: SeriesUrl; + seriesConfig?: DataSeries; +} + export function SeriesBuilder({ seriesBuilderRef, - seriesId, + lastUpdated, + multiSeries, }: { - seriesId: string; seriesBuilderRef: RefObject<HTMLDivElement>; + lastUpdated?: number; + multiSeries?: boolean; }) { - const { getSeries, setSeries, removeSeries } = useSeriesStorage(); - - const series = getSeries(seriesId); - - const { - dataType, - seriesType, - reportType, - reportDefinitions = {}, - filters = [], - operationType, - breakdown, - time, - } = series; - - const { indexPattern, loading, hasData } = useAppIndexPatternContext(); - - const getDataViewSeries = () => { - return getDefaultConfigs({ - dataType, - indexPattern, - reportType: reportType!, - }); - }; + const [editorItems, setEditorItems] = useState<BuilderItem[]>([]); + const { getSeries, allSeries, allSeriesIds, setSeries, removeSeries } = useSeriesStorage(); + + const { loading, indexPatterns } = useAppIndexPatternContext(); + + useEffect(() => { + const getDataViewSeries = (dataType: AppDataType, reportType: SeriesUrl['reportType']) => { + if (indexPatterns?.[dataType]) { + return getDefaultConfigs({ + dataType, + indexPattern: indexPatterns[dataType], + reportType: reportType!, + }); + } + }; + + const seriesToEdit: BuilderItem[] = + allSeriesIds + .filter((sId) => { + return allSeries?.[sId]?.isNew; + }) + .map((sId) => { + const series = getSeries(sId); + const seriesConfig = getDataViewSeries(series.dataType, series.reportType); + + return { id: sId, series, seriesConfig }; + }) ?? []; + const initSeries: BuilderItem[] = [{ id: 'series-id', series: {} as SeriesUrl }]; + setEditorItems(multiSeries || seriesToEdit.length > 0 ? seriesToEdit : initSeries); + }, [allSeries, allSeriesIds, getSeries, indexPatterns, loading, multiSeries]); const columns = [ { name: i18n.translate('xpack.observability.expView.seriesBuilder.dataType', { defaultMessage: 'Data Type', }), + field: 'id', width: '15%', - render: (val: string) => <DataTypesCol seriesId={seriesId} />, + render: (seriesId: string) => <DataTypesCol seriesId={seriesId} />, }, { name: i18n.translate('xpack.observability.expView.seriesBuilder.report', { defaultMessage: 'Report', }), width: '15%', - render: (val: string) => ( + field: 'id', + render: (seriesId: string, { series: { dataType } }: BuilderItem) => ( <ReportTypesCol seriesId={seriesId} reportTypes={dataType ? ReportTypes[dataType] : []} /> ), }, @@ -99,12 +133,16 @@ export function SeriesBuilder({ defaultMessage: 'Definition', }), width: '30%', - render: (val: string) => { - if (dataType && hasData) { + field: 'id', + render: ( + seriesId: string, + { series: { dataType, reportType }, seriesConfig }: BuilderItem + ) => { + if (dataType && seriesConfig) { return loading ? ( LOADING_VIEW ) : reportType ? ( - <ReportDefinitionCol seriesId={seriesId} dataViewSeries={getDataViewSeries()} /> + <ReportDefinitionCol seriesId={seriesId} dataViewSeries={seriesConfig} /> ) : ( SELECT_REPORT_TYPE ); @@ -118,9 +156,10 @@ export function SeriesBuilder({ defaultMessage: 'Filters', }), width: '20%', - render: (val: string) => - reportType && indexPattern ? ( - <ReportFilters seriesId={seriesId} dataViewSeries={getDataViewSeries()} /> + field: 'id', + render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) => + reportType && seriesConfig ? ( + <ReportFilters seriesId={seriesId} dataViewSeries={seriesConfig} /> ) : null, }, { @@ -129,53 +168,126 @@ export function SeriesBuilder({ }), width: '20%', field: 'id', - render: (val: string) => - reportType && indexPattern ? ( - <ReportBreakdowns seriesId={seriesId} dataViewSeries={getDataViewSeries()} /> + render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) => + reportType && seriesConfig ? ( + <ReportBreakdowns seriesId={seriesId} dataViewSeries={seriesConfig} /> ) : null, }, + ...(multiSeries + ? [ + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.actions', { + defaultMessage: 'Actions', + }), + align: 'center' as const, + width: '10%', + field: 'id', + render: (seriesId: string, item: BuilderItem) => ( + <SeriesActions seriesId={seriesId} editorMode={true} /> + ), + }, + ] + : []), ]; - // TODO: Remove this if remain unused during multiple series view - // @ts-expect-error - const addSeries = () => { - if (reportType) { - const newSeriesId = `${ - reportDefinitions?.['service.name'] || - reportDefinitions?.['monitor.id'] || - ReportViewTypes[reportType] - }`; - - const newSeriesN: SeriesUrl = { - dataType, - time, - filters, - breakdown, - reportType, - seriesType, - operationType, - reportDefinitions, - }; - - setSeries(newSeriesId, newSeriesN); - removeSeries(NEW_SERIES_KEY); - } + const applySeries = () => { + editorItems.forEach(({ series, id: seriesId }) => { + const { reportType, reportDefinitions, isNew, ...restSeries } = series; + + if (reportType && !isEmpty(reportDefinitions)) { + const reportDefId = Object.values(reportDefinitions ?? {})[0]; + const newSeriesId = `${reportDefId}-${reportType}`; + + const newSeriesN: SeriesUrl = { + ...restSeries, + reportType, + reportDefinitions, + }; + + setSeries(newSeriesId, newSeriesN); + removeSeries(seriesId); + } + }); }; - const items = [{ id: seriesId }]; + const addSeries = () => { + const prevSeries = allSeries?.[allSeriesIds?.[0]]; + setSeries( + `${NEW_SERIES_KEY}-${editorItems.length + 1}`, + prevSeries + ? ({ isNew: true, time: prevSeries.time } as SeriesUrl) + : ({ isNew: true } as SeriesUrl) + ); + }; return ( - <div ref={seriesBuilderRef}> - <EuiBasicTable - items={items as any} - columns={columns} - cellProps={{ style: { borderRight: '1px solid #d3dae6', verticalAlign: 'initial' } }} - tableLayout="auto" - /> - </div> + <Wrapper ref={seriesBuilderRef}> + {multiSeries && ( + <EuiFlexGroup justifyContent="flexEnd" alignItems="center"> + <EuiFlexItem> + <LastUpdated lastUpdated={lastUpdated} /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiSwitch + label={i18n.translate('xpack.observability.expView.seriesBuilder.autoApply', { + defaultMessage: 'Auto apply', + })} + checked={true} + onChange={(e) => {}} + compressed + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton onClick={() => applySeries()} isDisabled={true} size="s"> + {i18n.translate('xpack.observability.expView.seriesBuilder.apply', { + defaultMessage: 'Apply changes', + })} + </EuiButton> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton color="secondary" onClick={() => addSeries()} size="s"> + {i18n.translate('xpack.observability.expView.seriesBuilder.addSeries', { + defaultMessage: 'Add Series', + })} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + )} + <div> + {multiSeries && <SeriesEditor />} + {editorItems.length > 0 && ( + <EuiBasicTable + items={editorItems} + columns={columns} + cellProps={{ style: { borderRight: '1px solid #d3dae6', verticalAlign: 'initial' } }} + tableLayout="auto" + /> + )} + <EuiSpacer /> + </div> + </Wrapper> ); } +const Wrapper = euiStyled.div` + max-height: 50vh; + overflow-y: scroll; + overflow-x: clip; + &::-webkit-scrollbar { + height: ${({ theme }) => theme.eui.euiScrollBar}; + width: ${({ theme }) => theme.eui.euiScrollBar}; + } + &::-webkit-scrollbar-thumb { + background-clip: content-box; + background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)}; + border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent; + } + &::-webkit-scrollbar-corner, + &::-webkit-scrollbar-track { + background-color: transparent; + } +`; + export const LOADING_VIEW = i18n.translate( 'xpack.observability.expView.seriesBuilder.loadingView', { @@ -189,3 +301,10 @@ export const SELECT_REPORT_TYPE = i18n.translate( defaultMessage: 'No report type selected', } ); + +export const SELECT_DATA_TYPE = i18n.translate( + 'xpack.observability.expView.seriesBuilder.selectDataType', + { + defaultMessage: 'No data type selected', + } +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx new file mode 100644 index 0000000000000..c30863585b3b0 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiDatePicker, EuiDatePickerRange } from '@elastic/eui'; +import DateMath from '@elastic/datemath'; +import { Moment } from 'moment'; +import { useSeriesStorage } from '../hooks/use_series_storage'; +import { useUiSetting } from '../../../../../../../../src/plugins/kibana_react/public'; + +export const parseAbsoluteDate = (date: string, options = {}) => { + return DateMath.parse(date, options)!; +}; +export function DateRangePicker({ seriesId }: { seriesId: string }) { + const { firstSeriesId, getSeries, setSeries } = useSeriesStorage(); + const dateFormat = useUiSetting<string>('dateFormat'); + + const { + time: { from, to }, + reportType, + } = getSeries(firstSeriesId); + + const series = getSeries(seriesId); + + const { + time: { from: seriesFrom, to: seriesTo }, + } = series; + + const startDate = parseAbsoluteDate(seriesFrom ?? from)!; + const endDate = parseAbsoluteDate(seriesTo ?? to, { roundUp: true })!; + + const onStartChange = (newDate: Moment) => { + if (reportType === 'kpi-over-time') { + const mainStartDate = parseAbsoluteDate(from)!; + const mainEndDate = parseAbsoluteDate(to, { roundUp: true })!; + const totalDuration = mainEndDate.diff(mainStartDate, 'millisecond'); + const newFrom = newDate.toISOString(); + const newTo = newDate.add(totalDuration, 'millisecond').toISOString(); + + setSeries(seriesId, { + ...series, + time: { from: newFrom, to: newTo }, + }); + } else { + const newFrom = newDate.toISOString(); + + setSeries(seriesId, { + ...series, + time: { from: newFrom, to: seriesTo }, + }); + } + }; + const onEndChange = (newDate: Moment) => { + if (reportType === 'kpi-over-time') { + const mainStartDate = parseAbsoluteDate(from)!; + const mainEndDate = parseAbsoluteDate(to, { roundUp: true })!; + const totalDuration = mainEndDate.diff(mainStartDate, 'millisecond'); + const newTo = newDate.toISOString(); + const newFrom = newDate.subtract(totalDuration, 'millisecond').toISOString(); + + setSeries(seriesId, { + ...series, + time: { from: newFrom, to: newTo }, + }); + } else { + const newTo = newDate.toISOString(); + + setSeries(seriesId, { + ...series, + time: { from: seriesFrom, to: newTo }, + }); + } + }; + + return ( + <EuiDatePickerRange + fullWidth + startDateControl={ + <EuiDatePicker + selected={startDate} + onChange={onStartChange} + startDate={startDate} + endDate={endDate} + isInvalid={startDate > endDate} + aria-label={i18n.translate('xpack.observability.expView.dateRanger.startDate', { + defaultMessage: 'Start date', + })} + dateFormat={dateFormat} + showTimeSelect + /> + } + endDateControl={ + <EuiDatePicker + selected={endDate} + onChange={onEndChange} + startDate={startDate} + endDate={endDate} + isInvalid={startDate > endDate} + aria-label={i18n.translate('xpack.observability.expView.dateRanger.endDate', { + defaultMessage: 'End date', + })} + dateFormat={dateFormat} + showTimeSelect + /> + } + /> + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx index d6a70532f4257..e21da424b58c8 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx @@ -43,7 +43,7 @@ export function SeriesDatePicker({ seriesId }: Props) { if (!series || !series.time) { setSeries(seriesId, { ...series, time: DEFAULT_TIME }); } - }, [seriesId, series, setSeries]); + }, [series, seriesId, setSeries]); return ( <EuiSuperDatePicker diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx index 2b46bb9a8cd62..931dfbe07cd23 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx @@ -17,7 +17,7 @@ describe('SeriesDatePicker', function () { data: { 'uptime-pings-histogram': { dataType: 'synthetics' as const, - reportType: 'dist' as const, + reportType: 'data-distribution' as const, breakdown: 'monitor.status', time: { from: 'now-30m', to: 'now' }, }, @@ -32,7 +32,7 @@ describe('SeriesDatePicker', function () { const initSeries = { data: { 'uptime-pings-histogram': { - reportType: 'kpi' as const, + reportType: 'kpi-over-time' as const, dataType: 'synthetics' as const, breakdown: 'monitor.status', }, @@ -46,7 +46,7 @@ describe('SeriesDatePicker', function () { expect(setSeries1).toHaveBeenCalledWith('uptime-pings-histogram', { breakdown: 'monitor.status', dataType: 'synthetics' as const, - reportType: 'kpi' as const, + reportType: 'kpi-over-time' as const, time: DEFAULT_TIME, }); }); @@ -56,7 +56,7 @@ describe('SeriesDatePicker', function () { data: { 'uptime-pings-histogram': { dataType: 'synthetics' as const, - reportType: 'kpi' as const, + reportType: 'kpi-over-time' as const, breakdown: 'monitor.status', time: { from: 'now-30m', to: 'now' }, }, @@ -79,7 +79,7 @@ describe('SeriesDatePicker', function () { expect(setSeries).toHaveBeenCalledWith('series-id', { breakdown: 'monitor.status', dataType: 'synthetics', - reportType: 'kpi', + reportType: 'kpi-over-time', time: { from: 'now/d', to: 'now/d' }, }); expect(setSeries).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx index 1d552486921e1..d180bf4529c20 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx @@ -14,7 +14,7 @@ import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fiel describe('Breakdowns', function () { const dataViewSeries = getDefaultConfigs({ - reportType: 'dist', + reportType: 'data-distribution', indexPattern: mockIndexPattern, dataType: 'ux', }); @@ -52,7 +52,7 @@ describe('Breakdowns', function () { expect(setSeries).toHaveBeenCalledWith('series-id', { breakdown: 'user_agent.name', dataType: 'ux', - reportType: 'dist', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }); expect(setSeries).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx index 8c99de51978a7..41e83f407af2b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx @@ -7,14 +7,23 @@ import React from 'react'; import { SeriesDatePicker } from '../../series_date_picker'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { DateRangePicker } from '../../series_date_picker/date_range_picker'; interface Props { seriesId: string; } export function DatePickerCol({ seriesId }: Props) { + const { firstSeriesId, getSeries } = useSeriesStorage(); + const { reportType } = getSeries(firstSeriesId); + return ( <div style={{ maxWidth: 300 }}> - <SeriesDatePicker seriesId={seriesId} /> + {firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? ( + <SeriesDatePicker seriesId={seriesId} /> + ) : ( + <DateRangePicker seriesId={seriesId} /> + )} </div> ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx index a78f6adeca39f..0f0cec0fbfcff 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx @@ -41,8 +41,6 @@ export function FilterExpanded({ isNegated, filters: defaultFilters, }: Props) { - const { indexPattern } = useAppIndexPatternContext(); - const [value, setValue] = useState(''); const [isOpen, setIsOpen] = useState({ value: '', negate: false }); @@ -53,23 +51,25 @@ export function FilterExpanded({ const queryFilters: ESFilter[] = []; + const { indexPatterns } = useAppIndexPatternContext(series.dataType); + defaultFilters?.forEach((qFilter: PersistableFilter | ExistsFilter) => { if (qFilter.query) { queryFilters.push(qFilter.query); } const asExistFilter = qFilter as ExistsFilter; if (asExistFilter?.exists) { - queryFilters.push(asExistFilter.exists as QueryDslQueryContainer); + queryFilters.push({ exists: asExistFilter.exists } as QueryDslQueryContainer); } }); const { values, loading } = useValuesList({ query: value, - indexPatternTitle: indexPattern?.title, sourceField: field, time: series.time, keepHistory: true, filters: queryFilters, + indexPatternTitle: indexPatterns[series.dataType]?.title, }); const filters = series?.filters ?? []; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx index 79eb858b7624b..c1790fea8c0c4 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx @@ -139,7 +139,7 @@ describe('FilterValueButton', function () { /> ); - expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledTimes(2); expect(spy).toBeCalledWith( expect.objectContaining({ filters: [ @@ -170,7 +170,7 @@ describe('FilterValueButton', function () { /> ); - expect(spy).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenCalledTimes(6); expect(spy).toBeCalledWith( expect.objectContaining({ filters: [ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx index f04295a90e475..bf4ca6eb83d94 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx @@ -41,7 +41,7 @@ export function FilterValueButton({ const series = getSeries(seriesId); - const { indexPattern } = useAppIndexPatternContext(); + const { indexPatterns } = useAppIndexPatternContext(series.dataType); const { setFilter, removeFilter } = useSeriesFilters({ seriesId }); @@ -96,7 +96,6 @@ export function FilterValueButton({ <FieldValueSuggestions button={button} label={'Version'} - indexPatternTitle={indexPattern?.title} sourceField={nestedField} onChange={onNestedChange} filters={filters} @@ -104,6 +103,7 @@ export function FilterValueButton({ anchorPosition="rightCenter" time={series.time} asCombobox={false} + indexPatternTitle={indexPatterns[series.dataType]?.title} /> ) : ( button diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx index dc84352ff3b3d..e75f308dab1e5 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx @@ -26,9 +26,9 @@ export function RemoveSeries({ seriesId }: Props) { defaultMessage: 'Click to remove series', })} iconType="cross" - color="primary" + color="danger" onClick={onClick} - size="m" + size="s" /> ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx index 086a1d4341bbc..51ebe6c6bd9d5 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx @@ -8,33 +8,93 @@ import React from 'react'; import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { isEmpty } from 'lodash'; import { RemoveSeries } from './remove_series'; -import { NEW_SERIES_KEY, useSeriesStorage } from '../../hooks/use_series_storage'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { SeriesUrl } from '../../types'; interface Props { seriesId: string; + editorMode?: boolean; } -export function SeriesActions({ seriesId }: Props) { - const { getSeries, removeSeries, setSeries } = useSeriesStorage(); +export function SeriesActions({ seriesId, editorMode = false }: Props) { + const { getSeries, setSeries, allSeriesIds, removeSeries } = useSeriesStorage(); const series = getSeries(seriesId); const onEdit = () => { - removeSeries(seriesId); - setSeries(NEW_SERIES_KEY, { ...series }); + setSeries(seriesId, { ...series, isNew: true }); + }; + + const copySeries = () => { + let copySeriesId: string = `${seriesId}-copy`; + if (allSeriesIds.includes(copySeriesId)) { + copySeriesId = copySeriesId + allSeriesIds.length; + } + setSeries(copySeriesId, series); + }; + + const { reportType, reportDefinitions, isNew, ...restSeries } = series; + const isSaveAble = reportType && !isEmpty(reportDefinitions); + + const saveSeries = () => { + if (isSaveAble) { + const reportDefId = Object.values(reportDefinitions ?? {})[0]; + let newSeriesId = `${reportDefId}-${reportType}`; + + if (allSeriesIds.includes(newSeriesId)) { + newSeriesId = `${newSeriesId}-${allSeriesIds.length}`; + } + const newSeriesN: SeriesUrl = { + ...restSeries, + reportType, + reportDefinitions, + }; + + setSeries(newSeriesId, newSeriesN); + removeSeries(seriesId); + } }; return ( - <EuiFlexGroup alignItems="center" gutterSize="xs" justifyContent="center"> - <EuiFlexItem grow={false}> - <EuiButtonIcon - iconType={'documentEdit'} - aria-label={i18n.translate('xpack.observability.seriesEditor.edit', { - defaultMessage: 'Edit series', - })} - size="m" - onClick={onEdit} - /> - </EuiFlexItem> + <EuiFlexGroup alignItems="center" gutterSize="none" justifyContent="center"> + {!editorMode && ( + <EuiFlexItem grow={false}> + <EuiButtonIcon + iconType="documentEdit" + aria-label={i18n.translate('xpack.observability.seriesEditor.edit', { + defaultMessage: 'Edit series', + })} + size="s" + onClick={onEdit} + /> + </EuiFlexItem> + )} + {editorMode && ( + <EuiFlexItem grow={false}> + <EuiButtonIcon + iconType={'save'} + aria-label={i18n.translate('xpack.observability.seriesEditor.save', { + defaultMessage: 'Save series', + })} + size="s" + onClick={saveSeries} + color="success" + isDisabled={!isSaveAble} + /> + </EuiFlexItem> + )} + {editorMode && ( + <EuiFlexItem grow={false}> + <EuiButtonIcon + iconType={'copy'} + aria-label={i18n.translate('xpack.observability.seriesEditor.clone', { + defaultMessage: 'Copy series', + })} + size="s" + onClick={copySeries} + /> + </EuiFlexItem> + )} <EuiFlexItem grow={false}> <RemoveSeries seriesId={seriesId} /> </EuiFlexItem> diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx index 8363b6b0eadfd..61081e7cc6f46 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx @@ -16,7 +16,7 @@ describe('SelectedFilters', function () { mockAppIndexPattern(); const dataViewSeries = getDefaultConfigs({ - reportType: 'dist', + reportType: 'data-distribution', indexPattern: mockIndexPattern, dataType: 'ux', }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx index 63abb581c9c72..33496e617a3a6 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx @@ -39,7 +39,7 @@ export function SelectedFilters({ seriesId, isNew, series: dataSeries }: Props) const { removeFilter } = useSeriesFilters({ seriesId }); - const { indexPattern } = useAppIndexPatternContext(); + const { indexPattern } = useAppIndexPatternContext(series.dataType); return (filters.length > 0 || definitionFilters.length > 0) && indexPattern ? ( <EuiFlexItem> @@ -55,6 +55,7 @@ export function SelectedFilters({ seriesId, isNew, series: dataSeries }: Props) value={val} removeFilter={() => removeFilter({ field, value: val, negate: false })} negate={false} + indexPattern={indexPattern} /> </EuiFlexItem> ))} @@ -67,6 +68,7 @@ export function SelectedFilters({ seriesId, isNew, series: dataSeries }: Props) value={val} negate={true} removeFilter={() => removeFilter({ field, value: val, negate: true })} + indexPattern={indexPattern} /> </EuiFlexItem> ))} @@ -87,6 +89,7 @@ export function SelectedFilters({ seriesId, isNew, series: dataSeries }: Props) }} negate={false} definitionFilter={true} + indexPattern={indexPattern} /> </EuiFlexItem> ))} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx index 17d4356dcf65b..bcceeb204a31e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx @@ -24,7 +24,7 @@ interface EditItem { } export function SeriesEditor() { - const { allSeries, firstSeriesId } = useSeriesStorage(); + const { allSeries, allSeriesIds } = useSeriesStorage(); const columns = [ { @@ -33,80 +33,77 @@ export function SeriesEditor() { }), field: 'id', width: '15%', - render: (val: string) => ( + render: (seriesId: string) => ( <EuiText> <EuiIcon type="dot" color="green" size="l" />{' '} - {val === NEW_SERIES_KEY ? 'series-preview' : val} + {seriesId === NEW_SERIES_KEY ? 'series-preview' : seriesId} </EuiText> ), }, - ...(firstSeriesId !== NEW_SERIES_KEY - ? [ - { - name: i18n.translate('xpack.observability.expView.seriesEditor.filters', { - defaultMessage: 'Filters', - }), - field: 'defaultFilters', - width: '15%', - render: (defaultFilters: string[], { id, seriesConfig }: EditItem) => ( - <SeriesFilter - defaultFilters={defaultFilters} - seriesId={id} - series={seriesConfig} - filters={seriesConfig.filters} - /> - ), - }, - { - name: i18n.translate('xpack.observability.expView.seriesEditor.breakdowns', { - defaultMessage: 'Breakdowns', - }), - field: 'breakdowns', - width: '25%', - render: (val: string[], item: EditItem) => ( - <ChartEditOptions seriesId={item.id} breakdowns={val} series={item.seriesConfig} /> - ), - }, - { - name: ( - <div> - <FormattedMessage - id="xpack.observability.expView.seriesEditor.time" - defaultMessage="Time" - /> - </div> - ), - width: '20%', - field: 'id', - align: 'right' as const, - render: (val: string, item: EditItem) => <DatePickerCol seriesId={item.id} />, - }, - { - name: i18n.translate('xpack.observability.expView.seriesEditor.actions', { - defaultMessage: 'Actions', - }), - align: 'center' as const, - width: '10%', - field: 'id', - render: (val: string, item: EditItem) => <SeriesActions seriesId={item.id} />, - }, - ] - : []), + { + name: i18n.translate('xpack.observability.expView.seriesEditor.filters', { + defaultMessage: 'Filters', + }), + field: 'defaultFilters', + width: '15%', + render: (seriesId: string, { seriesConfig, id }: EditItem) => ( + <SeriesFilter + defaultFilters={seriesConfig.defaultFilters} + seriesId={id} + series={seriesConfig} + filters={seriesConfig.filters} + /> + ), + }, + { + name: i18n.translate('xpack.observability.expView.seriesEditor.breakdowns', { + defaultMessage: 'Breakdowns', + }), + field: 'id', + width: '25%', + render: (seriesId: string, { seriesConfig, id }: EditItem) => ( + <ChartEditOptions + seriesId={id} + breakdowns={seriesConfig.breakdowns} + series={seriesConfig} + /> + ), + }, + { + name: ( + <div> + <FormattedMessage + id="xpack.observability.expView.seriesEditor.time" + defaultMessage="Time" + /> + </div> + ), + width: '20%', + field: 'id', + align: 'right' as const, + render: (seriesId: string, item: EditItem) => <DatePickerCol seriesId={seriesId} />, + }, + { + name: i18n.translate('xpack.observability.expView.seriesEditor.actions', { + defaultMessage: 'Actions', + }), + align: 'center' as const, + width: '10%', + field: 'id', + render: (seriesId: string, item: EditItem) => <SeriesActions seriesId={seriesId} />, + }, ]; - const allSeriesKeys = Object.keys(allSeries); - + const { indexPatterns } = useAppIndexPatternContext(); const items: EditItem[] = []; - const { indexPattern } = useAppIndexPatternContext(); - - allSeriesKeys.forEach((seriesKey) => { + allSeriesIds.forEach((seriesKey) => { const series = allSeries[seriesKey]; - if (series.reportType && indexPattern) { + if (series?.reportType && indexPatterns[series.dataType] && !series.isNew) { items.push({ id: seriesKey, seriesConfig: getDefaultConfigs({ - indexPattern, + indexPattern: indexPatterns[series.dataType], reportType: series.reportType, dataType: series.dataType, }), @@ -114,6 +111,10 @@ export function SeriesEditor() { } }); + if (items.length === 0 && allSeriesIds.length > 0) { + return null; + } + return ( <> <EuiSpacer /> @@ -121,8 +122,7 @@ export function SeriesEditor() { items={items} rowHeader="firstName" columns={columns} - rowProps={() => (firstSeriesId === NEW_SERIES_KEY ? {} : { height: 100 })} - noItemsMessage={i18n.translate('xpack.observability.expView.seriesEditor.notFound', { + noItemsMessage={i18n.translate('xpack.observability.expView.seriesEditor.seriesNotFound', { defaultMessage: 'No series found, please add a series.', })} cellProps={{ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts index 73b4d7794dd51..e8fccc5baab34 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts @@ -23,7 +23,7 @@ export const ReportViewTypes = { dist: 'data-distribution', kpi: 'kpi-over-time', cwv: 'core-web-vitals', - mdd: 'mobile-device-distribution', + mdd: 'device-data-distribution', } as const; type ValueOf<T> = T[keyof T]; @@ -56,7 +56,6 @@ export interface DataSeries { reportType: ReportViewType; xAxisColumn: Partial<LastValueIndexPatternColumn> | Partial<DateHistogramIndexPatternColumn>; yAxisColumns: Array<Partial<FieldBasedIndexPatternColumn>>; - breakdowns: string[]; defaultSeriesType: SeriesType; defaultFilters: Array<string | { field: string; nested?: string; isNegated?: boolean }>; @@ -80,10 +79,11 @@ export interface SeriesUrl { breakdown?: string; filters?: UrlFilter[]; seriesType?: SeriesType; - reportType: ReportViewTypeId; + reportType: ReportViewType; operationType?: OperationType; dataType: AppDataType; reportDefinitions?: URLReportDefinition; + isNew?: boolean; } export interface UrlFilter { @@ -94,6 +94,7 @@ export interface UrlFilter { export interface ConfigProps { indexPattern: IIndexPattern; + series?: SeriesUrl; } export type AppDataType = 'synthetics' | 'ux' | 'infra_logs' | 'infra_metrics' | 'apm' | 'mobile'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.test.ts new file mode 100644 index 0000000000000..fe545fff5498d --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.test.ts @@ -0,0 +1,148 @@ +/* + * 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 { urlFiltersToKueryString } from './stringify_kueries'; +import { UrlFilter } from '../types'; +import { USER_AGENT_NAME } from '../configurations/constants/elasticsearch_fieldnames'; + +describe('stringifyKueries', () => { + let filters: UrlFilter[]; + beforeEach(() => { + filters = [ + { + field: USER_AGENT_NAME, + values: ['Chrome', 'Firefox'], + notValues: [], + }, + ]; + }); + + it('stringifies the current values', () => { + expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot( + `"user_agent.name: (\\"Chrome\\" or \\"Firefox\\")"` + ); + }); + + it('correctly stringifies a single value', () => { + filters = [ + { + field: USER_AGENT_NAME, + values: ['Chrome'], + notValues: [], + }, + ]; + expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot( + `"user_agent.name: (\\"Chrome\\")"` + ); + }); + + it('returns an empty string for an empty array', () => { + expect(urlFiltersToKueryString([])).toMatchInlineSnapshot(`""`); + }); + + it('returns an empty string for an empty value', () => { + filters = [ + { + field: USER_AGENT_NAME, + values: [], + notValues: [], + }, + ]; + expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot(`""`); + }); + + it('adds quotations if the value contains a space', () => { + filters = [ + { + field: USER_AGENT_NAME, + values: ['Google Chrome'], + notValues: [], + }, + ]; + expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot( + `"user_agent.name: (\\"Google Chrome\\")"` + ); + }); + + it('adds quotations inside parens if there are values containing spaces', () => { + filters = [ + { + field: USER_AGENT_NAME, + values: ['Google Chrome'], + notValues: ['Apple Safari'], + }, + ]; + expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot( + `"user_agent.name: (\\"Google Chrome\\") and not (user_agent.name: (\\"Apple Safari\\"))"` + ); + }); + + it('handles parens for values with greater than 2 items', () => { + filters = [ + { + field: USER_AGENT_NAME, + values: ['Chrome', 'Firefox', 'Safari', 'Opera'], + notValues: ['Safari'], + }, + ]; + expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot( + `"user_agent.name: (\\"Chrome\\" or \\"Firefox\\" or \\"Safari\\" or \\"Opera\\") and not (user_agent.name: (\\"Safari\\"))"` + ); + }); + + it('handles colon characters in values', () => { + filters = [ + { + field: 'url', + values: ['https://elastic.co', 'https://example.com'], + notValues: [], + }, + ]; + expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot( + `"url: (\\"https://elastic.co\\" or \\"https://example.com\\")"` + ); + }); + + it('handles precending empty array', () => { + filters = [ + { + field: 'url', + values: ['https://elastic.co', 'https://example.com'], + notValues: [], + }, + { + field: USER_AGENT_NAME, + values: [], + }, + ]; + expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot( + `"url: (\\"https://elastic.co\\" or \\"https://example.com\\")"` + ); + }); + + it('handles skipped empty arrays', () => { + filters = [ + { + field: 'url', + values: ['https://elastic.co', 'https://example.com'], + notValues: [], + }, + { + field: USER_AGENT_NAME, + values: [], + }, + { + field: 'url', + values: ['https://elastic.co', 'https://example.com'], + notValues: [], + }, + ]; + expect(urlFiltersToKueryString(filters)).toMatchInlineSnapshot( + `"url: (\\"https://elastic.co\\" or \\"https://example.com\\") and url: (\\"https://elastic.co\\" or \\"https://example.com\\")"` + ); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.ts new file mode 100644 index 0000000000000..8a92c724338ef --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.ts @@ -0,0 +1,37 @@ +/* + * 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 { UrlFilter } from '../types'; + +/** + * Extract a map's keys to an array, then map those keys to a string per key. + * The strings contain all of the values chosen for the given field (which is also the key value). + * Reduce the list of query strings to a singular string, with AND operators between. + */ +export const urlFiltersToKueryString = (urlFilters: UrlFilter[]): string => { + let kueryString = ''; + urlFilters.forEach(({ field, values, notValues }) => { + const valuesT = values?.map((val) => `"${val}"`); + const notValuesT = notValues?.map((val) => `"${val}"`); + + if (valuesT && valuesT?.length > 0) { + if (kueryString.length > 0) { + kueryString += ' and '; + } + kueryString += `${field}: (${valuesT.join(' or ')})`; + } + + if (notValuesT && notValuesT?.length > 0) { + if (kueryString.length > 0) { + kueryString += ' and '; + } + kueryString += `not (${field}: (${notValuesT.join(' or ')}))`; + } + }); + + return kueryString; +}; diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/__stories__/field_value_selection.stories.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/__stories__/field_value_selection.stories.tsx index 665010be3aff2..80a25b82eb8cb 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/__stories__/field_value_selection.stories.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/__stories__/field_value_selection.stories.tsx @@ -6,7 +6,7 @@ */ import React, { ComponentType, useEffect, useState } from 'react'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { Observable } from 'rxjs'; import { CoreStart } from 'src/core/public'; import { text } from '@storybook/addon-knobs'; diff --git a/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx b/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx index 5a7ce3502ce84..896aca79114d7 100644 --- a/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx +++ b/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiSideNavItemType, ExclusiveUnion } from '@elastic/eui'; +import { EuiSideNavItemType } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import { matchPath, useLocation } from 'react-router-dom'; @@ -28,13 +28,9 @@ export type WrappedPageTemplateProps = Pick< | 'pageContentProps' | 'pageHeader' | 'restrictWidth' + | 'template' | 'isEmptyState' -> & - // recreate the exclusivity of bottomBar-related props - ExclusiveUnion< - { template?: 'default' } & Pick<KibanaPageTemplateProps, 'bottomBar' | 'bottomBarProps'>, - { template: KibanaPageTemplateProps['template'] } - >; +>; export interface ObservabilityPageTemplateDependencies { currentAppId$: Observable<string | undefined>; diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts.stories.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts.stories.tsx index ba714a679f1e5..ccc85d7f40187 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts.stories.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts.stories.tsx @@ -7,7 +7,7 @@ import { StoryContext } from '@storybook/react'; import React, { ComponentType } from 'react'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { MemoryRouter } from 'react-router-dom'; import { AlertsPage } from '.'; import { HttpSetup } from '../../../../../../src/core/public'; diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx index 92f51aeff9bd6..f97e3fb996441 100644 --- a/x-pack/plugins/observability/public/routes/index.tsx +++ b/x-pack/plugins/observability/public/routes/index.tsx @@ -112,4 +112,18 @@ export const routes = { }), }, }, + // enable this to test multi series architecture + // '/exploratory-view/multi': { + // handler: () => { + // return <ExploratoryViewPage multiSeries={true} />; + // }, + // params: { + // query: t.partial({ + // rangeFrom: t.string, + // rangeTo: t.string, + // refreshPaused: jsonRt.pipe(t.boolean), + // refreshInterval: jsonRt.pipe(t.number), + // }), + // }, + // }, }; diff --git a/x-pack/plugins/observability/public/utils/test_helper.tsx b/x-pack/plugins/observability/public/utils/test_helper.tsx index feacb011e0701..ce71a5640515b 100644 --- a/x-pack/plugins/observability/public/utils/test_helper.tsx +++ b/x-pack/plugins/observability/public/utils/test_helper.tsx @@ -8,7 +8,7 @@ import { render as testLibRender } from '@testing-library/react'; import { AppMountParameters, CoreStart } from 'kibana/public'; import React from 'react'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { of } from 'rxjs'; import { KibanaContextProvider, diff --git a/x-pack/plugins/osquery/cypress/README.md b/x-pack/plugins/osquery/cypress/README.md new file mode 100644 index 0000000000000..0df311ebc0a05 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/README.md @@ -0,0 +1,138 @@ +# Cypress Tests + +The `osquery/cypress` directory contains functional UI tests that execute using [Cypress](https://www.cypress.io/). + +## Running the tests + +There are currently three ways to run the tests, comprised of two execution modes and two target environments, which will be detailed below. + +### Execution modes + +#### Interactive mode + +When you run Cypress in interactive mode, an interactive runner is displayed that allows you to see commands as they execute while also viewing the application under test. For more information, please see [cypress documentation](https://docs.cypress.io/guides/core-concepts/test-runner.html#Overview). + +#### Headless mode + +A headless browser is a browser simulation program that does not have a user interface. These programs operate like any other browser, but do not display any UI. This is why meanwhile you are executing the tests on this mode you are not going to see the application under test. Just the output of the test is displayed on the terminal once the execution is finished. + +### Target environments + +#### FTR (CI) + +This is the configuration used by CI. It uses the FTR to spawn both a Kibana instance (http://localhost:5620) and an Elasticsearch instance (http://localhost:9220) with a preloaded minimum set of data (see preceding "Test data" section), and then executes cypress against this stack. You can find this configuration in `x-pack/test/security_solution_cypress` + +### Test Execution: Examples + +#### FTR + Headless (Chrome) + +Since this is how tests are run on CI, this will likely be the configuration you want to reproduce failures locally, etc. + +```shell +# bootstrap kibana from the project root +yarn kbn bootstrap + +# build the plugins/assets that cypress will execute against +node scripts/build_kibana_platform_plugins + +# launch the cypress test runner +cd x-pack/plugins/security_solution +yarn cypress:run-as-ci +``` +#### FTR + Interactive + +This is the preferred mode for developing new tests. + +```shell +# bootstrap kibana from the project root +yarn kbn bootstrap + +# build the plugins/assets that cypress will execute against +node scripts/build_kibana_platform_plugins + +# launch the cypress test runner +cd x-pack/plugins/security_solution +yarn cypress:open-as-ci +``` + +Note that you can select the browser you want to use on the top right side of the interactive runner. + +## Folder Structure + +### integration/ + +Cypress convention. Contains the specs that are going to be executed. + +### fixtures/ + +Cypress convention. Fixtures are used as external pieces of static data when we stub responses. + +### plugins/ + +Cypress convention. As a convenience, by default Cypress will automatically include the plugins file cypress/plugins/index.js before every single spec file it runs. + +### screens/ + +Contains the elements we want to interact with in our tests. + +Each file inside the screens folder represents a screen in our application. + +### tasks/ + +_Tasks_ are functions that may be reused across tests. + +Each file inside the tasks folder represents a screen of our application. + +## Test data + +The data the tests need: + +- Is generated on the fly using our application APIs (preferred way) +- Is ingested on the ELS instance using the `es_archive` utility + +### How to generate a new archive + +**Note:** As mentioned above, archives are only meant to contain external data, e.g. beats data. Due to the tendency for archived domain objects (rules, signals) to quickly become out of date, it is strongly suggested that you generate this data within the test, through interaction with either the UI or the API. + +We use es_archiver to manage the data that our Cypress tests need. + +1. Set up a clean instance of kibana and elasticsearch (if this is not possible, try to clean/minimize the data that you are going to archive). +2. With the kibana and elasticsearch instance up and running, create the data that you need for your test. +3. When you are sure that you have all the data you need run the following command from: `x-pack/plugins/security_solution` + +```sh +node ../../../scripts/es_archiver save <nameOfTheFolderWhereDataIsSaved> <indexPatternsToBeSaved> --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.js --es-url http://<elasticsearchUsername>:<elasticsearchPassword>@<elasticsearchHost>:<elasticsearchPort> +``` + +Example: + +```sh +node ../../../scripts/es_archiver save custom_rules ".kibana",".siem-signal*" --dir ../../test/security_solution_cypress/es_archives --config ../../../test/functional/config.js --es-url http://elastic:changeme@localhost:9220 +``` + +Note that the command will create the folder if it does not exist. + +## Development Best Practices + +### Clean up the state + +Remember to clean up the state of the test after its execution, typically with the `cleanKibana` function. Be mindful of failure scenarios, as well: if your test fails, will it leave the environment in a recoverable state? + +### Minimize the use of es_archive + +When possible, create all the data that you need for executing the tests using the application APIS or the UI. + +### Speed up test execution time + +Loading the web page takes a big amount of time, in order to minimize that impact, the following points should be +taken into consideration until another solution is implemented: + +- Group the tests that are similar in different contexts. +- For every context login only once, clean the state between tests if needed without re-loading the page. +- All tests in a spec file must be order-independent. + +Remember that minimizing the number of times the web page is loaded, we minimize as well the execution time. + +## Linting + +Optional linting rules for Cypress and linting setup can be found [here](https://github.com/cypress-io/eslint-plugin-cypress#usage) diff --git a/x-pack/plugins/osquery/cypress/cypress.json b/x-pack/plugins/osquery/cypress/cypress.json new file mode 100644 index 0000000000000..eb24616607ec3 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/cypress.json @@ -0,0 +1,14 @@ +{ + "baseUrl": "http://localhost:5620", + "defaultCommandTimeout": 60000, + "execTimeout": 120000, + "pageLoadTimeout": 120000, + "nodeVersion": "system", + "retries": { + "runMode": 2 + }, + "trashAssetsBeforeRuns": false, + "video": false, + "viewportHeight": 900, + "viewportWidth": 1440 +} \ No newline at end of file diff --git a/x-pack/plugins/osquery/cypress/integration/osquery_manager.spec.ts b/x-pack/plugins/osquery/cypress/integration/osquery_manager.spec.ts new file mode 100644 index 0000000000000..0babfd2f10a8e --- /dev/null +++ b/x-pack/plugins/osquery/cypress/integration/osquery_manager.spec.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 { HEADER } from '../screens/osquery'; +import { OSQUERY_NAVIGATION_LINK } from '../screens/navigation'; + +import { INTEGRATIONS, OSQUERY, openNavigationFlyout, navigateTo } from '../tasks/navigation'; +import { addIntegration } from '../tasks/integrations'; + +describe('Osquery Manager', () => { + before(() => { + navigateTo(INTEGRATIONS); + addIntegration('Osquery Manager'); + }); + + it('Displays Osquery on the navigation flyout once installed ', () => { + openNavigationFlyout(); + cy.get(OSQUERY_NAVIGATION_LINK).should('exist'); + }); + + it('Displays Live queries history title when navigating to Osquery', () => { + navigateTo(OSQUERY); + cy.get(HEADER).should('have.text', 'Live queries history'); + }); +}); diff --git a/x-pack/plugins/osquery/cypress/plugins/index.js b/x-pack/plugins/osquery/cypress/plugins/index.js new file mode 100644 index 0000000000000..7dbb69ced7016 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/plugins/index.js @@ -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. + */ + +/// <reference types="cypress" /> +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +/** + * @type {Cypress.PluginConfig} + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +module.exports = (_on, _config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +}; diff --git a/x-pack/plugins/osquery/cypress/screens/integrations.ts b/x-pack/plugins/osquery/cypress/screens/integrations.ts new file mode 100644 index 0000000000000..0b29e857f46ee --- /dev/null +++ b/x-pack/plugins/osquery/cypress/screens/integrations.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 const ADD_POLICY_BTN = '[data-test-subj="addIntegrationPolicyButton"]'; +export const CREATE_PACKAGE_POLICY_SAVE_BTN = '[data-test-subj="createPackagePolicySaveButton"]'; +export const INTEGRATIONS_CARD = '.euiCard__titleAnchor'; diff --git a/x-pack/plugins/osquery/cypress/screens/navigation.ts b/x-pack/plugins/osquery/cypress/screens/navigation.ts new file mode 100644 index 0000000000000..7884cf347d7c0 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/screens/navigation.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 const TOGGLE_NAVIGATION_BTN = '[data-test-subj="toggleNavButton"]'; +export const OSQUERY_NAVIGATION_LINK = '[data-test-subj="collapsibleNavAppLink"] [title="Osquery"]'; diff --git a/x-pack/plugins/osquery/cypress/screens/osquery.ts b/x-pack/plugins/osquery/cypress/screens/osquery.ts new file mode 100644 index 0000000000000..bc387a57e9e3c --- /dev/null +++ b/x-pack/plugins/osquery/cypress/screens/osquery.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 const HEADER = 'h1'; diff --git a/x-pack/plugins/osquery/cypress/support/commands.js b/x-pack/plugins/osquery/cypress/support/commands.js new file mode 100644 index 0000000000000..66f9435035571 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/support/commands.js @@ -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. + */ + +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) diff --git a/x-pack/plugins/osquery/cypress/support/index.ts b/x-pack/plugins/osquery/cypress/support/index.ts new file mode 100644 index 0000000000000..72618c943f4d2 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/support/index.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'; + +// Alternatively you can use CommonJS syntax: +// require('./commands') +Cypress.on('uncaught:exception', () => { + return false; +}); diff --git a/x-pack/plugins/osquery/cypress/tasks/integrations.ts b/x-pack/plugins/osquery/cypress/tasks/integrations.ts new file mode 100644 index 0000000000000..f85ef56550af5 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/tasks/integrations.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 { + ADD_POLICY_BTN, + CREATE_PACKAGE_POLICY_SAVE_BTN, + INTEGRATIONS_CARD, +} from '../screens/integrations'; + +export const addIntegration = (integration: string) => { + cy.get(INTEGRATIONS_CARD).contains(integration).click(); + cy.get(ADD_POLICY_BTN).click(); + cy.get(CREATE_PACKAGE_POLICY_SAVE_BTN).click(); + cy.get(CREATE_PACKAGE_POLICY_SAVE_BTN).should('not.exist'); + cy.reload(); +}; diff --git a/x-pack/plugins/osquery/cypress/tasks/navigation.ts b/x-pack/plugins/osquery/cypress/tasks/navigation.ts new file mode 100644 index 0000000000000..63d6b205b433b --- /dev/null +++ b/x-pack/plugins/osquery/cypress/tasks/navigation.ts @@ -0,0 +1,19 @@ +/* + * 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 { TOGGLE_NAVIGATION_BTN } from '../screens/navigation'; + +export const INTEGRATIONS = 'app/integrations#/'; +export const OSQUERY = 'app/osquery/live_queries'; + +export const navigateTo = (page: string) => { + cy.visit(page); +}; + +export const openNavigationFlyout = () => { + cy.get(TOGGLE_NAVIGATION_BTN).click(); +}; diff --git a/x-pack/plugins/osquery/cypress/tsconfig.json b/x-pack/plugins/osquery/cypress/tsconfig.json new file mode 100644 index 0000000000000..467ea13fc4869 --- /dev/null +++ b/x-pack/plugins/osquery/cypress/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../../tsconfig.base.json", + "exclude": [], + "include": [ + "./**/*" + ], + "compilerOptions": { + "tsBuildInfoFile": "../../../../build/tsbuildinfo/osquery/cypress", + "types": [ + "cypress", + "node" + ], + "resolveJsonModule": true, + }, + } diff --git a/x-pack/plugins/osquery/package.json b/x-pack/plugins/osquery/package.json new file mode 100644 index 0000000000000..5bbb95e556d6b --- /dev/null +++ b/x-pack/plugins/osquery/package.json @@ -0,0 +1,13 @@ +{ + "author": "Elastic", + "name": "osquery", + "version": "8.0.0", + "private": true, + "license": "Elastic-License", + "scripts": { + "cypress:open": "../../../node_modules/.bin/cypress open --config-file ./cypress/cypress.json", + "cypress:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/osquery_cypress/visual_config.ts", + "cypress:run": "../../../node_modules/.bin/cypress run --config-file ./cypress/cypress.json", + "cypress:run-as-ci": "node ../../../scripts/functional_tests --config ../../test/osquery_cypress/cli_config.ts" + } +} diff --git a/x-pack/plugins/osquery/server/usage/fetchers.ts b/x-pack/plugins/osquery/server/usage/fetchers.ts index 6a4236b5adccd..3d5f3592101fd 100644 --- a/x-pack/plugins/osquery/server/usage/fetchers.ts +++ b/x-pack/plugins/osquery/server/usage/fetchers.ts @@ -56,6 +56,7 @@ export async function getPolicyLevelUsage( }, }, index: '.fleet-agents', + ignore_unavailable: true, }); const policied = agentResponse.body.aggregations?.policied as AggregationsSingleBucketAggregate; if (policied && typeof policied.doc_count === 'number') { @@ -118,6 +119,7 @@ export async function getLiveQueryUsage( }, }, index: '.fleet-actions', + ignore_unavailable: true, }); const result: LiveQueryUsage = { session: await getRouteMetric(soClient, 'live_query'), @@ -226,6 +228,7 @@ export async function getBeatUsage(esClient: ElasticsearchClient) { }, }, index: METRICS_INDICES, + ignore_unavailable: true, }); return extractBeatUsageMetrics(metricResponse); diff --git a/x-pack/plugins/reporting/common/types.ts b/x-pack/plugins/reporting/common/types.ts index 2148cf983d889..8205b4f13a320 100644 --- a/x-pack/plugins/reporting/common/types.ts +++ b/x-pack/plugins/reporting/common/types.ts @@ -68,6 +68,7 @@ export interface ReportSource { }; meta: { objectType: string; layout?: string }; browser_type: string; + migration_version: string; max_attempts: number; timeout: number; @@ -77,7 +78,7 @@ export interface ReportSource { started_at?: string; completed_at?: string; created_at: string; - process_expiration?: string; + process_expiration?: string | null; // must be set to null to clear the expiration } /* diff --git a/x-pack/plugins/reporting/public/components/__snapshots__/screen_capture_panel_content.test.tsx.snap b/x-pack/plugins/reporting/public/components/__snapshots__/screen_capture_panel_content.test.tsx.snap index a6753211fba3b..01a8be98bc4be 100644 --- a/x-pack/plugins/reporting/public/components/__snapshots__/screen_capture_panel_content.test.tsx.snap +++ b/x-pack/plugins/reporting/public/components/__snapshots__/screen_capture_panel_content.test.tsx.snap @@ -64,7 +64,7 @@ exports[`ScreenCapturePanelContent properly renders a view with "canvas" layout className="euiFormRow__fieldWrapper" > <EuiSwitch - aria-describedby="generated-id-help" + aria-describedby="generated-id-help-0" checked={false} data-test-subj="reportModeToggle" id="generated-id" @@ -84,7 +84,7 @@ exports[`ScreenCapturePanelContent properly renders a view with "canvas" layout > <button aria-checked={false} - aria-describedby="generated-id-help" + aria-describedby="generated-id-help-0" aria-labelledby="generated-id" className="euiSwitch__button" data-test-subj="reportModeToggle" @@ -148,11 +148,12 @@ exports[`ScreenCapturePanelContent properly renders a view with "canvas" layout </EuiSwitch> <EuiFormHelpText className="euiFormRow__text" - id="generated-id-help" + id="generated-id-help-0" + key="0" > <div className="euiFormHelpText euiFormRow__text" - id="generated-id-help" + id="generated-id-help-0" > <FormattedMessage defaultMessage="Remove borders and footer logo" @@ -503,7 +504,7 @@ exports[`ScreenCapturePanelContent properly renders a view with "print" layout o className="euiFormRow__fieldWrapper" > <EuiSwitch - aria-describedby="generated-id-help" + aria-describedby="generated-id-help-0" checked={false} data-test-subj="usePrintLayout" id="generated-id" @@ -523,7 +524,7 @@ exports[`ScreenCapturePanelContent properly renders a view with "print" layout o > <button aria-checked={false} - aria-describedby="generated-id-help" + aria-describedby="generated-id-help-0" aria-labelledby="generated-id" className="euiSwitch__button" data-test-subj="usePrintLayout" @@ -587,11 +588,12 @@ exports[`ScreenCapturePanelContent properly renders a view with "print" layout o </EuiSwitch> <EuiFormHelpText className="euiFormRow__text" - id="generated-id-help" + id="generated-id-help-0" + key="0" > <div className="euiFormHelpText euiFormRow__text" - id="generated-id-help" + id="generated-id-help-0" > <FormattedMessage defaultMessage="Uses multiple pages, showing at most 2 visualizations per page" diff --git a/x-pack/plugins/reporting/public/components/buttons/__snapshots__/report_info_button.test.tsx.snap b/x-pack/plugins/reporting/public/components/buttons/__snapshots__/report_info_button.test.tsx.snap index 8c95363a843ec..cb36584960867 100644 --- a/x-pack/plugins/reporting/public/components/buttons/__snapshots__/report_info_button.test.tsx.snap +++ b/x-pack/plugins/reporting/public/components/buttons/__snapshots__/report_info_button.test.tsx.snap @@ -36,161 +36,68 @@ Array [ ownFocus={true} size="s" > - <EuiWindowEvent - event="keydown" - handler={[Function]} - /> - <EuiOverlayMask - headerZindexLocation="below" - onClick={[Function]} + <div + data-eui="EuiFlyout" + data-test-subj="reportInfoFlyout" + role="dialog" > - <Portal - containerInfo={ - <div - class="euiOverlayMask euiOverlayMask--belowHeader" - /> - } + <button + data-test-subj="euiFlyoutCloseButton" + onClick={[Function]} + type="button" /> - </EuiOverlayMask> - <EuiFocusTrap - clickOutsideDisables={false} - > - <div - data-eui="EuiFocusTrap" + <EuiFlyoutHeader + hasBorder={true} > <div - aria-labelledby="flyoutTitle" - className="euiFlyout euiFlyout--small euiFlyout--paddingLarge" - data-test-subj="reportInfoFlyout" - role="dialog" - tabIndex={0} + className="euiFlyoutHeader euiFlyoutHeader--hasBorder" > - <EuiI18n - default="Close this dialog" - token="euiFlyout.closeAriaLabel" + <EuiTitle + size="m" > - <EuiButtonIcon - aria-label="Close this dialog" - className="euiFlyout__closeButton" - color="text" - data-test-subj="euiFlyoutCloseButton" - iconType="cross" - onClick={[Function]} + <h2 + className="euiTitle euiTitle--medium" + id="flyoutTitle" > - <button - aria-label="Close this dialog" - className="euiButtonIcon euiButtonIcon--text euiButtonIcon--empty euiButtonIcon--xSmall euiFlyout__closeButton" - data-test-subj="euiFlyoutCloseButton" - disabled={false} - onClick={[Function]} - type="button" - > - <EuiIcon - aria-hidden="true" - className="euiButtonIcon__icon" - color="inherit" - size="m" - type="cross" - > - <span - aria-hidden="true" - className="euiButtonIcon__icon" - color="inherit" - data-euiicon-type="cross" - size="m" - /> - </EuiIcon> - </button> - </EuiButtonIcon> - </EuiI18n> - <EuiFlyoutHeader - hasBorder={true} + Unable to fetch report info + </h2> + </EuiTitle> + </div> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <div + className="euiFlyoutBody" + > + <div + className="euiFlyoutBody__overflow" + tabIndex={0} > <div - className="euiFlyoutHeader euiFlyoutHeader--hasBorder" + className="euiFlyoutBody__overflowContent" > - <EuiTitle - size="m" - > - <h2 - className="euiTitle euiTitle--medium" - id="flyoutTitle" - > - Unable to fetch report info - </h2> - </EuiTitle> - </div> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <div - className="euiFlyoutBody" - > - <div - className="euiFlyoutBody__overflow" - > + <EuiText> <div - className="euiFlyoutBody__overflowContent" + className="euiText euiText--medium" > - <EuiText> - <div - className="euiText euiText--medium" - > - Could not fetch the job info - </div> - </EuiText> + Could not fetch the job info </div> - </div> + </EuiText> </div> - </EuiFlyoutBody> + </div> </div> - </div> - </EuiFocusTrap> + </EuiFlyoutBody> + </div> </EuiFlyout>, <div - aria-labelledby="flyoutTitle" - className="euiFlyout euiFlyout--small euiFlyout--paddingLarge" + data-eui="EuiFlyout" data-test-subj="reportInfoFlyout" role="dialog" - tabIndex={0} > - <EuiI18n - default="Close this dialog" - token="euiFlyout.closeAriaLabel" - > - <EuiButtonIcon - aria-label="Close this dialog" - className="euiFlyout__closeButton" - color="text" - data-test-subj="euiFlyoutCloseButton" - iconType="cross" - onClick={[Function]} - > - <button - aria-label="Close this dialog" - className="euiButtonIcon euiButtonIcon--text euiButtonIcon--empty euiButtonIcon--xSmall euiFlyout__closeButton" - data-test-subj="euiFlyoutCloseButton" - disabled={false} - onClick={[Function]} - type="button" - > - <EuiIcon - aria-hidden="true" - className="euiButtonIcon__icon" - color="inherit" - size="m" - type="cross" - > - <span - aria-hidden="true" - className="euiButtonIcon__icon" - color="inherit" - data-euiicon-type="cross" - size="m" - /> - </EuiIcon> - </button> - </EuiButtonIcon> - </EuiI18n> + <button + data-test-subj="euiFlyoutCloseButton" + onClick={[Function]} + type="button" + /> <EuiFlyoutHeader hasBorder={true} > @@ -215,6 +122,7 @@ Array [ > <div className="euiFlyoutBody__overflow" + tabIndex={0} > <div className="euiFlyoutBody__overflowContent" @@ -243,159 +151,66 @@ Array [ ownFocus={true} size="s" > - <EuiWindowEvent - event="keydown" - handler={[Function]} - /> - <EuiOverlayMask - headerZindexLocation="below" - onClick={[Function]} + <div + data-eui="EuiFlyout" + data-test-subj="reportInfoFlyout" + role="dialog" > - <Portal - containerInfo={ - <div - class="euiOverlayMask euiOverlayMask--belowHeader" - /> - } + <button + data-test-subj="euiFlyoutCloseButton" + onClick={[Function]} + type="button" /> - </EuiOverlayMask> - <EuiFocusTrap - clickOutsideDisables={false} - > - <div - data-eui="EuiFocusTrap" + <EuiFlyoutHeader + hasBorder={true} > <div - aria-labelledby="flyoutTitle" - className="euiFlyout euiFlyout--small euiFlyout--paddingLarge" - data-test-subj="reportInfoFlyout" - role="dialog" - tabIndex={0} + className="euiFlyoutHeader euiFlyoutHeader--hasBorder" > - <EuiI18n - default="Close this dialog" - token="euiFlyout.closeAriaLabel" + <EuiTitle + size="m" > - <EuiButtonIcon - aria-label="Close this dialog" - className="euiFlyout__closeButton" - color="text" - data-test-subj="euiFlyoutCloseButton" - iconType="cross" - onClick={[Function]} + <h2 + className="euiTitle euiTitle--medium" + id="flyoutTitle" > - <button - aria-label="Close this dialog" - className="euiButtonIcon euiButtonIcon--text euiButtonIcon--empty euiButtonIcon--xSmall euiFlyout__closeButton" - data-test-subj="euiFlyoutCloseButton" - disabled={false} - onClick={[Function]} - type="button" - > - <EuiIcon - aria-hidden="true" - className="euiButtonIcon__icon" - color="inherit" - size="m" - type="cross" - > - <span - aria-hidden="true" - className="euiButtonIcon__icon" - color="inherit" - data-euiicon-type="cross" - size="m" - /> - </EuiIcon> - </button> - </EuiButtonIcon> - </EuiI18n> - <EuiFlyoutHeader - hasBorder={true} + Job Info + </h2> + </EuiTitle> + </div> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <div + className="euiFlyoutBody" + > + <div + className="euiFlyoutBody__overflow" + tabIndex={0} > <div - className="euiFlyoutHeader euiFlyoutHeader--hasBorder" - > - <EuiTitle - size="m" - > - <h2 - className="euiTitle euiTitle--medium" - id="flyoutTitle" - > - Job Info - </h2> - </EuiTitle> - </div> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <div - className="euiFlyoutBody" + className="euiFlyoutBody__overflowContent" > - <div - className="euiFlyoutBody__overflow" - > + <EuiText> <div - className="euiFlyoutBody__overflowContent" - > - <EuiText> - <div - className="euiText euiText--medium" - /> - </EuiText> - </div> - </div> + className="euiText euiText--medium" + /> + </EuiText> </div> - </EuiFlyoutBody> + </div> </div> - </div> - </EuiFocusTrap> + </EuiFlyoutBody> + </div> </EuiFlyout>, <div - aria-labelledby="flyoutTitle" - className="euiFlyout euiFlyout--small euiFlyout--paddingLarge" + data-eui="EuiFlyout" data-test-subj="reportInfoFlyout" role="dialog" - tabIndex={0} > - <EuiI18n - default="Close this dialog" - token="euiFlyout.closeAriaLabel" - > - <EuiButtonIcon - aria-label="Close this dialog" - className="euiFlyout__closeButton" - color="text" - data-test-subj="euiFlyoutCloseButton" - iconType="cross" - onClick={[Function]} - > - <button - aria-label="Close this dialog" - className="euiButtonIcon euiButtonIcon--text euiButtonIcon--empty euiButtonIcon--xSmall euiFlyout__closeButton" - data-test-subj="euiFlyoutCloseButton" - disabled={false} - onClick={[Function]} - type="button" - > - <EuiIcon - aria-hidden="true" - className="euiButtonIcon__icon" - color="inherit" - size="m" - type="cross" - > - <span - aria-hidden="true" - className="euiButtonIcon__icon" - color="inherit" - data-euiicon-type="cross" - size="m" - /> - </EuiIcon> - </button> - </EuiButtonIcon> - </EuiI18n> + <button + data-test-subj="euiFlyoutCloseButton" + onClick={[Function]} + type="button" + /> <EuiFlyoutHeader hasBorder={true} > @@ -420,6 +235,7 @@ Array [ > <div className="euiFlyoutBody__overflow" + tabIndex={0} > <div className="euiFlyoutBody__overflowContent" diff --git a/x-pack/plugins/reporting/public/components/screen_capture_panel_content.test.tsx b/x-pack/plugins/reporting/public/components/screen_capture_panel_content.test.tsx index 84a6dcc3c0ba3..a023eae512d54 100644 --- a/x-pack/plugins/reporting/public/components/screen_capture_panel_content.test.tsx +++ b/x-pack/plugins/reporting/public/components/screen_capture_panel_content.test.tsx @@ -7,7 +7,7 @@ import { mount } from 'enzyme'; import React from 'react'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { coreMock } from '../../../../../src/core/public/mocks'; import { BaseParams } from '../../common/types'; import { ReportingAPIClient } from '../lib/reporting_api_client'; diff --git a/x-pack/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/plugins/reporting/server/lib/enqueue_job.ts index b0e5d7bafb03c..70492b415f961 100644 --- a/x-pack/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/plugins/reporting/server/lib/enqueue_job.ts @@ -68,7 +68,7 @@ export function enqueueJobFactory( // 2. Schedule the report with Task Manager const task = await reporting.scheduleTask(report.toReportTaskJSON()); logger.info( - `Scheduled ${exportType.name} reporting task. Task ID: ${task.id}. Report ID: ${report._id}` + `Scheduled ${exportType.name} reporting task. Task ID: task:${task.id}. Report ID: ${report._id}` ); return report; diff --git a/x-pack/plugins/reporting/server/lib/statuses.ts b/x-pack/plugins/reporting/server/lib/statuses.ts index 1aa6b6d5ac8ff..2c25708078aaf 100644 --- a/x-pack/plugins/reporting/server/lib/statuses.ts +++ b/x-pack/plugins/reporting/server/lib/statuses.ts @@ -5,11 +5,12 @@ * 2.0. */ -export const statuses = { +import { JobStatus } from '../../common/types'; + +export const statuses: Record<string, JobStatus> = { JOB_STATUS_PENDING: 'pending', JOB_STATUS_PROCESSING: 'processing', JOB_STATUS_COMPLETED: 'completed', JOB_STATUS_WARNINGS: 'completed_with_warnings', JOB_STATUS_FAILED: 'failed', - JOB_STATUS_CANCELLED: 'cancelled', }; diff --git a/x-pack/plugins/reporting/server/lib/store/mapping.ts b/x-pack/plugins/reporting/server/lib/store/mapping.ts index ce8f768ef077f..69f432562ec98 100644 --- a/x-pack/plugins/reporting/server/lib/store/mapping.ts +++ b/x-pack/plugins/reporting/server/lib/store/mapping.ts @@ -7,15 +7,10 @@ export const mapping = { meta: { - // We are indexing these properties with both text and keyword fields because that's what will be auto generated - // when an index already exists. This schema is only used when a reporting index doesn't exist. This way existing - // reporting indexes and new reporting indexes will look the same and the data can be queried in the same - // manner. + // We are indexing these properties with both text and keyword fields + // because that's what will be auto generated when an index already exists. properties: { - /** - * Type of object that is triggering this report. Should be either search, visualization or dashboard. - * Used for job listing and telemetry stats only. - */ + // ID of the app this report: search, visualization or dashboard, etc objectType: { type: 'text', fields: { @@ -25,10 +20,6 @@ export const mapping = { }, }, }, - /** - * Can be either preserve_layout, print or none (in the case of csv export). - * Used for phone home stats only. - */ layout: { type: 'text', fields: { @@ -41,9 +32,10 @@ export const mapping = { }, }, browser_type: { type: 'keyword' }, + migration_version: { type: 'keyword' }, // new field (7.14) to distinguish reports that were scheduled with Task Manager jobtype: { type: 'keyword' }, payload: { type: 'object', enabled: false }, - priority: { type: 'byte' }, // NOTE: this is unused, but older data may have a mapping for this field + priority: { type: 'byte' }, // TODO: remove: this is unused timeout: { type: 'long' }, process_expiration: { type: 'date' }, created_by: { type: 'keyword' }, // `null` if security is disabled diff --git a/x-pack/plugins/reporting/server/lib/store/report.test.ts b/x-pack/plugins/reporting/server/lib/store/report.test.ts index 23d766f2190f6..a8d14e12a738b 100644 --- a/x-pack/plugins/reporting/server/lib/store/report.test.ts +++ b/x-pack/plugins/reporting/server/lib/store/report.test.ts @@ -20,21 +20,18 @@ describe('Class Report', () => { timeout: 30000, }); - expect(report.toEsDocsJSON()).toMatchObject({ - _index: '.reporting-test-index-12345', - _source: { - attempts: 0, - browser_type: 'browser_type_test_string', - completed_at: undefined, - created_by: 'created_by_test_string', - jobtype: 'test-report', - max_attempts: 50, - meta: { objectType: 'test' }, - payload: { headers: 'payload_test_field', objectType: 'testOt' }, - started_at: undefined, - status: 'pending', - timeout: 30000, - }, + expect(report.toReportSource()).toMatchObject({ + attempts: 0, + browser_type: 'browser_type_test_string', + completed_at: undefined, + created_by: 'created_by_test_string', + jobtype: 'test-report', + max_attempts: 50, + meta: { objectType: 'test' }, + payload: { headers: 'payload_test_field', objectType: 'testOt' }, + started_at: undefined, + status: 'pending', + timeout: 30000, }); expect(report.toReportTaskJSON()).toMatchObject({ attempts: 0, @@ -80,22 +77,18 @@ describe('Class Report', () => { }; report.updateWithEsDoc(metadata); - expect(report.toEsDocsJSON()).toMatchObject({ - _id: '12342p9o387549o2345', - _index: '.reporting-test-update', - _source: { - attempts: 0, - browser_type: 'browser_type_test_string', - completed_at: undefined, - created_by: 'created_by_test_string', - jobtype: 'test-report', - max_attempts: 50, - meta: { objectType: 'stange' }, - payload: { objectType: 'testOt' }, - started_at: undefined, - status: 'pending', - timeout: 30000, - }, + expect(report.toReportSource()).toMatchObject({ + attempts: 0, + browser_type: 'browser_type_test_string', + completed_at: undefined, + created_by: 'created_by_test_string', + jobtype: 'test-report', + max_attempts: 50, + meta: { objectType: 'stange' }, + payload: { objectType: 'testOt' }, + started_at: undefined, + status: 'pending', + timeout: 30000, }); expect(report.toReportTaskJSON()).toMatchObject({ attempts: 0, diff --git a/x-pack/plugins/reporting/server/lib/store/report.ts b/x-pack/plugins/reporting/server/lib/store/report.ts index 9b98650e1d984..fa5b91527ccc4 100644 --- a/x-pack/plugins/reporting/server/lib/store/report.ts +++ b/x-pack/plugins/reporting/server/lib/store/report.ts @@ -21,8 +21,13 @@ export { ReportDocument }; export { ReportApiJSON, ReportSource }; const puid = new Puid(); +export const MIGRATION_VERSION = '7.14.0'; -export class Report implements Partial<ReportSource> { +/* + * The public fields are a flattened version what Elasticsearch returns when you + * `GET` a document. + */ +export class Report implements Partial<ReportSource & ReportDocumentHead> { public _index?: string; public _id: string; public _primary_term?: number; // set by ES @@ -47,6 +52,7 @@ export class Report implements Partial<ReportSource> { public readonly timeout?: ReportSource['timeout']; public process_expiration?: ReportSource['process_expiration']; + public migration_version: string; /* * Create an unsaved report @@ -58,6 +64,8 @@ export class Report implements Partial<ReportSource> { this._primary_term = opts._primary_term; this._seq_no = opts._seq_no; + this.migration_version = MIGRATION_VERSION; + this.payload = opts.payload!; this.kibana_name = opts.kibana_name!; this.kibana_id = opts.kibana_id!; @@ -80,7 +88,7 @@ export class Report implements Partial<ReportSource> { /* * Update the report with "live" storage metadata */ - updateWithEsDoc(doc: Partial<Report>) { + updateWithEsDoc(doc: Partial<Report>): void { if (doc._index == null || doc._id == null) { throw new Error(`Report object from ES has missing fields!`); } @@ -89,30 +97,31 @@ export class Report implements Partial<ReportSource> { this._index = doc._index; this._primary_term = doc._primary_term; this._seq_no = doc._seq_no; + this.migration_version = MIGRATION_VERSION; } /* * Data structure for writing to Elasticsearch index */ - toEsDocsJSON() { + toReportSource(): ReportSource { return { - _id: this._id, - _index: this._index, - _source: { - jobtype: this.jobtype, - created_at: this.created_at, - created_by: this.created_by, - payload: this.payload, - meta: this.meta, - timeout: this.timeout, - max_attempts: this.max_attempts, - browser_type: this.browser_type, - status: this.status, - attempts: this.attempts, - started_at: this.started_at, - completed_at: this.completed_at, - process_expiration: this.process_expiration, - }, + migration_version: MIGRATION_VERSION, + kibana_name: this.kibana_name, + kibana_id: this.kibana_id, + jobtype: this.jobtype, + created_at: this.created_at, + created_by: this.created_by, + payload: this.payload, + meta: this.meta, + timeout: this.timeout!, + max_attempts: this.max_attempts, + browser_type: this.browser_type!, + status: this.status, + attempts: this.attempts, + started_at: this.started_at, + completed_at: this.completed_at, + process_expiration: this.process_expiration, + output: this.output || null, }; } diff --git a/x-pack/plugins/reporting/server/lib/store/store.test.ts b/x-pack/plugins/reporting/server/lib/store/store.test.ts index 7f96433fcc6ce..8bb5c7fb8bbf9 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.test.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.test.ts @@ -184,6 +184,7 @@ describe('ReportingStore', () => { _source: { kibana_name: 'test', kibana_id: 'test123', + migration_version: 'X.0.0', created_at: 'some time', created_by: 'some security person', jobtype: 'csv', @@ -222,6 +223,7 @@ describe('ReportingStore', () => { "meta": Object { "testMeta": "meta", }, + "migration_version": "7.14.0", "output": null, "payload": Object { "testPayload": "payload", @@ -239,6 +241,8 @@ describe('ReportingStore', () => { const report = new Report({ _id: 'id-of-processing', _index: '.reporting-test-index-12345', + _seq_no: 42, + _primary_term: 10002, jobtype: 'test-report', created_by: 'created_by_test_string', browser_type: 'browser_type_test_string', @@ -254,24 +258,12 @@ describe('ReportingStore', () => { await store.setReportClaimed(report, { testDoc: 'test' } as any); - const [updateCall] = mockEsClient.update.mock.calls; - expect(updateCall).toMatchInlineSnapshot(` - Array [ - Object { - "body": Object { - "doc": Object { - "status": "processing", - "testDoc": "test", - }, - }, - "id": "id-of-processing", - "if_primary_term": undefined, - "if_seq_no": undefined, - "index": ".reporting-test-index-12345", - "refresh": true, - }, - ] - `); + const [[updateCall]] = mockEsClient.update.mock.calls; + const response = updateCall.body?.doc as Report; + expect(response.migration_version).toBe(`7.14.0`); + expect(response.status).toBe(`processing`); + expect(updateCall.if_seq_no).toBe(42); + expect(updateCall.if_primary_term).toBe(10002); }); it('setReportFailed sets the status of a record to failed', async () => { @@ -279,6 +271,8 @@ describe('ReportingStore', () => { const report = new Report({ _id: 'id-of-failure', _index: '.reporting-test-index-12345', + _seq_no: 43, + _primary_term: 10002, jobtype: 'test-report', created_by: 'created_by_test_string', browser_type: 'browser_type_test_string', @@ -294,24 +288,12 @@ describe('ReportingStore', () => { await store.setReportFailed(report, { errors: 'yes' } as any); - const [updateCall] = mockEsClient.update.mock.calls; - expect(updateCall).toMatchInlineSnapshot(` - Array [ - Object { - "body": Object { - "doc": Object { - "errors": "yes", - "status": "failed", - }, - }, - "id": "id-of-failure", - "if_primary_term": undefined, - "if_seq_no": undefined, - "index": ".reporting-test-index-12345", - "refresh": true, - }, - ] - `); + const [[updateCall]] = mockEsClient.update.mock.calls; + const response = updateCall.body?.doc as Report; + expect(response.migration_version).toBe(`7.14.0`); + expect(response.status).toBe(`failed`); + expect(updateCall.if_seq_no).toBe(43); + expect(updateCall.if_primary_term).toBe(10002); }); it('setReportCompleted sets the status of a record to completed', async () => { @@ -319,6 +301,8 @@ describe('ReportingStore', () => { const report = new Report({ _id: 'vastly-great-report-id', _index: '.reporting-test-index-12345', + _seq_no: 44, + _primary_term: 10002, jobtype: 'test-report', created_by: 'created_by_test_string', browser_type: 'browser_type_test_string', @@ -334,31 +318,21 @@ describe('ReportingStore', () => { await store.setReportCompleted(report, { certainly_completed: 'yes' } as any); - const [updateCall] = mockEsClient.update.mock.calls; - expect(updateCall).toMatchInlineSnapshot(` - Array [ - Object { - "body": Object { - "doc": Object { - "certainly_completed": "yes", - "status": "completed", - }, - }, - "id": "vastly-great-report-id", - "if_primary_term": undefined, - "if_seq_no": undefined, - "index": ".reporting-test-index-12345", - "refresh": true, - }, - ] - `); + const [[updateCall]] = mockEsClient.update.mock.calls; + const response = updateCall.body?.doc as Report; + expect(response.migration_version).toBe(`7.14.0`); + expect(response.status).toBe(`completed`); + expect(updateCall.if_seq_no).toBe(44); + expect(updateCall.if_primary_term).toBe(10002); }); - it('setReportCompleted sets the status of a record to completed_with_warnings', async () => { + it('sets the status of a record to completed_with_warnings', async () => { const store = new ReportingStore(mockCore, mockLogger); const report = new Report({ _id: 'vastly-great-report-id', _index: '.reporting-test-index-12345', + _seq_no: 45, + _primary_term: 10002, jobtype: 'test-report', created_by: 'created_by_test_string', browser_type: 'browser_type_test_string', @@ -379,28 +353,52 @@ describe('ReportingStore', () => { }, } as any); - const [updateCall] = mockEsClient.update.mock.calls; - expect(updateCall).toMatchInlineSnapshot(` - Array [ - Object { - "body": Object { - "doc": Object { - "certainly_completed": "pretty_much", - "output": Object { - "warnings": Array [ - "those pants don't go with that shirt", - ], - }, - "status": "completed_with_warnings", - }, - }, - "id": "vastly-great-report-id", - "if_primary_term": undefined, - "if_seq_no": undefined, - "index": ".reporting-test-index-12345", - "refresh": true, - }, - ] + const [[updateCall]] = mockEsClient.update.mock.calls; + const response = updateCall.body?.doc as Report; + + expect(response.migration_version).toBe(`7.14.0`); + expect(response.status).toBe(`completed_with_warnings`); + expect(updateCall.if_seq_no).toBe(45); + expect(updateCall.if_primary_term).toBe(10002); + expect(response.output).toMatchInlineSnapshot(` + Object { + "warnings": Array [ + "those pants don't go with that shirt", + ], + } `); }); + + it('prepareReportForRetry resets the expiration and status on the report document', async () => { + const store = new ReportingStore(mockCore, mockLogger); + const report = new Report({ + _id: 'pretty-good-report-id', + _index: '.reporting-test-index-94058763', + _seq_no: 46, + _primary_term: 10002, + jobtype: 'test-report-2', + created_by: 'created_by_test_string', + browser_type: 'browser_type_test_string', + status: 'processing', + process_expiration: '2002', + max_attempts: 3, + payload: { + title: 'test report', + headers: 'rp_test_headers', + objectType: 'testOt', + browserTimezone: 'utc', + }, + timeout: 30000, + }); + + await store.prepareReportForRetry(report); + + const [[updateCall]] = mockEsClient.update.mock.calls; + const response = updateCall.body?.doc as Report; + + expect(response.migration_version).toBe(`7.14.0`); + expect(response.status).toBe(`pending`); + expect(updateCall.if_seq_no).toBe(46); + expect(updateCall.if_primary_term).toBe(10002); + }); }); diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts index fc7bd9c23d769..8f1e6c315a2d1 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.ts @@ -5,15 +5,38 @@ * 2.0. */ +import { IndexResponse, UpdateResponse } from '@elastic/elasticsearch/api/types'; import { ElasticsearchClient } from 'src/core/server'; import { LevelLogger, statuses } from '../'; import { ReportingCore } from '../../'; -import { numberToDuration } from '../../../common/schema_utils'; import { JobStatus } from '../../../common/types'; import { ReportTaskParams } from '../tasks'; import { indexTimestamp } from './index_timestamp'; import { mapping } from './mapping'; -import { Report, ReportDocument, ReportSource } from './report'; +import { MIGRATION_VERSION, Report, ReportDocument, ReportSource } from './report'; + +/* + * When an instance of Kibana claims a report job, this information tells us about that instance + */ +export type ReportProcessingFields = Required<{ + kibana_id: Report['kibana_id']; + kibana_name: Report['kibana_name']; + browser_type: Report['browser_type']; + attempts: Report['attempts']; + started_at: Report['started_at']; + timeout: Report['timeout']; + process_expiration: Report['process_expiration']; +}>; + +export type ReportFailedFields = Required<{ + completed_at: Report['completed_at']; + output: Report['output']; +}>; + +export type ReportCompletedFields = Required<{ + completed_at: Report['completed_at']; + output: Report['output']; +}>; /* * When searching for long-pending reports, we get a subset of fields @@ -24,15 +47,38 @@ export interface ReportRecordTimeout { _source: { status: JobStatus; process_expiration?: string; - created_at?: string; }; } const checkReportIsEditable = (report: Report) => { - if (!report._id || !report._index) { - throw new Error(`Report object is not synced with ES!`); + const { _id, _index, _seq_no, _primary_term } = report; + if (_id == null || _index == null) { + throw new Error(`Report is not editable: Job [${_id}] is not synced with ES!`); + } + + if (_seq_no == null || _primary_term == null) { + throw new Error( + `Report is not editable: Job [${_id}] is missing _seq_no and _primary_term fields!` + ); } }; +/* + * When searching for long-pending reports, we get a subset of fields + */ +const sourceDoc = (doc: Partial<ReportSource>): Partial<ReportSource> => { + return { + ...doc, + migration_version: MIGRATION_VERSION, + }; +}; + +const jobDebugMessage = (report: Report) => + `${report._id} ` + + `[_index: ${report._index}] ` + + `[_seq_no: ${report._seq_no}] ` + + `[_primary_term: ${report._primary_term}]` + + `[attempts: ${report.attempts}] ` + + `[process_expiration: ${report.process_expiration}]`; /* * A class to give an interface to historical reports in the reporting.index @@ -43,7 +89,6 @@ const checkReportIsEditable = (report: Report) => { export class ReportingStore { private readonly indexPrefix: string; // config setting of index prefix in system index name private readonly indexInterval: string; // config setting of index prefix: how often to poll for pending work - private readonly queueTimeoutMins: number; // config setting of queue timeout, rounded up to nearest minute private client?: ElasticsearchClient; constructor(private reportingCore: ReportingCore, private logger: LevelLogger) { @@ -52,7 +97,6 @@ export class ReportingStore { this.indexPrefix = config.get('index'); this.indexInterval = config.get('queue', 'indexInterval'); this.logger = logger.clone(['store']); - this.queueTimeoutMins = Math.ceil(numberToDuration(config.get('queue', 'timeout')).asMinutes()); } private async getClient() { @@ -103,18 +147,20 @@ export class ReportingStore { /* * Called from addReport, which handles any errors */ - private async indexReport(report: Report) { + private async indexReport(report: Report): Promise<IndexResponse> { const doc = { index: report._index!, id: report._id, + refresh: true, body: { - ...report.toEsDocsJSON()._source, - process_expiration: new Date(0), // use epoch so the job query works - attempts: 0, - status: statuses.JOB_STATUS_PENDING, + ...report.toReportSource(), + ...sourceDoc({ + process_expiration: new Date(0).toISOString(), + attempts: 0, + status: statuses.JOB_STATUS_PENDING, + }), }, }; - const client = await this.getClient(); const { body } = await client.index(doc); @@ -140,8 +186,7 @@ export class ReportingStore { await this.createIndex(index); try { - const doc = await this.indexReport(report); - report.updateWithEsDoc(doc); + report.updateWithEsDoc(await this.indexReport(report)); await this.refreshIndex(index); @@ -156,7 +201,9 @@ export class ReportingStore { /* * Search for a report from task data and return back the report */ - public async findReportFromTask(taskJson: ReportTaskParams): Promise<Report> { + public async findReportFromTask( + taskJson: Pick<ReportTaskParams, 'id' | 'index'> + ): Promise<Report> { if (!taskJson.index) { throw new Error('Task JSON is missing index field!'); } @@ -186,41 +233,23 @@ export class ReportingStore { timeout: document._source?.timeout, }); } catch (err) { - this.logger.error('Error in finding a report! ' + JSON.stringify({ report: taskJson })); - this.logger.error(err); - throw err; - } - } - - public async setReportPending(report: Report) { - const doc = { status: statuses.JOB_STATUS_PENDING }; - - try { - checkReportIsEditable(report); - - const client = await this.getClient(); - const { body } = await client.update<ReportDocument>({ - id: report._id, - index: report._index!, - if_seq_no: report._seq_no, - if_primary_term: report._primary_term, - refresh: true, - body: { doc }, - }); - - return (body as unknown) as ReportDocument; - } catch (err) { - this.logger.error('Error in setting report pending status!'); + this.logger.error( + `Error in finding the report from the scheduled task info! ` + + `[id: ${taskJson.id}] [index: ${taskJson.index}]` + ); this.logger.error(err); throw err; } } - public async setReportClaimed(report: Report, stats: Partial<Report>): Promise<ReportDocument> { - const doc = { - ...stats, + public async setReportClaimed( + report: Report, + processingInfo: ReportProcessingFields + ): Promise<UpdateResponse<ReportDocument>> { + const doc = sourceDoc({ + ...processingInfo, status: statuses.JOB_STATUS_PROCESSING, - }; + }); try { checkReportIsEditable(report); @@ -235,19 +264,24 @@ export class ReportingStore { body: { doc }, }); - return (body as unknown) as ReportDocument; + return body; } catch (err) { - this.logger.error('Error in setting report processing status!'); + this.logger.error( + `Error in updating status to processing! Report: ` + jobDebugMessage(report) + ); this.logger.error(err); throw err; } } - public async setReportFailed(report: Report, stats: Partial<Report>): Promise<ReportDocument> { - const doc = { - ...stats, + public async setReportFailed( + report: Report, + failedInfo: ReportFailedFields + ): Promise<UpdateResponse<ReportDocument>> { + const doc = sourceDoc({ + ...failedInfo, status: statuses.JOB_STATUS_FAILED, - }; + }); try { checkReportIsEditable(report); @@ -261,26 +295,29 @@ export class ReportingStore { refresh: true, body: { doc }, }); - - return (body as unknown) as ReportDocument; + return body; } catch (err) { - this.logger.error('Error in setting report failed status!'); + this.logger.error(`Error in updating status to failed! Report: ` + jobDebugMessage(report)); this.logger.error(err); throw err; } } - public async setReportCompleted(report: Report, stats: Partial<Report>): Promise<ReportDocument> { + public async setReportCompleted( + report: Report, + completedInfo: ReportCompletedFields + ): Promise<UpdateResponse<ReportDocument>> { + const { output } = completedInfo; + const status = + output && output.warnings && output.warnings.length > 0 + ? statuses.JOB_STATUS_WARNINGS + : statuses.JOB_STATUS_COMPLETED; + const doc = sourceDoc({ + ...completedInfo, + status, + }); + try { - const { output } = stats; - const status = - output && output.warnings && output.warnings.length > 0 - ? statuses.JOB_STATUS_WARNINGS - : statuses.JOB_STATUS_COMPLETED; - const doc = { - ...stats, - status, - }; checkReportIsEditable(report); const client = await this.getClient(); @@ -292,16 +329,20 @@ export class ReportingStore { refresh: true, body: { doc }, }); - - return (body as unknown) as ReportDocument; + return body; } catch (err) { - this.logger.error('Error in setting report complete status!'); + this.logger.error(`Error in updating status to complete! Report: ` + jobDebugMessage(report)); this.logger.error(err); throw err; } } - public async clearExpiration(report: Report): Promise<ReportDocument> { + public async prepareReportForRetry(report: Report): Promise<UpdateResponse<ReportDocument>> { + const doc = sourceDoc({ + status: statuses.JOB_STATUS_PENDING, + process_expiration: null, + }); + try { checkReportIsEditable(report); @@ -312,50 +353,54 @@ export class ReportingStore { if_seq_no: report._seq_no, if_primary_term: report._primary_term, refresh: true, - body: { doc: { process_expiration: null } }, + body: { doc }, }); - - return (body as unknown) as ReportDocument; + return body; } catch (err) { - this.logger.error('Error in clearing expiration!'); + this.logger.error( + `Error in clearing expiration and status for retry! Report: ` + jobDebugMessage(report) + ); this.logger.error(err); throw err; } } /* - * A zombie report document is one that isn't completed or failed, isn't - * being executed, and isn't scheduled to run. They arise: - * - when the cluster has processing documents in ESQueue before upgrading to v7.13 when ESQueue was removed - * - if Kibana crashes while a report task is executing and it couldn't be rescheduled on its own - * - * Pending reports are not included in this search: they may be scheduled in TM just not run yet. - * TODO Should we get a list of the reports that are pending and scheduled in TM so we can exclude them from this query? + * A report needs to be rescheduled when: + * 1. An older version of Kibana created jobs with ESQueue, and they have + * not yet started running. + * 2. The report process_expiration field is overdue, which happens if the + * report runs too long or Kibana restarts during execution */ - public async findZombieReportDocuments(): Promise<ReportRecordTimeout[] | null> { + public async findStaleReportJob(): Promise<ReportRecordTimeout> { const client = await this.getClient(); + + const expiredFilter = { + bool: { + must: [ + { range: { process_expiration: { lt: `now` } } }, + { terms: { status: [statuses.JOB_STATUS_PROCESSING] } }, + ], + }, + }; + const oldVersionFilter = { + bool: { + must: [{ terms: { status: [statuses.JOB_STATUS_PENDING] } }], + must_not: [{ exists: { field: 'migration_version' } }], + }, + }; + const { body } = await client.search<ReportRecordTimeout['_source']>({ + size: 1, index: this.indexPrefix + '-*', - filter_path: 'hits.hits', + seq_no_primary_term: true, + _source_excludes: ['output'], body: { - sort: { created_at: { order: 'desc' } }, - query: { - bool: { - filter: [ - { - bool: { - must: [ - { range: { process_expiration: { lt: `now-${this.queueTimeoutMins}m` } } }, - { terms: { status: [statuses.JOB_STATUS_PROCESSING] } }, - ], - }, - }, - ], - }, - }, + sort: { created_at: { order: 'asc' as const } }, // find the oldest first + query: { bool: { filter: { bool: { should: [expiredFilter, oldVersionFilter] } } } }, }, }); - return body.hits?.hits as ReportRecordTimeout[]; + return body.hits?.hits[0] as ReportRecordTimeout; } } diff --git a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts index 2960ce457b7ae..f9e2cd82b0805 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { UpdateResponse } from '@elastic/elasticsearch/api/types'; import moment from 'moment'; import * as Rx from 'rxjs'; import { timeout } from 'rxjs/operators'; @@ -19,9 +20,9 @@ import { CancellationToken } from '../../../common'; import { durationToNumber, numberToDuration } from '../../../common/schema_utils'; import { ReportingConfigType } from '../../config'; import { BasePayload, RunTaskFn } from '../../types'; -import { Report, ReportingStore } from '../store'; +import { Report, ReportDocument, ReportingStore } from '../store'; +import { ReportFailedFields, ReportProcessingFields } from '../store/store'; import { - ReportingExecuteTaskInstance, ReportingTask, ReportingTaskStatus, REPORTING_EXECUTE_TYPE, @@ -30,6 +31,13 @@ import { } from './'; import { errorLogger } from './error_logger'; +interface ReportingExecuteTaskInstance { + state: object; + taskType: string; + params: ReportTaskParams; + runAt?: Date; +} + function isOutput(output: TaskRunResult | Error): output is TaskRunResult { return typeof output === 'object' && (output as TaskRunResult).content != null; } @@ -101,15 +109,21 @@ export class ExecuteReportTask implements ReportingTask { } public async _claimJob(task: ReportTaskParams): Promise<Report> { - const store = await this.getStore(); + if (this.kibanaId == null) { + throw new Error(`Kibana instance ID is undefined!`); + } + if (this.kibanaName == null) { + throw new Error(`Kibana instance name is undefined!`); + } + const store = await this.getStore(); let report: Report; if (task.id && task.index) { // if this is an ad-hoc report, there is a corresponding "pending" record in ReportingStore in need of updating - report = await store.findReportFromTask(task); // update seq_no + report = await store.findReportFromTask(task); // receives seq_no and primary_term } else { // if this is a scheduled report (not implemented), the report object needs to be instantiated - throw new Error('scheduled reports are not supported!'); + throw new Error('Could not find matching report document!'); } // Check if this is a completed job. This may happen if the `reports:monitor` @@ -126,7 +140,7 @@ export class ExecuteReportTask implements ReportingTask { const maxAttempts = task.max_attempts; if (report.attempts >= maxAttempts) { const err = new Error(`Max attempts reached (${maxAttempts}). Queue timeout reached.`); - await this._failJob(task, err); + await this._failJob(report, err); throw err; } @@ -134,7 +148,7 @@ export class ExecuteReportTask implements ReportingTask { const startTime = m.toISOString(); const expirationTime = m.add(queueTimeout).toISOString(); - const stats = { + const doc: ReportProcessingFields = { kibana_id: this.kibanaId, kibana_name: this.kibanaName, browser_type: this.config.capture.browser.type, @@ -144,19 +158,28 @@ export class ExecuteReportTask implements ReportingTask { process_expiration: expirationTime, }; - this.logger.debug(`Claiming ${report.jobtype} job ${report._id}`); - const claimedReport = new Report({ ...report, - ...stats, + ...doc, }); - await store.setReportClaimed(claimedReport, stats); + this.logger.debug( + `Claiming ${claimedReport.jobtype} ${report._id} ` + + `[_index: ${report._index}] ` + + `[_seq_no: ${report._seq_no}] ` + + `[_primary_term: ${report._primary_term}] ` + + `[attempts: ${report.attempts}] ` + + `[process_expiration: ${expirationTime}]` + ); + + const resp = await store.setReportClaimed(claimedReport, doc); + claimedReport._seq_no = resp._seq_no; + claimedReport._primary_term = resp._primary_term; return claimedReport; } - private async _failJob(task: ReportTaskParams, error?: Error) { - const message = `Failing ${task.jobtype} job ${task.id}`; + private async _failJob(report: Report, error?: Error): Promise<UpdateResponse<ReportDocument>> { + const message = `Failing ${report.jobtype} job ${report._id}`; // log the error let docOutput; @@ -169,9 +192,8 @@ export class ExecuteReportTask implements ReportingTask { // update the report in the store const store = await this.getStore(); - const report = await store.findReportFromTask(task); const completedTime = moment().toISOString(); - const doc = { + const doc: ReportFailedFields = { completed_at: completedTime, output: docOutput, }; @@ -179,7 +201,7 @@ export class ExecuteReportTask implements ReportingTask { return await store.setReportFailed(report, doc); } - private _formatOutput(output: TaskRunResult | Error) { + private _formatOutput(output: TaskRunResult | Error): TaskRunResult { const docOutput = {} as TaskRunResult; const unknownMime = null; @@ -201,7 +223,10 @@ export class ExecuteReportTask implements ReportingTask { return docOutput; } - public async _performJob(task: ReportTaskParams, cancellationToken: CancellationToken) { + public async _performJob( + task: ReportTaskParams, + cancellationToken: CancellationToken + ): Promise<TaskRunResult> { if (!this.taskExecutors) { throw new Error(`Task run function factories have not been called yet!`); } @@ -220,10 +245,10 @@ export class ExecuteReportTask implements ReportingTask { .toPromise(); } - public async _completeJob(task: ReportTaskParams, output: TaskRunResult) { - let docId = `/${task.index}/_doc/${task.id}`; + public async _completeJob(report: Report, output: TaskRunResult): Promise<Report> { + let docId = `/${report._index}/_doc/${report._id}`; - this.logger.info(`Saving ${task.jobtype} job ${docId}.`); + this.logger.debug(`Saving ${report.jobtype} to ${docId}.`); const completedTime = moment().toISOString(); const docOutput = this._formatOutput(output); @@ -233,16 +258,13 @@ export class ExecuteReportTask implements ReportingTask { completed_at: completedTime, output: docOutput, }; - const report = await store.findReportFromTask(task); // update seq_no and primary_term docId = `/${report._index}/_doc/${report._id}`; - try { - await store.setReportCompleted(report, doc); - this.logger.debug(`Saved ${report.jobtype} job ${docId}`); - } catch (err) { - if (err.statusCode === 409) return false; - errorLogger(this.logger, `Failure saving completed job ${docId}!`); - } + const resp = await store.setReportCompleted(report, doc); + this.logger.info(`Saved ${report.jobtype} job ${docId}`); + report._seq_no = resp._seq_no; + report._primary_term = resp._primary_term; + return report; } /* @@ -264,7 +286,6 @@ export class ExecuteReportTask implements ReportingTask { */ run: async () => { let report: Report | undefined; - let attempts = 0; // find the job in the store and set status to processing const task = context.taskInstance.params as ReportTaskParams; @@ -278,64 +299,73 @@ export class ExecuteReportTask implements ReportingTask { // Update job status to claimed report = await this._claimJob(task); - - const { jobtype: jobType, attempts: attempt, max_attempts: maxAttempts } = task; - this.logger.info( - `Starting ${jobType} report ${jobId}: attempt ${attempt + 1} of ${maxAttempts}.` - ); - this.logger.debug(`Reports running: ${this.reporting.countConcurrentReports()}.`); } catch (failedToClaim) { // error claiming report - log the error // could be version conflict, or no longer connected to ES - errorLogger(this.logger, `Error in claiming report!`, failedToClaim); + errorLogger(this.logger, `Error in claiming ${jobId}`, failedToClaim); } if (!report) { - errorLogger(this.logger, `Report could not be claimed. Exiting...`); + errorLogger(this.logger, `Job ${jobId} could not be claimed. Exiting...`); return; } - attempts = report.attempts; + const { jobtype: jobType, attempts, max_attempts: maxAttempts } = report; + this.logger.debug( + `Starting ${jobType} report ${jobId}: attempt ${attempts} of ${maxAttempts}.` + ); + this.logger.debug(`Reports running: ${this.reporting.countConcurrentReports()}.`); try { const output = await this._performJob(task, cancellationToken); if (output) { - await this._completeJob(task, output); + report = await this._completeJob(report, output); } - // untrack the report for concurrency awareness this.logger.debug(`Stopping ${jobId}.`); - this.reporting.untrackReport(jobId); - this.logger.debug(`Reports running: ${this.reporting.countConcurrentReports()}.`); } catch (failedToExecuteErr) { cancellationToken.cancel(); - const maxAttempts = this.config.capture.maxAttempts; if (attempts < maxAttempts) { - // attempts remain - reschedule + // attempts remain, reschedule try { + if (report == null) { + throw new Error(`Report ${jobId} is null!`); + } // reschedule to retry const remainingAttempts = maxAttempts - report.attempts; errorLogger( this.logger, - `Scheduling retry. Retries remaining: ${remainingAttempts}.`, + `Scheduling retry for job ${jobId}. Retries remaining: ${remainingAttempts}.`, failedToExecuteErr ); await this.rescheduleTask(reportFromTask(task).toReportTaskJSON(), this.logger); } catch (rescheduleErr) { // can not be rescheduled - log the error - errorLogger(this.logger, `Could not reschedule the errored job!`, rescheduleErr); + errorLogger( + this.logger, + `Could not reschedule the errored job ${jobId}!`, + rescheduleErr + ); } } else { // 0 attempts remain - fail the job try { - const maxAttemptsMsg = `Max attempts reached (${attempts}). Failed with: ${failedToExecuteErr}`; - await this._failJob(task, new Error(maxAttemptsMsg)); + const maxAttemptsMsg = `Max attempts (${attempts}) reached for job ${jobId}. Failed with: ${failedToExecuteErr}`; + if (report == null) { + throw new Error(`Report ${jobId} is null!`); + } + const resp = await this._failJob(report, new Error(maxAttemptsMsg)); + report._seq_no = resp._seq_no; + report._primary_term = resp._primary_term; } catch (failedToFailError) { - errorLogger(this.logger, `Could not fail the job!`, failedToFailError); + errorLogger(this.logger, `Could not fail ${jobId}!`, failedToFailError); } } + } finally { + this.reporting.untrackReport(jobId); + this.logger.debug(`Reports running: ${this.reporting.countConcurrentReports()}.`); } }, @@ -374,11 +404,12 @@ export class ExecuteReportTask implements ReportingTask { state: {}, params: report, }; + return await this.getTaskManagerStart().schedule(taskInstance); } private async rescheduleTask(task: ReportTaskParams, logger: LevelLogger) { - logger.info(`Rescheduling ${task.id} to retry after error.`); + logger.info(`Rescheduling task:${task.id} to retry after error.`); const oldTaskInstance: ReportingExecuteTaskInstance = { taskType: REPORTING_EXECUTE_TYPE, @@ -386,7 +417,7 @@ export class ExecuteReportTask implements ReportingTask { params: task, }; const newTask = await this.getTaskManagerStart().schedule(oldTaskInstance); - logger.debug(`Rescheduled ${task.id}`); + logger.debug(`Rescheduled task:${task.id}. New task: task:${newTask.id}`); return newTask; } diff --git a/x-pack/plugins/reporting/server/lib/tasks/index.ts b/x-pack/plugins/reporting/server/lib/tasks/index.ts index ec9e85e957d03..c02b06d97adc7 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/index.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/index.ts @@ -32,13 +32,6 @@ export interface ReportTaskParams<JobPayloadType = BasePayload> { meta: ReportSource['meta']; } -export interface ReportingExecuteTaskInstance /* extends TaskInstanceWithDeprecatedFields */ { - state: object; - taskType: string; - params: ReportTaskParams; - runAt?: Date; -} - export enum ReportingTaskStatus { UNINITIALIZED = 'uninitialized', INITIALIZED = 'initialized', @@ -52,6 +45,5 @@ export interface ReportingTask { maxAttempts: number; timeout: string; }; - getStatus: () => ReportingTaskStatus; } diff --git a/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts b/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts index 36380f767e6d9..9e1bc49739c93 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts @@ -11,21 +11,29 @@ import { ReportingCore } from '../../'; import { TaskManagerStartContract, TaskRunCreatorFunction } from '../../../../task_manager/server'; import { numberToDuration } from '../../../common/schema_utils'; import { ReportingConfigType } from '../../config'; +import { statuses } from '../statuses'; import { Report } from '../store'; -import { - ReportingExecuteTaskInstance, - ReportingTask, - ReportingTaskStatus, - REPORTING_EXECUTE_TYPE, - REPORTING_MONITOR_TYPE, - ReportTaskParams, -} from './'; +import { ReportingTask, ReportingTaskStatus, REPORTING_MONITOR_TYPE, ReportTaskParams } from './'; /* - * Task for finding the ReportingRecords left in the ReportingStore and stuck - * in pending or processing. It could happen if the server crashed while running - * a report and was cancelled. Normally a failure would mean scheduling a - * retry or failing the report, but the retry is not guaranteed to be scheduled. + * Task for finding the ReportingRecords left in the ReportingStore (.reporting index) and stuck in + * a pending or processing status. + * + * Stuck in pending: + * - This can happen if the report was scheduled in an earlier version of Kibana that used ESQueue. + * - Task Manager doesn't know about these types of reports because there was never a task + * scheduled for them. + * Stuck in processing: + * - This can could happen if the server crashed while a report was executing. + * - Task Manager doesn't know about these reports, because the task is completed in Task + * Manager when Reporting starts executing the report. We are not using Task Manager's retry + * mechanisms, which defer the retry for a few minutes. + * + * These events require us to reschedule the report with Task Manager, so that the jobs can be + * distributed and executed. + * + * The runner function reschedules a single report job per task run, to avoid flooding Task Manager + * in case many report jobs need to be recovered. */ export class MonitorReportsTask implements ReportingTask { public TYPE = REPORTING_MONITOR_TYPE; @@ -77,36 +85,41 @@ export class MonitorReportsTask implements ReportingTask { const reportingStore = await this.getStore(); try { - const results = await reportingStore.findZombieReportDocuments(); - if (results && results.length) { - this.logger.info( - `Found ${results.length} reports to reschedule: ${results - .map((pending) => pending._id) - .join(',')}` - ); - } else { - this.logger.debug(`Found 0 pending reports.`); + const recoveredJob = await reportingStore.findStaleReportJob(); + if (!recoveredJob) { + // no reports need to be rescheduled return; } - for (const pending of results) { - const { - _id: jobId, - _source: { process_expiration: processExpiration, status }, - } = pending; - const expirationTime = moment(processExpiration); // If it is the start of the Epoch, something went wrong - const timeWaitValue = moment().valueOf() - expirationTime.valueOf(); - const timeWaitTime = moment.duration(timeWaitValue); + const { + _id: jobId, + _source: { process_expiration: processExpiration, status }, + } = recoveredJob; + + if (![statuses.JOB_STATUS_PENDING, statuses.JOB_STATUS_PROCESSING].includes(status)) { + throw new Error(`Invalid job status in the monitoring search result: ${status}`); // only pending or processing jobs possibility need rescheduling + } + + if (status === statuses.JOB_STATUS_PENDING) { this.logger.info( - `Task ${jobId} has ${status} status for ${timeWaitTime.humanize()}. The queue timeout is ${this.timeout.humanize()}.` + `${jobId} was scheduled in a previous version and left in [${status}] status. Rescheduling...` ); + } - // clear process expiration and reschedule - const oldReport = new Report({ ...pending, ...pending._source }); - const reschedulingTask = oldReport.toReportTaskJSON(); - await reportingStore.clearExpiration(oldReport); - await this.rescheduleTask(reschedulingTask, this.logger); + if (status === statuses.JOB_STATUS_PROCESSING) { + const expirationTime = moment(processExpiration); + const overdueValue = moment().valueOf() - expirationTime.valueOf(); + this.logger.info( + `${jobId} status is [${status}] and the expiration time was [${overdueValue}ms] ago. Rescheduling...` + ); } + + // clear process expiration and set status to pending + const report = new Report({ ...recoveredJob, ...recoveredJob._source }); + await reportingStore.prepareReportForRetry(report); // if there is a version conflict response, this just throws and logs an error + + // clear process expiration and reschedule + await this.rescheduleTask(report.toReportTaskJSON(), this.logger); // a recovered report job must be scheduled by only a sinle Kibana instance } catch (err) { this.logger.error(err); } @@ -126,33 +139,19 @@ export class MonitorReportsTask implements ReportingTask { createTaskRunner: this.getTaskRunner(), maxAttempts: 1, // round the timeout value up to the nearest second, since Task Manager - // doesn't support milliseconds + // doesn't support milliseconds or > 1s timeout: Math.ceil(this.timeout.asSeconds()) + 's', }; } - // reschedule the task with TM and update the report document status to "Pending" + // reschedule the task with TM private async rescheduleTask(task: ReportTaskParams, logger: LevelLogger) { if (!this.taskManagerStart) { throw new Error('Reporting task runner has not been initialized!'); } - logger.info(`Rescheduling ${task.id} to retry after timeout expiration.`); - - const store = await this.getStore(); - - const oldTaskInstance: ReportingExecuteTaskInstance = { - taskType: REPORTING_EXECUTE_TYPE, // schedule a task to EXECUTE - state: {}, - params: task, - }; - - const [report, newTask] = await Promise.all([ - await store.findReportFromTask(task), - await this.taskManagerStart.schedule(oldTaskInstance), - ]); - - await store.setReportPending(report); + logger.info(`Rescheduling task:${task.id} to retry.`); + const newTask = await this.reporting.scheduleTask(task); return newTask; } diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary.tsx index 749c1c8ccb4e2..48a0d18653053 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary.tsx @@ -12,7 +12,6 @@ import { EuiFlyoutBody, EuiFlyoutFooter, EuiFlyoutHeader, - EuiOverlayMask, EuiTitle, } from '@elastic/eui'; import React, { Fragment, useState } from 'react'; @@ -47,37 +46,39 @@ export const PrivilegeSummary = (props: Props) => { /> </EuiButtonEmpty> {isOpen && ( - <EuiOverlayMask headerZindexLocation="below"> - <EuiFlyout onClose={() => setIsOpen(false)} size={flyoutSize}> - <EuiFlyoutHeader> - <EuiTitle size="m"> - <h2> - <FormattedMessage - id="xpack.security.management.editRole.privilegeSummary.modalHeaderTitle" - defaultMessage="Privilege summary" - /> - </h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <PrivilegeSummaryTable - role={props.role} - spaces={props.spaces} - kibanaPrivileges={props.kibanaPrivileges} - canCustomizeSubFeaturePrivileges={props.canCustomizeSubFeaturePrivileges} - spacesApiUi={props.spacesApiUi} - /> - </EuiFlyoutBody> - <EuiFlyoutFooter> - <EuiButton onClick={() => setIsOpen(false)}> + <EuiFlyout + onClose={() => setIsOpen(false)} + size={flyoutSize} + maskProps={{ headerZindexLocation: 'below' }} + > + <EuiFlyoutHeader> + <EuiTitle size="m"> + <h2> <FormattedMessage - id="xpack.security.management.editRole.privilegeSummary.closeSummaryButtonText" - defaultMessage="Close" + id="xpack.security.management.editRole.privilegeSummary.modalHeaderTitle" + defaultMessage="Privilege summary" /> - </EuiButton> - </EuiFlyoutFooter> - </EuiFlyout> - </EuiOverlayMask> + </h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <PrivilegeSummaryTable + role={props.role} + spaces={props.spaces} + kibanaPrivileges={props.kibanaPrivileges} + canCustomizeSubFeaturePrivileges={props.canCustomizeSubFeaturePrivileges} + spacesApiUi={props.spacesApiUi} + /> + </EuiFlyoutBody> + <EuiFlyoutFooter> + <EuiButton onClick={() => setIsOpen(false)}> + <FormattedMessage + id="xpack.security.management.editRole.privilegeSummary.closeSummaryButtonText" + defaultMessage="Close" + /> + </EuiButton> + </EuiFlyoutFooter> + </EuiFlyout> )} </Fragment> ); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx index b290cb301866d..8f62acd463e6a 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx @@ -20,7 +20,6 @@ import { EuiFlyoutHeader, EuiForm, EuiFormRow, - EuiOverlayMask, EuiSpacer, EuiText, EuiTitle, @@ -93,64 +92,67 @@ export class PrivilegeSpaceForm extends Component<Props, State> { public render() { return ( - <EuiOverlayMask headerZindexLocation="below"> - <EuiFlyout onClose={this.closeFlyout} size="m" maxWidth={true}> - <EuiFlyoutHeader hasBorder> - <EuiTitle size="m"> - <h2> - <FormattedMessage - id="xpack.security.management.editRole.spacePrivilegeForm.modalTitle" - defaultMessage="Kibana privileges" - /> - </h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <EuiErrorBoundary>{this.getForm()}</EuiErrorBoundary> - </EuiFlyoutBody> - <EuiFlyoutFooter> - {this.state.privilegeCalculator.hasSupersededInheritedPrivileges( - this.state.privilegeIndex - ) && ( - <Fragment> - <EuiCallOut - color="warning" - iconType="alert" - data-test-subj="spaceFormGlobalPermissionsSupersedeWarning" - title={ - <FormattedMessage - id="xpack.security.management.editRole.spacePrivilegeForm.supersededWarningTitle" - defaultMessage="Superseded by global privileges" - /> - } - > - <FormattedMessage - id="xpack.security.management.editRole.spacePrivilegeForm.supersededWarning" - defaultMessage="Declared privileges are less permissive than configured global privileges. View the privilege summary to see effective privileges." - /> - </EuiCallOut> - <EuiSpacer size="s" /> - </Fragment> - )} - <EuiFlexGroup justifyContent="spaceBetween"> - <EuiFlexItem grow={false}> - <EuiButtonEmpty - iconType="cross" - onClick={this.closeFlyout} - flush="left" - data-test-subj={'cancelSpacePrivilegeButton'} - > + <EuiFlyout + onClose={this.closeFlyout} + size="m" + maxWidth={true} + maskProps={{ headerZindexLocation: 'below' }} + > + <EuiFlyoutHeader hasBorder> + <EuiTitle size="m"> + <h2> + <FormattedMessage + id="xpack.security.management.editRole.spacePrivilegeForm.modalTitle" + defaultMessage="Kibana privileges" + /> + </h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiErrorBoundary>{this.getForm()}</EuiErrorBoundary> + </EuiFlyoutBody> + <EuiFlyoutFooter> + {this.state.privilegeCalculator.hasSupersededInheritedPrivileges( + this.state.privilegeIndex + ) && ( + <Fragment> + <EuiCallOut + color="warning" + iconType="alert" + data-test-subj="spaceFormGlobalPermissionsSupersedeWarning" + title={ <FormattedMessage - id="xpack.security.management.editRole.spacePrivilegeForm.cancelButton" - defaultMessage="Cancel" + id="xpack.security.management.editRole.spacePrivilegeForm.supersededWarningTitle" + defaultMessage="Superseded by global privileges" /> - </EuiButtonEmpty> - </EuiFlexItem> - <EuiFlexItem grow={false}>{this.getSaveButton()}</EuiFlexItem> - </EuiFlexGroup> - </EuiFlyoutFooter> - </EuiFlyout> - </EuiOverlayMask> + } + > + <FormattedMessage + id="xpack.security.management.editRole.spacePrivilegeForm.supersededWarning" + defaultMessage="Declared privileges are less permissive than configured global privileges. View the privilege summary to see effective privileges." + /> + </EuiCallOut> + <EuiSpacer size="s" /> + </Fragment> + )} + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty + iconType="cross" + onClick={this.closeFlyout} + flush="left" + data-test-subj={'cancelSpacePrivilegeButton'} + > + <FormattedMessage + id="xpack.security.management.editRole.spacePrivilegeForm.cancelButton" + defaultMessage="Cancel" + /> + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}>{this.getSaveButton()}</EuiFlexItem> + </EuiFlexGroup> + </EuiFlyoutFooter> + </EuiFlyout> ); } diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap b/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap index eb266ce93338c..f36a1bf477b06 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap @@ -58,33 +58,33 @@ exports[`<RolesGridPage /> renders permission denied if required 1`] = ` /> </EuiIcon> <EuiSpacer - size="s" + size="m" > <div - className="euiSpacer euiSpacer--s" + className="euiSpacer euiSpacer--m" /> </EuiSpacer> + <EuiTitle + size="m" + > + <h2 + className="euiTitle euiTitle--medium" + > + <FormattedMessage + defaultMessage="You need permission to manage roles" + id="xpack.security.management.roles.deniedPermissionTitle" + values={Object {}} + > + You need permission to manage roles + </FormattedMessage> + </h2> + </EuiTitle> <EuiTextColor color="subdued" > <span className="euiTextColor euiTextColor--subdued" > - <EuiTitle - size="m" - > - <h2 - className="euiTitle euiTitle--medium" - > - <FormattedMessage - defaultMessage="You need permission to manage roles" - id="xpack.security.management.roles.deniedPermissionTitle" - values={Object {}} - > - You need permission to manage roles - </FormattedMessage> - </h2> - </EuiTitle> <EuiSpacer size="m" > diff --git a/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap b/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap index bcb97538b4f05..2ee2337fc9aeb 100644 --- a/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap +++ b/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PromptPage renders as expected with additional scripts 1`] = `"<html lang=\\"en\\"><head><title>Elastic</title><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.v7.light.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/node_modules/@kbn/ui-framework/dist/kui_light.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/ui/legacy_light_theme.css\\" rel=\\"stylesheet\\"/>MockedFonts<link rel=\\"alternate icon\\" type=\\"image/png\\" href=\\"/mock-server-basepath/ui/favicons/favicon.png\\"/><link rel=\\"icon\\" type=\\"image/svg+xml\\" href=\\"/mock-server-basepath/ui/favicons/favicon.svg\\"/><script src=\\"/mock-basepath/some/script1.js\\"></script><script src=\\"/mock-basepath/some/script2.js\\"></script><meta name=\\"theme-color\\" content=\\"#ffffff\\"/><meta name=\\"color-scheme\\" content=\\"light dark\\"/></head><body><div class=\\"euiPage euiPage--grow\\" style=\\"min-height:100vh\\" data-test-subj=\\"promptPage\\"><main class=\\"euiPageBody euiPageBody--borderRadiusNone\\"><div class=\\"euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPageContent euiPageContent--verticalCenter euiPageContent--horizontalCenter\\" role=\\"main\\"><div class=\\"euiEmptyPrompt\\"><span data-euiicon-type=\\"alert\\" color=\\"danger\\"></span><div class=\\"euiSpacer euiSpacer--s\\"></div><span class=\\"euiTextColor euiTextColor--subdued\\"><h2 class=\\"euiTitle euiTitle--medium\\">Some Title</h2><div class=\\"euiSpacer euiSpacer--m\\"></div><div class=\\"euiText euiText--medium\\"><div>Some Body</div></div></span><div class=\\"euiSpacer euiSpacer--l\\"></div><div class=\\"euiSpacer euiSpacer--s\\"></div><div class=\\"euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentCenter euiFlexGroup--directionColumn euiFlexGroup--responsive\\"><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><span>Action#1</span></div><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><span>Action#2</span></div></div></div></div></main></div></body></html>"`; +exports[`PromptPage renders as expected with additional scripts 1`] = `"<html lang=\\"en\\"><head><title>Elastic</title><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.v7.light.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/node_modules/@kbn/ui-framework/dist/kui_light.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/ui/legacy_light_theme.css\\" rel=\\"stylesheet\\"/>MockedFonts<link rel=\\"alternate icon\\" type=\\"image/png\\" href=\\"/mock-server-basepath/ui/favicons/favicon.png\\"/><link rel=\\"icon\\" type=\\"image/svg+xml\\" href=\\"/mock-server-basepath/ui/favicons/favicon.svg\\"/><script src=\\"/mock-basepath/some/script1.js\\"></script><script src=\\"/mock-basepath/some/script2.js\\"></script><meta name=\\"theme-color\\" content=\\"#ffffff\\"/><meta name=\\"color-scheme\\" content=\\"light dark\\"/></head><body><div class=\\"euiPage euiPage--grow\\" style=\\"min-height:100vh\\" data-test-subj=\\"promptPage\\"><div class=\\"euiPageBody euiPageBody--borderRadiusNone\\"><div class=\\"euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPageContent euiPageContent--verticalCenter euiPageContent--horizontalCenter\\" role=\\"main\\"><div class=\\"euiEmptyPrompt\\"><span data-euiicon-type=\\"alert\\" color=\\"danger\\"></span><div class=\\"euiSpacer euiSpacer--m\\"></div><h2 class=\\"euiTitle euiTitle--medium\\">Some Title</h2><span class=\\"euiTextColor euiTextColor--subdued\\"><div class=\\"euiSpacer euiSpacer--m\\"></div><div class=\\"euiText euiText--medium\\"><div>Some Body</div></div></span><div class=\\"euiSpacer euiSpacer--l\\"></div><div class=\\"euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentCenter euiFlexGroup--directionColumn euiFlexGroup--responsive\\"><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><span>Action#1</span></div><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><span>Action#2</span></div></div></div></div></div></div></body></html>"`; -exports[`PromptPage renders as expected without additional scripts 1`] = `"<html lang=\\"en\\"><head><title>Elastic</title><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.v7.light.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/node_modules/@kbn/ui-framework/dist/kui_light.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/ui/legacy_light_theme.css\\" rel=\\"stylesheet\\"/>MockedFonts<link rel=\\"alternate icon\\" type=\\"image/png\\" href=\\"/mock-server-basepath/ui/favicons/favicon.png\\"/><link rel=\\"icon\\" type=\\"image/svg+xml\\" href=\\"/mock-server-basepath/ui/favicons/favicon.svg\\"/><meta name=\\"theme-color\\" content=\\"#ffffff\\"/><meta name=\\"color-scheme\\" content=\\"light dark\\"/></head><body><div class=\\"euiPage euiPage--grow\\" style=\\"min-height:100vh\\" data-test-subj=\\"promptPage\\"><main class=\\"euiPageBody euiPageBody--borderRadiusNone\\"><div class=\\"euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPageContent euiPageContent--verticalCenter euiPageContent--horizontalCenter\\" role=\\"main\\"><div class=\\"euiEmptyPrompt\\"><span data-euiicon-type=\\"alert\\" color=\\"danger\\"></span><div class=\\"euiSpacer euiSpacer--s\\"></div><span class=\\"euiTextColor euiTextColor--subdued\\"><h2 class=\\"euiTitle euiTitle--medium\\">Some Title</h2><div class=\\"euiSpacer euiSpacer--m\\"></div><div class=\\"euiText euiText--medium\\"><div>Some Body</div></div></span><div class=\\"euiSpacer euiSpacer--l\\"></div><div class=\\"euiSpacer euiSpacer--s\\"></div><div class=\\"euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentCenter euiFlexGroup--directionColumn euiFlexGroup--responsive\\"><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><span>Action#1</span></div><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><span>Action#2</span></div></div></div></div></main></div></body></html>"`; +exports[`PromptPage renders as expected without additional scripts 1`] = `"<html lang=\\"en\\"><head><title>Elastic</title><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.v7.light.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/node_modules/@kbn/ui-framework/dist/kui_light.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/ui/legacy_light_theme.css\\" rel=\\"stylesheet\\"/>MockedFonts<link rel=\\"alternate icon\\" type=\\"image/png\\" href=\\"/mock-server-basepath/ui/favicons/favicon.png\\"/><link rel=\\"icon\\" type=\\"image/svg+xml\\" href=\\"/mock-server-basepath/ui/favicons/favicon.svg\\"/><meta name=\\"theme-color\\" content=\\"#ffffff\\"/><meta name=\\"color-scheme\\" content=\\"light dark\\"/></head><body><div class=\\"euiPage euiPage--grow\\" style=\\"min-height:100vh\\" data-test-subj=\\"promptPage\\"><div class=\\"euiPageBody euiPageBody--borderRadiusNone\\"><div class=\\"euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPageContent euiPageContent--verticalCenter euiPageContent--horizontalCenter\\" role=\\"main\\"><div class=\\"euiEmptyPrompt\\"><span data-euiicon-type=\\"alert\\" color=\\"danger\\"></span><div class=\\"euiSpacer euiSpacer--m\\"></div><h2 class=\\"euiTitle euiTitle--medium\\">Some Title</h2><span class=\\"euiTextColor euiTextColor--subdued\\"><div class=\\"euiSpacer euiSpacer--m\\"></div><div class=\\"euiText euiText--medium\\"><div>Some Body</div></div></span><div class=\\"euiSpacer euiSpacer--l\\"></div><div class=\\"euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentCenter euiFlexGroup--directionColumn euiFlexGroup--responsive\\"><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><span>Action#1</span></div><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><span>Action#2</span></div></div></div></div></div></div></body></html>"`; diff --git a/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap b/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap index 55168401992f7..2e7f3d49e478f 100644 --- a/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap +++ b/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`UnauthenticatedPage renders as expected 1`] = `"<html lang=\\"en\\"><head><title>Elastic</title><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.v7.light.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/node_modules/@kbn/ui-framework/dist/kui_light.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/ui/legacy_light_theme.css\\" rel=\\"stylesheet\\"/>MockedFonts<link rel=\\"alternate icon\\" type=\\"image/png\\" href=\\"/mock-server-basepath/ui/favicons/favicon.png\\"/><link rel=\\"icon\\" type=\\"image/svg+xml\\" href=\\"/mock-server-basepath/ui/favicons/favicon.svg\\"/><meta name=\\"theme-color\\" content=\\"#ffffff\\"/><meta name=\\"color-scheme\\" content=\\"light dark\\"/></head><body><div class=\\"euiPage euiPage--grow\\" style=\\"min-height:100vh\\" data-test-subj=\\"promptPage\\"><main class=\\"euiPageBody euiPageBody--borderRadiusNone\\"><div class=\\"euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPageContent euiPageContent--verticalCenter euiPageContent--horizontalCenter\\" role=\\"main\\"><div class=\\"euiEmptyPrompt\\"><span data-euiicon-type=\\"alert\\" color=\\"danger\\"></span><div class=\\"euiSpacer euiSpacer--s\\"></div><span class=\\"euiTextColor euiTextColor--subdued\\"><h2 class=\\"euiTitle euiTitle--medium\\">We couldn't log you in</h2><div class=\\"euiSpacer euiSpacer--m\\"></div><div class=\\"euiText euiText--medium\\"><p>We hit an authentication error. Please check your credentials and try again. If you still can't log in, contact your system administrator.</p></div></span><div class=\\"euiSpacer euiSpacer--l\\"></div><div class=\\"euiSpacer euiSpacer--s\\"></div><div class=\\"euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentCenter euiFlexGroup--directionColumn euiFlexGroup--responsive\\"><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><a class=\\"euiButton euiButton--primary euiButton--fill\\" href=\\"/some/url?some-query=some-value#some-hash\\" rel=\\"noreferrer\\" data-test-subj=\\"logInButton\\"><span class=\\"euiButtonContent euiButton__content\\"><span class=\\"euiButton__text\\">Log in</span></span></a></div></div></div></div></main></div></body></html>"`; +exports[`UnauthenticatedPage renders as expected 1`] = `"<html lang=\\"en\\"><head><title>Elastic</title><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.v7.light.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/node_modules/@kbn/ui-framework/dist/kui_light.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/ui/legacy_light_theme.css\\" rel=\\"stylesheet\\"/>MockedFonts<link rel=\\"alternate icon\\" type=\\"image/png\\" href=\\"/mock-server-basepath/ui/favicons/favicon.png\\"/><link rel=\\"icon\\" type=\\"image/svg+xml\\" href=\\"/mock-server-basepath/ui/favicons/favicon.svg\\"/><meta name=\\"theme-color\\" content=\\"#ffffff\\"/><meta name=\\"color-scheme\\" content=\\"light dark\\"/></head><body><div class=\\"euiPage euiPage--grow\\" style=\\"min-height:100vh\\" data-test-subj=\\"promptPage\\"><div class=\\"euiPageBody euiPageBody--borderRadiusNone\\"><div class=\\"euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPageContent euiPageContent--verticalCenter euiPageContent--horizontalCenter\\" role=\\"main\\"><div class=\\"euiEmptyPrompt\\"><span data-euiicon-type=\\"alert\\" color=\\"danger\\"></span><div class=\\"euiSpacer euiSpacer--m\\"></div><h2 class=\\"euiTitle euiTitle--medium\\">We couldn't log you in</h2><span class=\\"euiTextColor euiTextColor--subdued\\"><div class=\\"euiSpacer euiSpacer--m\\"></div><div class=\\"euiText euiText--medium\\"><p>We hit an authentication error. Please check your credentials and try again. If you still can't log in, contact your system administrator.</p></div></span><div class=\\"euiSpacer euiSpacer--l\\"></div><div class=\\"euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentCenter euiFlexGroup--directionColumn euiFlexGroup--responsive\\"><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><a class=\\"euiButton euiButton--primary euiButton--fill\\" href=\\"/some/url?some-query=some-value#some-hash\\" rel=\\"noreferrer\\" data-test-subj=\\"logInButton\\"><span class=\\"euiButtonContent euiButton__content\\"><span class=\\"euiButton__text\\">Log in</span></span></a></div></div></div></div></div></div></body></html>"`; diff --git a/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap b/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap index 1011d82eb1f73..8d31770cd9385 100644 --- a/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap +++ b/x-pack/plugins/security/server/authorization/__snapshots__/reset_session_page.test.tsx.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ResetSessionPage renders as expected 1`] = `"<html lang=\\"en\\"><head><title>Elastic</title><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.v7.light.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/node_modules/@kbn/ui-framework/dist/kui_light.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/ui/legacy_light_theme.css\\" rel=\\"stylesheet\\"/>MockedFonts<link rel=\\"alternate icon\\" type=\\"image/png\\" href=\\"/mock-server-basepath/ui/favicons/favicon.png\\"/><link rel=\\"icon\\" type=\\"image/svg+xml\\" href=\\"/mock-server-basepath/ui/favicons/favicon.svg\\"/><script src=\\"/mock-basepath/internal/security/reset_session_page.js\\"></script><meta name=\\"theme-color\\" content=\\"#ffffff\\"/><meta name=\\"color-scheme\\" content=\\"light dark\\"/></head><body><div class=\\"euiPage euiPage--grow\\" style=\\"min-height:100vh\\" data-test-subj=\\"promptPage\\"><main class=\\"euiPageBody euiPageBody--borderRadiusNone\\"><div class=\\"euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPageContent euiPageContent--verticalCenter euiPageContent--horizontalCenter\\" role=\\"main\\"><div class=\\"euiEmptyPrompt\\"><span data-euiicon-type=\\"alert\\" color=\\"danger\\"></span><div class=\\"euiSpacer euiSpacer--s\\"></div><span class=\\"euiTextColor euiTextColor--subdued\\"><h2 class=\\"euiTitle euiTitle--medium\\">You do not have permission to access the requested page</h2><div class=\\"euiSpacer euiSpacer--m\\"></div><div class=\\"euiText euiText--medium\\"><p>Either go back to the previous page or log in as a different user.</p></div></span><div class=\\"euiSpacer euiSpacer--l\\"></div><div class=\\"euiSpacer euiSpacer--s\\"></div><div class=\\"euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentCenter euiFlexGroup--directionColumn euiFlexGroup--responsive\\"><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><a class=\\"euiButton euiButton--primary euiButton--fill\\" href=\\"/path/to/logout\\" rel=\\"noreferrer\\" data-test-subj=\\"ResetSessionButton\\"><span class=\\"euiButtonContent euiButton__content\\"><span class=\\"euiButton__text\\">Log in as different user</span></span></a></div><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><button class=\\"euiButtonEmpty euiButtonEmpty--primary\\" type=\\"button\\" id=\\"goBackButton\\"><span class=\\"euiButtonContent euiButtonEmpty__content\\"><span class=\\"euiButtonEmpty__text\\">Go back</span></span></button></div></div></div></div></main></div></body></html>"`; +exports[`ResetSessionPage renders as expected 1`] = `"<html lang=\\"en\\"><head><title>Elastic</title><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/100500/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.v7.light.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/node_modules/@kbn/ui-framework/dist/kui_light.css\\" rel=\\"stylesheet\\"/><link href=\\"/mock-server-basepath/ui/legacy_light_theme.css\\" rel=\\"stylesheet\\"/>MockedFonts<link rel=\\"alternate icon\\" type=\\"image/png\\" href=\\"/mock-server-basepath/ui/favicons/favicon.png\\"/><link rel=\\"icon\\" type=\\"image/svg+xml\\" href=\\"/mock-server-basepath/ui/favicons/favicon.svg\\"/><script src=\\"/mock-basepath/internal/security/reset_session_page.js\\"></script><meta name=\\"theme-color\\" content=\\"#ffffff\\"/><meta name=\\"color-scheme\\" content=\\"light dark\\"/></head><body><div class=\\"euiPage euiPage--grow\\" style=\\"min-height:100vh\\" data-test-subj=\\"promptPage\\"><div class=\\"euiPageBody euiPageBody--borderRadiusNone\\"><div class=\\"euiPanel euiPanel--paddingLarge euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPageContent euiPageContent--verticalCenter euiPageContent--horizontalCenter\\" role=\\"main\\"><div class=\\"euiEmptyPrompt\\"><span data-euiicon-type=\\"alert\\" color=\\"danger\\"></span><div class=\\"euiSpacer euiSpacer--m\\"></div><h2 class=\\"euiTitle euiTitle--medium\\">You do not have permission to access the requested page</h2><span class=\\"euiTextColor euiTextColor--subdued\\"><div class=\\"euiSpacer euiSpacer--m\\"></div><div class=\\"euiText euiText--medium\\"><p>Either go back to the previous page or log in as a different user.</p></div></span><div class=\\"euiSpacer euiSpacer--l\\"></div><div class=\\"euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentCenter euiFlexGroup--directionColumn euiFlexGroup--responsive\\"><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><a class=\\"euiButton euiButton--primary euiButton--fill\\" href=\\"/path/to/logout\\" rel=\\"noreferrer\\" data-test-subj=\\"ResetSessionButton\\"><span class=\\"euiButtonContent euiButton__content\\"><span class=\\"euiButton__text\\">Log in as different user</span></span></a></div><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><button class=\\"euiButtonEmpty euiButtonEmpty--primary\\" type=\\"button\\" id=\\"goBackButton\\"><span class=\\"euiButtonContent euiButtonEmpty__content\\"><span class=\\"euiButtonEmpty__text\\">Go back</span></span></button></div></div></div></div></div></div></body></html>"`; diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index e65ff1afcc9c3..d59d7e7b7da4f 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -44,7 +44,8 @@ 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 GLOBAL_HEADER_HEIGHT = 96; // px +export const GLOBAL_HEADER_HEIGHT_WITH_GLOBAL_BANNER = 128; // px export const FILTERS_GLOBAL_HEIGHT = 109; // px export const FULL_SCREEN_TOGGLED_CLASS_NAME = 'fullScreenToggled'; export const NO_ALERT_INDEX = 'no-alert-index-049FC71A-4C2C-446F-9901-37XMC5024C51'; @@ -240,6 +241,7 @@ export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [ '.email', '.slack', '.pagerduty', + '.swimlane', '.webhook', '.servicenow', '.jira', diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index 99753242e7627..dfaad68e295eb 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -58,7 +58,6 @@ export interface ActivityLogActionResponse { } export type ActivityLogEntry = ActivityLogAction | ActivityLogActionResponse; export interface ActivityLog { - total: number; page: number; pageSize: number; data: ActivityLogEntry[]; diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index b20b1501eecc5..a9a81aa285af7 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -15,6 +15,7 @@ const allowedExperimentalValues = Object.freeze({ trustedAppsByPolicyEnabled: false, metricsEntitiesEnabled: false, ruleRegistryEnabled: false, + tGridEnabled: false, }); type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>; diff --git a/x-pack/plugins/security_solution/common/index.ts b/x-pack/plugins/security_solution/common/index.ts index 1fec1c76430eb..e6d7bcc9bd506 100644 --- a/x-pack/plugins/security_solution/common/index.ts +++ b/x-pack/plugins/security_solution/common/index.ts @@ -4,3 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + +export * from './types'; +export * from './search_strategy'; +export * from './utility_types'; diff --git a/x-pack/plugins/security_solution/common/search_strategy/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/common/index.ts index 4fcfbdac3c1b4..095ba4ca20afc 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/common/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/common/index.ts @@ -4,52 +4,27 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { estypes } from '@elastic/elasticsearch'; import { IEsSearchResponse } from '../../../../../../src/plugins/data/common'; +export type { + Inspect, + SortField, + TimerangeInput, + PaginationInputPaginated, + DocValueFields, + CursorType, + TotalValue, +} from '../../../../timelines/common'; +export { Direction } from '../../../../timelines/common'; export type Maybe<T> = T | null; export type SearchHit = IEsSearchResponse<object>['rawResponse']['hits']['hits'][0]; -export interface TotalValue { - value: number; - relation: string; -} - -export interface Inspect { - dsl: string[]; -} - export interface PageInfoPaginated { activePage: number; fakeTotalCount: number; showMorePagesIndicator: boolean; } - -export interface CursorType { - value?: Maybe<string>; - tiebreaker?: Maybe<string>; -} - -export enum Direction { - asc = 'asc', - desc = 'desc', -} - -export interface SortField<Field = string> { - field: Field; - direction: Direction; -} - -export interface TimerangeInput { - /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */ - interval: string; - /** The end of the timerange */ - to: string; - /** The beginning of the timerange */ - from: string; -} - export interface PaginationInput { /** The limit parameter allows you to configure the maximum amount of items to be returned */ limit: number; @@ -59,19 +34,6 @@ export interface PaginationInput { tiebreaker?: Maybe<string>; } -export interface PaginationInputPaginated { - /** The activePage parameter defines the page of results you want to fetch */ - activePage: number; - /** The cursorStart parameter defines the start of the results to be displayed */ - cursorStart: number; - /** The fakePossibleCount parameter determines the total count in order to show 5 additional pages */ - fakePossibleCount: number; - /** The querySize parameter is the number of items to be returned */ - querySize: number; -} - -export type DocValueFields = estypes.SearchDocValueField; - export interface Explanation { value: number; description: string; @@ -111,13 +73,3 @@ export interface GenericBuckets { } export type StringOrNumber = string | number; - -export interface TimerangeFilter { - range: { - [timestamp: string]: { - gte: string; - lte: string; - format: string; - }; - }; -} diff --git a/x-pack/plugins/security_solution/common/search_strategy/index.ts b/x-pack/plugins/security_solution/common/search_strategy/index.ts index 575256b991d16..e3d6736878063 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/index.ts @@ -8,3 +8,4 @@ export * from './common'; export * from './security_solution'; export * from './timeline'; +export * from './index_fields'; diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/all/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/all/index.ts index d747758640fab..4e5f8af41a2ef 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/all/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/all/index.ts @@ -5,37 +5,10 @@ * 2.0. */ -import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; -import { Ecs } from '../../../../ecs'; -import { CursorType, Inspect, Maybe, PaginationInputPaginated } from '../../../common'; -import { TimelineRequestOptionsPaginated } from '../..'; - -export interface TimelineEdges { - node: TimelineItem; - cursor: CursorType; -} - -export interface TimelineItem { - _id: string; - _index?: Maybe<string>; - data: TimelineNonEcsData[]; - ecs: Ecs; -} - -export interface TimelineNonEcsData { - field: string; - value?: Maybe<string[]>; -} - -export interface TimelineEventsAllStrategyResponse extends IEsSearchResponse { - edges: TimelineEdges[]; - totalCount: number; - pageInfo: Pick<PaginationInputPaginated, 'activePage' | 'querySize'>; - inspect?: Maybe<Inspect>; -} - -export interface TimelineEventsAllRequestOptions extends TimelineRequestOptionsPaginated { - fields: string[] | Array<{ field: string; include_unmapped: boolean }>; - fieldRequested: string[]; - language: 'eql' | 'kuery' | 'lucene'; -} +export type { + TimelineEdges, + TimelineItem, + TimelineNonEcsData, + TimelineEventsAllStrategyResponse, + TimelineEventsAllRequestOptions, +} from '../../../../../../timelines/common'; diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/common/index.ts index 4a5bd2c99a0eb..e4d2ea52ffdff 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/common/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/common/index.ts @@ -5,22 +5,8 @@ * 2.0. */ -import { Ecs } from '../../../../ecs'; -import { CursorType, Maybe } from '../../../common'; - -export interface TimelineEdges { - node: TimelineItem; - cursor: CursorType; -} - -export interface TimelineItem { - _id: string; - _index?: Maybe<string>; - data: TimelineNonEcsData[]; - ecs: Ecs; -} - -export interface TimelineNonEcsData { - field: string; - value?: Maybe<string[] | string>; -} +export type { + TimelineEdges, + TimelineItem, + TimelineNonEcsData, +} from '../../../../../../timelines/common'; diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts index 1f9820f8e5c2b..3fd13e56cc7e7 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts @@ -5,27 +5,8 @@ * 2.0. */ -import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; -import { Inspect, Maybe } from '../../../common'; -import { TimelineRequestOptionsPaginated } from '../..'; - -export interface TimelineEventsDetailsItem { - ariaRowindex?: Maybe<number>; - category?: string; - field: string; - values?: Maybe<string[]>; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - originalValue?: Maybe<any>; - isObjectArray: boolean; -} - -export interface TimelineEventsDetailsStrategyResponse extends IEsSearchResponse { - data?: Maybe<TimelineEventsDetailsItem[]>; - inspect?: Maybe<Inspect>; -} - -export interface TimelineEventsDetailsRequestOptions - extends Partial<TimelineRequestOptionsPaginated> { - indexName: string; - eventId: string; -} +export type { + TimelineEventsDetailsItem, + TimelineEventsDetailsStrategyResponse, + TimelineEventsDetailsRequestOptions, +} from '../../../../../../timelines/common'; diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/eql/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/eql/index.ts index c508876032fca..10e9bbd7670cd 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/eql/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/eql/index.ts @@ -5,43 +5,10 @@ * 2.0. */ -import { EuiComboBoxOptionOption } from '@elastic/eui'; -import { - EqlSearchStrategyRequest, - EqlSearchStrategyResponse, -} from '../../../../../../../../src/plugins/data/common'; -import { Inspect, Maybe, PaginationInputPaginated } from '../../..'; -import { TimelineEdges, TimelineEventsAllRequestOptions } from '../..'; -import { EqlSearchResponse } from '../../../../detection_engine/types'; - -export interface TimelineEqlRequestOptions - extends EqlSearchStrategyRequest, - Omit<TimelineEventsAllRequestOptions, 'params'> { - eventCategoryField?: string; - tiebreakerField?: string; - timestampField?: string; - size?: number; -} - -export interface TimelineEqlResponse extends EqlSearchStrategyResponse<EqlSearchResponse<unknown>> { - edges: TimelineEdges[]; - totalCount: number; - pageInfo: Pick<PaginationInputPaginated, 'activePage' | 'querySize'>; - inspect: Maybe<Inspect>; -} - -export interface EqlOptionsData { - keywordFields: EuiComboBoxOptionOption[]; - dateFields: EuiComboBoxOptionOption[]; - nonDateFields: EuiComboBoxOptionOption[]; -} - -export interface EqlOptionsSelected { - eventCategoryField?: string; - tiebreakerField?: string; - timestampField?: string; - query?: string; - size?: number; -} - -export type FieldsEqlOptions = keyof EqlOptionsSelected; +export type { + TimelineEqlRequestOptions, + TimelineEqlResponse, + EqlOptionsData, + EqlOptionsSelected, + FieldsEqlOptions, +} from '../../../../../../timelines/common'; diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/last_event_time/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/last_event_time/index.ts index f29dc4a3c7450..39f23a63c8afe 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/last_event_time/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/last_event_time/index.ts @@ -5,38 +5,11 @@ * 2.0. */ -import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; -import { Inspect, Maybe } from '../../../common'; -import { TimelineRequestBasicOptions } from '../..'; - -export enum LastEventIndexKey { - hostDetails = 'hostDetails', - hosts = 'hosts', - ipDetails = 'ipDetails', - network = 'network', -} - -export interface LastTimeDetails { - hostName?: Maybe<string>; - ip?: Maybe<string>; -} - -export interface TimelineEventsLastEventTimeStrategyResponse extends IEsSearchResponse { - lastSeen: Maybe<string>; - inspect?: Maybe<Inspect>; -} - -export interface TimelineKpiStrategyResponse extends IEsSearchResponse { - destinationIpCount: number; - inspect?: Maybe<Inspect>; - hostCount: number; - processCount: number; - sourceIpCount: number; - userCount: number; -} - -export interface TimelineEventsLastEventTimeRequestOptions - extends Omit<TimelineRequestBasicOptions, 'filterQuery' | 'timerange'> { - indexKey: LastEventIndexKey; - details: LastTimeDetails; -} +export { LastEventIndexKey } from '../../../../../../timelines/common'; + +export type { + LastTimeDetails, + TimelineEventsLastEventTimeStrategyResponse, + TimelineKpiStrategyResponse, + TimelineEventsLastEventTimeRequestOptions, +} from '../../../../../../timelines/common'; diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts index 9c2c23eb334a3..7064ef033fc5a 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts @@ -24,7 +24,12 @@ import { SortField, Maybe, } from '../common'; -import { DataProviderType, TimelineType, TimelineStatus } from '../../types/timeline'; +import { + DataProviderType, + TimelineType, + TimelineStatus, + RowRendererId, +} from '../../types/timeline'; export * from './events'; @@ -165,25 +170,6 @@ export interface SortTimelineInput { sortDirection?: Maybe<string>; } -export enum RowRendererId { - alerts = 'alerts', - auditd = 'auditd', - auditd_file = 'auditd_file', - library = 'library', - netflow = 'netflow', - plain = 'plain', - registry = 'registry', - suricata = 'suricata', - system = 'system', - system_dns = 'system_dns', - system_endgame_process = 'system_endgame_process', - system_file = 'system_file', - system_fim = 'system_fim', - system_security_event = 'system_security_event', - system_socket = 'system_socket', - zeek = 'zeek', -} - export interface TimelineInput { columns?: Maybe<ColumnHeaderInput[]>; dataProviders?: Maybe<DataProviderInput[]>; diff --git a/x-pack/plugins/index_management/public/application/components/page_error/index.ts b/x-pack/plugins/security_solution/common/types/index.ts similarity index 80% rename from x-pack/plugins/index_management/public/application/components/page_error/index.ts rename to x-pack/plugins/security_solution/common/types/index.ts index 040edfa362c63..9464a33082a49 100644 --- a/x-pack/plugins/index_management/public/application/components/page_error/index.ts +++ b/x-pack/plugins/security_solution/common/types/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { PageErrorForbidden } from './page_error_forbidden'; +export * from './timeline'; diff --git a/x-pack/plugins/security_solution/common/types/timeline/actions/index.ts b/x-pack/plugins/security_solution/common/types/timeline/actions/index.ts new file mode 100644 index 0000000000000..782af107417c2 --- /dev/null +++ b/x-pack/plugins/security_solution/common/types/timeline/actions/index.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 type { + ActionProps, + HeaderActionProps, + GenericActionRowCellRenderProps, + HeaderCellRender, + RowCellRender, + ControlColumnProps, +} from '../../../../../timelines/common'; diff --git a/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts b/x-pack/plugins/security_solution/common/types/timeline/cells/index.ts new file mode 100644 index 0000000000000..83b0ced332a62 --- /dev/null +++ b/x-pack/plugins/security_solution/common/types/timeline/cells/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 type { CellValueElementProps } from '../../../../../timelines/common'; diff --git a/x-pack/plugins/security_solution/common/types/timeline/columns/index.ts b/x-pack/plugins/security_solution/common/types/timeline/columns/index.ts new file mode 100644 index 0000000000000..ee4d621e35d6c --- /dev/null +++ b/x-pack/plugins/security_solution/common/types/timeline/columns/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type { + ColumnHeaderType, + ColumnId, + ColumnHeaderOptions, + ColumnRenderer, +} from '../../../../../timelines/common'; diff --git a/x-pack/plugins/security_solution/common/types/timeline/data_provider/index.ts b/x-pack/plugins/security_solution/common/types/timeline/data_provider/index.ts new file mode 100644 index 0000000000000..f363176ac0a88 --- /dev/null +++ b/x-pack/plugins/security_solution/common/types/timeline/data_provider/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 { IS_OPERATOR, EXISTS_OPERATOR } from '../../../../../timelines/common'; + +export type { + QueryOperator, + DataProviderType, + QueryMatch, + DataProvider, + DataProvidersAnd, +} from '../../../../../timelines/common'; diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 7ae52a3990ff7..05cf99195774b 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -23,6 +23,13 @@ import { FlowTarget } from '../../search_strategy/security_solution/network'; import { errorSchema } from '../../detection_engine/schemas/response/error_schema'; import { Direction, Maybe } from '../../search_strategy'; +export * from './actions'; +export * from './cells'; +export * from './columns'; +export * from './data_provider'; +export * from './rows'; +export * from './store'; + /* * ColumnHeader Types */ @@ -492,6 +499,11 @@ export type TimelineExpandedDetail = { [tab in TimelineTabs]?: TimelineExpandedDetailType; }; +export type ToggleDetailPanel = TimelineExpandedDetailType & { + tabType?: TimelineTabs; + timelineId: string; +}; + export const pageInfoTimeline = runtimeTypes.type({ pageIndex: runtimeTypes.number, pageSize: runtimeTypes.number, diff --git a/x-pack/plugins/security_solution/common/types/timeline/rows/index.ts b/x-pack/plugins/security_solution/common/types/timeline/rows/index.ts new file mode 100644 index 0000000000000..ae2d19a5e2ca8 --- /dev/null +++ b/x-pack/plugins/security_solution/common/types/timeline/rows/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export type { RowRenderer } from '../../../../../timelines/common'; diff --git a/x-pack/plugins/security_solution/common/types/timeline/store.ts b/x-pack/plugins/security_solution/common/types/timeline/store.ts new file mode 100644 index 0000000000000..01fc9db7c8e1d --- /dev/null +++ b/x-pack/plugins/security_solution/common/types/timeline/store.ts @@ -0,0 +1,97 @@ +/* + * 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 { + ColumnHeaderOptions, + ColumnId, + RowRendererId, + TimelineExpandedDetail, + TimelineTypeLiteral, +} from '.'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { Filter } from '../../../../../../src/plugins/data/public'; + +import { Direction } from '../../search_strategy'; +import { DataProvider } from './data_provider'; + +export type KueryFilterQueryKind = 'kuery' | 'lucene' | 'eql'; + +export interface KueryFilterQuery { + kind: KueryFilterQueryKind; + expression: string; +} + +export interface SerializedFilterQuery { + kuery: KueryFilterQuery | null; + serializedQuery: string; +} + +export type SortDirection = 'none' | 'asc' | 'desc' | Direction; +export interface SortColumnTimeline { + columnId: string; + columnType: string; + sortDirection: SortDirection; +} + +export interface TimelinePersistInput { + id: string; + dataProviders?: DataProvider[]; + dateRange?: { + start: string; + end: string; + }; + excludedRowRendererIds?: RowRendererId[]; + expandedDetail?: TimelineExpandedDetail; + filters?: Filter[]; + columns: ColumnHeaderOptions[]; + itemsPerPage?: number; + indexNames: string[]; + kqlQuery?: { + filterQuery: SerializedFilterQuery | null; + }; + show?: boolean; + sort?: SortColumnTimeline[]; + showCheckboxes?: boolean; + timelineType?: TimelineTypeLiteral; + templateTimelineId?: string | null; + templateTimelineVersion?: number | null; +} + +/** Invoked when a column is sorted */ +export type OnColumnSorted = (sorted: { columnId: ColumnId; sortDirection: SortDirection }) => void; + +export type OnColumnsSorted = ( + sorted: Array<{ columnId: ColumnId; sortDirection: SortDirection }> +) => void; + +export type OnColumnRemoved = (columnId: ColumnId) => void; + +export type OnColumnResized = ({ columnId, delta }: { columnId: ColumnId; delta: number }) => void; + +/** Invoked when a user clicks to load more item */ +export type OnChangePage = (nextPage: number) => void; + +/** Invoked when a user checks/un-checks a row */ +export type OnRowSelected = ({ + eventIds, + isSelected, +}: { + eventIds: string[]; + isSelected: boolean; +}) => void; + +/** Invoked when a user checks/un-checks the select all checkbox */ +export type OnSelectAll = ({ isSelected }: { isSelected: boolean }) => void; + +/** Invoked when columns are updated */ +export type OnUpdateColumns = (columns: ColumnHeaderOptions[]) => void; + +/** Invoked when a user pins an event */ +export type OnPinEvent = (eventId: string) => void; + +/** Invoked when a user unpins an event */ +export type OnUnPinEvent = (eventId: string) => void; diff --git a/x-pack/plugins/security_solution/common/utils/field_formatters.test.ts b/x-pack/plugins/security_solution/common/utils/field_formatters.test.ts index b724c0f672b50..64d4f2986903a 100644 --- a/x-pack/plugins/security_solution/common/utils/field_formatters.test.ts +++ b/x-pack/plugins/security_solution/common/utils/field_formatters.test.ts @@ -7,7 +7,7 @@ import { EventHit, EventSource } from '../search_strategy'; import { getDataFromFieldsHits, getDataFromSourceHits, getDataSafety } from './field_formatters'; -import { eventDetailsFormattedFields, eventHit } from './mock_event_details'; +import { eventDetailsFormattedFields, eventHit } from '@kbn/securitysolution-t-grid'; describe('Events Details Helpers', () => { const fields: EventHit['fields'] = eventHit.fields; diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts index f1ee0d39f545f..bf5c281a43e39 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts @@ -129,7 +129,13 @@ describe('Alerts detection rules', () => { }); it('Auto refreshes rules', () => { - cy.clock(Date.now()); + /** + * Ran into the error: timer created with setInterval() but cleared with cancelAnimationFrame() + * There are no cancelAnimationFrames in the codebase that are used to clear a setInterval so + * explicitly set the below overrides. see https://docs.cypress.io/api/commands/clock#Function-names + */ + + cy.clock(Date.now(), ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'Date']); goToManageAlertsDetectionRules(); waitForRulesTableToBeLoaded(); diff --git a/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts index 78ee3fdcdcdd5..3ff036fa0107f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/overview/overview.spec.ts @@ -45,7 +45,7 @@ describe('Overview Page', () => { describe('with no data', () => { it('Splash screen should be here', () => { - cy.stubSearchStrategyApi(emptyInstance, undefined, 'securitySolutionIndexFields'); + cy.stubSearchStrategyApi(emptyInstance, undefined, 'indexFields'); loginAndWaitForPage(OVERVIEW_URL); cy.get(OVERVIEW_EMPTY_PAGE).should('be.visible'); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/data_providers.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/data_providers.spec.ts index d42632a66eb26..a89ddf3e0b250 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/data_providers.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/data_providers.spec.ts @@ -12,6 +12,7 @@ import { TIMELINE_DATA_PROVIDERS_ACTION_MENU, IS_DRAGGING_DATA_PROVIDERS, TIMELINE_FLYOUT_HEADER, + TIMELINE_FLYOUT, } from '../../screens/timeline'; import { HOSTS_NAMES_DRAGGABLE } from '../../screens/hosts/all_hosts'; @@ -46,7 +47,7 @@ describe('timeline data providers', () => { it('renders the data provider of a host dragged from the All Hosts widget on the hosts page', () => { dragAndDropFirstHostToTimeline(); openTimelineUsingToggle(); - cy.get(TIMELINE_DROPPED_DATA_PROVIDERS) + cy.get(`${TIMELINE_FLYOUT} ${TIMELINE_DROPPED_DATA_PROVIDERS}`) .first() .invoke('text') .then((dataProviderText) => { diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts index 568fb90568fb3..b569ea7cc082f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts @@ -12,6 +12,7 @@ import { TIMELINE_EVENTS_COUNT_PER_PAGE_BTN, TIMELINE_EVENTS_COUNT_PER_PAGE_OPTION, TIMELINE_EVENTS_COUNT_PREV_PAGE, + TIMELINE_FLYOUT, } from '../../screens/timeline'; import { cleanKibana } from '../../tasks/common'; @@ -50,10 +51,10 @@ describe('Pagination', () => { it('should be able to go to next / previous page', () => { cy.intercept('POST', '/internal/bsearch').as('refetch'); - cy.get(TIMELINE_EVENTS_COUNT_NEXT_PAGE).first().click(); + cy.get(`${TIMELINE_FLYOUT} ${TIMELINE_EVENTS_COUNT_NEXT_PAGE}`).first().click(); cy.wait('@refetch').its('response.statusCode').should('eq', 200); - cy.get(TIMELINE_EVENTS_COUNT_PREV_PAGE).first().click(); + cy.get(`${TIMELINE_FLYOUT} ${TIMELINE_EVENTS_COUNT_PREV_PAGE}`).first().click(); cy.wait('@refetch').its('response.statusCode').should('eq', 200); }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index 0a9e5b44feb1f..63c4c1364fcd0 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -143,6 +143,8 @@ export const TIMELINE_CORRELATION_TAB = '[data-test-subj="timelineTabs-eql"]'; export const IS_DRAGGING_DATA_PROVIDERS = '.is-dragging'; +export const TIMELINE_BOTTOM_BAR_CONTAINER = '[data-test-subj="timeline-bottom-bar-container"]'; + export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]'; export const TIMELINE_DATA_PROVIDERS_ACTION_MENU = '[data-test-subj="providerActions"]'; @@ -175,9 +177,11 @@ export const TIMELINE_EVENTS_COUNT_PER_PAGE_BTN = '[data-test-subj="local-events export const TIMELINE_EVENTS_COUNT_PER_PAGE_OPTION = (itemsPerPage: number) => `[data-test-subj="items-per-page-option-${itemsPerPage}"]`; -export const TIMELINE_EVENTS_COUNT_NEXT_PAGE = '[data-test-subj="pagination-button-next"]'; +export const TIMELINE_EVENTS_COUNT_NEXT_PAGE = + '[data-test-subj="timeline"] [data-test-subj="pagination-button-next"]'; -export const TIMELINE_EVENTS_COUNT_PREV_PAGE = '[data-test-subj="pagination-button-previous"]'; +export const TIMELINE_EVENTS_COUNT_PREV_PAGE = + '[data-test-subj="timeline"] [data-test-subj="pagination-button-previous"]'; export const TIMELINE_FIELDS_BUTTON = '[data-test-subj="timeline"] [data-test-subj="show-field-browser"]'; @@ -234,7 +238,7 @@ export const TIMELINE_EDIT_MODAL_SAVE_BUTTON = '[data-test-subj="save-button"]'; export const TIMELINE_EXIT_FULL_SCREEN_BUTTON = '[data-test-subj="exit-full-screen"]'; -export const TIMELINE_FLYOUT_WRAPPER = '[data-test-subj="flyout-pane-wrapper"]'; +export const TIMELINE_FLYOUT_WRAPPER = '[data-test-subj="flyout-pane"]'; export const TIMELINE_FULL_SCREEN_BUTTON = '[data-test-subj="full-screen-active"]'; diff --git a/x-pack/plugins/security_solution/cypress/support/commands.js b/x-pack/plugins/security_solution/cypress/support/commands.js index 90eb9a38d7509..e74d06cd621fb 100644 --- a/x-pack/plugins/security_solution/cypress/support/commands.js +++ b/x-pack/plugins/security_solution/cypress/support/commands.js @@ -35,7 +35,7 @@ Cypress.Commands.add( 'stubSearchStrategyApi', function (stubObject, factoryQueryType, searchStrategyName = 'securitySolutionSearchStrategy') { cy.intercept('POST', '/internal/bsearch', (req) => { - if (searchStrategyName === 'securitySolutionIndexFields') { + if (searchStrategyName === 'indexFields') { req.reply(stubObject.rawResponse); } else if (factoryQueryType === 'overviewHost') { req.reply(stubObject.overviewHost); diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json index 02dbc56bd3397..e26f0d9b65bfa 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -17,6 +17,7 @@ "inspector", "licensing", "maps", + "timelines", "triggersActionsUi", "uiActions" ], diff --git a/x-pack/plugins/security_solution/public/app/404.tsx b/x-pack/plugins/security_solution/public/app/404.tsx index c21f7a4d4d578..2634ffd47bff1 100644 --- a/x-pack/plugins/security_solution/public/app/404.tsx +++ b/x-pack/plugins/security_solution/public/app/404.tsx @@ -8,15 +8,15 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { WrapperPage } from '../common/components/wrapper_page'; +import { SecuritySolutionPageWrapper } from '../common/components/page_wrapper'; export const NotFoundPage = React.memo(() => ( - <WrapperPage> + <SecuritySolutionPageWrapper> <FormattedMessage id="xpack.securitySolution.pages.fourohfour.noContentFoundDescription" defaultMessage="No content found" /> - </WrapperPage> + </SecuritySolutionPageWrapper> )); NotFoundPage.displayName = 'NotFoundPage'; diff --git a/x-pack/plugins/security_solution/public/app/app.tsx b/x-pack/plugins/security_solution/public/app/app.tsx index cfb25c4436db3..c223570c77201 100644 --- a/x-pack/plugins/security_solution/public/app/app.tsx +++ b/x-pack/plugins/security_solution/public/app/app.tsx @@ -11,7 +11,7 @@ import { Store, Action } from 'redux'; import { Provider as ReduxStoreProvider } from 'react-redux'; import { EuiErrorBoundary } from '@elastic/eui'; -import { AppLeaveHandler } from '../../../../../src/core/public'; +import { AppLeaveHandler, AppMountParameters } from '../../../../../src/core/public'; import { ManageUserInfo } from '../detections/components/user_info'; import { DEFAULT_DARK_MODE, APP_NAME } from '../../common/constants'; @@ -21,7 +21,6 @@ import { GlobalToaster, ManageGlobalToaster } from '../common/components/toaster import { KibanaContextProvider, useKibana, useUiSetting$ } from '../common/lib/kibana'; import { State } from '../common/store'; -import { ManageGlobalTimeline } from '../timelines/components/manage_timeline'; import { StartServices } from '../types'; import { PageRouter } from './routes'; import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; @@ -31,10 +30,17 @@ interface StartAppComponent { children: React.ReactNode; history: History; onAppLeave: (handler: AppLeaveHandler) => void; + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; store: Store<State, Action>; } -const StartAppComponent: FC<StartAppComponent> = ({ children, history, onAppLeave, store }) => { +const StartAppComponent: FC<StartAppComponent> = ({ + children, + history, + setHeaderActionMenu, + onAppLeave, + store, +}) => { const { i18n } = useKibana().services; const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE); @@ -42,23 +48,25 @@ const StartAppComponent: FC<StartAppComponent> = ({ children, history, onAppLeav <EuiErrorBoundary> <i18n.Context> <ManageGlobalToaster> - <ManageGlobalTimeline> - <ReduxStoreProvider store={store}> - <EuiThemeProvider darkMode={darkMode}> - <MlCapabilitiesProvider> - <UserPrivilegesProvider> - <ManageUserInfo> - <PageRouter history={history} onAppLeave={onAppLeave}> - {children} - </PageRouter> - </ManageUserInfo> - </UserPrivilegesProvider> - </MlCapabilitiesProvider> - </EuiThemeProvider> - <ErrorToastDispatcher /> - <GlobalToaster /> - </ReduxStoreProvider> - </ManageGlobalTimeline> + <ReduxStoreProvider store={store}> + <EuiThemeProvider darkMode={darkMode}> + <MlCapabilitiesProvider> + <UserPrivilegesProvider> + <ManageUserInfo> + <PageRouter + history={history} + onAppLeave={onAppLeave} + setHeaderActionMenu={setHeaderActionMenu} + > + {children} + </PageRouter> + </ManageUserInfo> + </UserPrivilegesProvider> + </MlCapabilitiesProvider> + </EuiThemeProvider> + <ErrorToastDispatcher /> + <GlobalToaster /> + </ReduxStoreProvider> </ManageGlobalToaster> </i18n.Context> </EuiErrorBoundary> @@ -72,6 +80,7 @@ interface SecurityAppComponentProps { history: History; onAppLeave: (handler: AppLeaveHandler) => void; services: StartServices; + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; store: Store<State, Action>; } @@ -80,6 +89,7 @@ const SecurityAppComponent: React.FC<SecurityAppComponentProps> = ({ history, onAppLeave, services, + setHeaderActionMenu, store, }) => ( <KibanaContextProvider @@ -88,7 +98,12 @@ const SecurityAppComponent: React.FC<SecurityAppComponentProps> = ({ ...services, }} > - <StartApp history={history} onAppLeave={onAppLeave} store={store}> + <StartApp + history={history} + onAppLeave={onAppLeave} + setHeaderActionMenu={setHeaderActionMenu} + store={store} + > {children} </StartApp> </KibanaContextProvider> diff --git a/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx b/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx new file mode 100644 index 0000000000000..98ff11423ce01 --- /dev/null +++ b/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx @@ -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 { + EuiHeaderSection, + EuiHeaderLinks, + EuiHeaderLink, + EuiHeaderSectionItem, +} from '@elastic/eui'; +import React, { useEffect, useMemo } from 'react'; +import { createPortalNode, OutPortal, InPortal } from 'react-reverse-portal'; +import { i18n } from '@kbn/i18n'; + +import { AppMountParameters } from '../../../../../../../src/core/public'; +import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; +import { MlPopover } from '../../../common/components/ml_popover/ml_popover'; +import { useKibana } from '../../../common/lib/kibana'; +import { ADD_DATA_PATH, APP_DETECTIONS_PATH } from '../../../../common/constants'; + +const BUTTON_ADD_DATA = i18n.translate('xpack.securitySolution.globalHeader.buttonAddData', { + defaultMessage: 'Add data', +}); + +/** + * This component uses the reverse portal to add the Add Data and ML job settings buttons on the + * right hand side of the Kibana global header + */ +export const GlobalHeader = React.memo( + ({ setHeaderActionMenu }: { setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'] }) => { + const portalNode = useMemo(() => createPortalNode(), []); + const { http } = useKibana().services; + + useEffect(() => { + let unmount = () => {}; + + setHeaderActionMenu((element) => { + const mount = toMountPoint(<OutPortal node={portalNode} />); + unmount = mount(element); + return unmount; + }); + + return () => { + portalNode.unmount(); + unmount(); + }; + }, [portalNode, setHeaderActionMenu]); + + return ( + <InPortal node={portalNode}> + <EuiHeaderSection side="right"> + {window.location.pathname.includes(APP_DETECTIONS_PATH) && ( + <EuiHeaderSectionItem> + <MlPopover /> + </EuiHeaderSectionItem> + )} + <EuiHeaderSectionItem> + <EuiHeaderLinks> + <EuiHeaderLink + color="primary" + data-test-subj="add-data" + href={http.basePath.prepend(ADD_DATA_PATH)} + iconType="indexOpen" + > + {BUTTON_ADD_DATA} + </EuiHeaderLink> + </EuiHeaderLinks> + </EuiHeaderSectionItem> + </EuiHeaderSection> + </InPortal> + ); + } +); +GlobalHeader.displayName = 'GlobalHeader'; diff --git a/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx b/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx index 7ebcc96753836..8358e2f9377b8 100644 --- a/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx +++ b/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import * as i18n from './translations'; +import * as i18n from '../translations'; import { SecurityPageName } from '../types'; import { SiemNavTab } from '../../common/components/navigation/types'; import { diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index 1b0ddcfb9ae7d..9a57ab3fc3a73 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -5,57 +5,35 @@ * 2.0. */ -import React, { useEffect, useRef, useState } from 'react'; -import styled from 'styled-components'; +import React, { useRef } from 'react'; -import { TimelineId } from '../../../common/types/timeline'; import { DragDropContextWrapper } from '../../common/components/drag_and_drop/drag_drop_context_wrapper'; -import { Flyout } from '../../timelines/components/flyout'; +import { AppLeaveHandler, AppMountParameters } from '../../../../../../src/core/public'; import { SecuritySolutionAppWrapper } from '../../common/components/page'; -import { HeaderGlobal } from '../../common/components/header_global'; import { HelpMenu } from '../../common/components/help_menu'; -import { AutoSaveWarningMsg } from '../../timelines/components/timeline/auto_save_warning'; import { UseUrlState } from '../../common/components/url_state'; -import { useShowTimeline } from '../../common/utils/timeline/use_show_timeline'; import { navTabs } from './home_navigations'; import { useInitSourcerer, useSourcererScope } from '../../common/containers/sourcerer'; import { useKibana } from '../../common/lib/kibana'; import { DETECTIONS_SUB_PLUGIN_ID } from '../../../common/constants'; import { SourcererScopeName } from '../../common/store/sourcerer/model'; import { useUpgradeEndpointPackage } from '../../common/hooks/endpoint/upgrade'; -import { useThrottledResizeObserver } from '../../common/components/utils'; -import { AppLeaveHandler } from '../../../../../../src/core/public'; - -const Main = styled.main.attrs<{ paddingTop: number }>(({ paddingTop }) => ({ - style: { - paddingTop: `${paddingTop}px`, - }, -}))<{ paddingTop: number }>` - overflow: auto; - display: flex; - flex-direction: column; - flex: 1 1 auto; -`; - -Main.displayName = 'Main'; +import { GlobalHeader } from './global_header'; +import { SecuritySolutionTemplateWrapper } from './template_wrapper'; interface HomePageProps { children: React.ReactNode; onAppLeave: (handler: AppLeaveHandler) => void; + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; } -const HomePageComponent: React.FC<HomePageProps> = ({ children, onAppLeave }) => { - const { application, overlays } = useKibana().services; +const HomePageComponent: React.FC<HomePageProps> = ({ + children, + onAppLeave, + setHeaderActionMenu, +}) => { + const { application } = useKibana().services; const subPluginId = useRef<string>(''); - const { ref, height = 0 } = useThrottledResizeObserver(300); - const banners$ = overlays.banners.get$(); - const [headerFixed, setHeaderFixed] = useState<boolean>(true); - const mainPaddingTop = headerFixed ? height : 0; - - useEffect(() => { - const subscription = banners$.subscribe((banners) => setHeaderFixed(!banners.length)); - return () => subscription.unsubscribe(); - }, [banners$]); // Only un/re-subscribe if the Observable changes application.currentAppId$.subscribe((appId) => { subPluginId.current = appId ?? ''; @@ -66,13 +44,13 @@ const HomePageComponent: React.FC<HomePageProps> = ({ children, onAppLeave }) => ? SourcererScopeName.detections : SourcererScopeName.default ); - const [showTimeline] = useShowTimeline(); - const { browserFields, indexPattern, indicesExist } = useSourcererScope( + const { browserFields, indexPattern } = useSourcererScope( subPluginId.current === DETECTIONS_SUB_PLUGIN_ID ? SourcererScopeName.detections : SourcererScopeName.default ); + // side effect: this will attempt to upgrade the endpoint package if it is not up to date // this will run when a user navigates to the Security Solution app and when they navigate between // tabs in the app. This is useful for keeping the endpoint package as up to date as possible until @@ -81,23 +59,14 @@ const HomePageComponent: React.FC<HomePageProps> = ({ children, onAppLeave }) => useUpgradeEndpointPackage(); return ( - <SecuritySolutionAppWrapper> - <HeaderGlobal ref={ref} isFixed={headerFixed} /> - - <Main paddingTop={mainPaddingTop} data-test-subj="pageContainer"> - <DragDropContextWrapper browserFields={browserFields}> - <UseUrlState indexPattern={indexPattern} navTabs={navTabs} /> - {indicesExist && showTimeline && ( - <> - <AutoSaveWarningMsg /> - <Flyout timelineId={TimelineId.active} onAppLeave={onAppLeave} /> - </> - )} - + <SecuritySolutionAppWrapper className="kbnAppWrapper"> + <GlobalHeader setHeaderActionMenu={setHeaderActionMenu} /> + <DragDropContextWrapper browserFields={browserFields}> + <UseUrlState indexPattern={indexPattern} navTabs={navTabs} /> + <SecuritySolutionTemplateWrapper onAppLeave={onAppLeave}> {children} - </DragDropContextWrapper> - </Main> - + </SecuritySolutionTemplateWrapper> + </DragDropContextWrapper> <HelpMenu /> </SecuritySolutionAppWrapper> ); diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx new file mode 100644 index 0000000000000..08ebbeaee55d4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable react/display-name */ + +import React, { useRef } from 'react'; +import { KibanaPageTemplateProps } from '../../../../../../../../src/plugins/kibana_react/public'; +import { AppLeaveHandler } from '../../../../../../../../src/core/public'; +import { useKibana } from '../../../../common/lib/kibana'; +import { useShowTimeline } from '../../../../common/utils/timeline/use_show_timeline'; +import { useSourcererScope } from '../../../../common/containers/sourcerer'; +import { DETECTIONS_SUB_PLUGIN_ID } from '../../../../../common/constants'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import { TimelineId } from '../../../../../common/types/timeline'; +import { AutoSaveWarningMsg } from '../../../../timelines/components/timeline/auto_save_warning'; +import { Flyout } from '../../../../timelines/components/flyout'; + +export const BOTTOM_BAR_CLASSNAME = 'timeline-bottom-bar'; + +export const SecuritySolutionBottomBar = React.memo( + ({ onAppLeave }: { onAppLeave: (handler: AppLeaveHandler) => void }) => { + const subPluginId = useRef<string>(''); + const { application } = useKibana().services; + application.currentAppId$.subscribe((appId) => { + subPluginId.current = appId ?? ''; + }); + + const [showTimeline] = useShowTimeline(); + + const { indicesExist } = useSourcererScope( + subPluginId.current === DETECTIONS_SUB_PLUGIN_ID + ? SourcererScopeName.detections + : SourcererScopeName.default + ); + + return indicesExist && showTimeline ? ( + <> + <AutoSaveWarningMsg /> + <Flyout timelineId={TimelineId.active} onAppLeave={onAppLeave} /> + </> + ) : null; + } +); + +export const SecuritySolutionBottomBarProps: KibanaPageTemplateProps['bottomBarProps'] = { + className: BOTTOM_BAR_CLASSNAME, + 'data-test-subj': 'timeline-bottom-bar-container', + position: 'fixed', + usePortal: false, +}; diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/global_kql_header/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/global_kql_header/index.tsx new file mode 100644 index 0000000000000..3e3c91133eab6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/global_kql_header/index.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import styled from 'styled-components'; +import { OutPortal } from 'react-reverse-portal'; +import { useGlobalHeaderPortal } from '../../../../common/hooks/use_global_header_portal'; + +const StyledStickyWrapper = styled.div` + position: sticky; + z-index: ${(props) => props.theme.eui.euiZLevel2}; + // TOP location is declared in src/public/rendering/_base.scss to keep in line with Kibana Chrome +`; + +export const GlobalKQLHeader = React.memo(() => { + const { globalKQLHeaderPortalNode } = useGlobalHeaderPortal(); + + return ( + <StyledStickyWrapper id="securitySolutionStickyKQL"> + <OutPortal node={globalKQLHeaderPortalNode} /> + </StyledStickyWrapper> + ); +}); + +GlobalKQLHeader.displayName = 'GlobalKQLHeader'; diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx new file mode 100644 index 0000000000000..02fd07151f111 --- /dev/null +++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx @@ -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 React, { useMemo } from 'react'; +import styled from 'styled-components'; +import { EuiPanel } from '@elastic/eui'; +import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid'; +import { AppLeaveHandler } from '../../../../../../../src/core/public'; +import { KibanaPageTemplate } from '../../../../../../../src/plugins/kibana_react/public'; +import { useSecuritySolutionNavigation } from '../../../common/components/navigation/use_security_solution_navigation'; +import { TimelineId } from '../../../../common/types/timeline'; +import { getTimelineShowStatusByIdSelector } from '../../../timelines/components/flyout/selectors'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; +import { GlobalKQLHeader } from './global_kql_header'; +import { + BOTTOM_BAR_CLASSNAME, + SecuritySolutionBottomBar, + SecuritySolutionBottomBarProps, +} from './bottom_bar'; +import { useShowTimeline } from '../../../common/utils/timeline/use_show_timeline'; +import { gutterTimeline } from '../../../common/lib/helpers'; + +/* eslint-disable react/display-name */ + +/** + * Need to apply the styles via a className to effect the containing bottom bar + * rather than applying them to the timeline bar directly + */ +const StyledKibanaPageTemplate = styled(KibanaPageTemplate)<{ + $isShowingTimelineOverlay?: boolean; + $isTimelineBottomBarVisible?: boolean; +}>` + .${BOTTOM_BAR_CLASSNAME} { + animation: 'none !important'; // disable the default bottom bar slide animation + background: ${({ theme }) => + theme.eui.euiColorEmptyShade}; // Override bottom bar black background + color: inherit; // Necessary to override the bottom bar 'white text' + transform: ${( + { $isShowingTimelineOverlay } // Since the bottom bar wraps the whole overlay now, need to override any transforms when it is open + ) => ($isShowingTimelineOverlay ? 'none' : 'translateY(calc(100% - 50px))')}; + z-index: ${({ theme }) => theme.eui.euiZLevel8}; + + .${IS_DRAGGING_CLASS_NAME} & { + // When a drag is in process the bottom flyout should slide up to allow a drop + transform: none; + } + } + + // If the bottom bar is visible add padding to the navigation + ${({ $isTimelineBottomBarVisible }) => + $isTimelineBottomBarVisible && + ` + @media (min-width: 768px) { + .kbnPageTemplateSolutionNav { + padding-bottom: ${gutterTimeline}; + } + } + `} +`; + +interface SecuritySolutionPageWrapperProps { + onAppLeave: (handler: AppLeaveHandler) => void; +} + +export const SecuritySolutionTemplateWrapper: React.FC<SecuritySolutionPageWrapperProps> = React.memo( + ({ children, onAppLeave }) => { + const solutionNav = useSecuritySolutionNavigation(); + const [isTimelineBottomBarVisible] = useShowTimeline(); + const getTimelineShowStatus = useMemo(() => getTimelineShowStatusByIdSelector(), []); + const { show: isShowingTimelineOverlay } = useDeepEqualSelector((state) => + getTimelineShowStatus(state, TimelineId.active) + ); + + return ( + <StyledKibanaPageTemplate + $isTimelineBottomBarVisible={isTimelineBottomBarVisible} + $isShowingTimelineOverlay={isShowingTimelineOverlay} + bottomBarProps={SecuritySolutionBottomBarProps} + bottomBar={<SecuritySolutionBottomBar onAppLeave={onAppLeave} />} + paddingSize="none" + solutionNav={solutionNav} + restrictWidth={false} + template="default" + > + <GlobalKQLHeader /> + <EuiPanel className="securityPageWrapper" data-test-subj="pageContainer" hasShadow={false}> + {children} + </EuiPanel> + </StyledKibanaPageTemplate> + ); + } +); diff --git a/x-pack/plugins/security_solution/public/app/index.tsx b/x-pack/plugins/security_solution/public/app/index.tsx index 1e304c2686960..194f119e35478 100644 --- a/x-pack/plugins/security_solution/public/app/index.tsx +++ b/x-pack/plugins/security_solution/public/app/index.tsx @@ -15,12 +15,19 @@ export const renderApp = ({ element, history, onAppLeave, + setHeaderActionMenu, services, store, SubPluginRoutes, }: RenderAppProps): (() => void) => { render( - <SecurityApp history={history} onAppLeave={onAppLeave} services={services} store={store}> + <SecurityApp + history={history} + onAppLeave={onAppLeave} + services={services} + setHeaderActionMenu={setHeaderActionMenu} + store={store} + > <SubPluginRoutes /> </SecurityApp>, element diff --git a/x-pack/plugins/security_solution/public/app/routes.tsx b/x-pack/plugins/security_solution/public/app/routes.tsx index 6454653af5214..a9a94a6998286 100644 --- a/x-pack/plugins/security_solution/public/app/routes.tsx +++ b/x-pack/plugins/security_solution/public/app/routes.tsx @@ -10,7 +10,7 @@ import React, { FC, memo, useEffect } from 'react'; import { Route, Router, Switch } from 'react-router-dom'; import { useDispatch } from 'react-redux'; -import { AppLeaveHandler } from '../../../../../src/core/public'; +import { AppLeaveHandler, AppMountParameters } from '../../../../../src/core/public'; import { ManageRoutesSpy } from '../common/utils/route/manage_spy_routes'; import { RouteCapture } from '../common/components/endpoint/route_capture'; import { AppAction } from '../common/store/actions'; @@ -21,9 +21,15 @@ interface RouterProps { children: React.ReactNode; history: History; onAppLeave: (handler: AppLeaveHandler) => void; + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; } -const PageRouterComponent: FC<RouterProps> = ({ children, history, onAppLeave }) => { +const PageRouterComponent: FC<RouterProps> = ({ + children, + history, + onAppLeave, + setHeaderActionMenu, +}) => { const dispatch = useDispatch<(action: AppAction) => void>(); useEffect(() => { return () => { @@ -42,7 +48,9 @@ const PageRouterComponent: FC<RouterProps> = ({ children, history, onAppLeave }) <RouteCapture> <Switch> <Route path="/"> - <HomePage onAppLeave={onAppLeave}>{children}</HomePage> + <HomePage onAppLeave={onAppLeave} setHeaderActionMenu={setHeaderActionMenu}> + {children} + </HomePage> </Route> <Route> <NotFoundPage /> diff --git a/x-pack/plugins/security_solution/public/app/home/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/app/home/translations.ts rename to x-pack/plugins/security_solution/public/app/translations.ts diff --git a/x-pack/plugins/security_solution/public/cases/components/create/flyout.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/flyout.test.tsx index caa88a8fd1c2a..d22aafa450694 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/flyout.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/flyout.test.tsx @@ -50,7 +50,7 @@ describe('CreateCaseFlyout', () => { </TestProviders> ); - wrapper.find('.euiFlyout__closeButton').first().simulate('click'); + wrapper.find(`[data-test-subj='euiFlyoutCloseButton']`).first().simulate('click'); expect(onCloseFlyout).toBeCalled(); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx b/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx index 1023bfc8b0206..f01ae342f8547 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import React, { memo } from 'react'; -import styled from 'styled-components'; +import React, { memo, ReactNode } from 'react'; +import styled, { StyledComponent } from 'styled-components'; import { EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui'; import * as i18n from '../../translations'; @@ -20,7 +20,11 @@ export interface CreateCaseModalProps { onSuccess: (theCase: Case) => Promise<void>; } -const StyledFlyout = styled(EuiFlyout)` +// TODO: EUI team follow up on complex types and styled-components `styled` +// https://github.com/elastic/eui/issues/4855 +const StyledFlyout: StyledComponent<typeof EuiFlyout, {}, { children?: ReactNode }> = styled( + EuiFlyout +)` ${({ theme }) => ` z-index: ${theme.eui.euiZModal}; `} diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx index 91fb45de04320..dfd53ae5cc0b0 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx @@ -38,7 +38,7 @@ export const Create = React.memo(() => { ); return ( - <EuiPanel> + <EuiPanel hasBorder> {cases.getCreateCase({ onCancel: handleSetIsCancel, onSuccess, diff --git a/x-pack/plugins/security_solution/public/cases/pages/case.tsx b/x-pack/plugins/security_solution/public/cases/pages/case.tsx index 647647afbe0a4..ad0176bda6905 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/case.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/case.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import { WrapperPage } from '../../common/components/wrapper_page'; +import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; import { useGetUserCasesPermissions } from '../../common/lib/kibana'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { AllCases } from '../components/all_cases'; @@ -20,9 +20,9 @@ export const CasesPage = React.memo(() => { return userPermissions == null || userPermissions?.read ? ( <> - <WrapperPage> + <SecuritySolutionPageWrapper> <AllCases userCanCrud={userPermissions?.crud ?? false} /> - </WrapperPage> + </SecuritySolutionPageWrapper> <SpyRoute pageName={SecurityPageName.case} /> </> ) : ( diff --git a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx index a086409e55df5..f6bb27b7b7104 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx @@ -10,7 +10,7 @@ import { useParams } from 'react-router-dom'; import { SecurityPageName } from '../../app/types'; import { SpyRoute } from '../../common/utils/route/spy_routes'; -import { WrapperPage } from '../../common/components/wrapper_page'; +import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; import { useGetUrlSearch } from '../../common/components/navigation/use_get_url_search'; import { useGetUserCasesPermissions, useKibana } from '../../common/lib/kibana'; import { getCaseUrl } from '../../common/components/link_to'; @@ -37,13 +37,13 @@ export const CaseDetailsPage = React.memo(() => { return caseId != null ? ( <> - <WrapperPage noPadding> + <SecuritySolutionPageWrapper noPadding> <CaseView caseId={caseId} subCaseId={subCaseId} userCanCrud={userPermissions?.crud ?? false} /> - </WrapperPage> + </SecuritySolutionPageWrapper> <SpyRoute pageName={SecurityPageName.case} /> </> ) : null; diff --git a/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx b/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx index c942065e45278..d3f235a5da7dc 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx @@ -11,7 +11,7 @@ import styled from 'styled-components'; import { SecurityPageName } from '../../app/types'; import { getCaseUrl } from '../../common/components/link_to'; import { useGetUrlSearch } from '../../common/components/navigation/use_get_url_search'; -import { WrapperPage } from '../../common/components/wrapper_page'; +import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; import { useGetUserCasesPermissions, useKibana } from '../../common/lib/kibana'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { navTabs } from '../../app/home/home_navigations'; @@ -51,7 +51,7 @@ const ConfigureCasesPageComponent: React.FC = () => { return ( <> - <WrapperPage noPadding> + <SecuritySolutionPageWrapper noPadding> <SectionWrapper> <HeaderWrapper> <CaseHeaderPage title={i18n.CONFIGURE_CASES_PAGE_TITLE} backOptions={backOptions} /> @@ -63,7 +63,7 @@ const ConfigureCasesPageComponent: React.FC = () => { owner: [APP_ID], })} </WhitePageWrapper> - </WrapperPage> + </SecuritySolutionPageWrapper> <SpyRoute pageName={SecurityPageName.case} /> </> ); diff --git a/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx b/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx index 3c5197f19eff1..6c88c4afb6395 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx @@ -10,7 +10,7 @@ import React, { useEffect, useMemo } from 'react'; import { SecurityPageName } from '../../app/types'; import { getCaseUrl } from '../../common/components/link_to'; import { useGetUrlSearch } from '../../common/components/navigation/use_get_url_search'; -import { WrapperPage } from '../../common/components/wrapper_page'; +import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; import { useGetUserCasesPermissions, useKibana } from '../../common/lib/kibana'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { navTabs } from '../../app/home/home_navigations'; @@ -45,10 +45,10 @@ export const CreateCasePage = React.memo(() => { return ( <> - <WrapperPage> + <SecuritySolutionPageWrapper> <CaseHeaderPage backOptions={backOptions} title={i18n.CREATE_TITLE} /> <Create /> - </WrapperPage> + </SecuritySolutionPageWrapper> <SpyRoute pageName={SecurityPageName.case} /> </> ); diff --git a/x-pack/plugins/security_solution/public/common/components/accessibility/index.ts b/x-pack/plugins/security_solution/public/common/components/accessibility/index.ts new file mode 100644 index 0000000000000..f05644c85e536 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/accessibility/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 './tooltip_with_keyboard_shortcut'; diff --git a/x-pack/plugins/security_solution/public/common/components/accessibility/tooltip_with_keyboard_shortcut/index.tsx b/x-pack/plugins/security_solution/public/common/components/accessibility/tooltip_with_keyboard_shortcut/index.tsx index 97922ecdc5b61..2d66b4e93e4dc 100644 --- a/x-pack/plugins/security_solution/public/common/components/accessibility/tooltip_with_keyboard_shortcut/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/accessibility/tooltip_with_keyboard_shortcut/index.tsx @@ -10,7 +10,7 @@ import React from 'react'; import * as i18n from './translations'; -interface Props { +export interface TooltipWithKeyboardShortcutProps { additionalScreenReaderOnlyContext?: string; content: React.ReactNode; shortcut: string; @@ -22,7 +22,7 @@ const TooltipWithKeyboardShortcutComponent = ({ content, shortcut, showShortcut, -}: Props) => ( +}: TooltipWithKeyboardShortcutProps) => ( <> <div data-test-subj="content">{content}</div> {additionalScreenReaderOnlyContext !== '' && ( diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx index 43d5c66655808..58cca7bcbd121 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx @@ -6,12 +6,12 @@ */ import React, { useEffect, useMemo } from 'react'; - +import { useDispatch } from 'react-redux'; +import { timelineActions } from '../../../timelines/store/timeline'; import { Filter } from '../../../../../../../src/plugins/data/public'; import { TimelineIdLiteral } from '../../../../common/types/timeline'; import { StatefulEventsViewer } from '../events_viewer'; import { alertsDefaultModel } from './default_headers'; -import { useManageTimeline } from '../../../timelines/components/manage_timeline'; import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import * as i18n from './translations'; @@ -70,22 +70,24 @@ const AlertsTableComponent: React.FC<Props> = ({ startDate, pageFilters = [], }) => { + const dispatch = useDispatch(); const alertsFilter = useMemo(() => [...defaultAlertsFilters, ...pageFilters], [pageFilters]); const { filterManager } = useKibana().services.data.query; - const { initializeTimeline } = useManageTimeline(); useEffect(() => { - initializeTimeline({ - id: timelineId, - documentType: i18n.ALERTS_DOCUMENT_TYPE, - filterManager, - defaultModel: alertsDefaultModel, - footerText: i18n.TOTAL_COUNT_OF_ALERTS, - title: i18n.ALERTS_TABLE_TITLE, - unit: i18n.UNIT, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + dispatch( + timelineActions.initializeTGridSettings({ + id: timelineId, + documentType: i18n.ALERTS_DOCUMENT_TYPE, + filterManager, + defaultColumns: alertsDefaultModel.columns, + excludedRowRendererIds: alertsDefaultModel.excludedRowRendererIds, + footerText: i18n.TOTAL_COUNT_OF_ALERTS, + title: i18n.ALERTS_TABLE_TITLE, + // TODO: avoid passing this through the store + }) + ); + }, [dispatch, filterManager, timelineId]); return ( <StatefulEventsViewer diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/default_headers.ts b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/default_headers.ts index 74ba4ec4a3be3..707a699b72e25 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/default_headers.ts +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/default_headers.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { RowRendererId } from '../../../../common/types/timeline'; +import { ColumnHeaderOptions, RowRendererId } from '../../../../common/types/timeline'; import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH, } from '../../../timelines/components/timeline/body/constants'; -import { ColumnHeaderOptions, SubsetTimelineModel } from '../../../timelines/store/timeline/model'; +import { SubsetTimelineModel } from '../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; export const alertsHeaders: ColumnHeaderOptions[] = [ diff --git a/x-pack/plugins/security_solution/public/common/components/callouts/callout_switcher.tsx b/x-pack/plugins/security_solution/public/common/components/callouts/callout_switcher.tsx index e700bb97e9893..43f10604d8582 100644 --- a/x-pack/plugins/security_solution/public/common/components/callouts/callout_switcher.tsx +++ b/x-pack/plugins/security_solution/public/common/components/callouts/callout_switcher.tsx @@ -6,6 +6,7 @@ */ import React, { FC, memo } from 'react'; +import { EuiSpacer } from '@elastic/eui'; import { CallOutMessage } from './callout_types'; import { CallOut } from './callout'; @@ -21,7 +22,12 @@ const CallOutSwitcherComponent: FC<CallOutSwitcherProps> = ({ namespace, conditi const { isVisible, dismiss } = useCallOutStorage([message], namespace); const shouldRender = condition && isVisible(message); - return shouldRender ? <CallOut message={message} onDismiss={dismiss} /> : null; + return shouldRender ? ( + <> + <CallOut message={message} onDismiss={dismiss} /> + <EuiSpacer size="l" /> + </> + ) : null; }; export const CallOutSwitcher = memo(CallOutSwitcherComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx index 544f9b1abf8f2..a01f22a0942de 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx @@ -15,6 +15,8 @@ import { TestProviders } from '../../mock'; import { MIN_LEGEND_HEIGHT, DraggableLegend } from './draggable_legend'; import { LegendItem } from './draggable_legend_item'; +jest.mock('../../lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx index 4958f6bae4a30..175239fcaebe7 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx @@ -14,6 +14,8 @@ import { TestProviders } from '../../mock'; import { DraggableLegendItem, LegendItem } from './draggable_legend_item'; +jest.mock('../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.test.tsx index dc0e24fcba8f5..bc3b9c3eaa1c6 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.test.tsx @@ -13,6 +13,8 @@ import { TestProviders } from '../../mock'; import { DragDropContextWrapper } from './drag_drop_context_wrapper'; +jest.mock('../../lib/kibana'); + describe('DragDropContextWrapper', () => { describe('rendering', () => { test('it renders against the snapshot', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx index 1073ed57dee19..1ab19c44e29b2 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx @@ -11,6 +11,7 @@ import { DropResult, DragDropContext } from 'react-beautiful-dnd'; import { useDispatch } from 'react-redux'; import { Dispatch } from 'redux'; import deepEqual from 'fast-deep-equal'; +import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid'; import { BeforeCapture } from './drag_drop_context'; import { BrowserFields } from '../../containers/source'; @@ -23,22 +24,24 @@ import { ADDED_TO_TIMELINE_MESSAGE, ADDED_TO_TIMELINE_TEMPLATE_MESSAGE, } from '../../hooks/translations'; -import { useAddToTimelineSensor } from '../../hooks/use_add_to_timeline'; import { displaySuccessToast, useStateToaster } from '../toasters'; import { TimelineId, TimelineType } from '../../../../common/types/timeline'; import { - addFieldToTimelineColumns, addProviderToTimeline, fieldWasDroppedOnTimelineColumns, - getTimelineIdFromColumnDroppableId, - IS_DRAGGING_CLASS_NAME, IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME, providerWasDroppedOnTimeline, draggableIsField, userIsReArrangingProviders, } from './helpers'; import { useDeepEqualSelector } from '../../hooks/use_selector'; +import { useKibana } from '../../lib/kibana'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; +import { + addFieldToTimelineColumns, + getTimelineIdFromColumnDroppableId, +} from '../../../../../timelines/public'; +import { alertsHeaders } from '../alerts_viewer/default_headers'; // @ts-expect-error window['__react-beautiful-dnd-disable-dev-warnings'] = true; @@ -85,6 +88,7 @@ const onDragEndHandler = ({ } else if (fieldWasDroppedOnTimelineColumns(result)) { addFieldToTimelineColumns({ browserFields, + defaultsHeader: alertsHeaders, dispatch, result, timelineId: getTimelineIdFromColumnDroppableId(result.destination?.droppableId ?? ''), @@ -92,8 +96,6 @@ const onDragEndHandler = ({ } }; -const sensors = [useAddToTimelineSensor]; - /** * DragDropContextWrapperComponent handles all drag end events */ @@ -101,7 +103,8 @@ export const DragDropContextWrapperComponent: React.FC<Props> = ({ browserFields const dispatch = useDispatch(); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const getDataProviders = useMemo(() => dragAndDropSelectors.getDataProvidersSelector(), []); - + const { timelines } = useKibana().services; + const sensors = [timelines.getUseAddToTimelineSensor()]; const { dataProviders: activeTimelineDataProviders, timelineType, diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx index 0d8011ee8b65d..bdc5545880e1c 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx @@ -17,6 +17,8 @@ import { DragDropContextWrapper } from './drag_drop_context_wrapper'; import { ConditionalPortal, DraggableWrapper, getStyle } from './draggable_wrapper'; import { useMountAppended } from '../../utils/use_mount_appended'; +jest.mock('../../lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx index 0cb030862a389..9db5b3899d8bc 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx @@ -6,6 +6,7 @@ */ import { EuiScreenReaderOnly } from '@elastic/eui'; +import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid'; import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import { Draggable, @@ -24,12 +25,12 @@ import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../../../timelines/com import { TruncatableText } from '../truncatable_text'; import { WithHoverActions } from '../with_hover_actions'; -import { useDraggableKeyboardWrapper } from './draggable_keyboard_wrapper_hook'; import { DraggableWrapperHoverContent, useGetTimelineId } from './draggable_wrapper_hover_content'; -import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME, getDraggableId, getDroppableId } from './helpers'; +import { getDraggableId, getDroppableId } from './helpers'; import { ProviderContainer } from './provider_container'; import * as i18n from './translations'; +import { useKibana } from '../../lib/kibana'; // As right now, we do not know what we want there, we will keep it as a placeholder export const DragEffects = styled.div``; @@ -142,6 +143,7 @@ const DraggableWrapperComponent: React.FC<Props> = ({ const isDisabled = dataProvider.id.includes(`-${ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID}-`); const [hoverActionsOwnFocus, setHoverActionsOwnFocus] = useState<boolean>(false); const dispatch = useDispatch(); + const { timelines } = useKibana().services; const handleClosePopOverTrigger = useCallback(() => { setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger); @@ -297,7 +299,7 @@ const DraggableWrapperComponent: React.FC<Props> = ({ setHoverActionsOwnFocus(true); }, []); - const { onBlur, onKeyDown } = useDraggableKeyboardWrapper({ + const { onBlur, onKeyDown } = timelines.getUseDraggableKeyboardWrapper()({ closePopover: handleClosePopOverTrigger, draggableId: getDraggableId(dataProvider.id), fieldName: dataProvider.queryMatch.field, diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx index 0d688bd805999..400b178c167f6 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx @@ -17,14 +17,10 @@ import { TestProviders } from '../../mock'; import { FilterManager } from '../../../../../../../src/plugins/data/public'; import { useSourcererScope } from '../../containers/sourcerer'; import { DraggableWrapperHoverContent } from './draggable_wrapper_hover_content'; -import { - ManageGlobalTimeline, - getTimelineDefaults, -} from '../../../timelines/components/manage_timeline'; import { TimelineId } from '../../../../common/types/timeline'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; jest.mock('../link_to'); - jest.mock('../../lib/kibana'); jest.mock('../../containers/sourcerer', () => { const original = jest.requireActual('../../containers/sourcerer'); @@ -42,29 +38,18 @@ jest.mock('uuid', () => { }; }); const mockStartDragToTimeline = jest.fn(); -jest.mock('../../hooks/use_add_to_timeline', () => { - const original = jest.requireActual('../../hooks/use_add_to_timeline'); +jest.mock('../../../../../timelines/public/hooks/use_add_to_timeline', () => { + const original = jest.requireActual('../../../../../timelines/public/hooks/use_add_to_timeline'); return { ...original, useAddToTimeline: () => ({ startDragToTimeline: mockStartDragToTimeline }), }; }); const mockAddFilters = jest.fn(); -const mockGetTimelineFilterManager = jest.fn().mockReturnValue({ - addFilters: mockAddFilters, -}); -jest.mock('../../../timelines/components/manage_timeline', () => { - const original = jest.requireActual('../../../timelines/components/manage_timeline'); - - return { - ...original, - useManageTimeline: () => ({ - getManageTimelineById: jest.fn().mockReturnValue({ indexToAdd: [] }), - getTimelineFilterManager: mockGetTimelineFilterManager, - isManagedTimeline: jest.fn().mockReturnValue(false), - }), - }; -}); +jest.mock('../../../common/hooks/use_selector', () => ({ + useShallowEqualSelector: jest.fn(), + useDeepEqualSelector: jest.fn(), +})); const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; const timelineId = TimelineId.active; @@ -85,6 +70,9 @@ const defaultProps = { describe('DraggableWrapperHoverContent', () => { beforeAll(() => { mockStartDragToTimeline.mockReset(); + (useDeepEqualSelector as jest.Mock).mockReturnValue({ + filterManager: { addFilters: mockAddFilters }, + }); (useSourcererScope as jest.Mock).mockReturnValue({ browserFields: mockBrowserFields, selectedPatterns: [], @@ -144,15 +132,10 @@ describe('DraggableWrapperHoverContent', () => { beforeEach(() => { onFilterAdded = jest.fn(); - const manageTimelineForTesting = { - [timelineId]: getTimelineDefaults(timelineId), - }; wrapper = mount( <TestProviders> - <ManageGlobalTimeline manageTimelineForTesting={manageTimelineForTesting}> - <DraggableWrapperHoverContent {...{ ...defaultProps, onFilterAdded }} /> - </ManageGlobalTimeline> + <DraggableWrapperHoverContent {...{ ...defaultProps, onFilterAdded }} /> </TestProviders> ); }); @@ -237,18 +220,9 @@ describe('DraggableWrapperHoverContent', () => { filterManager.addFilters = jest.fn(); onFilterAdded = jest.fn(); - const manageTimelineForTesting = { - [timelineId]: { - ...getTimelineDefaults(timelineId), - filterManager, - }, - }; - wrapper = mount( <TestProviders> - <ManageGlobalTimeline manageTimelineForTesting={manageTimelineForTesting}> - <DraggableWrapperHoverContent {...{ ...defaultProps, onFilterAdded, value: '' }} /> - </ManageGlobalTimeline> + <DraggableWrapperHoverContent {...{ ...defaultProps, onFilterAdded, value: '' }} /> </TestProviders> ); }); @@ -586,39 +560,4 @@ describe('DraggableWrapperHoverContent', () => { expect(wrapper.find(`[data-test-subj="copy-to-clipboard"]`).first().exists()).toBe(false); }); }); - - describe('Filter Manager', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - test('filter manager, not active timeline', () => { - mount( - <TestProviders> - <DraggableWrapperHoverContent {...{ ...defaultProps, timelineId: TimelineId.test }} /> - </TestProviders> - ); - - expect(mockGetTimelineFilterManager).not.toBeCalled(); - }); - test('filter manager, active timeline', () => { - mount( - <TestProviders> - <DraggableWrapperHoverContent {...defaultProps} /> - </TestProviders> - ); - - expect(mockGetTimelineFilterManager).toBeCalled(); - }); - test('filter manager, active timeline in draggableId', () => { - mount( - <TestProviders> - <DraggableWrapperHoverContent - {...{ ...defaultProps, draggableId: `blahblah-${TimelineId.active}-lala` }} - /> - </TestProviders> - ); - - expect(mockGetTimelineFilterManager).toBeCalled(); - }); - }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx index 880f0b4e18aca..71c3114015a03 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx @@ -12,14 +12,12 @@ import { EuiScreenReaderOnly, EuiToolTip, } from '@elastic/eui'; + import React, { useCallback, useEffect, useRef, useMemo, useState } from 'react'; import { DraggableId } from 'react-beautiful-dnd'; import styled from 'styled-components'; -import { stopPropagationAndPreventDefault } from '../accessibility/helpers'; -import { TooltipWithKeyboardShortcut } from '../accessibility/tooltip_with_keyboard_shortcut'; import { getAllFieldsByName } from '../../containers/source'; -import { useAddToTimeline } from '../../hooks/use_add_to_timeline'; import { COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME } from '../../lib/clipboard/clipboard'; import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; import { useKibana } from '../../lib/kibana'; @@ -28,11 +26,14 @@ import { StatefulTopN } from '../top_n'; import { allowTopN } from './helpers'; import * as i18n from './translations'; -import { useManageTimeline } from '../../../timelines/components/manage_timeline'; +import { useDeepEqualSelector } from '../../hooks/use_selector'; import { TimelineId } from '../../../../common/types/timeline'; import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { useSourcererScope } from '../../containers/sourcerer'; +import { timelineSelectors } from '../../../timelines/store/timeline'; +import { stopPropagationAndPreventDefault } from '../../../../../timelines/public'; +import { TooltipWithKeyboardShortcut } from '../accessibility'; export const AdditionalContent = styled.div` padding: 2px; @@ -102,21 +103,25 @@ const DraggableWrapperHoverContentComponent: React.FC<Props> = ({ toggleTopN, value, }) => { - const { startDragToTimeline } = useAddToTimeline({ draggableId, fieldName: field }); const kibana = useKibana(); + const { timelines } = kibana.services; + const { startDragToTimeline } = timelines.getUseAddToTimeline()({ + draggableId, + fieldName: field, + }); const filterManagerBackup = useMemo(() => kibana.services.data.query.filterManager, [ kibana.services.data.query.filterManager, ]); - const { getTimelineFilterManager } = useManageTimeline(); + const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []); + const { filterManager: activeFilterMananager } = useDeepEqualSelector((state) => + getManageTimeline(state, timelineId ?? '') + ); const defaultFocusedButtonRef = useRef<HTMLButtonElement | null>(null); const panelRef = useRef<HTMLDivElement | null>(null); const filterManager = useMemo( - () => - timelineId === TimelineId.active - ? getTimelineFilterManager(TimelineId.active) - : filterManagerBackup, - [timelineId, getTimelineFilterManager, filterManagerBackup] + () => (timelineId === TimelineId.active ? activeFilterMananager : filterManagerBackup), + [timelineId, activeFilterMananager, filterManagerBackup] ); // Regarding data from useManageTimeline: diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.test.tsx index 42f70e9d296b3..73a732b5d6458 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.test.tsx @@ -15,6 +15,8 @@ import { DragDropContextWrapper } from './drag_drop_context_wrapper'; import { DroppableWrapper } from './droppable_wrapper'; import { useMountAppended } from '../../utils/use_mount_appended'; +jest.mock('../../lib/kibana'); + describe('DroppableWrapper', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.test.ts b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.test.ts index 58d2e0e7dc70f..a14a44cd9a68b 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.test.ts @@ -7,6 +7,7 @@ import { omit } from 'lodash/fp'; import { DropResult } from 'react-beautiful-dnd'; +import { getTimelineIdFromColumnDroppableId } from '../../../../../timelines/public'; import { IdToDataProvider } from '../../store/drag_and_drop/model'; @@ -33,7 +34,6 @@ import { getDroppableId, getFieldIdFromDraggable, getProviderIdFromDraggable, - getTimelineIdFromColumnDroppableId, getTimelineProviderDraggableId, getTimelineProviderDroppableId, providerWasDroppedOnTimeline, diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts index e2e506e6e1a3f..9717e1e1eda91 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/helpers.ts @@ -4,138 +4,53 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { isString } from 'lodash/fp'; -import { DropResult, FluidDragActions, Position } from 'react-beautiful-dnd'; +import { DropResult } from 'react-beautiful-dnd'; import { Dispatch } from 'redux'; import { ActionCreator } from 'typescript-fsa'; +import { getProviderIdFromDraggable } from '@kbn/securitysolution-t-grid'; -import { stopPropagationAndPreventDefault } from '../accessibility/helpers'; -import { alertsHeaders } from '../alerts_viewer/default_headers'; -import { BrowserField, BrowserFields, getAllFieldsByName } from '../../containers/source'; +import { BrowserField } from '../../containers/source'; import { dragAndDropActions } from '../../store/actions'; import { IdToDataProvider } from '../../store/drag_and_drop/model'; -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; -import { timelineActions } from '../../../timelines/store/timeline'; -import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants'; import { addContentToTimeline } from '../../../timelines/components/timeline/data_providers/helpers'; import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; -import { TimelineId } from '../../../../common/types/timeline'; - -export const draggableIdPrefix = 'draggableId'; - -export const droppableIdPrefix = 'droppableId'; - -export const draggableContentPrefix = `${draggableIdPrefix}.content.`; - -export const draggableTimelineProvidersPrefix = `${draggableIdPrefix}.timelineProviders.`; - -export const draggableFieldPrefix = `${draggableIdPrefix}.field.`; - -export const droppableContentPrefix = `${droppableIdPrefix}.content.`; - -export const droppableFieldPrefix = `${droppableIdPrefix}.field.`; - -export const droppableTimelineProvidersPrefix = `${droppableIdPrefix}.timelineProviders.`; - -export const droppableTimelineColumnsPrefix = `${droppableIdPrefix}.timelineColumns.`; - -export const droppableTimelineFlyoutBottomBarPrefix = `${droppableIdPrefix}.flyoutButton.`; - -export const getDraggableId = (dataProviderId: string): string => - `${draggableContentPrefix}${dataProviderId}`; - -export const getDraggableFieldId = ({ - contextId, - fieldId, -}: { - contextId: string; - fieldId: string; -}): string => `${draggableFieldPrefix}${escapeContextId(contextId)}.${escapeFieldId(fieldId)}`; - -export const getTimelineProviderDroppableId = ({ - groupIndex, - timelineId, -}: { - groupIndex: number; - timelineId: string; -}): string => `${droppableTimelineProvidersPrefix}${timelineId}.group.${groupIndex}`; - -export const getTimelineProviderDraggableId = ({ - dataProviderId, - groupIndex, - timelineId, -}: { - dataProviderId: string; - groupIndex: number; - timelineId: string; -}): string => - `${draggableTimelineProvidersPrefix}${timelineId}.group.${groupIndex}.${dataProviderId}`; - -export const getDroppableId = (visualizationPlaceholderId: string): string => - `${droppableContentPrefix}${visualizationPlaceholderId}`; - -export const sourceIsContent = (result: DropResult): boolean => - result.source.droppableId.startsWith(droppableContentPrefix); - -export const sourceAndDestinationAreSameTimelineProviders = (result: DropResult): boolean => { - const regex = /^droppableId\.timelineProviders\.(\S+)\./; - const sourceMatches = result.source.droppableId.match(regex) ?? []; - const destinationMatches = result.destination?.droppableId.match(regex) ?? []; - - return ( - sourceMatches.length >= 2 && - destinationMatches.length >= 2 && - sourceMatches[1] === destinationMatches[1] - ); -}; - -export const draggableIsContent = (result: DropResult | { draggableId: string }): boolean => - result.draggableId.startsWith(draggableContentPrefix); - -export const draggableIsField = (result: DropResult | { draggableId: string }): boolean => - result.draggableId.startsWith(draggableFieldPrefix); - -export const reasonIsDrop = (result: DropResult): boolean => result.reason === 'DROP'; - -export const destinationIsTimelineProviders = (result: DropResult): boolean => - result.destination != null && - result.destination.droppableId.startsWith(droppableTimelineProvidersPrefix); - -export const destinationIsTimelineColumns = (result: DropResult): boolean => - result.destination != null && - result.destination.droppableId.startsWith(droppableTimelineColumnsPrefix); - -export const destinationIsTimelineButton = (result: DropResult): boolean => - result.destination != null && - result.destination.droppableId.startsWith(droppableTimelineFlyoutBottomBarPrefix); - -export const getProviderIdFromDraggable = (result: DropResult): string => - result.draggableId.substring(result.draggableId.lastIndexOf('.') + 1); - -export const getFieldIdFromDraggable = (result: DropResult): string => - unEscapeFieldId(result.draggableId.substring(result.draggableId.lastIndexOf('.') + 1)); - -export const escapeDataProviderId = (path: string) => path.replace(/\./g, '_'); - -export const escapeContextId = (path: string) => path.replace(/\./g, '_'); - -export const escapeFieldId = (path: string) => path.replace(/\./g, '!!!DOT!!!'); - -export const unEscapeFieldId = (path: string) => path.replace(/!!!DOT!!!/g, '.'); - -export const providerWasDroppedOnTimeline = (result: DropResult): boolean => - reasonIsDrop(result) && - draggableIsContent(result) && - sourceIsContent(result) && - destinationIsTimelineProviders(result); - -export const userIsReArrangingProviders = (result: DropResult): boolean => - reasonIsDrop(result) && sourceAndDestinationAreSameTimelineProviders(result); - -export const fieldWasDroppedOnTimelineColumns = (result: DropResult): boolean => - reasonIsDrop(result) && draggableIsField(result) && destinationIsTimelineColumns(result); +export { + draggableIdPrefix, + droppableIdPrefix, + draggableContentPrefix, + draggableTimelineProvidersPrefix, + draggableFieldPrefix, + draggableIsField, + droppableContentPrefix, + droppableFieldPrefix, + droppableTimelineProvidersPrefix, + droppableTimelineColumnsPrefix, + droppableTimelineFlyoutBottomBarPrefix, + getDraggableId, + getDraggableFieldId, + getTimelineProviderDroppableId, + getTimelineProviderDraggableId, + getDroppableId, + sourceIsContent, + sourceAndDestinationAreSameTimelineProviders, + draggableIsContent, + reasonIsDrop, + destinationIsTimelineProviders, + destinationIsTimelineColumns, + destinationIsTimelineButton, + getProviderIdFromDraggable, + getFieldIdFromDraggable, + escapeDataProviderId, + escapeContextId, + escapeFieldId, + unEscapeFieldId, + providerWasDroppedOnTimeline, + userIsReArrangingProviders, + fieldWasDroppedOnTimelineColumns, + DRAG_TYPE_FIELD, + IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME, +} from '@kbn/securitysolution-t-grid'; interface AddProviderToTimelineParams { activeTimelineDataProviders: DataProvider[]; dataProviders: IdToDataProvider; @@ -148,18 +63,6 @@ interface AddProviderToTimelineParams { timelineId: string; } -interface AddFieldToTimelineColumnsParams { - upsertColumn?: ActionCreator<{ - column: ColumnHeaderOptions; - id: string; - index: number; - }>; - browserFields: BrowserFields; - dispatch: Dispatch; - result: DropResult; - timelineId: string; -} - export const addProviderToTimeline = ({ activeTimelineDataProviders, dataProviders, @@ -186,73 +89,6 @@ export const addProviderToTimeline = ({ } }; -const linkFields: Record<string, string> = { - 'signal.rule.name': 'signal.rule.id', - 'event.module': 'rule.reference', -}; - -export const addFieldToTimelineColumns = ({ - upsertColumn = timelineActions.upsertColumn, - browserFields, - dispatch, - result, - timelineId, -}: AddFieldToTimelineColumnsParams): void => { - const fieldId = getFieldIdFromDraggable(result); - const allColumns = getAllFieldsByName(browserFields); - const column = allColumns[fieldId]; - const initColumnHeader = - timelineId === TimelineId.detectionsPage || timelineId === TimelineId.detectionsRulesDetailsPage - ? alertsHeaders.find((c) => c.id === fieldId) ?? {} - : {}; - - if (column != null) { - dispatch( - upsertColumn({ - column: { - category: column.category, - columnHeaderType: 'not-filtered', - description: isString(column.description) ? column.description : undefined, - example: isString(column.example) ? column.example : undefined, - id: fieldId, - linkField: linkFields[fieldId] ?? undefined, - type: column.type, - aggregatable: column.aggregatable, - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - ...initColumnHeader, - }, - id: timelineId, - index: result.destination != null ? result.destination.index : 0, - }) - ); - } else { - // create a column definition, because it doesn't exist in the browserFields: - dispatch( - upsertColumn({ - column: { - columnHeaderType: 'not-filtered', - id: fieldId, - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - }, - id: timelineId, - index: result.destination != null ? result.destination.index : 0, - }) - ); - } -}; - -/** - * Prevents fields from being dragged or dropped to any area other than column - * header drop zone in the timeline - */ -export const DRAG_TYPE_FIELD = 'drag-type-field'; - -/** This class is added to the document body while dragging */ -export const IS_DRAGGING_CLASS_NAME = 'is-dragging'; - -/** This class is added to the document body while timeline field dragging */ -export const IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME = 'is-timeline-field-dragging'; - export const allowTopN = ({ browserField, fieldName, @@ -347,125 +183,3 @@ export const allowTopN = ({ return isAllowlistedNonBrowserField || (isAggregatable && isAllowedType); }; - -export const getTimelineIdFromColumnDroppableId = (droppableId: string) => - droppableId.slice(droppableId.lastIndexOf('.') + 1); - -/** The draggable will move this many pixes via the keyboard when the arrow key is pressed */ -export const KEYBOARD_DRAG_OFFSET = 20; - -export const DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME = 'draggable-keyboard-wrapper'; - -/** - * Temporarily disables tab focus on child links of the draggable to work - * around an issue where tab focus becomes stuck on the interactive children - * - * NOTE: This function is (intentionally) only effective when used in a key - * event handler, because it automatically restores focus capabilities on - * the next tick. - */ -export const temporarilyDisableInteractiveChildTabIndexes = (draggableElement: HTMLDivElement) => { - const interactiveChildren = draggableElement.querySelectorAll('a, button'); - interactiveChildren.forEach((interactiveChild) => { - interactiveChild.setAttribute('tabindex', '-1'); // DOM mutation - }); - - // restore the default tabindexs on the next tick: - setTimeout(() => { - interactiveChildren.forEach((interactiveChild) => { - interactiveChild.setAttribute('tabindex', '0'); // DOM mutation - }); - }, 0); -}; - -export const draggableKeyDownHandler = ({ - beginDrag, - cancelDragActions, - closePopover, - draggableElement, - dragActions, - dragToLocation, - endDrag, - keyboardEvent, - openPopover, - setDragActions, -}: { - beginDrag: () => FluidDragActions | null; - cancelDragActions: () => void; - closePopover?: () => void; - draggableElement: HTMLDivElement; - dragActions: FluidDragActions | null; - dragToLocation: ({ - // eslint-disable-next-line @typescript-eslint/no-shadow - dragActions, - position, - }: { - dragActions: FluidDragActions | null; - position: Position; - }) => void; - keyboardEvent: React.KeyboardEvent; - endDrag: (dragActions: FluidDragActions | null) => void; - openPopover?: () => void; - setDragActions: (value: React.SetStateAction<FluidDragActions | null>) => void; -}) => { - let currentPosition: DOMRect | null = null; - - switch (keyboardEvent.key) { - case ' ': - if (!dragActions) { - // start dragging, because space was pressed - if (closePopover != null) { - closePopover(); - } - setDragActions(beginDrag()); - } else { - // end dragging, because space was pressed - endDrag(dragActions); - setDragActions(null); - } - break; - case 'Escape': - cancelDragActions(); - break; - case 'Tab': - // IMPORTANT: we do NOT want to stop propagation and prevent default when Tab is pressed - temporarilyDisableInteractiveChildTabIndexes(draggableElement); - break; - case 'ArrowUp': - currentPosition = draggableElement.getBoundingClientRect(); - dragToLocation({ - dragActions, - position: { x: currentPosition.x, y: currentPosition.y - KEYBOARD_DRAG_OFFSET }, - }); - break; - case 'ArrowDown': - currentPosition = draggableElement.getBoundingClientRect(); - dragToLocation({ - dragActions, - position: { x: currentPosition.x, y: currentPosition.y + KEYBOARD_DRAG_OFFSET }, - }); - break; - case 'ArrowLeft': - currentPosition = draggableElement.getBoundingClientRect(); - dragToLocation({ - dragActions, - position: { x: currentPosition.x - KEYBOARD_DRAG_OFFSET, y: currentPosition.y }, - }); - break; - case 'ArrowRight': - currentPosition = draggableElement.getBoundingClientRect(); - dragToLocation({ - dragActions, - position: { x: currentPosition.x + KEYBOARD_DRAG_OFFSET, y: currentPosition.y }, - }); - break; - case 'Enter': - stopPropagationAndPreventDefault(keyboardEvent); // prevents the first item in the popover from getting an errant ENTER - if (!dragActions && openPopover != null) { - openPopover(); - } - break; - default: - break; - } -}; diff --git a/x-pack/plugins/security_solution/public/common/components/draggables/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/draggables/index.test.tsx index 9c6b8c485986e..f77bf0f347f79 100644 --- a/x-pack/plugins/security_solution/public/common/components/draggables/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/draggables/index.test.tsx @@ -21,6 +21,8 @@ import { tooltipContentIsExplicitlyNull, } from '.'; +jest.mock('../../lib/kibana'); + describe('draggables', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_form.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_form.tsx index a66d1d05025cb..2998b96fcf6ee 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_form.tsx +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_form.tsx @@ -11,10 +11,10 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, - EuiSpacer, + EuiForm, + EuiFormRow, EuiText, EuiTextArea, - EuiTitle, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { CANCEL, COMMENT, COMMENT_PLACEHOLDER, CONFIRM } from './translations'; @@ -41,56 +41,62 @@ export const EndpointIsolateForm = memo<EndpointIsolatedFormProps>( ); return ( - <> - <EuiText size="s"> - <p> - <FormattedMessage - id="xpack.securitySolution.endpoint.hostIsolation.isolateThisHost" - defaultMessage="Isolate host {hostName} from network." - values={{ hostName: <b>{hostName}</b> }} - />{' '} - {messageAppend} - </p> - </EuiText> + <EuiForm> + <EuiFormRow fullWidth> + <EuiText size="s"> + <p> + <FormattedMessage + id="xpack.securitySolution.endpoint.hostIsolation.isolateThisHost" + defaultMessage="Isolate host {hostName} from network." + values={{ hostName: <b>{hostName}</b> }} + /> + <br /> + </p> + <p> + <FormattedMessage + id="xpack.securitySolution.endpoint.hostIsolation.isolateThisHostAbout" + defaultMessage="Isolating a host will disconnect it from the network. The host will only be able to communicate with the Kibana platform." + />{' '} + {messageAppend} + </p> + </EuiText> + </EuiFormRow> - <EuiSpacer size="m" /> + <EuiFormRow label={COMMENT} fullWidth> + <EuiTextArea + data-test-subj="host_isolation_comment" + fullWidth + placeholder={COMMENT_PLACEHOLDER} + value={comment} + onChange={handleCommentChange} + /> + </EuiFormRow> - <EuiTitle size="xs"> - <h4>{COMMENT}</h4> - </EuiTitle> - <EuiTextArea - data-test-subj="host_isolation_comment" - fullWidth - placeholder={COMMENT_PLACEHOLDER} - value={comment} - onChange={handleCommentChange} - /> - - <EuiSpacer size="m" /> - - <EuiFlexGroup justifyContent="flexEnd"> - <EuiFlexItem grow={false}> - <EuiButtonEmpty - onClick={onCancel} - disabled={isLoading} - data-test-subj="hostIsolateCancelButton" - > - {CANCEL} - </EuiButtonEmpty> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButton - fill - onClick={onConfirm} - disabled={isLoading} - isLoading={isLoading} - data-test-subj="hostIsolateConfirmButton" - > - {CONFIRM} - </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - </> + <EuiFormRow fullWidth> + <EuiFlexGroup justifyContent="flexEnd"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty + onClick={onCancel} + disabled={isLoading} + data-test-subj="hostIsolateCancelButton" + > + {CANCEL} + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + fill + onClick={onConfirm} + disabled={isLoading} + isLoading={isLoading} + data-test-subj="hostIsolateConfirmButton" + > + {CONFIRM} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFormRow> + </EuiForm> ); } ); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx index b8f29996d603b..c782804b0592b 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx @@ -17,6 +17,8 @@ import { TestProviders } from '../../mock'; import { mockBrowserFields } from '../../containers/source/mock'; import { useMountAppended } from '../../utils/use_mount_appended'; +jest.mock('../../lib/kibana'); + jest.mock('../../../detections/containers/detection_engine/rules/use_rule_async', () => { return { useRuleAsync: jest.fn(), 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 204b8c088304b..1be05cc560552 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 @@ -21,9 +21,8 @@ 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 { onFocusReFocusDraggable } from '../../../../../timelines/public'; import { BrowserFields } from '../../containers/source'; -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { DragEffects } from '../drag_and_drop/draggable_wrapper'; import { DroppableWrapper } from '../drag_and_drop/droppable_wrapper'; import { DRAG_TYPE_FIELD, getDroppableId } from '../drag_and_drop/helpers'; @@ -38,6 +37,7 @@ import { OnUpdateColumns } from '../../../timelines/components/timeline/events'; import { getIconFromType, getExampleText } from './helpers'; import * as i18n from './translations'; import { EventFieldsData } from './types'; +import { ColumnHeaderOptions } from '../../../../common'; const HoverActionsContainer = styled(EuiPanel)` align-items: center; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx index 0c7515fe75d86..6aff259d8220e 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx @@ -20,6 +20,8 @@ import { mockAlertDetailsData } from './__mocks__'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; import { TimelineTabs } from '../../../../common/types/timeline'; +jest.mock('../../../common/lib/kibana'); + jest.mock('../link_to'); describe('EventDetails', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx index f0865e1b8e083..555b67da953d6 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx @@ -16,6 +16,8 @@ import { mockBrowserFields } from '../../containers/source/mock'; import { useMountAppended } from '../../utils/use_mount_appended'; import { TimelineTabs } from '../../../../common/types/timeline'; +jest.mock('../../lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx index 93d0e6ccfbe3c..3ad7e9aef19dc 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx @@ -11,26 +11,24 @@ import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import { useDispatch } from 'react-redux'; import { rgba } from 'polished'; import styled from 'styled-components'; - import { arrayIndexToAriaIndex, DATA_COLINDEX_ATTRIBUTE, DATA_ROWINDEX_ATTRIBUTE, isTab, onKeyDownFocusHandler, -} from '../accessibility/helpers'; +} from '../../../../../timelines/public'; + import { ADD_TIMELINE_BUTTON_CLASS_NAME } from '../../../timelines/components/flyout/add_timeline_button'; import { timelineActions, timelineSelectors } from '../../../timelines/store/timeline'; -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { BrowserFields, getAllFieldsByName } from '../../containers/source'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline'; import { getColumnHeaders } from '../../../timelines/components/timeline/body/column_headers/helpers'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; - import { getColumns } from './columns'; import { EVENT_FIELDS_TABLE_CLASS_NAME, onEventDetailsTabKeyPressed, search } from './helpers'; import { useDeepEqualSelector } from '../../hooks/use_selector'; -import { TimelineTabs } from '../../../../common/types/timeline'; +import { ColumnHeaderOptions, TimelineTabs } from '../../../../common/types/timeline'; interface Props { browserFields: BrowserFields; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx index 1f12c2de5e24f..8392be420a2c5 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx @@ -15,15 +15,15 @@ import { getTableSkipFocus, handleSkipFocus, stopPropagationAndPreventDefault, -} from '../accessibility/helpers'; +} from '../../../../../timelines/public'; import { BrowserField, BrowserFields } from '../../containers/source'; -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { DEFAULT_DATE_COLUMN_MIN_WIDTH, DEFAULT_COLUMN_MIN_WIDTH, } from '../../../timelines/components/timeline/body/constants'; import * as i18n from './translations'; +import { ColumnHeaderOptions } from '../../../../common'; /** * Defines the behavior of the search input that appears above the table of data diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/default_headers.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/default_headers.tsx index 7c84a325cb667..5051b39fe6093 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/default_headers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/default_headers.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; +import { ColumnHeaderOptions } from '../../../../common'; import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; import { DEFAULT_COLUMN_MIN_WIDTH, diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index 36986f5f8d353..90a4e67d76b99 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -21,9 +21,8 @@ import { mockBrowserFields, mockDocValueFields } from '../../containers/source/m import { eventsDefaultModel } from './default_model'; import { useMountAppended } from '../../utils/use_mount_appended'; import { inputsModel } from '../../store/inputs'; -import { TimelineId } from '../../../../common/types/timeline'; +import { TimelineId, SortDirection } from '../../../../common/types/timeline'; import { KqlMode } from '../../../timelines/store/timeline/model'; -import { SortDirection } from '../../../timelines/components/timeline/body/sort'; import { AlertsTableFilterGroup } from '../../../detections/components/alerts_table/alerts_filter_group'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; @@ -31,6 +30,8 @@ import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell import { useTimelineEvents } from '../../../timelines/containers'; import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; +jest.mock('../../lib/kibana'); + jest.mock('../../hooks/use_experimental_features'); const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; @@ -144,18 +145,18 @@ describe('EventsViewer', () => { mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponseWithEvents]); }); - test('call the right reduce action to show event details', async () => { + test('call the right reduce action to show event details', () => { const wrapper = mount( <TestProviders> <StatefulEventsViewer {...testProps} /> </TestProviders> ); - await act(async () => { + act(() => { wrapper.find(`[data-test-subj="expand-event"]`).first().simulate('click'); }); - await waitFor(() => { + waitFor(() => { expect(mockDispatch).toBeCalledTimes(2); expect(mockDispatch.mock.calls[1][0]).toEqual({ payload: { @@ -197,7 +198,7 @@ describe('EventsViewer', () => { ); expect(wrapper.find(`[data-test-subj="show-field-browser"]`).first().exists()).toBe(true); }); - // TO DO sourcerer @X + test('it renders the footer containing the pagination', () => { const wrapper = mount( <TestProviders> 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 c99275ec49ab3..5dadd740ae3bc 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 @@ -10,11 +10,12 @@ import React, { useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; +import { useDispatch } from 'react-redux'; import { Direction } from '../../../../common/search_strategy'; import { BrowserFields, DocValueFields } from '../../containers/source'; import { useTimelineEvents } from '../../../timelines/containers'; import { useKibana } from '../../lib/kibana'; -import { ColumnHeaderOptions, KqlMode } from '../../../timelines/store/timeline/model'; +import { KqlMode } from '../../../timelines/store/timeline/model'; import { HeaderSection } from '../header_section'; import { defaultHeaders } from '../../../timelines/components/timeline/body/column_headers/default_headers'; import { Sort } from '../../../timelines/components/timeline/body/sort'; @@ -36,18 +37,21 @@ import { Query, } from '../../../../../../../src/plugins/data/public'; import { inputsModel } from '../../store'; -import { useManageTimeline } from '../../../timelines/components/manage_timeline'; import { ExitFullScreen } from '../exit_full_screen'; import { useGlobalFullScreen } from '../../containers/use_full_screen'; -import { TimelineId, TimelineTabs } from '../../../../common/types/timeline'; -import { RowRenderer } from '../../../timelines/components/timeline/body/renderers/row_renderer'; +import { + ColumnHeaderOptions, + ControlColumnProps, + RowRenderer, + TimelineId, + TimelineTabs, +} from '../../../../common/types/timeline'; 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'; +import { defaultControlColumn } from '../../../timelines/components/timeline/body/control_columns'; +import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline'; +import { useDeepEqualSelector } from '../../hooks/use_selector'; export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px const UTILITY_BAR_HEIGHT = 19; // px @@ -162,21 +166,19 @@ const EventsViewerComponent: React.FC<Props> = ({ utilityBar, graphEventId, }) => { + const dispatch = useDispatch(); const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; const kibana = useKibana(); const [isQueryLoading, setIsQueryLoading] = useState(false); - const { getManageTimelineById, setIsTimelineLoading } = useManageTimeline(); - useEffect(() => { - setIsTimelineLoading({ id, isLoading: isQueryLoading }); - }, [id, isQueryLoading, setIsTimelineLoading]); + dispatch(timelineActions.updateIsLoading({ id, isLoading: isQueryLoading })); + }, [dispatch, id, isQueryLoading]); - const { queryFields, title, unit } = useMemo(() => getManageTimelineById(id), [ - getManageTimelineById, - id, - ]); + const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []); + const unit = useMemo(() => (n: number) => i18n.UNIT(n), []); + const { queryFields, title } = useDeepEqualSelector((state) => getManageTimeline(state, id)); const justTitle = useMemo(() => <TitleText data-test-subj="title">{title}</TitleText>, [title]); @@ -284,6 +286,7 @@ const EventsViewerComponent: React.FC<Props> = ({ <StyledEuiPanel data-test-subj="events-viewer-panel" $isFullScreen={globalFullScreen && id !== TimelineId.active} + hasBorder > {canQueryTimeline ? ( <EventDetailsWidthProvider> diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx index cd27177643b44..571e04a106cf0 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx @@ -22,6 +22,8 @@ import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell import { useTimelineEvents } from '../../../timelines/containers'; import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; +jest.mock('../../../common/lib/kibana'); + jest.mock('../../../timelines/containers', () => ({ useTimelineEvents: jest.fn(), })); @@ -60,7 +62,9 @@ describe('StatefulEventsViewer', () => { await waitFor(() => { wrapper.update(); - expect(wrapper.find('[data-test-subj="events-viewer-panel"]').first().exists()).toBe(true); + expect(wrapper.text()).toMatchInlineSnapshot( + `"Showing: 12 events1 fields sorted@timestamp1event.severityevent.categoryevent.actionhost.namesource.ipdestination.ipdestination.bytesuser.name_idmessage0 of 12 events123"` + ); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index b58aa2236d292..32aa716d4bce3 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -12,23 +12,23 @@ import styled from 'styled-components'; import { inputsModel, inputsSelectors, State } from '../../store'; import { inputsActions } from '../../store/actions'; -import { TimelineId } from '../../../../common/types/timeline'; +import { ControlColumnProps, RowRenderer, TimelineId } from '../../../../common/types/timeline'; import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline'; import { SubsetTimelineModel, TimelineModel } from '../../../timelines/store/timeline/model'; import { Filter } from '../../../../../../../src/plugins/data/public'; -import { EventsViewer } from './events_viewer'; import { InspectButtonContainer } from '../inspect'; import { useGlobalFullScreen } from '../../containers/use_full_screen'; +import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { useSourcererScope } from '../../containers/sourcerer'; import { DetailsPanel } from '../../../timelines/components/side_panel'; -import { RowRenderer } from '../../../timelines/components/timeline/body/renderers/row_renderer'; import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering'; - -const DEFAULT_EVENTS_VIEWER_HEIGHT = 652; +import { useKibana } from '../../lib/kibana'; +import { defaultControlColumn } from '../../../timelines/components/timeline/body/control_columns'; +import { EventsViewer } from './events_viewer'; const FullScreenContainer = styled.div<{ $isFullScreen: boolean }>` - height: ${({ $isFullScreen }) => ($isFullScreen ? '100%' : `${DEFAULT_EVENTS_VIEWER_HEIGHT}px`)}; + height: ${({ $isFullScreen }) => ($isFullScreen ? '100%' : undefined)}; flex: 1 1 auto; display: flex; width: 100%; @@ -83,6 +83,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({ // If truthy, the graph viewer (Resolver) is showing graphEventId, }) => { + const { timelines: timelinesUi } = useKibana().services; const { browserFields, docValueFields, @@ -90,8 +91,9 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({ selectedPatterns, loading: isLoadingIndexPattern, } = useSourcererScope(scopeId); - const { globalFullScreen } = useGlobalFullScreen(); - + const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); + // TODO: Once we are past experimental phase this code should be removed + const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); useEffect(() => { if (createTimeline != null) { createTimeline({ @@ -111,37 +113,73 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({ }, []); const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]); + const leadingControlColumns: ControlColumnProps[] = [defaultControlColumn]; + const trailingControlColumns: ControlColumnProps[] = []; return ( <> <FullScreenContainer $isFullScreen={globalFullScreen}> <InspectButtonContainer> - <EventsViewer - browserFields={browserFields} - columns={columns} - docValueFields={docValueFields} - id={id} - dataProviders={dataProviders!} - deletedEventIds={deletedEventIds} - end={end} - isLoadingIndexPattern={isLoadingIndexPattern} - filters={globalFilters} - headerFilterGroup={headerFilterGroup} - indexNames={selectedPatterns} - indexPattern={indexPattern} - isLive={isLive} - itemsPerPage={itemsPerPage!} - itemsPerPageOptions={itemsPerPageOptions!} - kqlMode={kqlMode} - query={query} - onRuleChange={onRuleChange} - renderCellValue={renderCellValue} - rowRenderers={rowRenderers} - start={start} - sort={sort} - utilityBar={utilityBar} - graphEventId={graphEventId} - /> + {tGridEnabled ? ( + timelinesUi.getTGrid<'embedded'>({ + type: 'embedded', + browserFields, + columns, + dataProviders: dataProviders!, + deletedEventIds, + docValueFields, + end, + filters: globalFilters, + globalFullScreen, + headerFilterGroup, + id, + indexNames: selectedPatterns, + indexPattern, + isLive, + isLoadingIndexPattern, + itemsPerPage, + itemsPerPageOptions: itemsPerPageOptions!, + kqlMode, + query, + onRuleChange, + renderCellValue, + rowRenderers, + setGlobalFullScreen, + start, + sort, + utilityBar, + graphEventId, + leadingControlColumns, + trailingControlColumns, + }) + ) : ( + <EventsViewer + browserFields={browserFields} + columns={columns} + docValueFields={docValueFields} + id={id} + dataProviders={dataProviders!} + deletedEventIds={deletedEventIds} + end={end} + isLoadingIndexPattern={isLoadingIndexPattern} + filters={globalFilters} + headerFilterGroup={headerFilterGroup} + indexNames={selectedPatterns} + indexPattern={indexPattern} + isLive={isLive} + itemsPerPage={itemsPerPage!} + itemsPerPageOptions={itemsPerPageOptions!} + kqlMode={kqlMode} + query={query} + onRuleChange={onRuleChange} + renderCellValue={renderCellValue} + rowRenderers={rowRenderers} + start={start} + sort={sort} + utilityBar={utilityBar} + graphEventId={graphEventId} + /> + )} </InspectButtonContainer> </FullScreenContainer> <DetailsPanel diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/translations.ts b/x-pack/plugins/security_solution/public/common/components/events_viewer/translations.ts index 176940d1d445c..133ba1d98e092 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/translations.ts @@ -22,13 +22,6 @@ export const EVENTS = i18n.translate('xpack.securitySolution.eventsViewer.events defaultMessage: 'Events', }); -export const LOADING_EVENTS = i18n.translate( - 'xpack.securitySolution.eventsViewer.footer.loadingEventsDataLabel', - { - defaultMessage: 'Loading Events', - } -); - export const UNIT = (totalCount: number) => i18n.translate('xpack.securitySolution.eventsViewer.unit', { values: { totalCount }, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx index c13a1b011ccbd..87f7f5fe2f507 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx @@ -106,7 +106,7 @@ export const AddExceptionComments = memo(function AddExceptionComments({ <EuiFlexGroup gutterSize={'none'}> <EuiFlexItem grow={false}> <MyAvatar - name={currentUser !== null ? currentUser.username.toUpperCase() ?? '' : ''} + name={currentUser != null ? currentUser.username.toUpperCase() ?? '' : ''} size="l" /> </EuiFlexItem> diff --git a/x-pack/plugins/security_solution/public/common/components/filters_global/__snapshots__/filters_global.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/filters_global/__snapshots__/filters_global.test.tsx.snap index 994e98d8619a1..51326d54a6161 100644 --- a/x-pack/plugins/security_solution/public/common/components/filters_global/__snapshots__/filters_global.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/filters_global/__snapshots__/filters_global.test.tsx.snap @@ -4,17 +4,19 @@ exports[`rendering renders correctly 1`] = ` <InPortal node={<div />} > - <FiltersGlobalContainer - data-test-subj="filters-global-container" - show={true} + <EuiPanel + borderRadius="none" + color="subdued" + paddingSize="s" > - <Wrapper - className="siemFiltersGlobal" + <FiltersGlobalContainer + data-test-subj="filters-global-container" + show={true} > <p> Additional filters here. </p> - </Wrapper> - </FiltersGlobalContainer> + </FiltersGlobalContainer> + </EuiPanel> </InPortal> `; diff --git a/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx b/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx index c6b5b6ccde5cd..79c08e50451f7 100644 --- a/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx +++ b/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx @@ -8,18 +8,9 @@ import React from 'react'; import styled from 'styled-components'; import { InPortal } from 'react-reverse-portal'; - +import { EuiPanel } from '@elastic/eui'; import { useGlobalHeaderPortal } from '../../hooks/use_global_header_portal'; -const Wrapper = styled.aside` - position: relative; - z-index: ${({ theme }) => theme.eui.euiZNavigation}; - background: ${({ theme }) => theme.eui.euiColorEmptyShade}; - border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; - padding: ${({ theme }) => theme.eui.paddingSizes.m} ${({ theme }) => theme.eui.paddingSizes.l}; -`; -Wrapper.displayName = 'Wrapper'; - const FiltersGlobalContainer = styled.header<{ show: boolean }>` display: ${({ show }) => (show ? 'block' : 'none')}; `; @@ -32,13 +23,15 @@ export interface FiltersGlobalProps { } export const FiltersGlobal = React.memo<FiltersGlobalProps>(({ children, show = true }) => { - const { globalHeaderPortalNode } = useGlobalHeaderPortal(); + const { globalKQLHeaderPortalNode } = useGlobalHeaderPortal(); return ( - <InPortal node={globalHeaderPortalNode}> - <FiltersGlobalContainer data-test-subj="filters-global-container" show={show}> - <Wrapper className="siemFiltersGlobal">{children}</Wrapper> - </FiltersGlobalContainer> + <InPortal node={globalKQLHeaderPortalNode}> + <EuiPanel borderRadius="none" color="subdued" paddingSize="s"> + <FiltersGlobalContainer data-test-subj="filters-global-container" show={show}> + {children} + </FiltersGlobalContainer> + </EuiPanel> </InPortal> ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.test.tsx deleted file mode 100644 index 96a7eacb7fb08..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/header_global/index.test.tsx +++ /dev/null @@ -1,51 +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 { mount } from 'enzyme'; - -import { useGetUserCasesPermissions } from '../../../common/lib/kibana'; -import { TestProviders } from '../../../common/mock'; -import { HeaderGlobal } from '.'; - -jest.mock('../../../common/lib/kibana'); - -describe('HeaderGlobal', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('does not display the cases tab when the user does not have read permissions', () => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ - crud: false, - read: false, - }); - - const wrapper = mount( - <TestProviders> - <HeaderGlobal /> - </TestProviders> - ); - - expect(wrapper.find(`[data-test-subj="navigation-case"]`).exists()).toBeFalsy(); - }); - - it('displays the cases tab when the user has read permissions', () => { - (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ - crud: true, - read: true, - }); - - const wrapper = mount( - <TestProviders> - <HeaderGlobal /> - </TestProviders> - ); - - expect(wrapper.find(`[data-test-subj="navigation-case"]`).exists()).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx deleted file mode 100644 index e91905183aab1..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx +++ /dev/null @@ -1,155 +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 { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; -import { pickBy } from 'lodash/fp'; -import React, { forwardRef, useCallback, useMemo } from 'react'; -import styled from 'styled-components'; -import { OutPortal } from 'react-reverse-portal'; - -import { navTabs } from '../../../app/home/home_navigations'; -import { useGlobalFullScreen, useTimelineFullScreen } from '../../containers/use_full_screen'; -import { SecurityPageName } from '../../../app/types'; -import { getAppOverviewUrl } from '../link_to'; -import { MlPopover } from '../ml_popover/ml_popover'; -import { SiemNavigation } from '../navigation'; -import * as i18n from './translations'; -import { useGetUrlSearch } from '../navigation/use_get_url_search'; -import { useGetUserCasesPermissions, useKibana } from '../../lib/kibana'; -import { APP_ID, ADD_DATA_PATH, APP_DETECTIONS_PATH } from '../../../../common/constants'; -import { useGlobalHeaderPortal } from '../../hooks/use_global_header_portal'; -import { LinkAnchor } from '../links'; - -const Wrapper = styled.header<{ $isFixed: boolean }>` - ${({ theme, $isFixed }) => ` - background: ${theme.eui.euiColorEmptyShade}; - border-bottom: ${theme.eui.euiBorderThin}; - width: 100%; - z-index: ${theme.eui.euiZNavigation}; - position: ${$isFixed ? 'fixed' : 'relative'}; - `} -`; -Wrapper.displayName = 'Wrapper'; - -const WrapperContent = styled.div<{ $globalFullScreen: boolean }>` - display: ${({ $globalFullScreen }) => ($globalFullScreen ? 'none' : 'block')}; - padding-top: ${({ $globalFullScreen, theme }) => - $globalFullScreen ? theme.eui.paddingSizes.s : theme.eui.paddingSizes.m}; -`; - -WrapperContent.displayName = 'WrapperContent'; - -const FlexItem = styled(EuiFlexItem)` - min-width: 0; -`; -FlexItem.displayName = 'FlexItem'; - -const FlexGroup = styled(EuiFlexGroup)<{ $hasSibling: boolean }>` - ${({ $hasSibling, theme }) => ` - border-bottom: ${theme.eui.euiBorderThin}; - margin-bottom: 1px; - padding-bottom: 4px; - padding-left: ${theme.eui.paddingSizes.l}; - padding-right: ${theme.eui.paddingSizes.l}; - ${$hasSibling ? `border-bottom: ${theme.eui.euiBorderThin};` : 'border-bottom-width: 0px;'} - `} -`; -FlexGroup.displayName = 'FlexGroup'; - -interface HeaderGlobalProps { - hideDetectionEngine?: boolean; - isFixed?: boolean; -} - -export const HeaderGlobal = React.memo( - forwardRef<HTMLDivElement, HeaderGlobalProps>( - ({ hideDetectionEngine = false, isFixed = true }, ref) => { - const { globalHeaderPortalNode } = useGlobalHeaderPortal(); - const { globalFullScreen } = useGlobalFullScreen(); - const { timelineFullScreen } = useTimelineFullScreen(); - const search = useGetUrlSearch(navTabs.overview); - const { application, http } = useKibana().services; - const { navigateToApp, getUrlForApp } = application; - const overviewPath = useMemo( - () => getUrlForApp(APP_ID, { path: SecurityPageName.overview }), - [getUrlForApp] - ); - const overviewHref = useMemo(() => getAppOverviewUrl(overviewPath, search), [ - overviewPath, - search, - ]); - - const basePath = http.basePath.get(); - const goToOverview = useCallback( - (ev) => { - ev.preventDefault(); - navigateToApp(`${APP_ID}:${SecurityPageName.overview}`, { path: search }); - }, - [navigateToApp, search] - ); - - const hasCasesReadPermissions = useGetUserCasesPermissions()?.read; - - // build a list of tabs to exclude - const tabsToExclude = new Set<string>([ - ...(hideDetectionEngine ? [SecurityPageName.detections] : []), - ...(!hasCasesReadPermissions ? [SecurityPageName.case] : []), - ]); - - // include the tab if it is not in the set of excluded ones - const tabsToDisplay = pickBy((_, key) => !tabsToExclude.has(key), navTabs); - - return ( - <Wrapper ref={ref} $isFixed={isFixed}> - <WrapperContent $globalFullScreen={globalFullScreen ?? timelineFullScreen}> - <FlexGroup - alignItems="center" - $hasSibling={globalHeaderPortalNode.hasChildNodes()} - justifyContent="spaceBetween" - wrap - > - <FlexItem> - <EuiFlexGroup alignItems="center" responsive={false}> - <FlexItem grow={false}> - <LinkAnchor onClick={goToOverview} href={overviewHref}> - <EuiIcon aria-label={i18n.SECURITY_SOLUTION} type="logoSecurity" size="l" /> - </LinkAnchor> - </FlexItem> - - <FlexItem component="nav"> - <SiemNavigation display="condensed" navTabs={tabsToDisplay} /> - </FlexItem> - </EuiFlexGroup> - </FlexItem> - <FlexItem grow={false}> - <EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap> - {window.location.pathname.includes(APP_DETECTIONS_PATH) && ( - <FlexItem grow={false}> - <MlPopover /> - </FlexItem> - )} - - <FlexItem grow={false}> - <EuiButtonEmpty - data-test-subj="add-data" - href={`${basePath}${ADD_DATA_PATH}`} - iconType="plusInCircle" - > - {i18n.BUTTON_ADD_DATA} - </EuiButtonEmpty> - </FlexItem> - </EuiFlexGroup> - </FlexItem> - </FlexGroup> - </WrapperContent> - <OutPortal node={globalHeaderPortalNode} /> - </Wrapper> - ); - } - ) -); -HeaderGlobal.displayName = 'HeaderGlobal'; diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap index 84c8971e3d352..9cb9f28612b15 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap @@ -1,14 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`HeaderPage it renders 1`] = ` -<Header - border={true} -> - <EuiFlexGroup +<Fragment> + <EuiPageHeader alignItems="center" - justifyContent="spaceBetween" + bottomBorder={true} > - <FlexItem> + <EuiPageHeaderSection> <Memo(TitleComponent) badgeOptions={ Object { @@ -27,18 +25,20 @@ exports[`HeaderPage it renders 1`] = ` data-test-subj="header-page-subtitle-2" items="Test subtitle 2" /> - </FlexItem> - <FlexItem + </EuiPageHeaderSection> + <EuiPageHeaderSection data-test-subj="header-page-supplements" - grow={false} > <p> Test supplement </p> - </FlexItem> - </EuiFlexGroup> - <Sourcerer - scope="default" + </EuiPageHeaderSection> + <Sourcerer + scope="default" + /> + </EuiPageHeader> + <EuiSpacer + size="l" /> -</Header> +</Fragment> `; diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx index 78bac02585b9f..8a1748de582c4 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx @@ -57,7 +57,7 @@ describe('HeaderPage', () => { </TestProviders> ); - expect(wrapper.find('.siemHeaderPage__linkBack').first().exists()).toBe(true); + expect(wrapper.find('.securitySolutionHeaderPage__linkBack').first().exists()).toBe(true); }); test('it DOES NOT render the back link when not provided', () => { @@ -67,7 +67,7 @@ describe('HeaderPage', () => { </TestProviders> ); - expect(wrapper.find('.siemHeaderPage__linkBack').first().exists()).toBe(false); + expect(wrapper.find('.securitySolutionHeaderPage__linkBack').first().exists()).toBe(false); }); test('it renders the first subtitle when provided', () => { @@ -134,27 +134,21 @@ describe('HeaderPage', () => { expect(wrapper.find('[data-test-subj="header-page-supplements"]').first().exists()).toBe(false); }); - test('it applies border styles when border is true', () => { - const wrapper = mount( - <TestProviders> - <HeaderPage border title="Test title" /> - </TestProviders> - ); - const siemHeaderPage = wrapper.find('.siemHeaderPage').first(); - - expect(siemHeaderPage).toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin); - expect(siemHeaderPage).toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.l); - }); - test('it DOES NOT apply border styles when border is false', () => { const wrapper = mount( <TestProviders> <HeaderPage title="Test title" /> </TestProviders> ); - const siemHeaderPage = wrapper.find('.siemHeaderPage').first(); + const securitySolutionHeaderPage = wrapper.find('.securitySolutionHeaderPage').first(); - expect(siemHeaderPage).not.toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin); - expect(siemHeaderPage).not.toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.l); + expect(securitySolutionHeaderPage).not.toHaveStyleRule( + 'border-bottom', + euiDarkVars.euiBorderThin + ); + expect(securitySolutionHeaderPage).not.toHaveStyleRule( + 'padding-bottom', + euiDarkVars.paddingSizes.l + ); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx index d01869bb6999b..1c87d70c0c7cb 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx @@ -5,7 +5,13 @@ * 2.0. */ -import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiProgress } from '@elastic/eui'; +import { + EuiBadge, + EuiProgress, + EuiPageHeader, + EuiPageHeaderSection, + EuiSpacer, +} from '@elastic/eui'; import React, { useCallback } from 'react'; import { useHistory } from 'react-router-dom'; import styled, { css } from 'styled-components'; @@ -25,36 +31,16 @@ interface HeaderProps { } const Header = styled.header.attrs({ - className: 'siemHeaderPage', + className: 'securitySolutionHeaderPage', })<HeaderProps>` ${({ border, theme }) => css` margin-bottom: ${theme.eui.euiSizeL}; - - ${border && - css` - border-bottom: ${theme.eui.euiBorderThin}; - padding-bottom: ${theme.eui.paddingSizes.l}; - .euiProgress { - top: ${theme.eui.paddingSizes.l}; - } - `} `} `; Header.displayName = 'Header'; -const FlexItem = styled(EuiFlexItem)` - ${({ theme }) => css` - display: block; - - @media only screen and (min-width: ${theme.eui.euiBreakpoints.m}) { - max-width: 50%; - } - `} -`; -FlexItem.displayName = 'FlexItem'; - const LinkBack = styled.div.attrs({ - className: 'siemHeaderPage__linkBack', + className: 'securitySolutionHeaderPage__linkBack', })` ${({ theme }) => css` font-size: ${theme.eui.euiFontSizeXS}; @@ -117,9 +103,9 @@ const HeaderPageComponent: React.FC<HeaderPageProps> = ({ [backOptions, history] ); return ( - <Header border={border} {...rest}> - <EuiFlexGroup alignItems="center" justifyContent="spaceBetween"> - <FlexItem> + <> + <EuiPageHeader alignItems="center" bottomBorder={border}> + <EuiPageHeaderSection> {backOptions && ( <LinkBack> <LinkIcon @@ -146,16 +132,18 @@ const HeaderPageComponent: React.FC<HeaderPageProps> = ({ {subtitle && <Subtitle data-test-subj="header-page-subtitle" items={subtitle} />} {subtitle2 && <Subtitle data-test-subj="header-page-subtitle-2" items={subtitle2} />} {border && isLoading && <EuiProgress size="xs" color="accent" />} - </FlexItem> + </EuiPageHeaderSection> {children && ( - <FlexItem data-test-subj="header-page-supplements" grow={false}> + <EuiPageHeaderSection data-test-subj="header-page-supplements"> {children} - </FlexItem> + </EuiPageHeaderSection> )} - </EuiFlexGroup> - {!hideSourcerer && <Sourcerer scope={SourcererScopeName.default} />} - </Header> + {!hideSourcerer && <Sourcerer scope={SourcererScopeName.default} />} + </EuiPageHeader> + {/* Manually add a 'padding-bottom' to header */} + <EuiSpacer size="l" /> + </> ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/title.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/title.test.tsx index 7ad9de29431c9..d21adbd00cc20 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/title.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_page/title.test.tsx @@ -13,6 +13,8 @@ import { TestProviders } from '../../mock'; import { Title } from './title'; import { useMountAppended } from '../../utils/use_mount_appended'; +jest.mock('../../lib/kibana'); + describe('Title', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx index ddbcf710aff30..a0e2ff266ad28 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx @@ -131,7 +131,7 @@ const InspectButtonComponent: React.FC<InspectButtonProps> = ({ color="text" iconSide="left" iconType="inspect" - isDisabled={loading || isDisabled} + isDisabled={loading || isDisabled || false} isLoading={loading} onClick={handleClick} > @@ -145,7 +145,7 @@ const InspectButtonComponent: React.FC<InspectButtonProps> = ({ data-test-subj="inspect-icon-button" iconSize="m" iconType="inspect" - isDisabled={loading || isDisabled} + isDisabled={loading || isDisabled || false} title={i18n.INSPECT} onClick={handleClick} /> diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap index c7841f6d6bbcc..f0fd8427140df 100644 --- a/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap @@ -14,6 +14,7 @@ exports[`item_details_card ItemDetailsAction should render correctly 1`] = ` exports[`item_details_card ItemDetailsCard should render correctly with actions 1`] = ` <EuiPanel + hasBorder={true} paddingSize="none" > <EuiFlexGroup @@ -115,6 +116,7 @@ exports[`item_details_card ItemDetailsCard should render correctly with actions exports[`item_details_card ItemDetailsCard should render correctly with no actions 1`] = ` <EuiPanel + hasBorder={true} paddingSize="none" > <EuiFlexGroup diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx index 47fe9dc175ce6..ea53777e59075 100644 --- a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx @@ -112,7 +112,7 @@ export const ItemDetailsCard = memo<ItemDetailsCardProps>( ); return ( - <EuiPanel paddingSize="none" data-test-subj={dataTestSubj} className={className}> + <EuiPanel paddingSize="none" data-test-subj={dataTestSubj} className={className} hasBorder> <EuiFlexGroup direction="row"> <SummarySection grow={2}> <EuiDescriptionList compressed type="column"> diff --git a/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx index 115fb65dc7011..f08edb114b9a9 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx @@ -13,6 +13,8 @@ import { EntityDraggableComponent } from './entity_draggable'; import { TestProviders } from '../../mock/test_providers'; import { useMountAppended } from '../../utils/use_mount_appended'; +jest.mock('../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx index 6ad2bd30283d2..0d9b4001c17aa 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx @@ -17,6 +17,8 @@ import { useMountAppended } from '../../../utils/use_mount_appended'; import { Anomalies } from '../types'; import { waitFor } from '@testing-library/dom'; +jest.mock('../../../lib/kibana'); + const startDate: string = '2020-07-07T08:20:18.966Z'; const endDate: string = '3000-01-01T00:00:00.000Z'; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_scores.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_scores.test.tsx index 6b569a67cfebf..5eb0751404872 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_scores.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_scores.test.tsx @@ -18,6 +18,8 @@ import { Anomalies } from '../types'; import { useMountAppended } from '../../../utils/use_mount_appended'; import { waitFor } from '@testing-library/dom'; +jest.mock('../../../lib/kibana'); + const startDate: string = '2020-07-07T08:20:18.966Z'; const endDate: string = '3000-01-01T00:00:00.000Z'; const narrowDateRange = jest.fn(); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx index ae6ef4e680ffa..2ecda8482e340 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx @@ -16,6 +16,8 @@ import { Columns } from '../../paginated_table'; import { TestProviders } from '../../../mock'; import { useMountAppended } from '../../../utils/use_mount_appended'; +jest.mock('../../../lib/kibana'); + const startDate = new Date(2001).toISOString(); const endDate = new Date(3000).toISOString(); const interval = 'days'; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx index b8a8ab88a74fd..48c2ec3ee38d8 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx @@ -15,6 +15,8 @@ import React from 'react'; import { TestProviders } from '../../../mock'; import { useMountAppended } from '../../../utils/use_mount_appended'; +jest.mock('../../../../common/lib/kibana'); + const startDate = new Date(2001).toISOString(); const endDate = new Date(3000).toISOString(); describe('get_anomalies_network_table_columns', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx index 561805217e8a1..cc6ac5355f90b 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx @@ -5,7 +5,13 @@ * 2.0. */ -import { EuiButtonEmpty, EuiCallOut, EuiPopover, EuiPopoverTitle, EuiSpacer } from '@elastic/eui'; +import { + EuiHeaderSectionItemButton, + EuiCallOut, + EuiPopover, + EuiPopoverTitle, + EuiSpacer, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import moment from 'moment'; import React, { Dispatch, useCallback, useReducer, useState } from 'react'; @@ -115,14 +121,19 @@ export const MlPopover = React.memo(() => { anchorPosition="downRight" id="integrations-popover" button={ - <EuiButtonEmpty + <EuiHeaderSectionItemButton + aria-expanded={isPopoverOpen} + aria-haspopup="true" + aria-label={i18n.ML_JOB_SETTINGS} + color="primary" data-test-subj="integrations-button" iconType="arrowDown" iconSide="right" onClick={() => setIsPopoverOpen(!isPopoverOpen)} + textProps={{ style: { fontSize: '1rem' } }} > {i18n.ML_JOB_SETTINGS} - </EuiButtonEmpty> + </EuiHeaderSectionItemButton> } isOpen={isPopoverOpen} closePopover={() => setIsPopoverOpen(!isPopoverOpen)} @@ -138,7 +149,11 @@ export const MlPopover = React.memo(() => { anchorPosition="downRight" id="integrations-popover" button={ - <EuiButtonEmpty + <EuiHeaderSectionItemButton + aria-expanded={isPopoverOpen} + aria-haspopup="true" + aria-label={i18n.ML_JOB_SETTINGS} + color="primary" data-test-subj="integrations-button" iconType="arrowDown" iconSide="right" @@ -146,9 +161,10 @@ export const MlPopover = React.memo(() => { setIsPopoverOpen(!isPopoverOpen); dispatch({ type: 'refresh' }); }} + textProps={{ style: { fontSize: '1rem' } }} > {i18n.ML_JOB_SETTINGS} - </EuiButtonEmpty> + </EuiHeaderSectionItemButton> } isOpen={isPopoverOpen} closePopover={() => setIsPopoverOpen(!isPopoverOpen)} diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts index dffc7becaf42a..c869df6ad388e 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts @@ -306,6 +306,29 @@ describe('Navigation Breadcrumbs', () => { }, ]); }); + + test('should set "timeline.isOpen" to false when timeline is open', () => { + const breadcrumbs = getBreadcrumbsForRoute( + { + ...getMockObject('timelines', '/', undefined), + timeline: { + activeTab: TimelineTabs.query, + id: 'TIMELINE_ID', + isOpen: true, + graphEventId: 'GRAPH_EVENT_ID', + }, + }, + getUrlForAppMock + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionoverview' }, + { + text: 'Timelines', + href: + "securitySolution:timelines?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))&timeline=(activeTab:query,graphEventId:GRAPH_EVENT_ID,id:TIMELINE_ID,isOpen:!f)", + }, + ]); + }); }); describe('setBreadcrumbs()', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts index 605478900d066..a09945f705c58 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts @@ -61,10 +61,14 @@ const isAdminRoutes = (spyState: RouteSpyState): spyState is AdministrationRoute // eslint-disable-next-line complexity export const getBreadcrumbsForRoute = ( - object: RouteSpyState & TabNavigationProps, + objectParam: RouteSpyState & TabNavigationProps, getUrlForApp: GetUrlForApp ): ChromeBreadcrumb[] | null => { - const spyState: RouteSpyState = omit('navTabs', object); + const spyState: RouteSpyState = omit('navTabs', objectParam); + + // Sets `timeline.isOpen` to false in the state to avoid reopening the timeline on breadcrumb click. https://github.com/elastic/kibana/issues/100322 + const object = { ...objectParam, timeline: { ...objectParam.timeline, isOpen: false } }; + const overviewPath = getUrlForApp(APP_ID, { path: SecurityPageName.overview }); const siemRootBreadcrumb: ChromeBreadcrumb = { text: APP_NAME, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx index 27db326dddec5..c75b38e03acb4 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx @@ -9,12 +9,12 @@ import { mount } from 'enzyme'; import React from 'react'; import { CONSTANTS } from '../url_state/constants'; -import { SiemNavigationComponent } from './'; +import { TabNavigationComponent } from './'; import { setBreadcrumbs } from './breadcrumbs'; import { navTabs } from '../../../app/home/home_navigations'; import { HostsTableType } from '../../../hosts/store/model'; import { RouteSpyState } from '../../utils/route/types'; -import { SiemNavigationProps, SiemNavigationComponentProps } from './types'; +import { TabNavigationComponentProps, SecuritySolutionTabNavigationProps } from './types'; import { TimelineTabs } from '../../../../common/types/timeline'; jest.mock('react-router-dom', () => { @@ -48,7 +48,9 @@ jest.mock('../../lib/kibana', () => { jest.mock('../link_to'); describe('SIEM Navigation', () => { - const mockProps: SiemNavigationComponentProps & SiemNavigationProps & RouteSpyState = { + const mockProps: TabNavigationComponentProps & + SecuritySolutionTabNavigationProps & + RouteSpyState = { pageName: 'hosts', pathName: '/', detailName: undefined, @@ -89,7 +91,7 @@ describe('SIEM Navigation', () => { }, }, }; - const wrapper = mount(<SiemNavigationComponent {...mockProps} />); + const wrapper = mount(<TabNavigationComponent {...mockProps} />); test('it calls setBreadcrumbs with correct path on mount', () => { expect(setBreadcrumbs).toHaveBeenNthCalledWith( 1, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx index 7ea0b26ae8b3b..233b4b2cb1d02 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx @@ -16,75 +16,93 @@ import { useRouteSpy } from '../../utils/route/use_route_spy'; import { makeMapStateToProps } from '../url_state/helpers'; import { setBreadcrumbs } from './breadcrumbs'; import { TabNavigation } from './tab_navigation'; -import { SiemNavigationProps, SiemNavigationComponentProps } from './types'; +import { TabNavigationComponentProps, SecuritySolutionTabNavigationProps } from './types'; -export const SiemNavigationComponent: React.FC< - SiemNavigationComponentProps & SiemNavigationProps & RouteSpyState -> = ({ - detailName, - display, - navTabs, - pageName, - pathName, - search, - tabName, - urlState, - flowTarget, - state, -}) => { - const { - chrome, - application: { getUrlForApp }, - } = useKibana().services; +/** + * @description - This component handels all of the tab navigation seen within a Security Soluton application page, not the Security Solution primary side navigation + * For the primary side nav see './use_security_solution_navigation' + */ +export const TabNavigationComponent: React.FC< + RouteSpyState & SecuritySolutionTabNavigationProps & TabNavigationComponentProps +> = React.memo( + ({ + detailName, + display, + flowTarget, + navTabs, + pageName, + pathName, + search, + state, + tabName, + urlState, + }) => { + const { + chrome, + application: { getUrlForApp }, + } = useKibana().services; - useEffect(() => { - if (pathName || pageName) { - setBreadcrumbs( - { - detailName, - filters: urlState.filters, - flowTarget, - navTabs, - pageName, - pathName, - query: urlState.query, - savedQuery: urlState.savedQuery, - search, - sourcerer: urlState.sourcerer, - state, - tabName, - timeline: urlState.timeline, - timerange: urlState.timerange, - }, - chrome, - getUrlForApp - ); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [chrome, pageName, pathName, search, navTabs, urlState, state]); + useEffect(() => { + if (pathName || pageName) { + setBreadcrumbs( + { + detailName, + filters: urlState.filters, + flowTarget, + navTabs, + pageName, + pathName, + query: urlState.query, + savedQuery: urlState.savedQuery, + search, + sourcerer: urlState.sourcerer, + state, + tabName, + timeline: urlState.timeline, + timerange: urlState.timerange, + }, + chrome, + getUrlForApp + ); + } + }, [ + chrome, + pageName, + pathName, + search, + navTabs, + urlState, + state, + detailName, + flowTarget, + tabName, + getUrlForApp, + ]); - return ( - <TabNavigation - query={urlState.query} - display={display} - filters={urlState.filters} - navTabs={navTabs} - pageName={pageName} - pathName={pathName} - sourcerer={urlState.sourcerer} - savedQuery={urlState.savedQuery} - tabName={tabName} - timeline={urlState.timeline} - timerange={urlState.timerange} - /> - ); -}; + return ( + <TabNavigation + query={urlState.query} + display={display} + filters={urlState.filters} + navTabs={navTabs} + pageName={pageName} + pathName={pathName} + sourcerer={urlState.sourcerer} + savedQuery={urlState.savedQuery} + tabName={tabName} + timeline={urlState.timeline} + timerange={urlState.timerange} + /> + ); + } +); +TabNavigationComponent.displayName = 'TabNavigationComponent'; -export const SiemNavigationRedux = compose< - React.ComponentClass<SiemNavigationProps & RouteSpyState> +export const SecuritySolutionTabNavigationRedux = compose< + React.ComponentClass<SecuritySolutionTabNavigationProps & RouteSpyState> >(connect(makeMapStateToProps))( React.memo( - SiemNavigationComponent, + TabNavigationComponent, (prevProps, nextProps) => prevProps.pathName === nextProps.pathName && prevProps.search === nextProps.search && @@ -94,16 +112,16 @@ export const SiemNavigationRedux = compose< ) ); -const SiemNavigationContainer: React.FC<SiemNavigationProps> = (props) => { - const [routeProps] = useRouteSpy(); - const stateNavReduxProps: RouteSpyState & SiemNavigationProps = { - ...routeProps, - ...props, - }; - - return <SiemNavigationRedux {...stateNavReduxProps} />; -}; +export const SecuritySolutionTabNavigation: React.FC<SecuritySolutionTabNavigationProps> = React.memo( + (props) => { + const [routeProps] = useRouteSpy(); + const stateNavReduxProps: RouteSpyState & SecuritySolutionTabNavigationProps = { + ...routeProps, + ...props, + }; -export const SiemNavigation = React.memo(SiemNavigationContainer, (prevProps, nextProps) => - deepEqual(prevProps.navTabs, nextProps.navTabs) + return <SecuritySolutionTabNavigationRedux {...stateNavReduxProps} />; + }, + (prevProps, nextProps) => deepEqual(prevProps.navTabs, nextProps.navTabs) ); +SecuritySolutionTabNavigation.displayName = 'SecuritySolutionTabNavigation'; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/types.ts index 4253d08d1ed19..53565d79e6948 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/types.ts @@ -7,17 +7,17 @@ import { UrlInputsModel } from '../../../store/inputs/model'; import { CONSTANTS } from '../../url_state/constants'; -import { HostsTableType } from '../../../../hosts/store/model'; import { SourcererScopePatterns } from '../../../store/sourcerer/model'; import { TimelineUrl } from '../../../../timelines/store/timeline/model'; import { Filter, Query } from '../../../../../../../../src/plugins/data/public'; -import { SiemNavigationProps } from '../types'; +import { SecuritySolutionTabNavigationProps } from '../types'; +import { SiemRouteType } from '../../../utils/route/types'; -export interface TabNavigationProps extends SiemNavigationProps { +export interface TabNavigationProps extends SecuritySolutionTabNavigationProps { pathName: string; pageName: string; - tabName: HostsTableType | undefined; + tabName: SiemRouteType | undefined; [CONSTANTS.appQuery]?: Query; [CONSTANTS.filters]?: Filter[]; [CONSTANTS.savedQuery]?: string; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts index 9700afcb8cd59..1c317700b1d15 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts @@ -5,31 +5,20 @@ * 2.0. */ -import { Filter, Query } from '../../../../../../../src/plugins/data/public'; -import { HostsTableType } from '../../../hosts/store/model'; -import { UrlInputsModel } from '../../store/inputs/model'; -import { TimelineUrl } from '../../../timelines/store/timeline/model'; -import { CONSTANTS, UrlStateType } from '../url_state/constants'; +import { UrlStateType } from '../url_state/constants'; import { SecurityPageName } from '../../../app/types'; -import { SourcererScopePatterns } from '../../store/sourcerer/model'; +import { UrlState } from '../url_state/types'; +import { SiemRouteType } from '../../utils/route/types'; -export interface SiemNavigationProps { +export interface SecuritySolutionTabNavigationProps { display?: 'default' | 'condensed'; navTabs: Record<string, NavTab>; } - -export interface SiemNavigationComponentProps { - pathName: string; +export interface TabNavigationComponentProps { pageName: string; - tabName: HostsTableType | undefined; - urlState: { - [CONSTANTS.appQuery]?: Query; - [CONSTANTS.filters]?: Filter[]; - [CONSTANTS.savedQuery]?: string; - [CONSTANTS.sourcerer]: SourcererScopePatterns; - [CONSTANTS.timerange]: UrlInputsModel; - [CONSTANTS.timeline]: TimelineUrl; - }; + tabName: SiemRouteType | undefined; + urlState: UrlState; + pathName: string; } export type SearchNavTab = NavTab | { urlKey: UrlStateType; isDetailPage: boolean }; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx new file mode 100644 index 0000000000000..ef00bef841305 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx @@ -0,0 +1,214 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { KibanaPageTemplateProps } from '../../../../../../../../src/plugins/kibana_react/public'; +import { useGetUserCasesPermissions, useKibana } from '../../../lib/kibana'; +import { SecurityPageName } from '../../../../app/types'; +import { useSecuritySolutionNavigation } from '.'; +import { CONSTANTS } from '../../url_state/constants'; +import { TimelineTabs } from '../../../../../common/types/timeline'; +import { useDeepEqualSelector } from '../../../hooks/use_selector'; +import { UrlInputsModel } from '../../../store/inputs/model'; +import { useRouteSpy } from '../../../utils/route/use_route_spy'; + +jest.mock('../../../lib/kibana'); +jest.mock('../../../hooks/use_selector'); +jest.mock('../../../utils/route/use_route_spy'); + +describe('useSecuritySolutionNavigation', () => { + const mockUrlState = { + [CONSTANTS.appQuery]: { query: 'host.name:"security-solution-es"', language: 'kuery' }, + [CONSTANTS.savedQuery]: '', + [CONSTANTS.sourcerer]: {}, + [CONSTANTS.timeline]: { + activeTab: TimelineTabs.query, + id: '', + isOpen: false, + graphEventId: '', + }, + [CONSTANTS.timerange]: { + global: { + [CONSTANTS.timerange]: { + from: '2020-07-07T08:20:18.966Z', + fromStr: 'now-24h', + kind: 'relative', + to: '2020-07-08T08:20:18.966Z', + toStr: 'now', + }, + linkTo: ['timeline'], + }, + timeline: { + [CONSTANTS.timerange]: { + from: '2020-07-07T08:20:18.966Z', + fromStr: 'now-24h', + kind: 'relative', + to: '2020-07-08T08:20:18.966Z', + toStr: 'now', + }, + linkTo: ['global'], + }, + } as UrlInputsModel, + }; + + const mockRouteSpy = [ + { + detailName: '', + flowTarget: '', + pathName: '', + search: '', + state: '', + tabName: '', + pageName: SecurityPageName.hosts, + }, + ]; + + beforeEach(() => { + (useDeepEqualSelector as jest.Mock).mockReturnValue({ urlState: mockUrlState }); + (useRouteSpy as jest.Mock).mockReturnValue(mockRouteSpy); + (useKibana as jest.Mock).mockReturnValue({ + services: { + application: { + navigateToApp: jest.fn(), + getUrlForApp: (appId: string, options?: { path?: string; absolute?: boolean }) => + `${appId}${options?.path ?? ''}`, + }, + chrome: { + setBreadcrumbs: jest.fn(), + }, + }, + }); + }); + + it('should create navigation config', async () => { + const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>(() => + useSecuritySolutionNavigation() + ); + + expect(result.current).toMatchInlineSnapshot(` + Object { + "icon": "logoSecurity", + "items": Array [ + Object { + "id": "securitySolution", + "items": Array [ + Object { + "data-href": "securitySolution:overview?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "data-test-subj": "navigation-overview", + "disabled": false, + "href": "securitySolution:overview?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "id": "overview", + "isSelected": false, + "name": "Overview", + "onClick": [Function], + }, + Object { + "data-href": "securitySolution:detections?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "data-test-subj": "navigation-detections", + "disabled": false, + "href": "securitySolution:detections?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "id": "detections", + "isSelected": false, + "name": "Detections", + "onClick": [Function], + }, + Object { + "data-href": "securitySolution:hosts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "data-test-subj": "navigation-hosts", + "disabled": false, + "href": "securitySolution:hosts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "id": "hosts", + "isSelected": true, + "name": "Hosts", + "onClick": [Function], + }, + Object { + "data-href": "securitySolution:network?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "data-test-subj": "navigation-network", + "disabled": false, + "href": "securitySolution:network?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "id": "network", + "isSelected": false, + "name": "Network", + "onClick": [Function], + }, + Object { + "data-href": "securitySolution:timelines?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "data-test-subj": "navigation-timelines", + "disabled": false, + "href": "securitySolution:timelines?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "id": "timelines", + "isSelected": false, + "name": "Timelines", + "onClick": [Function], + }, + Object { + "data-href": "securitySolution:administration", + "data-test-subj": "navigation-administration", + "disabled": false, + "href": "securitySolution:administration", + "id": "administration", + "isSelected": false, + "name": "Administration", + "onClick": [Function], + }, + ], + "name": "", + }, + ], + "name": "Security", + } + `); + }); + + describe('Permission gated routes', () => { + describe('cases', () => { + it('should display the cases navigation item when the user has read permissions', () => { + (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ + crud: true, + read: true, + }); + + const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>(() => + useSecuritySolutionNavigation() + ); + + const caseNavItem = (result.current?.items || [])[0].items?.find( + (item) => item['data-test-subj'] === 'navigation-case' + ); + expect(caseNavItem).toMatchInlineSnapshot(` + Object { + "data-href": "securitySolution:case?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "data-test-subj": "navigation-case", + "disabled": false, + "href": "securitySolution:case?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "id": "case", + "isSelected": false, + "name": "Cases", + "onClick": [Function], + } + `); + }); + + it('should not display the cases navigation item when the user does not have read permissions', () => { + (useGetUserCasesPermissions as jest.Mock).mockReturnValue({ + crud: false, + read: false, + }); + + const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>(() => + useSecuritySolutionNavigation() + ); + + const caseNavItem = (result.current?.items || [])[0].items?.find( + (item) => item['data-test-subj'] === 'navigation-case' + ); + expect(caseNavItem).toBeFalsy(); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx new file mode 100644 index 0000000000000..f2aee86912dd7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect } from 'react'; +import { pickBy } from 'lodash/fp'; +import { usePrimaryNavigation } from './use_primary_navigation'; +import { useGetUserCasesPermissions, useKibana } from '../../../lib/kibana'; +import { setBreadcrumbs } from '../breadcrumbs'; +import { makeMapStateToProps } from '../../url_state/helpers'; +import { useRouteSpy } from '../../../utils/route/use_route_spy'; +import { navTabs } from '../../../../app/home/home_navigations'; +import { useDeepEqualSelector } from '../../../hooks/use_selector'; +import { SecurityPageName } from '../../../../../common/constants'; + +/** + * @description - This hook provides the structure necessary by the KibanaPageTemplate for rendering the primary security_solution side navigation. + * TODO: Consolidate & re-use the logic in the hooks in this directory that are replicated from the tab_navigation to maintain breadcrumbs, telemetry, etc... + */ +export const useSecuritySolutionNavigation = () => { + const [routeProps] = useRouteSpy(); + const urlMapState = makeMapStateToProps(); + const { urlState } = useDeepEqualSelector(urlMapState); + const { + chrome, + application: { getUrlForApp }, + } = useKibana().services; + + const { detailName, flowTarget, pageName, pathName, search, state, tabName } = routeProps; + + useEffect(() => { + if (pathName || pageName) { + setBreadcrumbs( + { + detailName, + filters: urlState.filters, + flowTarget, + navTabs, + pageName, + pathName, + query: urlState.query, + savedQuery: urlState.savedQuery, + search, + sourcerer: urlState.sourcerer, + state, + tabName, + timeline: urlState.timeline, + timerange: urlState.timerange, + }, + chrome, + getUrlForApp + ); + } + }, [ + chrome, + pageName, + pathName, + search, + urlState, + state, + detailName, + flowTarget, + tabName, + getUrlForApp, + ]); + + const hasCasesReadPermissions = useGetUserCasesPermissions()?.read; + + // build a list of tabs to exclude + const tabsToExclude = new Set<string>([ + ...(!hasCasesReadPermissions ? [SecurityPageName.case] : []), + ]); + + // include the tab if it is not in the set of excluded ones + const tabsToDisplay = pickBy((_, key) => !tabsToExclude.has(key), navTabs); + + return usePrimaryNavigation({ + query: urlState.query, + filters: urlState.filters, + navTabs: tabsToDisplay, + pageName, + sourcerer: urlState.sourcerer, + savedQuery: urlState.savedQuery, + timeline: urlState.timeline, + timerange: urlState.timerange, + }); +}; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/types.ts new file mode 100644 index 0000000000000..f639b8a37f0da --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/types.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. + */ + +import { TabNavigationProps } from '../tab_navigation/types'; + +export type PrimaryNavigationItemsProps = Omit< + TabNavigationProps, + 'pathName' | 'pageName' | 'tabName' +> & { selectedTabId: string }; + +export type PrimaryNavigationProps = Omit<TabNavigationProps, 'pathName' | 'tabName'>; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx new file mode 100644 index 0000000000000..42ca7f4c65460 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { APP_ID } from '../../../../../common/constants'; +import { track, METRIC_TYPE, TELEMETRY_EVENT } from '../../../lib/telemetry'; +import { getSearch } from '../helpers'; +import { PrimaryNavigationItemsProps } from './types'; +import { useKibana } from '../../../lib/kibana'; + +export const usePrimaryNavigationItems = ({ + filters, + navTabs, + query, + savedQuery, + selectedTabId, + sourcerer, + timeline, + timerange, +}: PrimaryNavigationItemsProps) => { + const { navigateToApp, getUrlForApp } = useKibana().services.application; + + const navItems = Object.values(navTabs).map((tab) => { + const { id, name, disabled } = tab; + const isSelected = selectedTabId === id; + const urlSearch = getSearch(tab, { + filters, + query, + savedQuery, + sourcerer, + timeline, + timerange, + }); + + const handleClick = (ev: React.MouseEvent) => { + ev.preventDefault(); + navigateToApp(`${APP_ID}:${id}`, { path: urlSearch }); + track(METRIC_TYPE.CLICK, `${TELEMETRY_EVENT.TAB_CLICKED}${id}`); + }; + + const appHref = getUrlForApp(`${APP_ID}:${id}`, { path: urlSearch }); + + return { + 'data-href': appHref, + 'data-test-subj': `navigation-${id}`, + disabled, + href: appHref, + id, + isSelected, + name, + onClick: handleClick, + }; + }); + + return [ + { + id: APP_ID, // TODO: When separating into sub-sections (detect, explore, investigate). Those names can also serve as the section id + items: navItems, + name: '', + }, + ]; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx new file mode 100644 index 0000000000000..390f44b48b0b1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getOr } from 'lodash/fp'; +import { useEffect, useState, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { PrimaryNavigationProps } from './types'; +import { usePrimaryNavigationItems } from './use_navigation_items'; +import { KibanaPageTemplateProps } from '../../../../../../../../src/plugins/kibana_react/public'; + +const translatedNavTitle = i18n.translate('xpack.securitySolution.navigation.mainLabel', { + defaultMessage: 'Security', +}); + +export const usePrimaryNavigation = ({ + filters, + query, + navTabs, + pageName, + savedQuery, + sourcerer, + timeline, + timerange, +}: PrimaryNavigationProps): KibanaPageTemplateProps['solutionNav'] => { + const mapLocationToTab = useCallback( + (): string => + getOr( + '', + 'id', + Object.values(navTabs).find((item) => pageName === item.id && item.pageId == null) + ), + [pageName, navTabs] + ); + + const [selectedTabId, setSelectedTabId] = useState(mapLocationToTab()); + + useEffect(() => { + const currentTabSelected = mapLocationToTab(); + + if (currentTabSelected !== selectedTabId) { + setSelectedTabId(currentTabSelected); + } + + // we do need navTabs in case the selectedTabId appears after initial load (ex. checking permissions for anomalies) + }, [pageName, navTabs, mapLocationToTab, selectedTabId]); + + const navItems = usePrimaryNavigationItems({ + filters, + navTabs, + query, + savedQuery, + selectedTabId, + sourcerer, + timeline, + timerange, + }); + + return { + name: translatedNavTitle, + icon: 'logoSecurity', + items: navItems, + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/page/index.tsx b/x-pack/plugins/security_solution/public/common/components/page/index.tsx index 30b89086fb99c..051c1bd8ae5cb 100644 --- a/x-pack/plugins/security_solution/public/common/components/page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/page/index.tsx @@ -5,14 +5,10 @@ * 2.0. */ -import { EuiBadge, EuiDescriptionList, EuiFlexGroup, EuiIcon, EuiPage } from '@elastic/eui'; +import { EuiBadge, EuiDescriptionList, EuiFlexGroup, EuiIcon } from '@elastic/eui'; import styled, { createGlobalStyle } from 'styled-components'; -import { - GLOBAL_HEADER_HEIGHT, - FULL_SCREEN_TOGGLED_CLASS_NAME, - SCROLLING_DISABLED_CLASS_NAME, -} from '../../../../common/constants'; +import { FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants'; export const SecuritySolutionAppWrapper = styled.div` display: flex; @@ -27,25 +23,6 @@ SecuritySolutionAppWrapper.displayName = 'SecuritySolutionAppWrapper'; and `EuiPopover`, `EuiToolTip` global styles */ export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimary: string } } }>` - // fixes double scrollbar on views with EventsTable - #kibana-body { - overflow: hidden; - } - - div.kbnAppWrapper { - background-color: rgba(0,0,0,0); - } - - div.application { - background-color: rgba(0,0,0,0); - - // Security App wrapper - > div { - display: flex; - flex: 1 1 auto; - } - } - .euiPopover__panel.euiPopover__panel-isOpen { z-index: 9900 !important; min-width: 24px; @@ -82,10 +59,6 @@ export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimar ${({ theme }) => `background-color: ${theme.eui.euiColorPrimary} !important`}; } - .${SCROLLING_DISABLED_CLASS_NAME} ${SecuritySolutionAppWrapper} { - max-height: calc(100vh - ${GLOBAL_HEADER_HEIGHT}px); - } - /* EuiScreenReaderOnly has a default 1px height and width. These extra pixels were adding additional height to every table row in the alerts table on the @@ -122,96 +95,6 @@ export const DescriptionListStyled = styled(EuiDescriptionList)` DescriptionListStyled.displayName = 'DescriptionListStyled'; -export const PageContainer = styled.div` - display: flex; - flex-direction: column; - align-items: stretch; - background-color: ${(props) => props.theme.eui.euiColorEmptyShade}; - height: 100%; - padding: 1rem; - overflow: hidden; - margin: 0px; -`; - -PageContainer.displayName = 'PageContainer'; - -export const PageContent = styled.div` - flex: 1 1 auto; - height: 100%; - position: relative; - overflow-y: hidden; - background-color: ${(props) => props.theme.eui.euiColorEmptyShade}; - margin-top: 62px; -`; - -PageContent.displayName = 'PageContent'; - -export const FlexPage = styled(EuiPage)` - flex: 1 0 0; -`; - -FlexPage.displayName = 'FlexPage'; - -export const PageHeader = styled.div` - background-color: ${(props) => props.theme.eui.euiColorEmptyShade}; - display: flex; - user-select: none; - padding: 1rem 1rem 0rem 1rem; - width: 100vw; - position: fixed; -`; - -PageHeader.displayName = 'PageHeader'; - -export const FooterContainer = styled.div` - flex: 0; - bottom: 0; - color: #666; - left: 0; - position: fixed; - text-align: left; - user-select: none; - width: 100%; - background-color: #f5f7fa; - padding: 16px; - border-top: 1px solid #d3dae6; -`; - -FooterContainer.displayName = 'FooterContainer'; - -export const PaneScrollContainer = styled.div` - height: 100%; - overflow-y: scroll; - > div:last-child { - margin-bottom: 3rem; - } -`; - -PaneScrollContainer.displayName = 'PaneScrollContainer'; - -export const Pane = styled.div` - height: 100%; - overflow: hidden; - user-select: none; -`; - -Pane.displayName = 'Pane'; - -export const PaneHeader = styled.div` - display: flex; -`; - -PaneHeader.displayName = 'PaneHeader'; - -export const Pane1FlexContent = styled.div` - display: flex; - flex-direction: row; - flex-wrap: wrap; - height: 100%; -`; - -Pane1FlexContent.displayName = 'Pane1FlexContent'; - export const CountBadge = (styled(EuiBadge)` margin-left: 5px; ` as unknown) as typeof EuiBadge; diff --git a/x-pack/plugins/security_solution/public/common/components/page_wrapper/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/page_wrapper/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..5da587f23693b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/page_wrapper/__snapshots__/index.test.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SecuritySolutionPageWrapper it renders 1`] = ` +<Memo(SecuritySolutionPageWrapperComponent)> + <p> + Test page + </p> +</Memo(SecuritySolutionPageWrapperComponent)> +`; diff --git a/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/page_wrapper/index.test.tsx similarity index 65% rename from x-pack/plugins/security_solution/public/common/components/wrapper_page/index.test.tsx rename to x-pack/plugins/security_solution/public/common/components/page_wrapper/index.test.tsx index 3ec1e44205dd3..f6ebf2a90abb4 100644 --- a/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/page_wrapper/index.test.tsx @@ -9,18 +9,18 @@ import { shallow } from 'enzyme'; import React from 'react'; import { TestProviders } from '../../mock'; -import { WrapperPage } from './index'; +import { SecuritySolutionPageWrapper } from './index'; -describe('WrapperPage', () => { +describe('SecuritySolutionPageWrapper', () => { test('it renders', () => { const wrapper = shallow( <TestProviders> - <WrapperPage> + <SecuritySolutionPageWrapper> <p>{'Test page'}</p> - </WrapperPage> + </SecuritySolutionPageWrapper> </TestProviders> ); - expect(wrapper.find('Memo(WrapperPageComponent)')).toMatchSnapshot(); + expect(wrapper.find('Memo(SecuritySolutionPageWrapperComponent)')).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx b/x-pack/plugins/security_solution/public/common/components/page_wrapper/index.tsx similarity index 68% rename from x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx rename to x-pack/plugins/security_solution/public/common/components/page_wrapper/index.tsx index a3eb76a2728bf..82e0ded264b06 100644 --- a/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/page_wrapper/index.tsx @@ -15,30 +15,26 @@ import { gutterTimeline } from '../../lib/helpers'; import { AppGlobalStyle } from '../page/index'; const Wrapper = styled.div` - padding: ${(props) => `${props.theme.eui.paddingSizes.l}`}; - - &.siemWrapperPage--fullHeight { + &.securitySolutionWrapper--fullHeight { height: 100%; display: flex; flex-direction: column; flex: 1 1 auto; } - - &.siemWrapperPage--noPadding { + &.securitySolutionWrapper--noPadding { padding: 0; display: flex; flex-direction: column; flex: 1 1 auto; } - - &.siemWrapperPage--withTimeline { + &.securitySolutionWrapper--withTimeline { padding-bottom: ${gutterTimeline}; } `; Wrapper.displayName = 'Wrapper'; -interface WrapperPageProps { +interface SecuritySolutionPageWrapperProps { children: React.ReactNode; restrictWidth?: boolean | number | string; style?: Record<string, string>; @@ -46,24 +42,19 @@ interface WrapperPageProps { noTimeline?: boolean; } -const WrapperPageComponent: React.FC<WrapperPageProps & CommonProps> = ({ - children, - className, - style, - noPadding, - noTimeline, - ...otherProps -}) => { +const SecuritySolutionPageWrapperComponent: React.FC< + SecuritySolutionPageWrapperProps & CommonProps +> = ({ children, className, style, noPadding, noTimeline, ...otherProps }) => { const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); useEffect(() => { setGlobalFullScreen(false); // exit full screen mode on page load }, [setGlobalFullScreen]); const classes = classNames(className, { - siemWrapperPage: true, - 'siemWrapperPage--noPadding': noPadding, - 'siemWrapperPage--withTimeline': !noTimeline, - 'siemWrapperPage--fullHeight': globalFullScreen, + securitySolutionWrapper: true, + 'securitySolutionWrapper--noPadding': noPadding, + 'securitySolutionWrapper--withTimeline': !noTimeline, + 'securitySolutionWrapper--fullHeight': globalFullScreen, }); return ( @@ -74,4 +65,4 @@ const WrapperPageComponent: React.FC<WrapperPageProps & CommonProps> = ({ ); }; -export const WrapperPage = React.memo(WrapperPageComponent); +export const SecuritySolutionPageWrapper = React.memo(SecuritySolutionPageWrapperComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/panel/index.tsx b/x-pack/plugins/security_solution/public/common/components/panel/index.tsx index 652d22409cb0c..802fd4c7f44a6 100644 --- a/x-pack/plugins/security_solution/public/common/components/panel/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/panel/index.tsx @@ -25,7 +25,7 @@ import { EuiPanel } from '@elastic/eui'; * Ref: https://www.styled-components.com/docs/faqs#why-am-i-getting-html-attribute-warnings * Ref: https://reactjs.org/blog/2017/09/08/dom-attributes-in-react-16.html */ -export const Panel = styled(({ loading, ...props }) => <EuiPanel {...props} />)` +export const Panel = styled(({ loading, ...props }) => <EuiPanel {...props} hasBorder />)` position: relative; ${({ loading }) => loading && diff --git a/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx b/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx index 5b4a8f67aa361..2d8d55a5c943f 100644 --- a/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx @@ -222,7 +222,7 @@ export const StatItemsComponent = React.memo<StatItemsProps>( return ( <FlexItem grow={grow} data-test-subj={`stat-${statKey}`}> <InspectButtonContainer> - <EuiPanel> + <EuiPanel hasBorder> <EuiFlexGroup gutterSize={'none'}> <EuiFlexItem> <EuiTitle size="xxxs"> diff --git a/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx index 8c2b97a4b8b38..c122138f9547a 100644 --- a/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx @@ -18,6 +18,9 @@ import { import { TestProviders } from '../../mock'; import { getEmptyValue } from '../empty_value'; import { useMountAppended } from '../../utils/use_mount_appended'; + +jest.mock('../../lib/kibana'); + describe('Table Helpers', () => { const items = ['item1', 'item2', 'item3']; const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts b/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts index 70e095c88576f..04ceafde7ef74 100644 --- a/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts +++ b/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts @@ -8,10 +8,10 @@ import type React from 'react'; import uuid from 'uuid'; import { isError } from 'lodash/fp'; +import { isAppError } from '@kbn/securitysolution-t-grid'; import { AppToast, ActionToaster } from './'; import { isToasterError } from './errors'; -import { isAppError } from '../../utils/api'; /** * Displays an error toast for the provided title and message diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx index 005602738f376..4f6834e84d83a 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx @@ -18,17 +18,11 @@ import { createSecuritySolutionStorageMock, mockIndexPattern, } from '../../mock'; -import { FilterManager } from '../../../../../../../src/plugins/data/public'; import { createStore, State } from '../../store'; import { Props } from './top_n'; import { StatefulTopN } from '.'; -import { - ManageGlobalTimeline, - getTimelineDefaults, -} from '../../../timelines/components/manage_timeline'; import { TimelineId } from '../../../../common/types/timeline'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -45,8 +39,6 @@ jest.mock('../link_to'); jest.mock('../../lib/kibana'); jest.mock('../../../timelines/store/timeline/actions'); -const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; - const field = 'process.name'; const value = 'nice'; @@ -175,9 +167,7 @@ describe('StatefulTopN', () => { beforeEach(() => { wrapper = mount( <TestProviders store={store}> - <ManageGlobalTimeline> - <StatefulTopN {...testProps} /> - </ManageGlobalTimeline> + <StatefulTopN {...testProps} /> </TestProviders> ); }); @@ -244,26 +234,16 @@ describe('StatefulTopN', () => { }); describe('rendering in a timeline context', () => { - let filterManager: FilterManager; let wrapper: ReactWrapper; beforeEach(() => { - filterManager = new FilterManager(mockUiSettingsForFilterManager); - const manageTimelineForTesting = { - [TimelineId.active]: { - ...getTimelineDefaults(TimelineId.active), - filterManager, - }, - }; testProps = { ...testProps, timelineId: TimelineId.active, }; wrapper = mount( <TestProviders store={store}> - <ManageGlobalTimeline manageTimelineForTesting={manageTimelineForTesting}> - <StatefulTopN {...testProps} /> - </ManageGlobalTimeline> + <StatefulTopN {...testProps} /> </TestProviders> ); }); @@ -320,25 +300,13 @@ describe('StatefulTopN', () => { }); describe('rendering in a NON-active timeline context', () => { test(`defaults to the 'Alert events' option when rendering in a NON-active timeline context (e.g. the Alerts table on the Detections page) when 'documentType' from 'useTimelineTypeContext()' is 'alerts'`, async () => { - const filterManager = new FilterManager(mockUiSettingsForFilterManager); - - const manageTimelineForTesting = { - [TimelineId.active]: { - ...getTimelineDefaults(TimelineId.active), - filterManager, - documentType: 'alerts', - }, - }; - testProps = { ...testProps, timelineId: TimelineId.detectionsPage, }; const wrapper = mount( <TestProviders store={store}> - <ManageGlobalTimeline manageTimelineForTesting={manageTimelineForTesting}> - <StatefulTopN {...testProps} /> - </ManageGlobalTimeline> + <StatefulTopN {...testProps} /> </TestProviders> ); await waitFor(() => { diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx index a2d5076031328..8a7c6bcb4a9b5 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx @@ -29,7 +29,6 @@ import { SecurityPageName } from '../../../../common/constants'; export const dispatchSetInitialStateFromUrl = ( dispatch: Dispatch ): DispatchSetInitialStateFromUrl => ({ - detailName, filterManager, indexPattern, pageName, diff --git a/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx b/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx index a8868436d9689..c867862e690bd 100644 --- a/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/with_hover_actions/index.tsx @@ -6,13 +6,13 @@ */ import { EuiPopover } from '@elastic/eui'; +import { + HOVER_ACTIONS_ALWAYS_SHOW_CLASS_NAME, + IS_DRAGGING_CLASS_NAME, +} from '@kbn/securitysolution-t-grid'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; -import { IS_DRAGGING_CLASS_NAME } from '../drag_and_drop/helpers'; - -export const HOVER_ACTIONS_ALWAYS_SHOW_CLASS_NAME = 'hover-actions-always-show'; - /** * To avoid expensive changes to the DOM, delay showing the popover menu */ diff --git a/x-pack/plugins/security_solution/public/common/components/wrapper_page/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/wrapper_page/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 89ed2f45a6bf1..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/wrapper_page/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`WrapperPage it renders 1`] = ` -<Memo(WrapperPageComponent)> - <p> - Test page - </p> -</Memo(WrapperPageComponent)> -`; diff --git a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts index 3e690e50b04b1..4f558412576b4 100644 --- a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts @@ -83,7 +83,7 @@ export const useTimelineLastEventTime = ({ TimelineEventsLastEventTimeRequestOptions, TimelineEventsLastEventTimeStrategyResponse >(request, { - strategy: 'securitySolutionTimelineSearchStrategy', + strategy: 'timelineSearchStrategy', abortSignal: abortCtrl.current.signal, }) .subscribe({ diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx index 1c17f95bb6ba0..3bc92dafd351f 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx @@ -151,7 +151,7 @@ export const useFetchIndex = ( { indices: iNames, onlyCheckIfIndicesExist }, { abortSignal: abortCtrl.current.signal, - strategy: 'securitySolutionIndexFields', + strategy: 'indexFields', } ) .subscribe({ @@ -235,7 +235,7 @@ export const useIndexFields = (sourcererScopeName: SourcererScopeName) => { { indices: indicesName, onlyCheckIfIndicesExist: false }, { abortSignal: abortCtrl.current.signal, - strategy: 'securitySolutionIndexFields', + strategy: 'indexFields', } ) .subscribe({ diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts index da6b41080c1c7..6c5caa25a1f96 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.test.ts @@ -7,9 +7,10 @@ import { renderHook } from '@testing-library/react-hooks'; import { IEsError } from 'src/plugins/data/public'; +import { KibanaError, SecurityAppError } from '@kbn/securitysolution-t-grid'; import { useToasts } from '../lib/kibana'; -import { KibanaError, SecurityAppError } from '../utils/api'; + import { appErrorToErrorStack, convertErrorToEnumerable, diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts index 61b20e137f870..0c2721e6ad416 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts @@ -7,11 +7,17 @@ import { useCallback, useRef } from 'react'; import { isString } from 'lodash/fp'; +import { + AppError, + isAppError, + isKibanaError, + isSecurityAppError, +} from '@kbn/securitysolution-t-grid'; + import { IEsError, isEsError } from '../../../../../../src/plugins/data/public'; import { ErrorToastOptions, ToastsStart, Toast } from '../../../../../../src/core/public'; import { useToasts } from '../lib/kibana'; -import { AppError, isAppError, isKibanaError, isSecurityAppError } from '../utils/api'; export type UseAppToasts = Pick<ToastsStart, 'addSuccess' | 'addWarning'> & { api: ToastsStart; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_global_header_portal.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_global_header_portal.tsx index 5b5877a4c2ded..8e8d73ff12849 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_global_header_portal.tsx +++ b/x-pack/plugins/security_solution/public/common/hooks/use_global_header_portal.tsx @@ -11,10 +11,10 @@ import { createPortalNode } from 'react-reverse-portal'; /** * A singleton portal for rendering content in the global header */ -const globalHeaderPortalNodeSingleton = createPortalNode(); +const globalKQLHeaderPortalNodeSingleton = createPortalNode(); export const useGlobalHeaderPortal = () => { - const [globalHeaderPortalNode] = useState(globalHeaderPortalNodeSingleton); + const [globalKQLHeaderPortalNode] = useState(globalKQLHeaderPortalNodeSingleton); - return { globalHeaderPortalNode }; + return { globalKQLHeaderPortalNode }; }; diff --git a/x-pack/plugins/security_solution/public/common/lib/clipboard/with_copy_to_clipboard.tsx b/x-pack/plugins/security_solution/public/common/lib/clipboard/with_copy_to_clipboard.tsx index 1baa57166de3f..2f5afc8a44489 100644 --- a/x-pack/plugins/security_solution/public/common/lib/clipboard/with_copy_to_clipboard.tsx +++ b/x-pack/plugins/security_solution/public/common/lib/clipboard/with_copy_to_clipboard.tsx @@ -6,9 +6,10 @@ */ import { EuiToolTip } from '@elastic/eui'; + import React from 'react'; -import { TooltipWithKeyboardShortcut } from '../../components/accessibility/tooltip_with_keyboard_shortcut'; +import { TooltipWithKeyboardShortcut } from '../../components/accessibility'; import * as i18n from '../../components/drag_and_drop/translations'; import { Clipboard } from './clipboard'; diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts index eb0ae1ae1dee9..09c3d2537e272 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts @@ -6,6 +6,10 @@ */ import { notificationServiceMock } from '../../../../../../../../src/core/public/mocks'; + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createTGridMocks } from '../../../../../../timelines/public/mock'; + import { createKibanaContextProviderMock, createUseUiSettingMock, @@ -30,14 +34,24 @@ export const useKibana = jest.fn().mockReturnValue({ })), })), }, + query: { + ...mockStartServicesMock.data.query, + filterManager: { + addFilters: jest.fn(), + getFilters: jest.fn(), + getUpdates$: jest.fn().mockReturnValue({ subscribe: jest.fn() }), + setAppFilters: jest.fn(), + }, + }, }, + timelines: createTGridMocks(), }, }); export const useUiSetting = jest.fn(createUseUiSettingMock()); export const useUiSetting$ = jest.fn(createUseUiSetting$Mock()); export const useHttp = jest.fn().mockReturnValue(createStartServicesMock().http); export const useTimeZone = jest.fn(); -export const useDateFormat = jest.fn(); +export const useDateFormat = jest.fn().mockReturnValue('MMM D, YYYY @ HH:mm:ss.SSS'); export const useBasePath = jest.fn(() => '/test/base/path'); export const useToasts = jest .fn() diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index 557c04e4e8a47..316f8b6214d1e 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -43,6 +43,7 @@ export const mockGlobalState: State = { trustedAppsByPolicyEnabled: false, metricsEntitiesEnabled: false, ruleRegistryEnabled: false, + tGridEnabled: false, }, }, hosts: { diff --git a/x-pack/plugins/security_solution/public/common/mock/header.ts b/x-pack/plugins/security_solution/public/common/mock/header.ts index ae7d3c9e576a8..029ddb00d1832 100644 --- a/x-pack/plugins/security_solution/public/common/mock/header.ts +++ b/x-pack/plugins/security_solution/public/common/mock/header.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ColumnHeaderOptions } from '../../timelines/store/timeline/model'; +import { ColumnHeaderOptions } from '../../../common'; import { defaultColumnHeaderType } from '../../timelines/components/timeline/body/column_headers/default_headers'; import { DEFAULT_COLUMN_MIN_WIDTH, diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_timeline_control_columns.tsx b/x-pack/plugins/security_solution/public/common/mock/mock_timeline_control_columns.tsx index 7604732f90203..7dae3e671d271 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_timeline_control_columns.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/mock_timeline_control_columns.tsx @@ -15,7 +15,7 @@ import { EuiPopoverTitle, EuiSpacer, } from '@elastic/eui'; -import { ControlColumnProps } from '../../timelines/components/timeline/body/control_columns'; +import { ControlColumnProps } from '../../../common/types/timeline'; const SelectionHeaderCell = () => { return ( diff --git a/x-pack/plugins/security_solution/public/common/mock/utils.ts b/x-pack/plugins/security_solution/public/common/mock/utils.ts index 30951b81611db..e0f8e651a5821 100644 --- a/x-pack/plugins/security_solution/public/common/mock/utils.ts +++ b/x-pack/plugins/security_solution/public/common/mock/utils.ts @@ -5,12 +5,20 @@ * 2.0. */ +import { AnyAction, Reducer } from 'redux'; +import reduceReducers from 'reduce-reducers'; + +import { tGridReducer } from '../../../../timelines/public'; + import { hostsReducer } from '../../hosts/store'; import { networkReducer } from '../../network/store'; import { timelineReducer } from '../../timelines/store/timeline/reducer'; import { managementReducer } from '../../management/store/reducer'; import { ManagementPluginReducer } from '../../management'; import { SubPluginsInitReducer } from '../store'; +import { mockGlobalState } from './global_state'; +import { TimelineState } from '../../timelines/store/timeline/types'; +import { defaultHeaders } from '../../timelines/components/timeline/body/column_headers/default_headers'; interface Global extends NodeJS.Global { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -19,10 +27,32 @@ interface Global extends NodeJS.Global { export const globalNode: Global = global; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const combineTimelineReducer = reduceReducers<any>( + { + ...mockGlobalState.timeline, + timelineById: { + ...mockGlobalState.timeline.timelineById, + test: { + ...mockGlobalState.timeline.timelineById.test, + defaultColumns: defaultHeaders, + loadingText: 'events', + footerText: 'events', + documentType: '', + selectAll: false, + queryFields: [], + unit: (n: number) => n, + }, + }, + }, + tGridReducer, + timelineReducer +) as Reducer<TimelineState, AnyAction>; + export const SUB_PLUGINS_REDUCER: SubPluginsInitReducer = { hosts: hostsReducer, network: networkReducer, - timeline: timelineReducer, + timeline: combineTimelineReducer, /** * These state's are wrapped in `Immutable`, but for compatibility with the overall app architecture, * they are cast to mutable versions here. diff --git a/x-pack/plugins/security_solution/public/common/store/inputs/model.ts b/x-pack/plugins/security_solution/public/common/store/inputs/model.ts index e784f6cebae17..5791a4940cbed 100644 --- a/x-pack/plugins/security_solution/public/common/store/inputs/model.ts +++ b/x-pack/plugins/security_solution/public/common/store/inputs/model.ts @@ -60,6 +60,7 @@ export interface GlobalGenericQuery { isInspected: boolean; loading: boolean; selectedInspectIndex: number; + invalidKqlQuery?: Error; } export interface GlobalGraphqlQuery extends GlobalGenericQuery { diff --git a/x-pack/plugins/security_solution/public/common/store/types.ts b/x-pack/plugins/security_solution/public/common/store/types.ts index fbf4caad9793d..21e833abe1f9b 100644 --- a/x-pack/plugins/security_solution/public/common/store/types.ts +++ b/x-pack/plugins/security_solution/public/common/store/types.ts @@ -37,18 +37,6 @@ export type StoreState = HostsPluginState & */ export type State = CombinedState<StoreState>; -export type KueryFilterQueryKind = 'kuery' | 'lucene' | 'eql'; - -export interface KueryFilterQuery { - kind: KueryFilterQueryKind; - expression: string; -} - -export interface SerializedFilterQuery { - kuery: KueryFilterQuery | null; - serializedQuery: string; -} - /** * like redux's `MiddlewareAPI` but `getState` returns an `Immutable` version of * state and `dispatch` accepts `Immutable` versions of actions. diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx index 91b5a10684405..d766104e356eb 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx @@ -298,7 +298,7 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>( return ( <InspectButtonContainer data-test-subj="alerts-histogram-panel" show={!isInitialLoading}> - <StyledEuiPanel height={panelHeight}> + <StyledEuiPanel height={panelHeight} hasBorder> <HeaderSection id={uniqueQueryId} title={titleText} diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx index e5cefca66d0fd..601e0509009ce 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx @@ -14,6 +14,7 @@ import { i18n } from '@kbn/i18n'; import type { Filter } from '../../../../../../../src/plugins/data/common/es_query/filters'; import { + KueryFilterQueryKind, TimelineId, TimelineResult, TimelineStatus, @@ -44,7 +45,6 @@ import { replaceTemplateFieldFromMatchFilters, replaceTemplateFieldFromDataProviders, } from './helpers'; -import { KueryFilterQueryKind } from '../../../common/store'; import { DataProvider, QueryOperator, @@ -399,7 +399,7 @@ export const sendAlertToTimelineAction = async ({ factoryQueryType: TimelineEventsQueries.details, }, { - strategy: 'securitySolutionTimelineSearchStrategy', + strategy: 'timelineSearchStrategy', } ) .toPromise(), diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx index 4ca2980dc74e5..a3d3bf4834376 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx @@ -11,6 +11,7 @@ import { shallow, mount } from 'enzyme'; import { AlertsUtilityBar, AlertsUtilityBarProps } from './index'; import { TestProviders } from '../../../../common/mock/test_providers'; +jest.useFakeTimers(); jest.mock('../../../../common/lib/kibana'); describe('AlertsUtilityBar', () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index 02a815bc59f3b..9a142f6cba247 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -6,11 +6,11 @@ */ import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; -import { RowRendererId } from '../../../../common/types/timeline'; +import { ColumnHeaderOptions, RowRendererId } from '../../../../common/types/timeline'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; -import { ColumnHeaderOptions, SubsetTimelineModel } from '../../../timelines/store/timeline/model'; +import { SubsetTimelineModel } from '../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { columns } from '../../configurations/security_solution_detections/columns'; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index f20754fc446d6..7980160fea76c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -8,11 +8,11 @@ import { EuiPanel, EuiLoadingContent } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import { connect, ConnectedProps, useDispatch } from 'react-redux'; import { Dispatch } from 'redux'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { Filter, esQuery } from '../../../../../../../src/plugins/data/public'; -import { TimelineIdLiteral } from '../../../../common/types/timeline'; +import { RowRendererId, TimelineIdLiteral } from '../../../../common/types/timeline'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; import { StatefulEventsViewer } from '../../../common/components/events_viewer'; import { HeaderSection } from '../../../common/components/header_section'; @@ -23,8 +23,6 @@ import { inputsSelectors, State, inputsModel } from '../../../common/store'; import { timelineActions, timelineSelectors } from '../../../timelines/store/timeline'; import { TimelineModel } from '../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; -import { useManageTimeline } from '../../../timelines/components/manage_timeline'; - import { updateAlertStatusAction } from './actions'; import { requiredFieldsForActions, @@ -95,6 +93,7 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({ timelineId, to, }) => { + const dispatch = useDispatch(); const [showClearSelectionAction, setShowClearSelectionAction] = useState(false); const [filterGroup, setFilterGroup] = useState<Status>(FILTER_OPEN); const { @@ -106,7 +105,6 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({ const kibana = useKibana(); const [, dispatchToaster] = useStateToaster(); const { addWarning } = useAppToasts(); - const { initializeTimeline, setSelectAll } = useManageTimeline(); // TODO: Once we are past experimental phase this code should be removed const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled'); @@ -195,14 +193,16 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({ // Catches state change isSelectAllChecked->false upon user selection change to reset utility bar useEffect(() => { if (isSelectAllChecked) { - setSelectAll({ - id: timelineId, - selectAll: false, - }); + dispatch( + timelineActions.setTGridSelectAll({ + id: timelineId, + selectAll: false, + }) + ); } else { setShowClearSelectionAction(false); } - }, [isSelectAllChecked, setSelectAll, timelineId]); + }, [dispatch, isSelectAllChecked, timelineId]); // Callback for when open/closed filter changes const onFilterGroupChangedCallback = useCallback( @@ -218,23 +218,27 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({ // Callback for clearing entire selection from utility bar const clearSelectionCallback = useCallback(() => { clearSelected!({ id: timelineId }); - setSelectAll({ - id: timelineId, - selectAll: false, - }); + dispatch( + timelineActions.setTGridSelectAll({ + id: timelineId, + selectAll: false, + }) + ); setShowClearSelectionAction(false); - }, [clearSelected, setSelectAll, setShowClearSelectionAction, timelineId]); + }, [clearSelected, dispatch, timelineId]); // Callback for selecting all events on all pages from utility bar // Dispatches to stateful_body's selectAll via TimelineTypeContext props // as scope of response data required to actually set selectedEvents const selectAllOnAllPagesCallback = useCallback(() => { - setSelectAll({ - id: timelineId, - selectAll: true, - }); + dispatch( + timelineActions.setTGridSelectAll({ + id: timelineId, + selectAll: true, + }) + ); setShowClearSelectionAction(true); - }, [setSelectAll, setShowClearSelectionAction, timelineId]); + }, [dispatch, timelineId]); const updateAlertsStatusCallback: UpdateAlertsStatusCallback = useCallback( async ( @@ -330,22 +334,22 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({ : alertsDefaultModel; useEffect(() => { - initializeTimeline({ - defaultModel: { - ...defaultTimelineModel, - columns, - }, - documentType: i18n.ALERTS_DOCUMENT_TYPE, - filterManager, - footerText: i18n.TOTAL_COUNT_OF_ALERTS, - id: timelineId, - loadingText: i18n.LOADING_ALERTS, - selectAll: false, - queryFields: requiredFieldsForActions, - title: '', - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + dispatch( + timelineActions.initializeTGridSettings({ + defaultColumns: columns, + documentType: i18n.ALERTS_DOCUMENT_TYPE, + excludedRowRendererIds: defaultTimelineModel.excludedRowRendererIds as RowRendererId[], + filterManager, + footerText: i18n.TOTAL_COUNT_OF_ALERTS, + id: timelineId, + loadingText: i18n.LOADING_ALERTS, + selectAll: false, + queryFields: requiredFieldsForActions, + title: '', + showCheckboxes: true, + }) + ); + }, [dispatch, defaultTimelineModel, filterManager, timelineId]); const headerFilterGroup = useMemo( () => <AlertsTableFilterGroup onFilterGroupChanged={onFilterGroupChangedCallback} />, @@ -354,7 +358,7 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({ if (loading || indexPatternsLoading || isEmpty(selectedPatterns)) { return ( - <EuiPanel> + <EuiPanel hasBorder> <HeaderSection title="" /> <EuiLoadingContent data-test-subj="loading-alerts-panel" /> </EuiPanel> diff --git a/x-pack/plugins/security_solution/public/detections/components/callouts/need_admin_for_update_callout/index.tsx b/x-pack/plugins/security_solution/public/detections/components/callouts/need_admin_for_update_callout/index.tsx index fd0be8e002193..3b41c9280998b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/callouts/need_admin_for_update_callout/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/callouts/need_admin_for_update_callout/index.tsx @@ -6,6 +6,7 @@ */ import React, { memo } from 'react'; +import { EuiSpacer } from '@elastic/eui'; import { CallOutMessage, CallOutPersistentSwitcher } from '../../../../common/components/callouts'; import { useUserData } from '../../user_info'; @@ -33,20 +34,22 @@ const needAdminForUpdateRulesMessage: CallOutMessage = { * hasIndexManage is also true, then the user should be performing the update on the page which is * why we do not show it for that condition. */ -const NeedAdminForUpdateCallOutComponent = (): JSX.Element => { +const NeedAdminForUpdateCallOutComponent = (): JSX.Element | null => { const [{ signalIndexMappingOutdated, hasIndexManage }] = useUserData(); const signalIndexMappingIsOutdated = signalIndexMappingOutdated != null && signalIndexMappingOutdated; const userDoesntHaveIndexManage = hasIndexManage != null && !hasIndexManage; - - return ( - <CallOutPersistentSwitcher - condition={signalIndexMappingIsOutdated && userDoesntHaveIndexManage} - message={needAdminForUpdateRulesMessage} - /> - ); + const shouldShowCallout = signalIndexMappingIsOutdated && userDoesntHaveIndexManage; + + // Passing shouldShowCallout to the condition param will end up with an unecessary spacer being rendered + return shouldShowCallout ? ( + <> + <CallOutPersistentSwitcher condition={true} message={needAdminForUpdateRulesMessage} /> + <EuiSpacer size="l" /> + </> + ) : null; }; export const NeedAdminForUpdateRulesCallOut = memo(NeedAdminForUpdateCallOutComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/callouts/no_api_integration_callout/index.tsx b/x-pack/plugins/security_solution/public/detections/components/callouts/no_api_integration_callout/index.tsx index f21c66380f30a..7b483930db505 100644 --- a/x-pack/plugins/security_solution/public/detections/components/callouts/no_api_integration_callout/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/callouts/no_api_integration_callout/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiCallOut, EuiButton } from '@elastic/eui'; +import { EuiCallOut, EuiButton, EuiSpacer } from '@elastic/eui'; import React, { memo, useCallback, useState } from 'react'; import * as i18n from './translations'; @@ -15,12 +15,15 @@ const NoApiIntegrationKeyCallOutComponent = () => { const handleCallOut = useCallback(() => setShowCallOut(false), [setShowCallOut]); return showCallOut ? ( - <EuiCallOut title={i18n.NO_API_INTEGRATION_KEY_CALLOUT_TITLE} color="danger" iconType="alert"> - <p>{i18n.NO_API_INTEGRATION_KEY_CALLOUT_MSG}</p> - <EuiButton color="danger" onClick={handleCallOut}> - {i18n.DISMISS_CALLOUT} - </EuiButton> - </EuiCallOut> + <> + <EuiCallOut title={i18n.NO_API_INTEGRATION_KEY_CALLOUT_TITLE} color="danger" iconType="alert"> + <p>{i18n.NO_API_INTEGRATION_KEY_CALLOUT_MSG}</p> + <EuiButton color="danger" onClick={handleCallOut}> + {i18n.DISMISS_CALLOUT} + </EuiButton> + </EuiCallOut> + <EuiSpacer size="l" /> + </> ) : null; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx index a09afa3ca2164..c1078e1ba77e7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule_details/index.tsx @@ -82,7 +82,7 @@ const StepAboutRuleToggleDetailsComponent: React.FC<StepPanelProps> = ({ ); return ( - <MyPanel> + <MyPanel hasBorder> {loading && ( <> <EuiProgress size="xs" color="accent" position="absolute" /> diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_panel/index.tsx index f9e6031d826ca..ac9a153ad76bf 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_panel/index.tsx @@ -24,7 +24,7 @@ const MyPanel = styled(EuiPanel)` MyPanel.displayName = 'MyPanel'; const StepPanelComponent: React.FC<StepPanelProps> = ({ children, loading, title }) => ( - <MyPanel> + <MyPanel hasBorder> {loading && <EuiProgress size="xs" color="accent" position="absolute" />} <HeaderSection title={title} /> {children} diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx index dbad1c57fda77..3d81735122e73 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx @@ -216,7 +216,7 @@ export const ValueListsModalComponent: React.FC<ValueListsModalProps> = ({ <EuiModalBody> <ValueListsForm onSuccess={handleUploadSuccess} onError={handleUploadError} /> <EuiSpacer /> - <EuiPanel> + <EuiPanel hasBorder> <EuiText size="s"> <h2>{i18n.TABLE_TITLE}</h2> </EuiText> diff --git a/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/columns.ts b/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/columns.ts index 8cbb532501a2c..70d2237a535eb 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/columns.ts +++ b/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/columns.ts @@ -6,10 +6,9 @@ */ import { EuiDataGridColumn } from '@elastic/eui'; - +import { ColumnHeaderOptions } from '../../../../../common'; import { defaultColumnHeaderType } from '../../../../timelines/components/timeline/body/column_headers/default_headers'; import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../../../../timelines/components/timeline/body/constants'; -import { ColumnHeaderOptions } from '../../../../timelines/store/timeline/model'; import * as i18n from '../../../components/alerts_table/translations'; diff --git a/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.test.tsx b/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.test.tsx index 9c2114a4ef085..7db75d3a73d90 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/configurations/examples/observablity_alerts/render_cell_value.test.tsx @@ -15,10 +15,12 @@ import { defaultHeaders, mockTimelineData, TestProviders } from '../../../../com import { TimelineNonEcsData } from '../../../../../common/search_strategy/timeline'; import { CellValueElementProps } from '../../../../timelines/components/timeline/cell_rendering'; import { DefaultCellRenderer } from '../../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; -import { ColumnHeaderOptions } from '../../../../timelines/store/timeline/model'; +import { ColumnHeaderOptions } from '../../../../../common'; import { RenderCellValue } from '.'; +jest.mock('../../../../common/lib/kibana/'); + describe('RenderCellValue', () => { const columnId = '@timestamp'; const eventId = '_id-123'; diff --git a/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/columns.ts b/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/columns.ts index 96d2d870b1270..3365ce5432940 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/columns.ts +++ b/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/columns.ts @@ -6,10 +6,9 @@ */ import { EuiDataGridColumn } from '@elastic/eui'; - +import { ColumnHeaderOptions } from '../../../../../common'; import { defaultColumnHeaderType } from '../../../../timelines/components/timeline/body/column_headers/default_headers'; import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../../../../timelines/components/timeline/body/constants'; -import { ColumnHeaderOptions } from '../../../../timelines/store/timeline/model'; import * as i18n from '../../../components/alerts_table/translations'; diff --git a/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.test.tsx b/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.test.tsx index aa4eb543a3d9b..a8f295df2540d 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/configurations/examples/security_solution_rac/render_cell_value.test.tsx @@ -15,9 +15,11 @@ import { defaultHeaders, mockTimelineData, TestProviders } from '../../../../com import { TimelineNonEcsData } from '../../../../../common/search_strategy/timeline'; import { CellValueElementProps } from '../../../../timelines/components/timeline/cell_rendering'; import { DefaultCellRenderer } from '../../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; -import { ColumnHeaderOptions } from '../../../../timelines/store/timeline/model'; import { RenderCellValue } from '.'; +import { ColumnHeaderOptions } from '../../../../../common'; + +jest.mock('../../../../common/lib/kibana/'); describe('RenderCellValue', () => { const columnId = '@timestamp'; diff --git a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts index 23a0740294e84..7f46c839ffe62 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts +++ b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts @@ -6,13 +6,13 @@ */ import { EuiDataGridColumn } from '@elastic/eui'; +import { ColumnHeaderOptions } from '../../../../common'; import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH, } from '../../../timelines/components/timeline/body/constants'; -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import * as i18n from '../../components/alerts_table/translations'; diff --git a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx index 18350c102c049..965ee913a1daa 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/render_cell_value.test.tsx @@ -9,16 +9,18 @@ import { mount } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; import React from 'react'; +import { ColumnHeaderOptions } from '../../../../common'; import { mockBrowserFields } from '../../../common/containers/source/mock'; import { DragDropContextWrapper } from '../../../common/components/drag_and_drop/drag_drop_context_wrapper'; import { defaultHeaders, mockTimelineData, TestProviders } from '../../../common/mock'; import { TimelineNonEcsData } from '../../../../common/search_strategy/timeline'; import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering'; import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { RenderCellValue } from '.'; +jest.mock('../../../common/lib/kibana'); + describe('RenderCellValue', () => { const columnId = '@timestamp'; const eventId = '_id-123'; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx index 84eaf8e3aa93c..6f8d938dd987e 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx @@ -6,13 +6,13 @@ */ import { useEffect, useState } from 'react'; +import { isSecurityAppError } from '@kbn/securitysolution-t-grid'; import { DEFAULT_ALERTS_INDEX } from '../../../../../common/constants'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { createSignalIndex, getSignalIndex } from './api'; import * as i18n from './translations'; -import { isSecurityAppError } from '../../../../common/utils/api'; import { useAlertsPrivileges } from './use_alerts_privileges'; type Func = () => Promise<void>; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx index 8e231f0d1fdbb..d55d171708963 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/lists/use_lists_index.tsx @@ -6,10 +6,9 @@ */ import { useEffect, useState, useCallback } from 'react'; - +import { isSecurityAppError } from '@kbn/securitysolution-t-grid'; import { useReadListIndex, useCreateListIndex } from '@kbn/securitysolution-list-hooks'; import { useHttp, useKibana } from '../../../../common/lib/kibana'; -import { isSecurityAppError } from '../../../../common/utils/api'; import * as i18n from './translations'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { useListsPrivileges } from './use_lists_privileges'; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx index f848b71cf7bd3..4f524886935cd 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx @@ -6,8 +6,8 @@ */ import { useEffect, useRef, useState } from 'react'; +import { isNotFoundError } from '@kbn/securitysolution-t-grid'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; -import { isNotFoundError } from '../../../../common/utils/api'; import { RuleStatusRowItemType } from '../../../pages/detection_engine/rules/all/columns'; import { getRuleStatusById, getRulesStatusByIds } from './api'; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx index 4a39e486b6fd5..abd5a2781c8a7 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.test.tsx @@ -6,11 +6,11 @@ */ import { renderHook, act } from '@testing-library/react-hooks'; +import { SecurityAppError } from '@kbn/securitysolution-t-grid'; import { useRuleWithFallback } from './use_rule_with_fallback'; import * as api from './api'; import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; -import { SecurityAppError } from '../../../../common/utils/api'; jest.mock('./api'); jest.mock('../alerts/api'); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.tsx index 11c30547848c3..da56275280f65 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_with_fallback.tsx @@ -6,9 +6,9 @@ */ import { useCallback, useEffect, useMemo } from 'react'; +import { isNotFoundError } from '@kbn/securitysolution-t-grid'; import { useAsync, withOptionalSignal } from '@kbn/securitysolution-hook-utils'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; -import { isNotFoundError } from '../../../../common/utils/api'; import { useQueryAlerts } from '../alerts/use_query'; import { fetchRuleById } from './api'; import { transformInput } from './transforms'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index 8ae7e4fb2852b..0c12d8256d66d 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -11,18 +11,18 @@ import { noop } from 'lodash/fp'; import React, { useCallback, useMemo, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; +import { isTab } from '../../../../../timelines/public'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector'; import { SecurityPageName } from '../../../app/types'; import { TimelineId } from '../../../../common/types/timeline'; import { useGlobalTime } from '../../../common/containers/use_global_time'; -import { isTab } from '../../../common/components/accessibility/helpers'; import { UpdateDateRange } from '../../../common/components/charts/common'; import { FiltersGlobal } from '../../../common/components/filters_global'; import { getRulesUrl } from '../../../common/components/link_to/redirect_to_detection_engine'; import { SiemSearchBar } from '../../../common/components/search_bar'; -import { WrapperPage } from '../../../common/components/wrapper_page'; +import { SecuritySolutionPageWrapper } from '../../../common/components/page_wrapper'; import { inputsSelectors } from '../../../common/store/inputs'; import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; import { SpyRoute } from '../../../common/utils/route/spy_routes'; @@ -197,22 +197,22 @@ const DetectionEnginePageComponent = () => { if (isUserAuthenticated != null && !isUserAuthenticated && !loading) { return ( - <WrapperPage> + <SecuritySolutionPageWrapper> <DetectionEngineHeaderPage border title={i18n.PAGE_TITLE} /> <DetectionEngineUserUnauthenticated /> - </WrapperPage> + </SecuritySolutionPageWrapper> ); } if (!loading && (isSignalIndexExists === false || needsListsConfiguration)) { return ( - <WrapperPage> + <SecuritySolutionPageWrapper> <DetectionEngineHeaderPage border title={i18n.PAGE_TITLE} /> <DetectionEngineNoIndex needsSignalsIndex={isSignalIndexExists === false} needsListsIndex={needsListsConfiguration} /> - </WrapperPage> + </SecuritySolutionPageWrapper> ); } @@ -228,7 +228,7 @@ const DetectionEnginePageComponent = () => { <SiemSearchBar id="global" indexPattern={indexPattern} /> </FiltersGlobal> - <WrapperPage noPadding={globalFullScreen}> + <SecuritySolutionPageWrapper noPadding={globalFullScreen}> <Display show={!globalFullScreen}> <DetectionEngineHeaderPage subtitle={ @@ -280,13 +280,13 @@ const DetectionEnginePageComponent = () => { onShowOnlyThreatIndicatorAlertsChanged={onShowOnlyThreatIndicatorAlertsCallback} to={to} /> - </WrapperPage> + </SecuritySolutionPageWrapper> </StyledFullHeightContainer> ) : ( - <WrapperPage> + <SecuritySolutionPageWrapper> <DetectionEngineHeaderPage border title={i18n.PAGE_TITLE} /> <OverviewEmpty /> - </WrapperPage> + </SecuritySolutionPageWrapper> )} <SpyRoute pageName={SecurityPageName.detections} /> </> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx index dd3549ea20d36..8cc3113a5706a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx @@ -42,6 +42,9 @@ describe('ExceptionListsTable', () => { addError: jest.fn(), }, }, + timelines: { + getLastUpdated: () => null, + }, }, }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx index 7f734b10fd020..f38bde4839f18 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx @@ -26,7 +26,6 @@ import { Loader } from '../../../../../../common/components/loader'; import { Panel } from '../../../../../../common/components/panel'; import * as i18n from './translations'; import { AllRulesUtilityBar } from '../utility_bar'; -import { LastUpdatedAt } from '../../../../../../common/components/last_updated'; import { AllExceptionListsColumns, getAllExceptionListsColumns } from './columns'; import { useAllExceptionLists } from './use_all_exception_lists'; import { ReferenceErrorModal } from '../../../../../components/value_lists_management_modal/reference_error_modal'; @@ -62,7 +61,7 @@ const exceptionReferenceModalInitialState: ReferenceModalState = { export const ExceptionListsTable = React.memo<ExceptionListsTableProps>( ({ formatUrl, history, hasPermissions, loading }) => { const { - services: { http, notifications }, + services: { http, notifications, timelines }, } = useKibana(); const { exportExceptionList, deleteExceptionList } = useApi(http); @@ -78,6 +77,7 @@ export const ExceptionListsTable = React.memo<ExceptionListsTableProps>( namespaceTypes: ['single', 'agnostic'], notifications, showTrustedApps: false, + showEventFilters: false, }); const [loadingTableInfo, exceptionListsWithRuleRefs, exceptionsListsRef] = useAllExceptionLists( { @@ -344,7 +344,7 @@ export const ExceptionListsTable = React.memo<ExceptionListsTableProps>( <HeaderSection split title={i18n.ALL_EXCEPTIONS} - subtitle={<LastUpdatedAt showUpdating={loading} updatedAt={lastUpdated} />} + subtitle={timelines.getLastUpdated({ showUpdating: loading, updatedAt: lastUpdated })} > {!initLoading && <ExceptionsSearchBar onSearch={handleSearch} />} </HeaderSection> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx index 8fd82a495e52f..2ec34aaece60b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx @@ -47,7 +47,6 @@ import { hasMlAdminPermissions } from '../../../../../../common/machine_learning import { hasMlLicense } from '../../../../../../common/machine_learning/has_ml_license'; import { isBoolean } from '../../../../../common/utils/privileges'; import { AllRulesUtilityBar } from './utility_bar'; -import { LastUpdatedAt } from '../../../../../common/components/last_updated'; import { DEFAULT_RULES_TABLE_REFRESH_SETTING } from '../../../../../../common/constants'; import { AllRulesTabs } from '.'; import { useValueChanged } from '../../../../../common/hooks/use_value_changed'; @@ -104,6 +103,7 @@ export const RulesTables = React.memo<RulesTableProps>( application: { capabilities: { actions }, }, + timelines, }, } = useKibana(); @@ -473,12 +473,10 @@ export const RulesTables = React.memo<RulesTableProps>( split growLeftSplit={false} title={i18n.ALL_RULES} - subtitle={ - <LastUpdatedAt - showUpdating={loading || isLoadingRules || isLoadingRulesStatuses} - updatedAt={lastUpdated} - /> - } + subtitle={timelines.getLastUpdated({ + showUpdating: loading || isLoadingRules || isLoadingRulesStatuses, + updatedAt: lastUpdated, + })} > {shouldShowRulesTable && ( <RulesTableFilters diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx index 90247d19e0503..23edf785a7f3a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx @@ -26,7 +26,7 @@ import { getRuleDetailsUrl, getRulesUrl, } from '../../../../../common/components/link_to/redirect_to_detection_engine'; -import { WrapperPage } from '../../../../../common/components/wrapper_page'; +import { SecuritySolutionPageWrapper } from '../../../../../common/components/page_wrapper'; import { displaySuccessToast, useStateToaster } from '../../../../../common/components/toasters'; import { SpyRoute } from '../../../../../common/utils/route/spy_routes'; import { useUserData } from '../../../../components/user_info'; @@ -287,7 +287,7 @@ const CreateRulePageComponent: React.FC = () => { return ( <> - <WrapperPage> + <SecuritySolutionPageWrapper> <EuiFlexGroup direction="row" justifyContent="spaceAround"> <MaxWidthEuiFlexItem> <DetectionEngineHeaderPage @@ -296,11 +296,10 @@ const CreateRulePageComponent: React.FC = () => { text: i18n.BACK_TO_RULES, pageId: SecurityPageName.detections, }} - border isLoading={isLoading || loading} title={i18n.PAGE_TITLE} /> - <MyEuiPanel zindex={4}> + <MyEuiPanel zindex={4} hasBorder> <StepDefineRuleAccordion initialIsOpen={true} id={RuleStep.defineRule} @@ -334,7 +333,7 @@ const CreateRulePageComponent: React.FC = () => { </StepDefineRuleAccordion> </MyEuiPanel> <EuiSpacer size="l" /> - <MyEuiPanel zindex={3}> + <MyEuiPanel hasBorder zindex={3}> <EuiAccordion initialIsOpen={false} id={RuleStep.aboutRule} @@ -369,7 +368,7 @@ const CreateRulePageComponent: React.FC = () => { </EuiAccordion> </MyEuiPanel> <EuiSpacer size="l" /> - <MyEuiPanel zindex={2}> + <MyEuiPanel hasBorder zindex={2}> <EuiAccordion initialIsOpen={false} id={RuleStep.scheduleRule} @@ -402,7 +401,7 @@ const CreateRulePageComponent: React.FC = () => { </EuiAccordion> </MyEuiPanel> <EuiSpacer size="l" /> - <MyEuiPanel zindex={1}> + <MyEuiPanel hasBorder zindex={1}> <EuiAccordion initialIsOpen={false} id={RuleStep.ruleActions} @@ -436,7 +435,7 @@ const CreateRulePageComponent: React.FC = () => { </MyEuiPanel> </MaxWidthEuiFlexItem> </EuiFlexGroup> - </WrapperPage> + </SecuritySolutionPageWrapper> <SpyRoute pageName={SecurityPageName.detections} /> </> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx index 417e1c989ce9b..2fedd6160af2c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx @@ -29,7 +29,7 @@ const FailureHistoryComponent: React.FC<FailureHistoryProps> = ({ id }) => { const [loading, ruleStatus] = useRuleStatus(id); if (loading) { return ( - <EuiPanel> + <EuiPanel hasBorder> <HeaderSection title={i18n.LAST_FIVE_ERRORS} /> <EuiLoadingContent /> </EuiPanel> @@ -60,7 +60,7 @@ const FailureHistoryComponent: React.FC<FailureHistoryProps> = ({ id }) => { }, ]; return ( - <EuiPanel> + <EuiPanel hasBorder> <HeaderSection title={i18n.LAST_FIVE_ERRORS} /> <EuiBasicTable columns={columns} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 6727db8aba3b4..b4f1af41a0606 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -27,11 +27,12 @@ import { useParams, useHistory } from 'react-router-dom'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; - import { ExceptionListTypeEnum, ExceptionListIdentifiers, } from '@kbn/securitysolution-io-ts-list-types'; + +import { isTab } from '../../../../../../../timelines/public'; import { useDeepEqualSelector, useShallowEqualSelector, @@ -48,7 +49,7 @@ import { getDetectionEngineUrl, } from '../../../../../common/components/link_to/redirect_to_detection_engine'; import { SiemSearchBar } from '../../../../../common/components/search_bar'; -import { WrapperPage } from '../../../../../common/components/wrapper_page'; +import { SecuritySolutionPageWrapper } from '../../../../../common/components/page_wrapper'; import { Rule, useRuleStatus, RuleInfoStatus } from '../../../../containers/detection_engine/rules'; import { useListsConfig } from '../../../../containers/detection_engine/lists/use_lists_config'; import { SpyRoute } from '../../../../../common/utils/route/spy_routes'; @@ -110,7 +111,6 @@ import * as detectionI18n from '../../translations'; import * as ruleI18n from '../translations'; import * as statusI18n from '../../../../components/rules/rule_status/translations'; import * as i18n from './translations'; -import { isTab } from '../../../../../common/components/accessibility/helpers'; import { NeedAdminForUpdateRulesCallOut } from '../../../../components/callouts/need_admin_for_update_callout'; import { getRuleStatusText } from '../../../../../../common/detection_engine/utils'; import { MissingPrivilegesCallOut } from '../../../../components/callouts/missing_privileges_callout'; @@ -563,7 +563,7 @@ const RuleDetailsPageComponent = () => { <SiemSearchBar id="global" indexPattern={indexPattern} /> </FiltersGlobal> - <WrapperPage noPadding={globalFullScreen}> + <SecuritySolutionPageWrapper noPadding={globalFullScreen}> <Display show={!globalFullScreen}> <DetectionEngineHeaderPage backOptions={{ @@ -728,14 +728,14 @@ const RuleDetailsPageComponent = () => { /> )} {ruleDetailTab === RuleDetailTabs.failures && <FailureHistory id={rule?.id} />} - </WrapperPage> + </SecuritySolutionPageWrapper> </StyledFullHeightContainer> ) : ( - <WrapperPage> + <SecuritySolutionPageWrapper> <DetectionEngineHeaderPage border title={i18n.PAGE_TITLE} /> <OverviewEmpty /> - </WrapperPage> + </SecuritySolutionPageWrapper> )} <SpyRoute pageName={SecurityPageName.detections} state={{ ruleName: rule?.name }} /> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx index 2d751459eb12f..41710a822e539 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx @@ -21,7 +21,7 @@ import { useParams, useHistory } from 'react-router-dom'; import { UpdateRulesSchema } from '../../../../../../common/detection_engine/schemas/request'; import { useRule, useUpdateRule } from '../../../../containers/detection_engine/rules'; import { useListsConfig } from '../../../../containers/detection_engine/lists/use_lists_config'; -import { WrapperPage } from '../../../../../common/components/wrapper_page'; +import { SecuritySolutionPageWrapper } from '../../../../../common/components/page_wrapper'; import { getRuleDetailsUrl, getDetectionEngineUrl, @@ -335,7 +335,7 @@ const EditRulePageComponent: FC = () => { return ( <> - <WrapperPage> + <SecuritySolutionPageWrapper> <EuiFlexGroup direction="row" justifyContent="spaceAround"> <MaxWidthEuiFlexItem> <DetectionEngineHeaderPage @@ -410,7 +410,7 @@ const EditRulePageComponent: FC = () => { </EuiFlexGroup> </MaxWidthEuiFlexItem> </EuiFlexGroup> - </WrapperPage> + </SecuritySolutionPageWrapper> <SpyRoute pageName={SecurityPageName.detections} state={{ ruleName: rule?.name }} /> </> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx index 8bacb10444a7d..29fd8e2e8b247 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx @@ -16,7 +16,7 @@ import { getCreateRuleUrl, } from '../../../../common/components/link_to/redirect_to_detection_engine'; import { DetectionEngineHeaderPage } from '../../../components/detection_engine_header_page'; -import { WrapperPage } from '../../../../common/components/wrapper_page'; +import { SecuritySolutionPageWrapper } from '../../../../common/components/page_wrapper'; import { SpyRoute } from '../../../../common/utils/route/spy_routes'; import { useUserData } from '../../../components/user_info'; @@ -182,7 +182,7 @@ const RulesPageComponent: React.FC = () => { subtitle={i18n.INITIAL_PROMPT_TEXT} title={i18n.IMPORT_RULE} /> - <WrapperPage> + <SecuritySolutionPageWrapper> <DetectionEngineHeaderPage backOptions={{ href: getDetectionEngineUrl(), @@ -258,7 +258,7 @@ const RulesPageComponent: React.FC = () => { rulesNotUpdated={rulesNotUpdated} setRefreshRulesData={handleSetRefreshRulesData} /> - </WrapperPage> + </SecuritySolutionPageWrapper> <SpyRoute pageName={SecurityPageName.detections} /> </> diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx index b1a0d13ed554b..413b8cda9b6ab 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx @@ -23,6 +23,8 @@ import { HostsTableType } from '../../../hosts/store/model'; import { HostsTable } from './index'; import { mockData } from './mock'; +jest.mock('../../../common/lib/kibana'); + // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar jest.mock('../../../common/components/search_bar', () => ({ diff --git a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx index 751a2bf5a2055..2cd4ed1f57f84 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx @@ -20,6 +20,8 @@ import { mockData } from './mock'; import { HostsType } from '../../store/model'; import * as i18n from './translations'; +jest.mock('../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx index 2333d5e9b127c..b51e20b801f40 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx @@ -19,6 +19,8 @@ import { type } from './utils'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { getHostDetailsPageFilters } from './helpers'; +jest.mock('../../../common/lib/kibana'); + jest.mock('../../../common/components/url_state/normalize_time_range.ts'); jest.mock('../../../common/containers/source', () => ({ diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index d88e4f048f917..22edd2c19d6bd 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -21,11 +21,11 @@ import { hostToCriteria } from '../../../common/components/ml/criteria/host_to_c import { hasMlUserPermissions } from '../../../../common/machine_learning/has_ml_user_permissions'; import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities'; import { scoreIntervalToDateTime } from '../../../common/components/ml/score/score_interval_to_datetime'; -import { SiemNavigation } from '../../../common/components/navigation'; +import { SecuritySolutionTabNavigation } from '../../../common/components/navigation'; import { HostsDetailsKpiComponent } from '../../components/kpi_hosts'; import { HostOverview } from '../../../overview/components/host_overview'; import { SiemSearchBar } from '../../../common/components/search_bar'; -import { WrapperPage } from '../../../common/components/wrapper_page'; +import { SecuritySolutionPageWrapper } from '../../../common/components/page_wrapper'; import { useGlobalTime } from '../../../common/containers/use_global_time'; import { useKibana } from '../../../common/lib/kibana'; import { convertToBuildEsQuery } from '../../../common/lib/keury'; @@ -123,7 +123,7 @@ const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDeta <SiemSearchBar indexPattern={indexPattern} id="global" /> </FiltersGlobal> - <WrapperPage noPadding={globalFullScreen}> + <SecuritySolutionPageWrapper noPadding={globalFullScreen}> <Display show={!globalFullScreen}> <HeaderPage border @@ -184,7 +184,7 @@ const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDeta <EuiSpacer /> - <SiemNavigation + <SecuritySolutionTabNavigation navTabs={navTabsHostDetails(detailName, hasMlUserPermissions(capabilities))} /> @@ -207,14 +207,14 @@ const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDeta indexPattern={indexPattern} setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} /> - </WrapperPage> + </SecuritySolutionPageWrapper> </> ) : ( - <WrapperPage> + <SecuritySolutionPageWrapper> <HeaderPage border title={detailName} /> <OverviewEmpty /> - </WrapperPage> + </SecuritySolutionPageWrapper> )} <SpyRoute pageName={SecurityPageName.hosts} /> diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx index f1eab38c56db0..d05b091381cca 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx @@ -18,7 +18,7 @@ import { kibanaObservable, createSecuritySolutionStorageMock, } from '../../common/mock'; -import { SiemNavigation } from '../../common/components/navigation'; +import { SecuritySolutionTabNavigation } from '../../common/components/navigation'; import { inputsActions } from '../../common/store/inputs'; import { State, createStore } from '../../common/store'; import { Hosts } from './hosts'; @@ -102,7 +102,7 @@ describe('Hosts - rendering', () => { </Router> </TestProviders> ); - expect(wrapper.find(SiemNavigation).exists()).toBe(true); + expect(wrapper.find(SecuritySolutionTabNavigation).exists()).toBe(true); }); test('it should add the new filters after init', async () => { diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index 57cded85d67cc..7d31d291e75f1 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -11,6 +11,7 @@ import { noop } from 'lodash/fp'; import React, { useCallback, useMemo, useRef } from 'react'; import { useDispatch } from 'react-redux'; import { useParams } from 'react-router-dom'; +import { isTab } from '../../../../timelines/public'; import { SecurityPageName } from '../../app/types'; import { UpdateDateRange } from '../../common/components/charts/common'; @@ -18,10 +19,10 @@ import { FiltersGlobal } from '../../common/components/filters_global'; import { HeaderPage } from '../../common/components/header_page'; import { LastEventTime } from '../../common/components/last_event_time'; import { hasMlUserPermissions } from '../../../common/machine_learning/has_ml_user_permissions'; -import { SiemNavigation } from '../../common/components/navigation'; +import { SecuritySolutionTabNavigation } from '../../common/components/navigation'; import { HostsKpiComponent } from '../components/kpi_hosts'; import { SiemSearchBar } from '../../common/components/search_bar'; -import { WrapperPage } from '../../common/components/wrapper_page'; +import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; import { useGlobalFullScreen } from '../../common/containers/use_full_screen'; import { useGlobalTime } from '../../common/containers/use_global_time'; import { TimelineId } from '../../../common/types/timeline'; @@ -42,7 +43,6 @@ import * as i18n from './translations'; import { filterHostData } from './navigation'; import { hostsModel } from '../store'; import { HostsTableType } from '../store/model'; -import { isTab } from '../../common/components/accessibility/helpers'; import { onTimelineTabKeyPressed, resetKeyboardFocus, @@ -164,10 +164,9 @@ const HostsComponent = () => { <SiemSearchBar indexPattern={indexPattern} id="global" /> </FiltersGlobal> - <WrapperPage noPadding={globalFullScreen}> + <SecuritySolutionPageWrapper noPadding={globalFullScreen}> <Display show={!globalFullScreen}> <HeaderPage - border subtitle={ <LastEventTime docValueFields={docValueFields} @@ -190,7 +189,9 @@ const HostsComponent = () => { <EuiSpacer /> - <SiemNavigation navTabs={navTabsHosts(hasMlUserPermissions(capabilities))} /> + <SecuritySolutionTabNavigation + navTabs={navTabsHosts(hasMlUserPermissions(capabilities))} + /> <EuiSpacer /> </Display> @@ -207,14 +208,14 @@ const HostsComponent = () => { from={from} type={hostsModel.HostsType.page} /> - </WrapperPage> + </SecuritySolutionPageWrapper> </StyledFullHeightContainer> ) : ( - <WrapperPage> + <SecuritySolutionPageWrapper> <HeaderPage border title={i18n.PAGE_TITLE} /> <OverviewEmpty /> - </WrapperPage> + </SecuritySolutionPageWrapper> )} <SpyRoute pageName={SecurityPageName.hosts} /> diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx index f88709e6e95ac..973dbc41925da 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx @@ -10,6 +10,7 @@ import { useDispatch } from 'react-redux'; import { TimelineId } from '../../../../common/types/timeline'; import { StatefulEventsViewer } from '../../../common/components/events_viewer'; +import { timelineActions } from '../../../timelines/store/timeline'; import { HostsComponentsQueryProps } from './types'; import { eventsDefaultModel } from '../../../common/components/events_viewer/default_model'; import { @@ -20,7 +21,6 @@ import { MatrixHistogram } from '../../../common/components/matrix_histogram'; import { useGlobalFullScreen } from '../../../common/containers/use_full_screen'; import * as i18n from '../translations'; import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; -import { useManageTimeline } from '../../../timelines/components/manage_timeline'; import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; @@ -64,14 +64,15 @@ const EventsQueryTabBodyComponent: React.FC<HostsComponentsQueryProps> = ({ startDate, }) => { const dispatch = useDispatch(); - const { initializeTimeline } = useManageTimeline(); const { globalFullScreen } = useGlobalFullScreen(); useEffect(() => { - initializeTimeline({ - id: TimelineId.hostsPageEvents, - defaultModel: eventsDefaultModel, - }); - }, [dispatch, initializeTimeline]); + dispatch( + timelineActions.initializeTGridSettings({ + id: TimelineId.hostsPageEvents, + defaultColumns: eventsDefaultModel.columns, + }) + ); + }, [dispatch]); useEffect(() => { return () => { diff --git a/x-pack/plugins/security_solution/public/index.ts b/x-pack/plugins/security_solution/public/index.ts index 55262fe039b4e..3d2412b326b54 100644 --- a/x-pack/plugins/security_solution/public/index.ts +++ b/x-pack/plugins/security_solution/public/index.ts @@ -8,6 +8,7 @@ import { PluginInitializerContext } from '../../../../src/core/public'; import { Plugin } from './plugin'; import { PluginSetup } from './types'; +export type { TimelineModel } from './timelines/store/timeline/model'; export const plugin = (context: PluginInitializerContext): Plugin => new Plugin(context); diff --git a/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts b/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts index 76acff7847671..3bcbd81621588 100644 --- a/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts +++ b/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts @@ -11,7 +11,7 @@ import { AdministrationSubTab } from '../types'; import { ENDPOINTS_TAB, EVENT_FILTERS_TAB, POLICIES_TAB, TRUSTED_APPS_TAB } from './translations'; import { AdministrationRouteSpyState } from '../../common/utils/route/types'; import { GetUrlForApp } from '../../common/components/navigation/types'; -import { ADMINISTRATION } from '../../app/home/translations'; +import { ADMINISTRATION } from '../../app/translations'; import { APP_ID, SecurityPageName } from '../../../common/constants'; const TabNameMappedToI18nKey: Record<AdministrationSubTab, string> = { diff --git a/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx b/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx index 72a6de2a2de8d..021c900824f8d 100644 --- a/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx +++ b/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx @@ -9,9 +9,9 @@ import React, { FC, memo } from 'react'; import { EuiPanel, EuiSpacer, CommonProps } from '@elastic/eui'; import styled from 'styled-components'; import { SecurityPageName } from '../../../common/constants'; -import { WrapperPage } from '../../common/components/wrapper_page'; +import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; import { HeaderPage } from '../../common/components/header_page'; -import { SiemNavigation } from '../../common/components/navigation'; +import { SecuritySolutionTabNavigation } from '../../common/components/navigation'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { AdministrationSubTab } from '../types'; import { @@ -46,7 +46,7 @@ export const AdministrationListPage: FC<AdministrationListPageProps & CommonProp const badgeOptions = !beta ? undefined : { beta: true, text: BETA_BADGE_LABEL }; return ( - <WrapperPage noTimeline {...otherProps}> + <SecuritySolutionPageWrapper noTimeline {...otherProps}> <HeaderPage hideSourcerer={true} title={title} @@ -57,7 +57,7 @@ export const AdministrationListPage: FC<AdministrationListPageProps & CommonProp {actions} </HeaderPage> - <SiemNavigation + <SecuritySolutionTabNavigation navTabs={{ [AdministrationSubTab.endpoints]: { name: ENDPOINTS_TAB, @@ -88,10 +88,10 @@ export const AdministrationListPage: FC<AdministrationListPageProps & CommonProp <EuiSpacer /> - <EuiPanelStyled>{children}</EuiPanelStyled> + <EuiPanelStyled hasBorder>{children}</EuiPanelStyled> <SpyRoute pageName={SecurityPageName.administration} /> - </WrapperPage> + </SecuritySolutionPageWrapper> ); } ); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts index 5b5bac3a0a6e1..949feb2964317 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts @@ -16,7 +16,7 @@ import { import { ServerApiError } from '../../../../common/types'; import { GetPolicyListResponse } from '../../policy/types'; import { GetPackagesResponse } from '../../../../../../fleet/common'; -import { EndpointState } from '../types'; +import { EndpointIndexUIQueryParams, EndpointState } from '../types'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/public'; export interface ServerReturnedEndpointList { @@ -163,12 +163,29 @@ export type EndpointPendingActionsStateChanged = Action<'endpointPendingActionsS payload: EndpointState['endpointPendingActions']; }; +export interface EndpointDetailsActivityLogUpdatePaging { + type: 'endpointDetailsActivityLogUpdatePaging'; + payload: { + // disable paging when no more data after paging + disabled: boolean; + page: number; + pageSize: number; + }; +} + +export interface EndpointDetailsFlyoutTabChanged { + type: 'endpointDetailsFlyoutTabChanged'; + payload: { flyoutView: EndpointIndexUIQueryParams['show'] }; +} + export type EndpointAction = | ServerReturnedEndpointList | ServerFailedToReturnEndpointList | ServerReturnedEndpointDetails | ServerFailedToReturnEndpointDetails | AppRequestedEndpointActivityLog + | EndpointDetailsActivityLogUpdatePaging + | EndpointDetailsFlyoutTabChanged | EndpointDetailsActivityLogChanged | ServerReturnedEndpointPolicyResponse | ServerFailedToReturnEndpointPolicyResponse diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts index d43f361a0e6bb..317b735e1169e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/builders.ts @@ -19,9 +19,13 @@ export const initialEndpointPageState = (): Immutable<EndpointState> => { loading: false, error: undefined, endpointDetails: { + flyoutView: undefined, activityLog: { - page: 1, - pageSize: 50, + paging: { + disabled: false, + page: 1, + pageSize: 50, + }, logData: createUninitialisedResourceState(), }, hostDetails: { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts index 7f7c5f84f8bff..68dd47362bc38 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts @@ -42,9 +42,13 @@ describe('EndpointList store concerns', () => { loading: false, error: undefined, endpointDetails: { + flyoutView: undefined, activityLog: { - page: 1, - pageSize: 50, + paging: { + disabled: false, + page: 1, + pageSize: 50, + }, logData: { type: 'UninitialisedResourceState' }, }, hostDetails: { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts index 52da30fabf95a..6cf5e989fb645 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts @@ -44,6 +44,7 @@ import { } from '../../../../common/lib/endpoint_isolation/mocks'; import { FleetActionGenerator } from '../../../../../common/endpoint/data_generators/fleet_action_generator'; import { endpointPageHttpMock } from '../mocks'; +import { EndpointDetailsTabsTypes } from '../view/details/components/endpoint_details_tabs'; jest.mock('../../policy/store/services/ingest', () => ({ sendGetAgentConfigList: () => Promise.resolve({ items: [] }), @@ -226,8 +227,16 @@ describe('endpoint list middleware', () => { const dispatchUserChangedUrl = () => { dispatchUserChangedUrlToEndpointList({ search: `?${search.split('?').pop()}` }); }; + const dispatchFlyoutViewChange = () => { + dispatch({ + type: 'endpointDetailsFlyoutTabChanged', + payload: { + flyoutView: EndpointDetailsTabsTypes.activityLog, + }, + }); + }; - const fleetActionGenerator = new FleetActionGenerator(Math.random().toString()); + const fleetActionGenerator = new FleetActionGenerator('seed'); const actionData = fleetActionGenerator.generate({ agents: [endpointList.hosts[0].metadata.agent.id], }); @@ -265,6 +274,7 @@ describe('endpoint list middleware', () => { it('should set ActivityLog state to loading', async () => { dispatchUserChangedUrl(); + dispatchFlyoutViewChange(); const loadingDispatched = waitForAction('endpointDetailsActivityLogChanged', { validate(action) { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index 4f96223e8b789..53b30aeb02bd5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -35,6 +35,7 @@ import { getActivityLogDataPaging, getLastLoadedActivityLogData, detailsData, + getEndpointDetailsFlyoutView, } from './selectors'; import { AgentIdsPendingActions, EndpointState, PolicyIds } from '../types'; import { @@ -48,6 +49,7 @@ import { ENDPOINT_ACTION_LOG_ROUTE, HOST_METADATA_GET_ROUTE, HOST_METADATA_LIST_ROUTE, + BASE_POLICY_RESPONSE_ROUTE, metadataCurrentIndexPattern, } from '../../../../../common/endpoint/constants'; import { IIndexPattern, Query } from '../../../../../../../../src/plugins/data/public'; @@ -61,6 +63,7 @@ import { AppAction } from '../../../../common/store/actions'; import { resolvePathVariables } from '../../../../common/utils/resolve_path_variables'; import { ServerReturnedEndpointPackageInfo } from './action'; import { fetchPendingActionsByAgentId } from '../../../../common/lib/endpoint_pending_actions'; +import { EndpointDetailsTabsTypes } from '../view/details/components/endpoint_details_tabs'; type EndpointPageStore = ImmutableMiddlewareAPI<EndpointState, AppAction>; @@ -339,6 +342,28 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory<EndpointState loadEndpointsPendingActions(store); + // call the policy response api + try { + const policyResponse = await coreStart.http.get(BASE_POLICY_RESPONSE_ROUTE, { + query: { agentId: selectedEndpoint }, + }); + dispatch({ + type: 'serverReturnedEndpointPolicyResponse', + payload: policyResponse, + }); + } catch (error) { + dispatch({ + type: 'serverFailedToReturnEndpointPolicyResponse', + payload: error, + }); + } + } + + if ( + action.type === 'userChangedUrl' && + hasSelectedEndpoint(getState()) === true && + getEndpointDetailsFlyoutView(getState()) === EndpointDetailsTabsTypes.activityLog + ) { // call the activity log api dispatch({ type: 'endpointDetailsActivityLogChanged', @@ -365,22 +390,6 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory<EndpointState payload: createFailedResourceState<ActivityLog>(error.body ?? error), }); } - - // call the policy response api - try { - const policyResponse = await coreStart.http.get(`/api/endpoint/policy_response`, { - query: { agentId: selectedEndpoint }, - }); - dispatch({ - type: 'serverReturnedEndpointPolicyResponse', - payload: policyResponse, - }); - } catch (error) { - dispatch({ - type: 'serverFailedToReturnEndpointPolicyResponse', - payload: error, - }); - } } // page activity log API @@ -408,17 +417,24 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory<EndpointState ] as ActivityLog['data']; const updatedLogData = { - total: activityLog.total, page: activityLog.page, pageSize: activityLog.pageSize, - data: updatedLogDataItems, + data: activityLog.page === 1 ? activityLog.data : updatedLogDataItems, }; dispatch({ type: 'endpointDetailsActivityLogChanged', payload: createLoadedResourceState<ActivityLog>(updatedLogData), }); - // TODO dispatch 'noNewLogData' if !activityLog.length - // resets paging to previous state + if (!activityLog.data.length) { + dispatch({ + type: 'endpointDetailsActivityLogUpdatePaging', + payload: { + disabled: true, + page: activityLog.page - 1, + pageSize: activityLog.pageSize, + }, + }); + } } else { dispatch({ type: 'endpointDetailsActivityLogChanged', diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts index 9460c27dfe705..44c63edd8e95c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts @@ -29,12 +29,23 @@ const handleEndpointDetailsActivityLogChanged: CaseReducer<EndpointDetailsActivi state, action ) => { + const pagingOptions = + action.payload.type === 'LoadedResourceState' + ? { + ...state.endpointDetails.activityLog, + paging: { + ...state.endpointDetails.activityLog.paging, + page: action.payload.data.page, + pageSize: action.payload.data.pageSize, + }, + } + : { ...state.endpointDetails.activityLog }; return { ...state!, endpointDetails: { ...state.endpointDetails!, activityLog: { - ...state.endpointDetails.activityLog, + ...pagingOptions, logData: action.payload, }, }, @@ -138,7 +149,8 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta }, }; } else if (action.type === 'appRequestedEndpointActivityLog') { - const pageData = { + const paging = { + disabled: state.endpointDetails.activityLog.paging.disabled, page: action.payload.page, pageSize: action.payload.pageSize, }; @@ -148,10 +160,32 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta ...state.endpointDetails!, activityLog: { ...state.endpointDetails.activityLog, - ...pageData, + paging, }, }, }; + } else if (action.type === 'endpointDetailsActivityLogUpdatePaging') { + const paging = { + ...action.payload, + }; + return { + ...state, + endpointDetails: { + ...state.endpointDetails!, + activityLog: { + ...state.endpointDetails.activityLog, + paging, + }, + }, + }; + } else if (action.type === 'endpointDetailsFlyoutTabChanged') { + return { + ...state, + endpointDetails: { + ...state.endpointDetails!, + flyoutView: action.payload.flyoutView, + }, + }; } else if (action.type === 'endpointDetailsActivityLogChanged') { return handleEndpointDetailsActivityLogChanged(state, action); } else if (action.type === 'endpointPendingActionsStateChanged') { @@ -255,8 +289,11 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta const activityLog = { logData: createUninitialisedResourceState(), - page: 1, - pageSize: 50, + paging: { + disabled: false, + page: 1, + pageSize: 50, + }, }; // Reset `isolationRequestState` if needed diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts index d9be85377c81d..eeb54379e8e7d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts @@ -364,13 +364,14 @@ export const getIsolationRequestError: ( } }); +export const getEndpointDetailsFlyoutView = ( + state: Immutable<EndpointState> +): EndpointIndexUIQueryParams['show'] => state.endpointDetails.flyoutView; + export const getActivityLogDataPaging = ( state: Immutable<EndpointState> -): Immutable<Omit<EndpointState['endpointDetails']['activityLog'], 'logData'>> => { - return { - page: state.endpointDetails.activityLog.page, - pageSize: state.endpointDetails.activityLog.pageSize, - }; +): Immutable<EndpointState['endpointDetails']['activityLog']['paging']> => { + return state.endpointDetails.activityLog.paging; }; export const getActivityLogData = ( diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts index 59aa2bd15dd74..c985259588cb0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts @@ -37,9 +37,13 @@ export interface EndpointState { /** api error from retrieving host list */ error?: ServerApiError; endpointDetails: { + flyoutView: EndpointIndexUIQueryParams['show']; activityLog: { - page: number; - pageSize: number; + paging: { + disabled: boolean; + page: number; + pageSize: number; + }; logData: AsyncResourceState<ActivityLog>; }; hostDetails: { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx index 3e228be4565b1..aa1f56529657e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx @@ -5,10 +5,15 @@ * 2.0. */ +import { useDispatch } from 'react-redux'; import React, { memo, useCallback, useMemo, useState } from 'react'; -import styled from 'styled-components'; -import { EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui'; +import { EuiTab, EuiTabs, EuiFlyoutBody, EuiTabbedContentTab, EuiSpacer } from '@elastic/eui'; import { EndpointIndexUIQueryParams } from '../../../types'; +import { EndpointAction } from '../../../store/action'; +import { useEndpointSelector } from '../../hooks'; +import { getActivityLogDataPaging } from '../../../store/selectors'; +import { EndpointDetailsFlyoutHeader } from './flyout_header'; + export enum EndpointDetailsTabsTypes { overview = 'overview', activityLog = 'activity_log', @@ -24,29 +29,18 @@ interface EndpointDetailsTabs { content: JSX.Element; } -const StyledEuiTabbedContent = styled(EuiTabbedContent)` - overflow: hidden; - padding-bottom: ${(props) => props.theme.eui.paddingSizes.xl}; - - > [role='tabpanel'] { - height: 100%; - padding-right: 12px; - overflow: hidden; - overflow-y: auto; - ::-webkit-scrollbar { - -webkit-appearance: none; - width: 4px; - } - ::-webkit-scrollbar-thumb { - border-radius: 2px; - background-color: rgba(0, 0, 0, 0.5); - -webkit-box-shadow: 0 0 1px rgba(255, 255, 255, 0.5); - } - } -`; - export const EndpointDetailsFlyoutTabs = memo( - ({ show, tabs }: { show: EndpointIndexUIQueryParams['show']; tabs: EndpointDetailsTabs[] }) => { + ({ + hostname, + show, + tabs, + }: { + hostname?: string; + show: EndpointIndexUIQueryParams['show']; + tabs: EndpointDetailsTabs[]; + }) => { + const dispatch = useDispatch<(action: EndpointAction) => void>(); + const { pageSize } = useEndpointSelector(getActivityLogDataPaging); const [selectedTabId, setSelectedTabId] = useState<EndpointDetailsTabsId>(() => { return show === 'details' ? EndpointDetailsTabsTypes.overview @@ -54,8 +48,33 @@ export const EndpointDetailsFlyoutTabs = memo( }); const handleTabClick = useCallback( - (tab: EuiTabbedContentTab) => setSelectedTabId(tab.id as EndpointDetailsTabsId), - [setSelectedTabId] + (tab: EuiTabbedContentTab) => { + dispatch({ + type: 'endpointDetailsFlyoutTabChanged', + payload: { + flyoutView: tab.id as EndpointIndexUIQueryParams['show'], + }, + }); + if (tab.id === EndpointDetailsTabsTypes.activityLog) { + const paging = { + page: 1, + pageSize, + }; + dispatch({ + type: 'appRequestedEndpointActivityLog', + payload: paging, + }); + dispatch({ + type: 'endpointDetailsActivityLogUpdatePaging', + payload: { + disabled: false, + ...paging, + }, + }); + } + return setSelectedTabId(tab.id as EndpointDetailsTabsId); + }, + [dispatch, pageSize, setSelectedTabId] ); const selectedTab = useMemo(() => tabs.find((tab) => tab.id === selectedTabId), [ @@ -63,14 +82,27 @@ export const EndpointDetailsFlyoutTabs = memo( selectedTabId, ]); + const renderTabs = tabs.map((tab) => ( + <EuiTab + onClick={() => handleTabClick(tab)} + isSelected={tab.id === selectedTabId} + key={tab.id} + data-test-subj={tab.id} + > + {tab.name} + </EuiTab> + )); + return ( - <StyledEuiTabbedContent - data-test-subj="endpointDetailsTabs" - tabs={tabs} - selectedTab={selectedTab} - onTabClick={handleTabClick} - key="endpoint-details-tabs" - /> + <> + <EndpointDetailsFlyoutHeader hostname={hostname} hasBorder> + <EuiSpacer size="s" /> + <EuiTabs style={{ marginBottom: '-25px' }}>{renderTabs}</EuiTabs> + </EndpointDetailsFlyoutHeader> + <EuiFlyoutBody data-test-subj="endpointDetailsFlyoutBody"> + {selectedTab?.content} + </EuiFlyoutBody> + </> ); } ); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/flyout_header.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/flyout_header.tsx new file mode 100644 index 0000000000000..f791c0d6adf17 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/flyout_header.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { EuiFlyoutHeader, EuiLoadingContent, EuiToolTip, EuiTitle } from '@elastic/eui'; +import { useEndpointSelector } from '../../hooks'; +import { detailsLoading } from '../../../store/selectors'; + +export const EndpointDetailsFlyoutHeader = memo( + ({ + hasBorder = false, + hostname, + children, + }: { + hasBorder?: boolean; + hostname?: string; + children?: React.ReactNode | React.ReactNodeArray; + }) => { + const hostDetailsLoading = useEndpointSelector(detailsLoading); + + return ( + <EuiFlyoutHeader hasBorder={hasBorder}> + {hostDetailsLoading ? ( + <EuiLoadingContent lines={1} /> + ) : ( + <EuiToolTip content={hostname} anchorClassName="eui-textTruncate"> + <EuiTitle> + <h2 + style={{ overflow: 'hidden', textOverflow: 'ellipsis' }} + data-test-subj="endpointDetailsFlyoutTitle" + > + {hostname} + </h2> + </EuiTitle> + </EuiToolTip> + )} + {children} + </EuiFlyoutHeader> + ); + } +); + +EndpointDetailsFlyoutHeader.displayName = 'EndpointDetailsFlyoutHeader'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx index c431cd682d25b..4fe70039d1251 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx @@ -78,7 +78,7 @@ const useLogEntryUIProps = ( if (isSuccessful) { return i18.ACTIVITY_LOG.LogEntry.response.isolationSuccessful; } else { - return i18.ACTIVITY_LOG.LogEntry.response.isolationSuccessful; + return i18.ACTIVITY_LOG.LogEntry.response.isolationFailed; } } else { if (isSuccessful) { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx index 55479845bce0a..f1701054c4d5f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx @@ -5,11 +5,19 @@ * 2.0. */ -import React, { memo, useCallback } from 'react'; +import React, { memo, useCallback, useEffect, useRef } from 'react'; +import styled from 'styled-components'; -import { EuiButton, EuiEmptyPrompt, EuiLoadingContent, EuiSpacer } from '@elastic/eui'; +import { + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingContent, + EuiEmptyPrompt, +} from '@elastic/eui'; import { useDispatch } from 'react-redux'; import { LogEntry } from './components/log_entry'; +import * as i18 from '../translations'; import { Immutable, ActivityLog } from '../../../../../../common/endpoint/types'; import { AsyncResourceState } from '../../../../state'; import { useEndpointSelector } from '../hooks'; @@ -19,54 +27,95 @@ import { getActivityLogError, getActivityLogIterableData, getActivityLogRequestLoaded, + getLastLoadedActivityLogData, getActivityLogRequestLoading, } from '../../store/selectors'; +const LoadMoreTrigger = styled.div` + height: 6px; + width: 100%; +`; + export const EndpointActivityLog = memo( ({ activityLog }: { activityLog: AsyncResourceState<Immutable<ActivityLog>> }) => { const activityLogLoading = useEndpointSelector(getActivityLogRequestLoading); const activityLogLoaded = useEndpointSelector(getActivityLogRequestLoaded); + const activityLastLogData = useEndpointSelector(getLastLoadedActivityLogData); const activityLogData = useEndpointSelector(getActivityLogIterableData); + const activityLogSize = activityLogData.length; const activityLogError = useEndpointSelector(getActivityLogError); - const dispatch = useDispatch<(a: EndpointAction) => void>(); - const { page, pageSize } = useEndpointSelector(getActivityLogDataPaging); + const dispatch = useDispatch<(action: EndpointAction) => void>(); + const { page, pageSize, disabled: isPagingDisabled } = useEndpointSelector( + getActivityLogDataPaging + ); + + const loadMoreTrigger = useRef<HTMLInputElement | null>(null); + const getActivityLog = useCallback( + (entries: IntersectionObserverEntry[]) => { + const isTargetIntersecting = entries.some((entry) => entry.isIntersecting); + if (isTargetIntersecting && activityLogLoaded && !isPagingDisabled) { + dispatch({ + type: 'appRequestedEndpointActivityLog', + payload: { + page: page + 1, + pageSize, + }, + }); + } + }, + [activityLogLoaded, dispatch, isPagingDisabled, page, pageSize] + ); - const getActivityLog = useCallback(() => { - dispatch({ - type: 'appRequestedEndpointActivityLog', - payload: { - page: page + 1, - pageSize, - }, - }); - }, [dispatch, page, pageSize]); + useEffect(() => { + const observer = new IntersectionObserver(getActivityLog); + const element = loadMoreTrigger.current; + if (element) { + observer.observe(element); + } + return () => { + observer.disconnect(); + }; + }, [getActivityLog]); return ( <> - <EuiSpacer size="l" /> - {activityLogLoading || activityLogError ? ( - <EuiEmptyPrompt - iconType="editorUnorderedList" - titleSize="s" - title={<h2>{'No logged actions'}</h2>} - body={<p>{'No actions have been logged for this endpoint.'}</p>} - /> - ) : ( - <> - <EuiSpacer size="l" /> - {activityLogLoading ? ( - <EuiLoadingContent lines={3} /> - ) : ( - activityLogLoaded && - activityLogData.map((logEntry) => ( - <LogEntry key={`${logEntry.item.id}`} logEntry={logEntry} /> - )) - )} - <EuiButton size="s" fill onClick={getActivityLog}> - {'show more'} - </EuiButton> - </> - )} + <EuiFlexGroup direction="column" style={{ height: '85vh' }}> + {(activityLogLoaded && !activityLogSize) || activityLogError ? ( + <EuiFlexItem> + <EuiEmptyPrompt + iconType="editorUnorderedList" + titleSize="s" + title={<h2>{i18.ACTIVITY_LOG.LogEntry.emptyState.title}</h2>} + body={<p>{i18.ACTIVITY_LOG.LogEntry.emptyState.body}</p>} + data-test-subj="activityLogEmpty" + /> + </EuiFlexItem> + ) : ( + <> + <EuiFlexItem grow={true}> + {activityLogLoaded && + activityLogData.map((logEntry) => ( + <LogEntry key={`${logEntry.item.id}`} logEntry={logEntry} /> + ))} + {activityLogLoading && + activityLastLogData?.data.map((logEntry) => ( + <LogEntry key={`${logEntry.item.id}`} logEntry={logEntry} /> + ))} + </EuiFlexItem> + <EuiFlexItem grow={false}> + {activityLogLoading && <EuiLoadingContent lines={3} />} + {(!activityLogLoading || !isPagingDisabled) && ( + <LoadMoreTrigger ref={loadMoreTrigger} /> + )} + {isPagingDisabled && !activityLogLoading && ( + <EuiText color="subdued" textAlign="center"> + <p>{i18.ACTIVITY_LOG.LogEntry.endOfLog}</p> + </EuiText> + )} + </EuiFlexItem> + </> + )} + </EuiFlexGroup> </> ); } diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx index d839bbfaae875..d3c91f6f18499 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx @@ -20,7 +20,6 @@ export const dummyEndpointActivityLog = ( ): AsyncResourceState<Immutable<ActivityLog>> => ({ type: 'LoadedResourceState', data: { - total: 20, page: 1, pageSize: 50, data: [ diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx index 59e0c0e787a22..edfa410ee5237 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx @@ -5,21 +5,16 @@ * 2.0. */ +import { useDispatch } from 'react-redux'; import React, { useCallback, useEffect, useMemo, memo } from 'react'; -import styled from 'styled-components'; import { EuiFlyout, EuiFlyoutBody, - EuiFlyoutHeader, EuiFlyoutFooter, EuiLoadingContent, - EuiTitle, EuiText, EuiSpacer, EuiEmptyPrompt, - EuiToolTip, - EuiFlexGroup, - EuiFlexItem, } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -30,7 +25,6 @@ import { uiQueryParams, detailsData, detailsError, - detailsLoading, getActivityLogData, showView, policyResponseConfigurations, @@ -59,23 +53,12 @@ import { BackToEndpointDetailsFlyoutSubHeader } from './components/back_to_endpo import { FlyoutBodyNoTopPadding } from './components/flyout_body_no_top_padding'; import { getEndpointListPath } from '../../../../common/routing'; import { ActionsMenu } from './components/actions_menu'; - -const DetailsFlyoutBody = styled(EuiFlyoutBody)` - overflow-y: hidden; - flex: 1; - - .euiFlyoutBody__overflow { - overflow: hidden; - mask-image: none; - } - - .euiFlyoutBody__overflowContent { - height: 100%; - display: flex; - } -`; +import { EndpointIndexUIQueryParams } from '../../types'; +import { EndpointAction } from '../../store/action'; +import { EndpointDetailsFlyoutHeader } from './components/flyout_header'; export const EndpointDetailsFlyout = memo(() => { + const dispatch = useDispatch<(action: EndpointAction) => void>(); const history = useHistory(); const toasts = useToasts(); const queryParams = useEndpointSelector(uiQueryParams); @@ -86,13 +69,24 @@ export const EndpointDetailsFlyout = memo(() => { const activityLog = useEndpointSelector(getActivityLogData); const hostDetails = useEndpointSelector(detailsData); - const hostDetailsLoading = useEndpointSelector(detailsLoading); const hostDetailsError = useEndpointSelector(detailsError); const policyInfo = useEndpointSelector(policyVersionInfo); const hostStatus = useEndpointSelector(hostStatusInfo); const show = useEndpointSelector(showView); + const setFlyoutView = useCallback( + (flyoutView: EndpointIndexUIQueryParams['show']) => { + dispatch({ + type: 'endpointDetailsFlyoutTabChanged', + payload: { + flyoutView, + }, + }); + }, + [dispatch] + ); + const ContentLoadingMarkup = useMemo( () => ( <> @@ -133,9 +127,11 @@ export const EndpointDetailsFlyout = memo(() => { ...urlSearchParams, }) ); - }, [history, queryParamsWithoutSelectedEndpoint]); + setFlyoutView(undefined); + }, [setFlyoutView, history, queryParamsWithoutSelectedEndpoint]); useEffect(() => { + setFlyoutView(show); if (hostDetailsError !== undefined) { toasts.addDanger({ title: i18n.translate('xpack.securitySolution.endpoint.details.errorTitle', { @@ -146,7 +142,10 @@ export const EndpointDetailsFlyout = memo(() => { }), }); } - }, [hostDetailsError, toasts]); + return () => { + setFlyoutView(undefined); + }; + }, [hostDetailsError, setFlyoutView, show, toasts]); return ( <EuiFlyout @@ -155,23 +154,11 @@ export const EndpointDetailsFlyout = memo(() => { data-test-subj="endpointDetailsFlyout" size="m" paddingSize="l" + ownFocus={false} > - <EuiFlyoutHeader> - {hostDetailsLoading ? ( - <EuiLoadingContent lines={1} /> - ) : ( - <EuiToolTip content={hostDetails?.host?.hostname} anchorClassName="eui-textTruncate"> - <EuiTitle> - <h2 - style={{ overflow: 'hidden', textOverflow: 'ellipsis' }} - data-test-subj="endpointDetailsFlyoutTitle" - > - {hostDetails?.host?.hostname} - </h2> - </EuiTitle> - </EuiToolTip> - )} - </EuiFlyoutHeader> + {(show === 'policy_response' || show === 'isolate' || show === 'unisolate') && ( + <EndpointDetailsFlyoutHeader hostname={hostDetails?.host?.hostname} /> + )} {hostDetails === undefined ? ( <EuiFlyoutBody> <EuiLoadingContent lines={3} /> <EuiSpacer size="l" /> <EuiLoadingContent lines={3} /> @@ -179,13 +166,11 @@ export const EndpointDetailsFlyout = memo(() => { ) : ( <> {(show === 'details' || show === 'activity_log') && ( - <DetailsFlyoutBody data-test-subj="endpointDetailsFlyoutBody"> - <EuiFlexGroup> - <EuiFlexItem> - <EndpointDetailsFlyoutTabs show={show} tabs={tabs} /> - </EuiFlexItem> - </EuiFlexGroup> - </DetailsFlyoutBody> + <EndpointDetailsFlyoutTabs + hostname={hostDetails?.host?.hostname} + show={show} + tabs={tabs} + /> )} {show === 'policy_response' && <PolicyResponseFlyoutPanel hostMeta={hostDetails} />} diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 6aab9336c21a4..4869ce84fad2c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -17,6 +17,7 @@ import { } from '../store/mock_endpoint_result_list'; import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; import { + ActivityLog, HostInfo, HostPolicyResponse, HostPolicyResponseActionStatus, @@ -32,12 +33,15 @@ import { KibanaServices, useKibana, useToasts } from '../../../../common/lib/kib import { hostIsolationHttpMocks } from '../../../../common/lib/endpoint_isolation/mocks'; import { fireEvent } from '@testing-library/dom'; import { + createFailedResourceState, + createLoadedResourceState, isFailedResourceState, isLoadedResourceState, isUninitialisedResourceState, } from '../../../state'; import { getCurrentIsolationRequestState } from '../store/selectors'; import { licenseService } from '../../../../common/hooks/use_license'; +import { FleetActionGenerator } from '../../../../../common/endpoint/data_generators/fleet_action_generator'; // not sure why this can't be imported from '../../../../common/mock/formatted_relative'; // but sure enough it needs to be inline in this one file @@ -625,6 +629,30 @@ describe('when on the endpoint list page', () => { }); }; + const dispatchEndpointDetailsActivityLogChanged = ( + dataState: 'failed' | 'success', + data: ActivityLog + ) => { + reactTestingLibrary.act(() => { + const getPayload = () => { + switch (dataState) { + case 'failed': + return createFailedResourceState({ + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred.', + }); + case 'success': + return createLoadedResourceState(data); + } + }; + store.dispatch({ + type: 'endpointDetailsActivityLogChanged', + payload: getPayload(), + }); + }); + }; + beforeEach(async () => { mockEndpointListApi(); @@ -746,6 +774,120 @@ describe('when on the endpoint list page', () => { expect(renderResult.getByTestId('endpointDetailsActionsButton')).not.toBeNull(); }); + describe('when showing Activity Log panel', () => { + let renderResult: ReturnType<typeof render>; + const agentId = 'some_agent_id'; + + let getMockData: () => ActivityLog; + beforeEach(async () => { + window.IntersectionObserver = jest.fn(() => ({ + root: null, + rootMargin: '', + thresholds: [], + takeRecords: jest.fn(), + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), + })); + + const fleetActionGenerator = new FleetActionGenerator('seed'); + const responseData = fleetActionGenerator.generateResponse({ + agent_id: agentId, + }); + const actionData = fleetActionGenerator.generate({ + agents: [agentId], + }); + getMockData = () => ({ + page: 1, + pageSize: 50, + data: [ + { + type: 'response', + item: { + id: 'some_id_0', + data: responseData, + }, + }, + { + type: 'action', + item: { + id: 'some_id_1', + data: actionData, + }, + }, + ], + }); + + renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedEndpointList'); + }); + const hostNameLinks = await renderResult.getAllByTestId('hostnameCellLink'); + reactTestingLibrary.fireEvent.click(hostNameLinks[0]); + }); + + afterEach(reactTestingLibrary.cleanup); + + it('should show the endpoint details flyout', async () => { + const activityLogTab = await renderResult.findByTestId('activity_log'); + reactTestingLibrary.act(() => { + reactTestingLibrary.fireEvent.click(activityLogTab); + }); + await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged'); + reactTestingLibrary.act(() => { + dispatchEndpointDetailsActivityLogChanged('success', getMockData()); + }); + const endpointDetailsFlyout = await renderResult.queryByTestId('endpointDetailsFlyoutBody'); + expect(endpointDetailsFlyout).not.toBeNull(); + }); + + it('should display log accurately', async () => { + const activityLogTab = await renderResult.findByTestId('activity_log'); + reactTestingLibrary.act(() => { + reactTestingLibrary.fireEvent.click(activityLogTab); + }); + await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged'); + reactTestingLibrary.act(() => { + dispatchEndpointDetailsActivityLogChanged('success', getMockData()); + }); + const logEntries = await renderResult.queryAllByTestId('timelineEntry'); + expect(logEntries.length).toEqual(2); + expect(`${logEntries[0]} .euiCommentTimeline__icon--update`).not.toBe(null); + expect(`${logEntries[1]} .euiCommentTimeline__icon--regular`).not.toBe(null); + }); + + it('should display empty state when API call has failed', async () => { + const activityLogTab = await renderResult.findByTestId('activity_log'); + reactTestingLibrary.act(() => { + reactTestingLibrary.fireEvent.click(activityLogTab); + }); + await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged'); + reactTestingLibrary.act(() => { + dispatchEndpointDetailsActivityLogChanged('failed', getMockData()); + }); + const emptyState = await renderResult.queryByTestId('activityLogEmpty'); + expect(emptyState).not.toBe(null); + }); + + it('should display empty state when no log data', async () => { + const activityLogTab = await renderResult.findByTestId('activity_log'); + reactTestingLibrary.act(() => { + reactTestingLibrary.fireEvent.click(activityLogTab); + }); + await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged'); + reactTestingLibrary.act(() => { + dispatchEndpointDetailsActivityLogChanged('success', { + page: 1, + pageSize: 50, + data: [], + }); + }); + + const emptyState = await renderResult.queryByTestId('activityLogEmpty'); + expect(emptyState).not.toBe(null); + }); + }); + describe('when showing host Policy Response panel', () => { let renderResult: ReturnType<typeof render>; beforeEach(async () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts index 18a5bd1e5130a..89ffd2d23807e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts @@ -16,6 +16,26 @@ export const ACTIVITY_LOG = { defaultMessage: 'Activity Log', }), LogEntry: { + endOfLog: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.action.endOfLog', + { + defaultMessage: 'Nothing more to show', + } + ), + emptyState: { + title: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.emptyState.title', + { + defaultMessage: 'No logged actions', + } + ), + body: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.emptyState.body', + { + defaultMessage: 'No actions have been logged for this endpoint.', + } + ), + }, action: { isolatedAction: i18n.translate( 'xpack.securitySolution.endpointDetails.activityLog.logEntry.action.isolated', diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx index 204c3a86ce3e6..e9cdd16554f33 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx @@ -42,7 +42,7 @@ import { useFormatUrl } from '../../../../common/components/link_to'; import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; import { MANAGEMENT_APP_ID } from '../../../common/constants'; import { PolicyDetailsRouteState } from '../../../../../common/endpoint/types'; -import { WrapperPage } from '../../../../common/components/wrapper_page'; +import { SecuritySolutionPageWrapper } from '../../../../common/components/page_wrapper'; import { HeaderPage } from '../../../../common/components/header_page'; import { PolicyDetailsForm } from './policy_details_form'; @@ -51,7 +51,7 @@ const PolicyDetailsHeader = styled.div` padding: ${(props) => props.theme.eui.paddingSizes.xl} 0; background-color: #fafbfd; border-bottom: 1px solid #d3dae6; - .siemHeaderPage { + .securitySolutionHeaderPage { max-width: ${maxFormWidth}; margin: 0 auto; } @@ -159,7 +159,7 @@ export const PolicyDetails = React.memo(() => { // Else, if we have an error, then show error on the page. if (!policyItem) { return ( - <WrapperPage noTimeline> + <SecuritySolutionPageWrapper noTimeline> {isPolicyLoading ? ( <EuiLoadingSpinner size="xl" /> ) : policyApiError ? ( @@ -168,7 +168,7 @@ export const PolicyDetails = React.memo(() => { </EuiCallOut> ) : null} <SpyRoute pageName={SecurityPageName.administration} /> - </WrapperPage> + </SecuritySolutionPageWrapper> ); } @@ -190,7 +190,7 @@ export const PolicyDetails = React.memo(() => { onConfirm={handleSaveConfirmation} /> )} - <WrapperPage + <SecuritySolutionPageWrapper noTimeline data-test-subj="policyDetailsPage" noPadding @@ -221,7 +221,7 @@ export const PolicyDetails = React.memo(() => { <PolicyDetailsForm /> </PolicyDetailsFormDiv> <EuiSpacer size="xxl" /> - </WrapperPage> + </SecuritySolutionPageWrapper> <EuiBottomBar paddingSize="s"> <EuiFlexGroup justifyContent="flexEnd" gutterSize="s"> <EuiFlexItem grow={false}> diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap index e984ea5bb1711..7b3ae2e2b3b27 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap @@ -34,15 +34,8 @@ exports[`TrustedAppsGrid renders correctly initially 1`] = ` <div class="euiEmptyPrompt" > - <span - class="euiTextColor euiTextColor--subdued" - > - <span> - No items found - </span> - <div - class="euiSpacer euiSpacer--m" - /> + <span> + No items found </span> </div> </div> @@ -427,7 +420,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="body-content undefined" > <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -722,7 +715,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -1017,7 +1010,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -1312,7 +1305,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -1607,7 +1600,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -1902,7 +1895,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -2197,7 +2190,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -2492,7 +2485,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -2787,7 +2780,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -3082,7 +3075,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -3685,7 +3678,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="body-content undefined" > <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -3980,7 +3973,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -4275,7 +4268,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -4570,7 +4563,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -4865,7 +4858,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -5160,7 +5153,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -5455,7 +5448,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -5750,7 +5743,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -6045,7 +6038,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -6340,7 +6333,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -6900,7 +6893,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="body-content undefined" > <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -7195,7 +7188,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -7490,7 +7483,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -7785,7 +7778,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -8080,7 +8073,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -8375,7 +8368,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -8670,7 +8663,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -8965,7 +8958,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -9260,7 +9253,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" @@ -9555,7 +9548,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not </div> </div> <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow trusted-app" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder trusted-app" > <div class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap index 543ca9695fce2..9a5137271b408 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap @@ -774,7 +774,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiTableCellContent euiTableCellContent--overflowingContent" > <div - class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow" + class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--hasShadow euiPanel--hasBorder" data-test-subj="trustedAppCard" > <div diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/embeddable.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/embeddable.test.tsx.snap index b03670b2b1cd4..8835a3ac390f3 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/embeddable.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/embeddable.test.tsx.snap @@ -5,6 +5,7 @@ exports[`Embeddable it renders 1`] = ` className="siemEmbeddable" > <Panel + hasBorder={true} paddingSize="none" > <p> diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embeddable.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embeddable.tsx index 82b5b8a3e7b3d..3087dbe4ad6ed 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embeddable.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embeddable.tsx @@ -20,7 +20,9 @@ export interface EmbeddableProps { export const Embeddable = React.memo<EmbeddableProps>(({ children }) => ( <section className="siemEmbeddable"> - <Panel paddingSize="none">{children}</Panel> + <Panel paddingSize="none" hasBorder> + {children} + </Panel> </section> )); Embeddable.displayName = 'Embeddable'; diff --git a/x-pack/plugins/security_solution/public/network/components/ip/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/ip/index.test.tsx index a3fd32008062c..63971ae508d5c 100644 --- a/x-pack/plugins/security_solution/public/network/components/ip/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/ip/index.test.tsx @@ -14,6 +14,8 @@ import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { Ip } from '.'; +jest.mock('../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx index 7ec18c078c73d..a811f5c92c37a 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx @@ -25,6 +25,8 @@ import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { NetworkDnsTable } from '.'; import { mockData } from './mock'; +jest.mock('../../../common/lib/kibana'); + describe('NetworkTopNFlow Table Component', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; diff --git a/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx index f7f75d9f0a365..f05372c76b36f 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx @@ -25,6 +25,7 @@ import { networkModel } from '../../store'; import { NetworkHttpTable } from '.'; import { mockData } from './mock'; +jest.mock('../../../common/lib/kibana'); jest.mock('../../../common/components/link_to'); describe('NetworkHttp Table Component', () => { diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx index 1501f56882290..a0727fad65f18 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx @@ -27,6 +27,8 @@ import { networkModel } from '../../store'; import { NetworkTopCountriesTable } from '.'; import { mockData } from './mock'; +jest.mock('../../../common/lib/kibana'); + describe('NetworkTopCountries Table Component', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx index cd8c8c6543299..e2b9447b58806 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx @@ -25,6 +25,7 @@ import { NetworkTopNFlowTable } from '.'; import { mockData } from './mock'; import { FlowTargetSourceDest } from '../../../../common/search_strategy'; +jest.mock('../../../common/lib/kibana'); jest.mock('../../../common/components/link_to'); describe('NetworkTopNFlow Table Component', () => { diff --git a/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx index ef1039bfc92e3..dd7ad20d2384a 100644 --- a/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx @@ -15,6 +15,8 @@ import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { Port } from '.'; +jest.mock('../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx index 01065ad5bf15f..b59eb25cbfe25 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx @@ -49,6 +49,8 @@ import { NETWORK_TRANSPORT_FIELD_NAME, } from './field_names'; +jest.mock('../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx index f767e793c8f21..91f7ea3d7ac7a 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx @@ -38,6 +38,8 @@ import { SOURCE_GEO_REGION_NAME_FIELD_NAME, } from './geo_fields'; +jest.mock('../../../common/lib/kibana'); + jest.mock('../../../common/components/link_to'); describe('SourceDestinationIp', () => { diff --git a/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx index 4b6c31f5b6176..8f2c7a098a045 100644 --- a/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx @@ -24,6 +24,8 @@ import { networkModel } from '../../store'; import { TlsTable } from '.'; import { mockTlsData } from './mock'; +jest.mock('../../../common/lib/kibana'); + describe('Tls Table Component', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; diff --git a/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx index 4b613e79a1d1a..69027ad9bd9f8 100644 --- a/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx @@ -26,6 +26,8 @@ import { UsersTable } from '.'; import { mockUsersData } from './mock'; import { FlowTarget } from '../../../../common/search_strategy'; +jest.mock('../../../common/lib/kibana'); + describe('Users Table Component', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; diff --git a/x-pack/plugins/security_solution/public/network/pages/details/index.tsx b/x-pack/plugins/security_solution/public/network/pages/details/index.tsx index 4cccb536c08bb..02be5f78261c1 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/index.tsx @@ -28,7 +28,7 @@ import { manageQuery } from '../../../common/components/page/manage_query'; import { FlowTargetSelectConnected } from '../../components/flow_target_select_connected'; import { IpOverview } from '../../components/details'; import { SiemSearchBar } from '../../../common/components/search_bar'; -import { WrapperPage } from '../../../common/components/wrapper_page'; +import { SecuritySolutionPageWrapper } from '../../../common/components/page_wrapper'; import { useNetworkDetails } from '../../containers/details'; import { useKibana } from '../../../common/lib/kibana'; import { decodeIpv6 } from '../../../common/lib/helpers'; @@ -128,7 +128,7 @@ const NetworkDetailsComponent: React.FC = () => { <SiemSearchBar indexPattern={indexPattern} id="global" /> </FiltersGlobal> - <WrapperPage> + <SecuritySolutionPageWrapper> <HeaderPage border data-test-subj="network-details-headline" @@ -289,14 +289,14 @@ const NetworkDetailsComponent: React.FC = () => { hideHistogramIfEmpty={true} AnomaliesTableComponent={AnomaliesNetworkTable} /> - </WrapperPage> + </SecuritySolutionPageWrapper> </> ) : ( - <WrapperPage> + <SecuritySolutionPageWrapper> <HeaderPage border title={ip} /> <OverviewEmpty /> - </WrapperPage> + </SecuritySolutionPageWrapper> )} <SpyRoute pageName={SecurityPageName.network} /> diff --git a/x-pack/plugins/security_solution/public/network/pages/network.tsx b/x-pack/plugins/security_solution/public/network/pages/network.tsx index 2bcc72d932a9b..13c04a5e5ec5b 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.tsx @@ -12,6 +12,7 @@ import { useDispatch } from 'react-redux'; import { useParams } from 'react-router-dom'; import styled from 'styled-components'; +import { isTab } from '../../../../timelines/public'; import { esQuery } from '../../../../../../src/plugins/data/public'; import { SecurityPageName } from '../../app/types'; import { UpdateDateRange } from '../../common/components/charts/common'; @@ -19,11 +20,11 @@ import { EmbeddedMap } from '../components/embeddables/embedded_map'; import { FiltersGlobal } from '../../common/components/filters_global'; import { HeaderPage } from '../../common/components/header_page'; import { LastEventTime } from '../../common/components/last_event_time'; -import { SiemNavigation } from '../../common/components/navigation'; +import { SecuritySolutionTabNavigation } from '../../common/components/navigation'; import { NetworkKpiComponent } from '../components/kpi_network'; import { SiemSearchBar } from '../../common/components/search_bar'; -import { WrapperPage } from '../../common/components/wrapper_page'; +import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; import { useGlobalFullScreen } from '../../common/containers/use_full_screen'; import { useGlobalTime } from '../../common/containers/use_global_time'; import { LastEventIndexKey } from '../../../common/search_strategy'; @@ -46,7 +47,6 @@ import { showGlobalFilters, } from '../../timelines/components/timeline/helpers'; import { timelineSelectors } from '../../timelines/store/timeline'; -import { isTab } from '../../common/components/accessibility/helpers'; import { TimelineId } from '../../../common/types/timeline'; import { timelineDefaults } from '../../timelines/store/timeline/defaults'; import { useSourcererScope } from '../../common/containers/sourcerer'; @@ -155,10 +155,9 @@ const NetworkComponent = React.memo<NetworkComponentProps>( <SiemSearchBar indexPattern={indexPattern} id="global" /> </FiltersGlobal> - <WrapperPage noPadding={globalFullScreen}> + <SecuritySolutionPageWrapper noPadding={globalFullScreen}> <Display show={!globalFullScreen}> <HeaderPage - border subtitle={ <LastEventTime docValueFields={docValueFields} @@ -195,7 +194,7 @@ const NetworkComponent = React.memo<NetworkComponentProps>( <Display show={!globalFullScreen}> <EuiSpacer /> - <SiemNavigation navTabs={navTabsNetwork(hasMlUserPermissions)} /> + <SecuritySolutionTabNavigation navTabs={navTabsNetwork(hasMlUserPermissions)} /> <EuiSpacer /> </Display> @@ -217,13 +216,13 @@ const NetworkComponent = React.memo<NetworkComponentProps>( ) : ( <NetworkRoutesLoading /> )} - </WrapperPage> + </SecuritySolutionPageWrapper> </StyledFullHeightContainer> ) : ( - <WrapperPage> + <SecuritySolutionPageWrapper> <HeaderPage border title={i18n.PAGE_TITLE} /> <OverviewEmpty /> - </WrapperPage> + </SecuritySolutionPageWrapper> )} <SpyRoute pageName={SecurityPageName.network} /> diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx index b43d5af029ec4..45898427ee60b 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx @@ -15,6 +15,8 @@ import { TestProviders } from '../../../../common/mock'; import { EndpointOverview } from './index'; import { HostPolicyResponseActionStatus } from '../../../../../common/search_strategy/security_solution/hosts'; +jest.mock('../../../../common/lib/kibana'); + describe('EndpointOverview Component', () => { test('it renders with endpoint data', () => { const endpointData = { diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx index 70f44a0008cbc..f11b849f5df6b 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx @@ -115,7 +115,7 @@ const OverviewHostComponent: React.FC<OverviewHostProps> = ({ return ( <EuiFlexItem> <InspectButtonContainer> - <EuiPanel> + <EuiPanel hasBorder> <HeaderSection id={OverviewHostQueryId} subtitle={subtitle} title={title}> <>{hostPageButton}</> </HeaderSection> diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx index 107a47f6cc132..39fb6ff08ee53 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx @@ -120,7 +120,7 @@ const OverviewNetworkComponent: React.FC<OverviewNetworkProps> = ({ return ( <EuiFlexItem> <InspectButtonContainer> - <EuiPanel data-test-subj="overview-network-query"> + <EuiPanel hasBorder data-test-subj="overview-network-query"> <> <HeaderSection id={OverviewNetworkQueryId} subtitle={subtitle} title={title}> {networkPageButton} diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index 4270d8ec164b3..2cf998e5e133a 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -12,7 +12,7 @@ import styled from 'styled-components'; import { AlertsByCategory } from '../components/alerts_by_category'; import { FiltersGlobal } from '../../common/components/filters_global'; import { SiemSearchBar } from '../../common/components/search_bar'; -import { WrapperPage } from '../../common/components/wrapper_page'; +import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; import { useGlobalTime } from '../../common/containers/use_global_time'; import { useFetchIndex } from '../../common/containers/source'; @@ -37,6 +37,10 @@ const SidebarFlexItem = styled(EuiFlexItem)` margin-right: 24px; `; +const StyledSecuritySolutionPageWrapper = styled(SecuritySolutionPageWrapper)` + overflow-x: auto; +`; + const OverviewComponent = () => { const getGlobalFiltersQuerySelector = useMemo( () => inputsSelectors.globalFiltersQuerySelector(), @@ -73,7 +77,7 @@ const OverviewComponent = () => { <SiemSearchBar id="global" indexPattern={indexPattern} /> </FiltersGlobal> - <WrapperPage> + <StyledSecuritySolutionPageWrapper> {!dismissMessage && !metadataIndexExists && isIngestEnabled && ( <> <EndpointNotice onDismiss={dismissEndpointNotice} /> @@ -139,7 +143,7 @@ const OverviewComponent = () => { </EuiFlexGroup> </EuiFlexItem> </EuiFlexGroup> - </WrapperPage> + </StyledSecuritySolutionPageWrapper> </> ) : ( <OverviewEmpty /> diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 781ed8ffdaa54..32e6748f38141 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -6,8 +6,10 @@ */ import { i18n } from '@kbn/i18n'; +import reduceReducers from 'reduce-reducers'; import { BehaviorSubject, Subject, Subscription } from 'rxjs'; import { pluck } from 'rxjs/operators'; +import { AnyAction, Reducer } from 'redux'; import { PluginSetup, PluginStart, @@ -59,7 +61,7 @@ import { DETECTION_ENGINE, CASE, ADMINISTRATION, -} from './app/home/translations'; +} from './app/translations'; import { IndexFieldsStrategyRequest, IndexFieldsStrategyResponse, @@ -72,6 +74,7 @@ import { getLazyEndpointPolicyEditExtension } from './management/pages/policy/vi import { LazyEndpointPolicyCreateExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension'; import { getLazyEndpointPackageCustomExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_package_custom_extension'; import { parseExperimentalConfigValue } from '../common/experimental_features'; +import type { TimelineState } from '../../timelines/public'; export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, StartPlugins> { private kibanaVersion: string; @@ -471,7 +474,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S .search<IndexFieldsStrategyRequest, IndexFieldsStrategyResponse>( { indices: defaultIndicesName, onlyCheckIfIndicesExist: true }, { - strategy: 'securitySolutionIndexFields', + strategy: 'indexFields', } ) .toPromise(), @@ -500,7 +503,6 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S const networkStart = networkSubPlugin.start(this.storage); const timelinesStart = timelinesSubPlugin.start(); const managementSubPluginStart = managementSubPlugin.start(coreStart, startPlugins); - const timelineInitialState = { timeline: { ...timelinesStart.store.initialState.timeline!, @@ -513,6 +515,13 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S }, }; + const tGridReducer = startPlugins.timelines?.getTGridReducer() ?? {}; + const timelineReducer = (reduceReducers( + timelineInitialState.timeline, + tGridReducer, + timelinesStart.store.reducer.timeline + ) as unknown) as Reducer<TimelineState, AnyAction>; + this._store = createStore( createInitialState( { @@ -531,13 +540,17 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S { ...hostsStart.store.reducer, ...networkStart.store.reducer, - ...timelinesStart.store.reducer, + timeline: timelineReducer, ...managementSubPluginStart.store.reducer, + ...tGridReducer, }, libs$.pipe(pluck('kibana')), this.storage, [...(managementSubPluginStart.store.middleware ?? [])] ); + if (startPlugins.timelines) { + startPlugins.timelines.setTGridEmbeddedStore(this._store); + } } return this._store; } diff --git a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx index 45f7e6950b006..1f520a1847053 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx @@ -207,7 +207,7 @@ export const GraphControls = React.memo( /> </StyledGraphControlsColumn> <StyledGraphControlsColumn> - <EuiPanel className="panning-controls" paddingSize="none" hasShadow> + <EuiPanel className="panning-controls" paddingSize="none" hasBorder> <div className="panning-controls-top"> <button className="north-button" @@ -265,7 +265,7 @@ export const GraphControls = React.memo( </button> </div> </EuiPanel> - <EuiPanel className="zoom-controls" paddingSize="none" hasShadow> + <EuiPanel className="zoom-controls" paddingSize="none" hasBorder> <button title={i18n.translate('xpack.securitySolution.resolver.graphControls.zoomIn', { defaultMessage: 'Zoom In', diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx index c587c454604e8..6fde15b85a2fc 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx @@ -60,7 +60,7 @@ export const EventDetail = memo(function EventDetail({ const event = useSelector(selectors.currentRelatedEventData); return isLoading ? ( - <StyledPanel> + <StyledPanel hasBorder> <PanelLoading /> </StyledPanel> ) : event ? ( @@ -71,7 +71,7 @@ export const EventDetail = memo(function EventDetail({ eventType={eventType} /> ) : ( - <StyledPanel> + <StyledPanel hasBorder> <PanelContentError translatedErrorMessage={eventDetailRequestError} /> </StyledPanel> ); @@ -105,7 +105,7 @@ const EventDetailContents = memo(function ({ const nodeName = processEvent ? eventModel.processNameSafeVersion(processEvent) : null; return ( - <StyledPanel data-test-subj="resolver:panel:event-detail"> + <StyledPanel hasBorder data-test-subj="resolver:panel:event-detail"> <EventDetailBreadcrumbs nodeID={nodeID} nodeName={nodeName} diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx index ed3507d8f4bc3..a3f1b1fccb5de 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx @@ -48,15 +48,15 @@ export const NodeDetail = memo(function ({ nodeID }: { nodeID: string }) { const nodeStatus = useSelector((state: ResolverState) => selectors.nodeDataStatus(state)(nodeID)); return nodeStatus === 'loading' ? ( - <StyledPanel> + <StyledPanel hasBorder> <PanelLoading /> </StyledPanel> ) : processEvent ? ( - <StyledPanel data-test-subj="resolver:panel:node-detail"> + <StyledPanel hasBorder data-test-subj="resolver:panel:node-detail"> <NodeDetailView nodeID={nodeID} processEvent={processEvent} /> </StyledPanel> ) : ( - <StyledPanel> + <StyledPanel hasBorder> <PanelContentError translatedErrorMessage={nodeDetailError} /> </StyledPanel> ); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events.tsx index 951d2ff9fdc43..e7cd37506134f 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events.tsx @@ -30,13 +30,13 @@ export function NodeEvents({ nodeID }: { nodeID: string }) { if (processEvent === undefined || nodeStats === undefined) { return ( - <StyledPanel> + <StyledPanel hasBorder> <PanelLoading /> </StyledPanel> ); } else { return ( - <StyledPanel> + <StyledPanel hasBorder> <NodeEventsBreadcrumbs nodeName={event.processNameSafeVersion(processEvent)} nodeID={nodeID} diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx index aff78e4b5965b..09437c662ead9 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx @@ -50,11 +50,11 @@ export const NodeEventsInCategory = memo(function ({ return ( <> {isLoading ? ( - <StyledPanel> + <StyledPanel hasBorder> <PanelLoading /> </StyledPanel> ) : ( - <StyledPanel data-test-subj="resolver:panel:events-in-category"> + <StyledPanel hasBorder data-test-subj="resolver:panel:events-in-category"> {hasError || !node ? ( <EuiCallOut title={i18n.translate( diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx index f3a85635eb9a8..682304b5395b1 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx @@ -124,7 +124,7 @@ export const NodeList = memo(() => { const showWarning = children === true || ancestors === true || generations === true; const rowProps = useMemo(() => ({ 'data-test-subj': 'resolver:node-list:item' }), []); return ( - <StyledPanel> + <StyledPanel hasBorder> <Breadcrumbs breadcrumbs={breadcrumbs} /> {showWarning && <LimitWarning numberDisplayed={numberOfProcesses} />} <EuiSpacer size="l" /> diff --git a/x-pack/plugins/security_solution/public/resolver/view/styles.tsx b/x-pack/plugins/security_solution/public/resolver/view/styles.tsx index 851e8a2fcf10e..44522441e6e99 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/styles.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/styles.tsx @@ -95,6 +95,11 @@ export const StyledMapContainer = styled.div<{ backgroundColor: string }>` justify-content: center; flex-grow: 1; } + /** + * Set to force base-height necessary for resolver to show up in timeline. + * Was previously set in events_viewer.tsx, but more appropriate here + */ + min-height: 652px; /** * The placeholder components use absolute positioning. */ diff --git a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx index 77f97b947d824..1315a7d6c45d9 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import React, { useMemo, useContext, useCallback } from 'react'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage } from '@kbn/i18n/react'; import { useDispatch } from 'react-redux'; import { EuiI18nNumber } from '@elastic/eui'; import { EventStats } from '../../../common/endpoint/types'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx index 7a38c873450ca..4ebb804eab8a4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx @@ -14,6 +14,8 @@ import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { CertificateFingerprint } from '.'; +jest.mock('../../../common/lib/kibana'); + describe('CertificateFingerprint', () => { const mount = useMountAppended(); test('renders the expected label', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/duration/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/duration/index.test.tsx index 4c90d3738a198..ea8317346cd99 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/duration/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/duration/index.test.tsx @@ -14,6 +14,8 @@ import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { Duration } from '.'; +jest.mock('../../../common/lib/kibana'); + describe('Duration', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx index 5becf7ea8bc6b..e2194156ecf4d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx @@ -29,6 +29,8 @@ import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { AutonomousSystem, FlowTarget } from '../../../../common/search_strategy'; import { HostEcs } from '../../../../common/ecs/host'; +jest.mock('../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.tsx index 77a8d0082bf23..da2ff248d9a5d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.tsx @@ -14,7 +14,7 @@ import { DATA_COLINDEX_ATTRIBUTE, DATA_ROWINDEX_ATTRIBUTE, onKeyDownFocusHandler, -} from '../../../common/components/accessibility/helpers'; +} from '../../../../../timelines/public'; import { BrowserFields } from '../../../common/containers/source'; import { getCategoryColumns } from './category_columns'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.test.tsx index c3c55206f8d53..c95463dea5b27 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.test.tsx @@ -17,6 +17,9 @@ import { TestProviders } from '../../../common/mock'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; import * as i18n from './translations'; + +jest.mock('../../../common/lib/kibana'); + describe('Category', () => { const timelineId = 'test'; const selectedCategoryId = 'client'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.tsx index 636ebf022cffb..deafda95ceab2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.tsx @@ -9,13 +9,13 @@ import { EuiInMemoryTable } from '@elastic/eui'; import { noop } from 'lodash/fp'; import React, { useCallback, useMemo, useRef } from 'react'; import styled from 'styled-components'; - import { arrayIndexToAriaIndex, DATA_COLINDEX_ATTRIBUTE, DATA_ROWINDEX_ATTRIBUTE, onKeyDownFocusHandler, -} from '../../../common/components/accessibility/helpers'; +} from '../../../../../timelines/public'; + import { BrowserFields } from '../../../common/containers/source'; import { OnUpdateColumns } from '../timeline/events'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.tsx index 15164cd151574..528791328fdb9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_columns.tsx @@ -18,6 +18,7 @@ import { import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { BrowserFields } from '../../../common/containers/source'; import { getColumnsWithTimestamp } from '../../../common/components/event_details/helpers'; import { CountBadge } from '../../../common/components/page'; @@ -29,7 +30,7 @@ import { VIEW_ALL_BUTTON_CLASS_NAME, } from './helpers'; import * as i18n from './translations'; -import { useManageTimeline } from '../manage_timeline'; +import { timelineSelectors } from '../../store/timeline'; const CategoryName = styled.span<{ bold: boolean }>` .euiText { @@ -67,11 +68,10 @@ interface ViewAllButtonProps { export const ViewAllButton = React.memo<ViewAllButtonProps>( ({ categoryId, browserFields, onUpdateColumns, timelineId }) => { - const { getManageTimelineById } = useManageTimeline(); - const { isLoading } = useMemo(() => getManageTimelineById(timelineId) ?? { isLoading: false }, [ - getManageTimelineById, - timelineId, - ]); + const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []); + const { isLoading } = useDeepEqualSelector((state) => + getManageTimeline(state, timelineId ?? '') + ); const handleClick = useCallback(() => { onUpdateColumns( diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_title.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_title.test.tsx index 70cc535cb59a9..6af4b5c5c312e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_title.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category_title.test.tsx @@ -9,6 +9,7 @@ import { mount } from 'enzyme'; import React from 'react'; import { mockBrowserFields } from '../../../common/containers/source/mock'; +import { TestProviders } from '../../../common/mock'; import { CategoryTitle } from './category_title'; import { getFieldCount } from './helpers'; @@ -19,12 +20,14 @@ describe('CategoryTitle', () => { test('it renders the category id as the value of the title', () => { const categoryId = 'client'; const wrapper = mount( - <CategoryTitle - categoryId={categoryId} - filteredBrowserFields={mockBrowserFields} - onUpdateColumns={jest.fn()} - timelineId={timelineId} - /> + <TestProviders> + <CategoryTitle + categoryId={categoryId} + filteredBrowserFields={mockBrowserFields} + onUpdateColumns={jest.fn()} + timelineId={timelineId} + /> + </TestProviders> ); expect(wrapper.find('[data-test-subj="selected-category-title"]').first().text()).toEqual( @@ -35,12 +38,14 @@ describe('CategoryTitle', () => { test('when `categoryId` specifies a valid category in `filteredBrowserFields`, a count of the field is displayed in the badge', () => { const validCategoryId = 'client'; const wrapper = mount( - <CategoryTitle - categoryId={validCategoryId} - filteredBrowserFields={mockBrowserFields} - onUpdateColumns={jest.fn()} - timelineId={timelineId} - /> + <TestProviders> + <CategoryTitle + categoryId={validCategoryId} + filteredBrowserFields={mockBrowserFields} + onUpdateColumns={jest.fn()} + timelineId={timelineId} + /> + </TestProviders> ); expect(wrapper.find(`[data-test-subj="selected-category-count-badge"]`).first().text()).toEqual( @@ -51,12 +56,14 @@ describe('CategoryTitle', () => { test('when `categoryId` specifies an INVALID category in `filteredBrowserFields`, a count of zero is displayed in the badge', () => { const invalidCategoryId = 'this.is.not.happening'; const wrapper = mount( - <CategoryTitle - categoryId={invalidCategoryId} - filteredBrowserFields={mockBrowserFields} - onUpdateColumns={jest.fn()} - timelineId={timelineId} - /> + <TestProviders> + <CategoryTitle + categoryId={invalidCategoryId} + filteredBrowserFields={mockBrowserFields} + onUpdateColumns={jest.fn()} + timelineId={timelineId} + /> + </TestProviders> ); expect(wrapper.find(`[data-test-subj="selected-category-count-badge"]`).first().text()).toEqual( diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx index c4f76c639c7c1..0496b9d7c8886 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx @@ -19,13 +19,8 @@ import { noop } from 'lodash/fp'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; -import { - isEscape, - isTab, - stopPropagationAndPreventDefault, -} from '../../../common/components/accessibility/helpers'; +import { isEscape, isTab, stopPropagationAndPreventDefault } from '../../../../../timelines/public'; import { BrowserFields } from '../../../common/containers/source'; -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { CategoriesPane } from './categories_pane'; import { FieldsPane } from './fields_pane'; import { Header } from './header'; @@ -42,6 +37,7 @@ import { FieldBrowserProps, OnHideFieldBrowser } from './types'; import { timelineActions } from '../../store/timeline'; import * as i18n from './translations'; +import { ColumnHeaderOptions } from '../../../../common'; const FieldsBrowserContainer = styled.div<{ width: number }>` background-color: ${({ theme }) => theme.eui.euiColorLightestShade}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx index 07911541bb2fe..e40807dc85dc7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx @@ -12,7 +12,6 @@ import { waitFor } from '@testing-library/react'; import { mockBrowserFields } from '../../../common/containers/source/mock'; import { TestProviders } from '../../../common/mock'; import '../../../common/mock/match_media'; -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../timeline/body/constants'; @@ -20,6 +19,9 @@ import { Category } from './category'; import { getFieldColumns, getFieldItems } from './field_items'; import { FIELDS_PANE_WIDTH } from './helpers'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; +import { ColumnHeaderOptions } from '../../../../common'; + +jest.mock('../../../common/lib/kibana'); const selectedCategoryId = 'base'; const selectedCategoryFields = mockBrowserFields[selectedCategoryId].fields; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.tsx index a2db284e51790..89a91ee6da305 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.tsx @@ -18,14 +18,12 @@ import React, { useCallback, useRef, useState } from 'react'; import { Draggable } from 'react-beautiful-dnd'; import styled from 'styled-components'; +import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid'; import { BrowserField, BrowserFields } from '../../../common/containers/source'; -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; -import { useDraggableKeyboardWrapper } from '../../../common/components/drag_and_drop/draggable_keyboard_wrapper_hook'; import { DragEffects } from '../../../common/components/drag_and_drop/draggable_wrapper'; import { DroppableWrapper } from '../../../common/components/drag_and_drop/droppable_wrapper'; import { DRAG_TYPE_FIELD, - DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME, getDraggableFieldId, getDroppableId, } from '../../../common/components/drag_and_drop/helpers'; @@ -43,6 +41,8 @@ import { TruncatableText } from '../../../common/components/truncatable_text'; import { FieldName } from './field_name'; import * as i18n from './translations'; import { getAlertColumnHeader } from './helpers'; +import { ColumnHeaderOptions } from '../../../../common'; +import { useKibana } from '../../../common/lib/kibana'; const TypeIcon = styled(EuiIcon)` margin: 0 4px; @@ -92,6 +92,7 @@ const DraggableFieldsBrowserFieldComponent = ({ const keyboardHandlerRef = useRef<HTMLDivElement | null>(null); const [closePopOverTrigger, setClosePopOverTrigger] = useState<boolean>(false); const [hoverActionsOwnFocus, setHoverActionsOwnFocus] = useState<boolean>(false); + const { timelines } = useKibana().services; const handleClosePopOverTrigger = useCallback(() => { setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger); @@ -115,7 +116,7 @@ const DraggableFieldsBrowserFieldComponent = ({ setHoverActionsOwnFocus(true); }, [setHoverActionsOwnFocus]); - const { onBlur, onKeyDown } = useDraggableKeyboardWrapper({ + const { onBlur, onKeyDown } = timelines.getUseDraggableKeyboardWrapper()({ closePopover: handleClosePopOverTrigger, draggableId: getDraggableFieldId({ contextId: `field-browser-field-items-field-draggable-${timelineId}-${categoryId}-${fieldName}`, diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx index 493f2e44263e3..5014a198e8bd5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx @@ -15,6 +15,8 @@ import { getColumnsWithTimestamp } from '../../../common/components/event_detail import { FieldName } from './field_name'; +jest.mock('../../../common/lib/kibana'); + const categoryId = 'base'; const timestampFieldId = '@timestamp'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx index 09bd18ef62fb1..2e76e43227506 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx @@ -9,13 +9,13 @@ import { EuiHighlight, EuiText } from '@elastic/eui'; import React, { useCallback, useState, useMemo, useRef } from 'react'; import styled from 'styled-components'; -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { OnUpdateColumns } from '../timeline/events'; import { WithHoverActions } from '../../../common/components/with_hover_actions'; import { DraggableWrapperHoverContent, useGetTimelineId, } from '../../../common/components/drag_and_drop/draggable_wrapper_hover_content'; +import { ColumnHeaderOptions } from '../../../../common'; /** * The name of a (draggable) field diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx index 3f1b0300ad70d..6d17f148aa1dc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx @@ -15,6 +15,8 @@ import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { FIELDS_PANE_WIDTH } from './helpers'; import { FieldsPane } from './fields_pane'; +jest.mock('../../../common/lib/kibana'); + const timelineId = 'test'; describe('FieldsPane', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.tsx index 15df232a1a454..dfb4edad17414 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.tsx @@ -11,7 +11,6 @@ import styled from 'styled-components'; import { useDispatch } from 'react-redux'; import { BrowserFields } from '../../../common/containers/source'; -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { timelineActions } from '../../../timelines/store/timeline'; import { OnUpdateColumns } from '../timeline/events'; import { Category } from './category'; @@ -20,6 +19,7 @@ import { getFieldItems } from './field_items'; import { FIELDS_PANE_WIDTH, TABLE_HEIGHT } from './helpers'; import * as i18n from './translations'; +import { ColumnHeaderOptions } from '../../../../common'; const NoFieldsPanel = styled.div` background-color: ${(props) => props.theme.eui.euiColorLightestShade}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.test.tsx index aa53b1922f3a3..89b361e86422e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.test.tsx @@ -9,7 +9,6 @@ import { mount } from 'enzyme'; import React from 'react'; import { mockBrowserFields } from '../../../common/containers/source/mock'; import { TestProviders } from '../../../common/mock'; -import { defaultHeaders } from '../timeline/body/column_headers/default_headers'; import { Header } from './header'; const timelineId = 'test'; @@ -72,7 +71,7 @@ describe('Header', () => { wrapper.find('[data-test-subj="reset-fields"]').first().simulate('click'); - expect(onUpdateColumns).toBeCalledWith(defaultHeaders); + expect(onUpdateColumns).toBeCalled(); }); test('it invokes onOutsideClick when the user clicks the Reset Fields button', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.tsx index 120a82a4046e3..b52c6cd672ac7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/header.tsx @@ -13,10 +13,12 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { BrowserFields } from '../../../common/containers/source'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; +import { timelineSelectors } from '../../store/timeline'; import { OnUpdateColumns } from '../timeline/events'; import { @@ -27,7 +29,6 @@ import { } from './helpers'; import * as i18n from './translations'; -import { useManageTimeline } from '../manage_timeline'; const CountsFlexGroup = styled(EuiFlexGroup)` margin-top: 5px; @@ -101,13 +102,13 @@ const TitleRow = React.memo<{ onOutsideClick: () => void; onUpdateColumns: OnUpdateColumns; }>(({ id, onOutsideClick, onUpdateColumns }) => { - const { getManageTimelineById } = useManageTimeline(); + const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []); + const { defaultColumns } = useDeepEqualSelector((state) => getManageTimeline(state, id)); const handleResetColumns = useCallback(() => { - const timeline = getManageTimelineById(id); - onUpdateColumns(timeline.defaultModel.columns); + onUpdateColumns(defaultColumns); onOutsideClick(); - }, [id, onUpdateColumns, onOutsideClick, getManageTimelineById]); + }, [onUpdateColumns, onOutsideClick, defaultColumns]); return ( <EuiFlexGroup diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/helpers.tsx index 4d06632d6441d..256c172721d35 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/helpers.tsx @@ -13,7 +13,7 @@ import { elementOrChildrenHasFocus, skipFocusInContainerTo, stopPropagationAndPreventDefault, -} from '../../../common/components/accessibility/helpers'; +} from '../../../../../timelines/public'; import { TimelineId } from '../../../../common/types/timeline'; import { BrowserField, BrowserFields } from '../../../common/containers/source'; import { alertsHeaders } from '../../../common/components/alerts_viewer/default_headers'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx index 08ce77cc2da45..381017f7f6260 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx @@ -18,6 +18,8 @@ import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from './helpers'; import { StatefulFieldsBrowserComponent } from '.'; +jest.mock('../../../common/lib/kibana'); + describe('StatefulFieldsBrowser', () => { const timelineId = 'test'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/types.ts b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/types.ts index 4d912f73c7ef2..ea71a8860ab01 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/types.ts @@ -5,8 +5,8 @@ * 2.0. */ +import { ColumnHeaderOptions } from '../../../../common'; import { BrowserFields } from '../../../common/containers/source'; -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; export type OnFieldSelected = (fieldId: string) => void; export type OnHideFieldBrowser = () => void; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.tsx index 7b43fb9c7194c..32d36006fffd5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/bottom_bar/index.tsx @@ -6,60 +6,19 @@ */ import { EuiPanel } from '@elastic/eui'; -import { rgba } from 'polished'; import React from 'react'; import styled from 'styled-components'; -import { IS_DRAGGING_CLASS_NAME } from '../../../../common/components/drag_and_drop/helpers'; import { DataProvider } from '../../timeline/data_providers/data_provider'; import { flattenIntoAndGroups } from '../../timeline/data_providers/helpers'; import { DataProviders } from '../../timeline/data_providers'; -import { FLYOUT_BUTTON_BAR_CLASS_NAME, FLYOUT_BUTTON_CLASS_NAME } from '../../timeline/helpers'; +import { FLYOUT_BUTTON_BAR_CLASS_NAME } from '../../timeline/helpers'; import { FlyoutHeaderPanel } from '../header'; import { TimelineTabs } from '../../../../../common/types/timeline'; export const getBadgeCount = (dataProviders: DataProvider[]): number => flattenIntoAndGroups(dataProviders).reduce((total, group) => total + group.length, 0); -const SHOW_HIDE_GLOBAL_TRANSLATE_Y = 50; // px -const SHOW_HIDE_TIMELINE_TRANSLATE_Y = 0; // px - -const Container = styled.div.attrs<{ $isGlobal: boolean }>(({ $isGlobal = true }) => ({ - style: { - transform: $isGlobal - ? `translateY(calc(100% - ${SHOW_HIDE_GLOBAL_TRANSLATE_Y}px))` - : `translateY(calc(100% - ${SHOW_HIDE_TIMELINE_TRANSLATE_Y}px))`, - }, -}))<{ $isGlobal: boolean }>` - position: fixed; - left: 0; - bottom: 0; - user-select: none; - width: 100%; - z-index: ${({ theme }) => theme.eui.euiZLevel8 + 1}; - - .${IS_DRAGGING_CLASS_NAME} & { - transform: none !important; - } - - .${FLYOUT_BUTTON_CLASS_NAME} { - background: ${({ theme }) => rgba(theme.eui.euiPageBackgroundColor, 1)}; - border-radius: 4px 4px 0 0; - box-shadow: none; - height: 46px; - } - - .${IS_DRAGGING_CLASS_NAME} & .${FLYOUT_BUTTON_CLASS_NAME} { - color: ${({ theme }) => theme.eui.euiColorSuccess}; - background: ${({ theme }) => rgba(theme.eui.euiColorSuccess, 0.2)} !important; - border: 1px solid ${({ theme }) => theme.eui.euiColorSuccess}; - border-bottom: none; - text-decoration: none; - } -`; - -Container.displayName = 'Container'; - const DataProvidersPanel = styled(EuiPanel)` border-radius: 0; padding: 0 4px 0 4px; @@ -76,18 +35,14 @@ interface FlyoutBottomBarProps { export const FlyoutBottomBar = React.memo<FlyoutBottomBarProps>( ({ activeTab, showDataproviders, timelineId }) => { return ( - <Container - className={FLYOUT_BUTTON_BAR_CLASS_NAME} - $isGlobal={showDataproviders} - data-test-subj="flyoutBottomBar" - > + <div className={FLYOUT_BUTTON_BAR_CLASS_NAME} data-test-subj="flyoutBottomBar"> {showDataproviders && <FlyoutHeaderPanel timelineId={timelineId} />} {(showDataproviders || (!showDataproviders && activeTab !== TimelineTabs.query)) && ( <DataProvidersPanel paddingSize="none"> <DataProviders timelineId={timelineId} data-test-subj="dataProviders-bottomBar" /> </DataProvidersPanel> )} - </Container> + </div> ); } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx index ec46985450d89..ad1d126e3c853 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx @@ -9,7 +9,6 @@ import { i18n } from '@kbn/i18n'; import { EuiFocusTrap, EuiOutsideClickDetector } from '@elastic/eui'; import React, { useEffect, useMemo, useCallback, useState, useRef } from 'react'; import { useDispatch } from 'react-redux'; -import styled from 'styled-components'; import { AppLeaveHandler } from '../../../../../../../src/core/public'; import { TimelineId, TimelineStatus, TimelineTabs } from '../../../../common/types/timeline'; @@ -19,12 +18,6 @@ import { FlyoutBottomBar } from './bottom_bar'; import { Pane } from './pane'; import { getTimelineShowStatusByIdSelector } from './selectors'; -const Visible = styled.div<{ show?: boolean }>` - visibility: ${({ show }) => (show ? 'visible' : 'hidden')}; -`; - -Visible.displayName = 'Visible'; - interface OwnProps { timelineId: TimelineId; onAppLeave: (handler: AppLeaveHandler) => void; @@ -124,9 +117,7 @@ const FlyoutComponent: React.FC<OwnProps> = ({ timelineId, onAppLeave }) => { <EuiOutsideClickDetector onOutsideClick={onOutsideClick}> <> <EuiFocusTrap disabled={!focusOwnership}> - <Visible show={show} data-test-subj="flyout-pane-wrapper"> - <Pane timelineId={timelineId} /> - </Visible> + <Pane timelineId={timelineId} visible={show} /> </EuiFocusTrap> <FlyoutBottomBar activeTab={activeTab} timelineId={timelineId} showDataproviders={!show} /> </> diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx index 459706de36569..35a13aba471fd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx @@ -20,6 +20,7 @@ import { focusActiveTimelineButton } from '../../timeline/helpers'; interface FlyoutPaneComponentProps { timelineId: TimelineId; + visible?: boolean; } const EuiFlyoutContainer = styled.div` @@ -31,7 +32,10 @@ const EuiFlyoutContainer = styled.div` } `; -const FlyoutPaneComponent: React.FC<FlyoutPaneComponentProps> = ({ timelineId }) => { +const FlyoutPaneComponent: React.FC<FlyoutPaneComponentProps> = ({ + timelineId, + visible = true, +}) => { const dispatch = useDispatch(); const handleClose = useCallback(() => { dispatch(timelineActions.showTimeline({ id: timelineId, show: false })); @@ -39,7 +43,10 @@ const FlyoutPaneComponent: React.FC<FlyoutPaneComponentProps> = ({ timelineId }) }, [dispatch, timelineId]); return ( - <EuiFlyoutContainer data-test-subj="flyout-pane"> + <EuiFlyoutContainer + data-test-subj="flyout-pane" + style={{ visibility: visible ? 'visible' : 'hidden' }} + > <EuiFlyout aria-label={i18n.TIMELINE_DESCRIPTION} className="timeline-flyout" @@ -47,6 +54,8 @@ const FlyoutPaneComponent: React.FC<FlyoutPaneComponentProps> = ({ timelineId }) hideCloseButton={true} onClose={handleClose} size="l" + ownFocus={false} + style={{ visibility: visible ? 'visible' : 'hidden' }} > <StatefulTimeline renderCellValue={DefaultCellRenderer} diff --git a/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx index 802dd74c1892b..31f2fec942490 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx @@ -14,6 +14,8 @@ import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { Ja3Fingerprint } from '.'; +jest.mock('../../../common/lib/kibana'); + describe('Ja3Fingerprint', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx deleted file mode 100644 index ed299c3a4ef1a..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx +++ /dev/null @@ -1,125 +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 { renderHook, act } from '@testing-library/react-hooks'; -import { getTimelineDefaults, useTimelineManager, UseTimelineManager } from './'; -import { FilterManager } from '../../../../../../../src/plugins/data/public/query/filter_manager'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; - -const isStringifiedComparisonEqual = (a: {}, b: {}): boolean => - JSON.stringify(a) === JSON.stringify(b); - -describe('useTimelineManager', () => { - const setupMock = coreMock.createSetup(); - const testId = 'coolness'; - const timelineDefaults = getTimelineDefaults(testId); - const mockFilterManager = new FilterManager(setupMock.uiSettings); - - beforeEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('initializes an undefined timeline', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, UseTimelineManager>(() => - useTimelineManager() - ); - await waitForNextUpdate(); - const uninitializedTimeline = result.current.getManageTimelineById(testId); - expect(isStringifiedComparisonEqual(uninitializedTimeline, timelineDefaults)).toBeTruthy(); - }); - }); - // TO DO sourcerer - // it('getIndexToAddById', async () => { - // await act(async () => { - // const { result, waitForNextUpdate } = renderHook<string, UseTimelineManager>(() => - // useTimelineManager() - // ); - // await waitForNextUpdate(); - // const data = result.current.getIndexToAddById(testId); - // expect(data).toEqual(timelineDefaults.indexToAdd); - // }); - // }); - // - // it('setIndexToAdd', async () => { - // await act(async () => { - // const indexToAddArgs = { id: testId, indexToAdd: ['example'] }; - // const { result, waitForNextUpdate } = renderHook<string, UseTimelineManager>(() => - // useTimelineManager() - // ); - // await waitForNextUpdate(); - // result.current.initializeTimeline({ - // id: testId, - // }); - // result.current.setIndexToAdd(indexToAddArgs); - // const data = result.current.getIndexToAddById(testId); - // expect(data).toEqual(indexToAddArgs.indexToAdd); - // }); - // }); - - it('setIsTimelineLoading', async () => { - await act(async () => { - const isLoadingArgs = { id: testId, isLoading: true }; - const { result, waitForNextUpdate } = renderHook<string, UseTimelineManager>(() => - useTimelineManager() - ); - await waitForNextUpdate(); - result.current.initializeTimeline({ - id: testId, - }); - let timeline = result.current.getManageTimelineById(testId); - expect(timeline.isLoading).toBeFalsy(); - result.current.setIsTimelineLoading(isLoadingArgs); - timeline = result.current.getManageTimelineById(testId); - expect(timeline.isLoading).toBeTruthy(); - }); - }); - - it('getTimelineFilterManager undefined on uninitialized', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, UseTimelineManager>(() => - useTimelineManager() - ); - await waitForNextUpdate(); - const data = result.current.getTimelineFilterManager(testId); - expect(data).toEqual(undefined); - }); - }); - - it('getTimelineFilterManager defined at initialize', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, UseTimelineManager>(() => - useTimelineManager() - ); - await waitForNextUpdate(); - result.current.initializeTimeline({ - id: testId, - filterManager: mockFilterManager, - }); - const data = result.current.getTimelineFilterManager(testId); - expect(data).toEqual(mockFilterManager); - }); - }); - - it('isManagedTimeline returns false when unset and then true when set', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, UseTimelineManager>(() => - useTimelineManager() - ); - await waitForNextUpdate(); - let data = result.current.isManagedTimeline(testId); - expect(data).toBeFalsy(); - result.current.initializeTimeline({ - id: testId, - filterManager: mockFilterManager, - }); - data = result.current.isManagedTimeline(testId); - expect(data).toBeTruthy(); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx deleted file mode 100644 index 1f215ee8f2141..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx +++ /dev/null @@ -1,212 +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, { createContext, useCallback, useContext, useReducer } from 'react'; -import { noop } from 'lodash/fp'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FilterManager } from '../../../../../../../src/plugins/data/public/query/filter_manager'; -import { SubsetTimelineModel } from '../../store/timeline/model'; -import * as i18n from '../../../common/components/events_viewer/translations'; -import * as i18nF from '../timeline/footer/translations'; -import { timelineDefaults as timelineDefaultModel } from '../../store/timeline/defaults'; - -interface ManageTimelineInit { - documentType?: string; - defaultModel?: SubsetTimelineModel; - filterManager?: FilterManager; - footerText?: string; - id: string; - loadingText?: string; - selectAll?: boolean; - queryFields?: string[]; - title?: string; - unit?: (totalCount: number) => string; -} - -interface ManageTimeline { - documentType: string; - defaultModel: SubsetTimelineModel; - filterManager?: FilterManager; - footerText: string; - id: string; - isLoading: boolean; - loadingText: string; - queryFields: string[]; - selectAll: boolean; - title: string; - unit: (totalCount: number) => string; -} - -interface ManageTimelineById { - [id: string]: ManageTimeline; -} -const initManageTimeline: ManageTimelineById = {}; -type ActionManageTimeline = - | { - type: 'INITIALIZE_TIMELINE'; - id: string; - payload: ManageTimelineInit; - } - | { - type: 'SET_IS_LOADING'; - id: string; - payload: boolean; - } - | { - type: 'SET_SELECT_ALL'; - id: string; - payload: boolean; - }; - -export const getTimelineDefaults = (id: string) => ({ - defaultModel: timelineDefaultModel, - loadingText: i18n.LOADING_EVENTS, - footerText: i18nF.TOTAL_COUNT_OF_EVENTS, - documentType: i18nF.TOTAL_COUNT_OF_EVENTS, - selectAll: false, - id, - isLoading: false, - queryFields: [], - title: i18n.EVENTS, - unit: (n: number) => i18n.UNIT(n), -}); -const reducerManageTimeline = ( - state: ManageTimelineById, - action: ActionManageTimeline -): ManageTimelineById => { - switch (action.type) { - case 'INITIALIZE_TIMELINE': - return { - ...state, - [action.id]: { - ...getTimelineDefaults(action.id), - ...state[action.id], - ...action.payload, - }, - } as ManageTimelineById; - case 'SET_SELECT_ALL': - return { - ...state, - [action.id]: { - ...state[action.id], - selectAll: action.payload, - }, - } as ManageTimelineById; - - case 'SET_IS_LOADING': - return { - ...state, - [action.id]: { - ...state[action.id], - isLoading: action.payload, - }, - } as ManageTimelineById; - default: - return state; - } -}; - -export interface UseTimelineManager { - getManageTimelineById: (id: string) => ManageTimeline; - getTimelineFilterManager: (id: string) => FilterManager | undefined; - initializeTimeline: (newTimeline: ManageTimelineInit) => void; - isManagedTimeline: (id: string) => boolean; - setIsTimelineLoading: (isLoadingArgs: { id: string; isLoading: boolean }) => void; - setSelectAll: (selectAllArgs: { id: string; selectAll: boolean }) => void; -} - -export const useTimelineManager = ( - manageTimelineForTesting?: ManageTimelineById -): UseTimelineManager => { - const [state, dispatch] = useReducer< - (state: ManageTimelineById, action: ActionManageTimeline) => ManageTimelineById - >(reducerManageTimeline, manageTimelineForTesting ?? initManageTimeline); - - const initializeTimeline = useCallback((newTimeline: ManageTimelineInit) => { - dispatch({ - type: 'INITIALIZE_TIMELINE', - id: newTimeline.id, - payload: newTimeline, - }); - }, []); - - const setIsTimelineLoading = useCallback( - ({ id, isLoading }: { id: string; isLoading: boolean }) => { - dispatch({ - type: 'SET_IS_LOADING', - id, - payload: isLoading, - }); - }, - [] - ); - - const setSelectAll = useCallback(({ id, selectAll }: { id: string; selectAll: boolean }) => { - dispatch({ - type: 'SET_SELECT_ALL', - id, - payload: selectAll, - }); - }, []); - - const getTimelineFilterManager = useCallback( - (id: string): FilterManager | undefined => state[id]?.filterManager, - [state] - ); - const getManageTimelineById = useCallback( - (id: string): ManageTimeline => { - if (state[id] != null) { - return state[id]; - } - initializeTimeline({ id }); - return getTimelineDefaults(id); - }, - [initializeTimeline, state] - ); - const isManagedTimeline = useCallback((id: string): boolean => state[id] != null, [state]); - - return { - getManageTimelineById, - getTimelineFilterManager, - initializeTimeline, - isManagedTimeline, - setIsTimelineLoading, - setSelectAll, - }; -}; - -const init = { - getManageTimelineById: (id: string) => getTimelineDefaults(id), - getTimelineFilterManager: () => undefined, - initializeTimeline: () => noop, - isManagedTimeline: () => false, - setIsTimelineLoading: () => noop, - setSelectAll: () => noop, -}; - -const ManageTimelineContext = createContext<UseTimelineManager>(init); - -export const useManageTimeline = () => useContext(ManageTimelineContext); - -interface ManageGlobalTimelineProps { - children: React.ReactNode; - manageTimelineForTesting?: ManageTimelineById; -} - -export const ManageGlobalTimeline = ({ - children, - manageTimelineForTesting, -}: ManageGlobalTimelineProps) => { - const timelineManager = useTimelineManager(manageTimelineForTesting); - - return ( - <ManageTimelineContext.Provider value={timelineManager}> - {children} - </ManageTimelineContext.Provider> - ); -}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx index e2c8b8854504a..c73e372b4a71c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx @@ -62,6 +62,8 @@ import { } from '../../../network/components/source_destination/field_names'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; +jest.mock('../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx index 0544b00a79227..00d2a7b35483e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx @@ -9,7 +9,7 @@ import { EuiFlexGroup, EuiPanel, EuiScreenReaderOnly } from '@elastic/eui'; import React, { useState, useCallback } from 'react'; import styled from 'styled-components'; -import { getNotesContainerClassName } from '../../../../common/components/accessibility/helpers'; +import { getNotesContainerClassName } from '../../../../../../timelines/public'; import { AddNote } from '../add_note'; import { AssociateNote } from '../helpers'; import { NotePreviews, NotePreviewsContainer } from '../../open_timeline/note_previews'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index c06c3f076e097..c0fea1f210a8a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -36,7 +36,6 @@ import { formatTimelineResultToModel, } from './helpers'; import { OpenTimelineResult, DispatchUpdateTimeline } from './types'; -import { KueryFilterQueryKind } from '../../../common/store'; import { Note } from '../../../common/lib/note'; import moment from 'moment'; import sinon from 'sinon'; @@ -45,6 +44,7 @@ import { TimelineType, TimelineStatus, TimelineTabs, + KueryFilterQueryKind, } from '../../../../common/types/timeline'; import { mockTimeline as mockSelectedTimeline, diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index e45a1a117769b..03ac0b3d14342 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -13,6 +13,7 @@ import { Dispatch } from 'redux'; import deepMerge from 'deepmerge'; import { + ColumnHeaderOptions, DataProviderType, TimelineId, TimelineStatus, @@ -37,7 +38,7 @@ import { addTimeline as dispatchAddTimeline, addNote as dispatchAddGlobalTimelineNote, } from '../../../timelines/store/timeline/actions'; -import { ColumnHeaderOptions, TimelineModel } from '../../../timelines/store/timeline/model'; +import { TimelineModel } from '../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx index 922e40d6d860e..1eafa51058bdd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx @@ -194,7 +194,7 @@ export const OpenTimeline = React.memo<OpenTimelineProps>( title={i18n.IMPORT_TIMELINE} /> - <EuiPanel className={OPEN_TIMELINE_CLASS_NAME}> + <EuiPanel className={OPEN_TIMELINE_CLASS_NAME} hasBorder> {!!timelineFilter && timelineFilter} <SearchRow data-test-subj="search-row" diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap index b74a3e1860b17..4c8139a78b012 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap @@ -282,240 +282,195 @@ Array [ onClose={[Function]} size="m" > - <EuiWindowEvent - event="keydown" - handler={[Function]} - /> - <EuiFocusTrap - clickOutsideDisables={true} + <div + data-eui="EuiFlyout" + data-test-subj="timeline:details-panel:flyout" + role="dialog" > - <div - data-eui="EuiFocusTrap" + <button + data-test-subj="euiFlyoutCloseButton" + onClick={[Function]} + type="button" + /> + <EventDetailsPanelComponent + browserFields={Object {}} + docValueFields={Array []} + expandedEvent={ + Object { + "eventId": "my-id", + "indexName": "my-index", + } + } + handleOnEventClosed={[Function]} + isFlyoutView={true} + tabType="query" + timelineId="test" > - <div - className="euiFlyout euiFlyout--medium euiFlyout--paddingLarge c0" - data-test-subj="timeline:details-panel:flyout" - role="dialog" - tabIndex={0} + <EuiFlyoutHeader + hasBorder={true} > - <EuiI18n - default="Close this dialog" - token="euiFlyout.closeAriaLabel" - > - <EuiButtonIcon - aria-label="Close this dialog" - className="euiFlyout__closeButton" - color="text" - data-test-subj="euiFlyoutCloseButton" - iconType="cross" - onClick={[Function]} - > - <button - aria-label="Close this dialog" - className="euiButtonIcon euiButtonIcon--text euiButtonIcon--empty euiButtonIcon--xSmall euiFlyout__closeButton" - data-test-subj="euiFlyoutCloseButton" - disabled={false} - onClick={[Function]} - type="button" - > - <EuiIcon - aria-hidden="true" - className="euiButtonIcon__icon" - color="inherit" - size="m" - type="cross" - > - <span - aria-hidden="true" - className="euiButtonIcon__icon" - color="inherit" - data-euiicon-type="cross" - size="m" - /> - </EuiIcon> - </button> - </EuiButtonIcon> - </EuiI18n> - <EventDetailsPanelComponent - browserFields={Object {}} - docValueFields={Array []} - expandedEvent={ - Object { - "eventId": "my-id", - "indexName": "my-index", - } - } - handleOnEventClosed={[Function]} - isFlyoutView={true} - tabType="query" - timelineId="test" + <div + className="euiFlyoutHeader euiFlyoutHeader--hasBorder" > - <EuiFlyoutHeader - hasBorder={true} + <ExpandableEventTitle + isAlert={false} + loading={true} > - <div - className="euiFlyoutHeader euiFlyoutHeader--hasBorder" + <Styled(EuiFlexGroup) + gutterSize="none" + justifyContent="spaceBetween" + wrap={true} > - <ExpandableEventTitle - isAlert={false} - loading={true} + <EuiFlexGroup + className="c1" + gutterSize="none" + justifyContent="spaceBetween" + wrap={true} > - <Styled(EuiFlexGroup) - gutterSize="none" - justifyContent="spaceBetween" - wrap={true} + <div + className="euiFlexGroup euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive euiFlexGroup--wrap c1" > - <EuiFlexGroup - className="c1" - gutterSize="none" - justifyContent="spaceBetween" - wrap={true} + <EuiFlexItem + grow={false} > <div - className="euiFlexGroup euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive euiFlexGroup--wrap c1" + className="euiFlexItem euiFlexItem--flexGrowZero" > - <EuiFlexItem - grow={false} - > - <div - className="euiFlexItem euiFlexItem--flexGrowZero" - > - <EuiTitle - size="s" - /> - </div> - </EuiFlexItem> + <EuiTitle + size="s" + /> </div> - </EuiFlexGroup> - </Styled(EuiFlexGroup)> - </ExpandableEventTitle> - </div> - </EuiFlyoutHeader> - <Styled(EuiFlyoutBody)> - <EuiFlyoutBody - className="c2" + </EuiFlexItem> + </div> + </EuiFlexGroup> + </Styled(EuiFlexGroup)> + </ExpandableEventTitle> + </div> + </EuiFlyoutHeader> + <Styled(EuiFlyoutBody)> + <EuiFlyoutBody + className="c2" + > + <div + className="euiFlyoutBody c2" + > + <div + className="euiFlyoutBody__overflow" + tabIndex={0} > <div - className="euiFlyoutBody c2" + className="euiFlyoutBody__overflowContent" > - <div - className="euiFlyoutBody__overflow" + <ExpandableEvent + browserFields={Object {}} + detailsData={null} + event={ + Object { + "eventId": "my-id", + "indexName": "my-index", + } + } + isAlert={false} + loading={true} + timelineId="test" + timelineTabType="flyout" > - <div - className="euiFlyoutBody__overflowContent" + <EuiLoadingContent + lines={10} > - <ExpandableEvent - browserFields={Object {}} - detailsData={null} - event={ - Object { - "eventId": "my-id", - "indexName": "my-index", - } - } - isAlert={false} - loading={true} - timelineId="test" - timelineTabType="flyout" + <span + className="euiLoadingContent" > - <EuiLoadingContent - lines={10} + <span + className="euiLoadingContent__singleLine" + key="0" > <span - className="euiLoadingContent" - > - <span - className="euiLoadingContent__singleLine" - key="0" - > - <span - className="euiLoadingContent__singleLineBackground" - /> - </span> - <span - className="euiLoadingContent__singleLine" - key="1" - > - <span - className="euiLoadingContent__singleLineBackground" - /> - </span> - <span - className="euiLoadingContent__singleLine" - key="2" - > - <span - className="euiLoadingContent__singleLineBackground" - /> - </span> - <span - className="euiLoadingContent__singleLine" - key="3" - > - <span - className="euiLoadingContent__singleLineBackground" - /> - </span> - <span - className="euiLoadingContent__singleLine" - key="4" - > - <span - className="euiLoadingContent__singleLineBackground" - /> - </span> - <span - className="euiLoadingContent__singleLine" - key="5" - > - <span - className="euiLoadingContent__singleLineBackground" - /> - </span> - <span - className="euiLoadingContent__singleLine" - key="6" - > - <span - className="euiLoadingContent__singleLineBackground" - /> - </span> - <span - className="euiLoadingContent__singleLine" - key="7" - > - <span - className="euiLoadingContent__singleLineBackground" - /> - </span> - <span - className="euiLoadingContent__singleLine" - key="8" - > - <span - className="euiLoadingContent__singleLineBackground" - /> - </span> - <span - className="euiLoadingContent__singleLine" - key="9" - > - <span - className="euiLoadingContent__singleLineBackground" - /> - </span> - </span> - </EuiLoadingContent> - </ExpandableEvent> - </div> - </div> + className="euiLoadingContent__singleLineBackground" + /> + </span> + <span + className="euiLoadingContent__singleLine" + key="1" + > + <span + className="euiLoadingContent__singleLineBackground" + /> + </span> + <span + className="euiLoadingContent__singleLine" + key="2" + > + <span + className="euiLoadingContent__singleLineBackground" + /> + </span> + <span + className="euiLoadingContent__singleLine" + key="3" + > + <span + className="euiLoadingContent__singleLineBackground" + /> + </span> + <span + className="euiLoadingContent__singleLine" + key="4" + > + <span + className="euiLoadingContent__singleLineBackground" + /> + </span> + <span + className="euiLoadingContent__singleLine" + key="5" + > + <span + className="euiLoadingContent__singleLineBackground" + /> + </span> + <span + className="euiLoadingContent__singleLine" + key="6" + > + <span + className="euiLoadingContent__singleLineBackground" + /> + </span> + <span + className="euiLoadingContent__singleLine" + key="7" + > + <span + className="euiLoadingContent__singleLineBackground" + /> + </span> + <span + className="euiLoadingContent__singleLine" + key="8" + > + <span + className="euiLoadingContent__singleLineBackground" + /> + </span> + <span + className="euiLoadingContent__singleLine" + key="9" + > + <span + className="euiLoadingContent__singleLineBackground" + /> + </span> + </span> + </EuiLoadingContent> + </ExpandableEvent> </div> - </EuiFlyoutBody> - </Styled(EuiFlyoutBody)> - </EventDetailsPanelComponent> - </div> - </div> - </EuiFocusTrap> + </div> + </div> + </EuiFlyoutBody> + </Styled(EuiFlyoutBody)> + </EventDetailsPanelComponent> + </div> </EuiFlyout> </Styled(EuiFlyout)>, .c1 { @@ -554,249 +509,204 @@ Array [ onClose={[Function]} size="m" > - <EuiWindowEvent - event="keydown" - handler={[Function]} - /> - <EuiFocusTrap - clickOutsideDisables={true} + <div + data-eui="EuiFlyout" + data-test-subj="timeline:details-panel:flyout" + role="dialog" > - <div - data-eui="EuiFocusTrap" + <button + data-test-subj="euiFlyoutCloseButton" + onClick={[Function]} + type="button" + /> + <EventDetailsPanelComponent + browserFields={Object {}} + docValueFields={Array []} + expandedEvent={ + Object { + "eventId": "my-id", + "indexName": "my-index", + } + } + handleOnEventClosed={[Function]} + isFlyoutView={true} + tabType="query" + timelineId="test" > - <div - className="euiFlyout euiFlyout--medium euiFlyout--paddingLarge c0" - data-test-subj="timeline:details-panel:flyout" - role="dialog" - tabIndex={0} + <EuiFlyoutHeader + hasBorder={true} > - <EuiI18n - default="Close this dialog" - token="euiFlyout.closeAriaLabel" - > - <EuiButtonIcon - aria-label="Close this dialog" - className="euiFlyout__closeButton" - color="text" - data-test-subj="euiFlyoutCloseButton" - iconType="cross" - onClick={[Function]} - > - <button - aria-label="Close this dialog" - className="euiButtonIcon euiButtonIcon--text euiButtonIcon--empty euiButtonIcon--xSmall euiFlyout__closeButton" - data-test-subj="euiFlyoutCloseButton" - disabled={false} - onClick={[Function]} - type="button" - > - <EuiIcon - aria-hidden="true" - className="euiButtonIcon__icon" - color="inherit" - size="m" - type="cross" - > - <span - aria-hidden="true" - className="euiButtonIcon__icon" - color="inherit" - data-euiicon-type="cross" - size="m" - /> - </EuiIcon> - </button> - </EuiButtonIcon> - </EuiI18n> - <EventDetailsPanelComponent - browserFields={Object {}} - docValueFields={Array []} - expandedEvent={ - Object { - "eventId": "my-id", - "indexName": "my-index", - } - } - handleOnEventClosed={[Function]} - isFlyoutView={true} - tabType="query" - timelineId="test" + <div + className="euiFlyoutHeader euiFlyoutHeader--hasBorder" > - <EuiFlyoutHeader - hasBorder={true} + <ExpandableEventTitle + isAlert={false} + loading={true} > - <div - className="euiFlyoutHeader euiFlyoutHeader--hasBorder" + <Styled(EuiFlexGroup) + gutterSize="none" + justifyContent="spaceBetween" + wrap={true} > - <ExpandableEventTitle - isAlert={false} - loading={true} + <EuiFlexGroup + className="c1" + gutterSize="none" + justifyContent="spaceBetween" + wrap={true} > - <Styled(EuiFlexGroup) - gutterSize="none" - justifyContent="spaceBetween" - wrap={true} + <div + className="euiFlexGroup euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive euiFlexGroup--wrap c1" > - <EuiFlexGroup - className="c1" - gutterSize="none" - justifyContent="spaceBetween" - wrap={true} + <EuiFlexItem + grow={false} > <div - className="euiFlexGroup euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive euiFlexGroup--wrap c1" + className="euiFlexItem euiFlexItem--flexGrowZero" > - <EuiFlexItem - grow={false} - > - <div - className="euiFlexItem euiFlexItem--flexGrowZero" - > - <EuiTitle - size="s" - /> - </div> - </EuiFlexItem> + <EuiTitle + size="s" + /> </div> - </EuiFlexGroup> - </Styled(EuiFlexGroup)> - </ExpandableEventTitle> - </div> - </EuiFlyoutHeader> - <Styled(EuiFlyoutBody)> - <EuiFlyoutBody - className="c2" + </EuiFlexItem> + </div> + </EuiFlexGroup> + </Styled(EuiFlexGroup)> + </ExpandableEventTitle> + </div> + </EuiFlyoutHeader> + <Styled(EuiFlyoutBody)> + <EuiFlyoutBody + className="c2" + > + <div + className="euiFlyoutBody c2" + > + <div + className="euiFlyoutBody__overflow" + tabIndex={0} > <div - className="euiFlyoutBody c2" + className="euiFlyoutBody__overflowContent" > - <div - className="euiFlyoutBody__overflow" + <ExpandableEvent + browserFields={Object {}} + detailsData={null} + event={ + Object { + "eventId": "my-id", + "indexName": "my-index", + } + } + isAlert={false} + loading={true} + timelineId="test" + timelineTabType="flyout" > - <div - className="euiFlyoutBody__overflowContent" + <EuiLoadingContent + lines={10} > - <ExpandableEvent - browserFields={Object {}} - detailsData={null} - event={ - Object { - "eventId": "my-id", - "indexName": "my-index", - } - } - isAlert={false} - loading={true} - timelineId="test" - timelineTabType="flyout" + <span + className="euiLoadingContent" > - <EuiLoadingContent - lines={10} + <span + className="euiLoadingContent__singleLine" + key="0" > <span - className="euiLoadingContent" - > - <span - className="euiLoadingContent__singleLine" - key="0" - > - <span - className="euiLoadingContent__singleLineBackground" - /> - </span> - <span - className="euiLoadingContent__singleLine" - key="1" - > - <span - className="euiLoadingContent__singleLineBackground" - /> - </span> - <span - className="euiLoadingContent__singleLine" - key="2" - > - <span - className="euiLoadingContent__singleLineBackground" - /> - </span> - <span - className="euiLoadingContent__singleLine" - key="3" - > - <span - className="euiLoadingContent__singleLineBackground" - /> - </span> - <span - className="euiLoadingContent__singleLine" - key="4" - > - <span - className="euiLoadingContent__singleLineBackground" - /> - </span> - <span - className="euiLoadingContent__singleLine" - key="5" - > - <span - className="euiLoadingContent__singleLineBackground" - /> - </span> - <span - className="euiLoadingContent__singleLine" - key="6" - > - <span - className="euiLoadingContent__singleLineBackground" - /> - </span> - <span - className="euiLoadingContent__singleLine" - key="7" - > - <span - className="euiLoadingContent__singleLineBackground" - /> - </span> - <span - className="euiLoadingContent__singleLine" - key="8" - > - <span - className="euiLoadingContent__singleLineBackground" - /> - </span> - <span - className="euiLoadingContent__singleLine" - key="9" - > - <span - className="euiLoadingContent__singleLineBackground" - /> - </span> - </span> - </EuiLoadingContent> - </ExpandableEvent> - </div> - </div> + className="euiLoadingContent__singleLineBackground" + /> + </span> + <span + className="euiLoadingContent__singleLine" + key="1" + > + <span + className="euiLoadingContent__singleLineBackground" + /> + </span> + <span + className="euiLoadingContent__singleLine" + key="2" + > + <span + className="euiLoadingContent__singleLineBackground" + /> + </span> + <span + className="euiLoadingContent__singleLine" + key="3" + > + <span + className="euiLoadingContent__singleLineBackground" + /> + </span> + <span + className="euiLoadingContent__singleLine" + key="4" + > + <span + className="euiLoadingContent__singleLineBackground" + /> + </span> + <span + className="euiLoadingContent__singleLine" + key="5" + > + <span + className="euiLoadingContent__singleLineBackground" + /> + </span> + <span + className="euiLoadingContent__singleLine" + key="6" + > + <span + className="euiLoadingContent__singleLineBackground" + /> + </span> + <span + className="euiLoadingContent__singleLine" + key="7" + > + <span + className="euiLoadingContent__singleLineBackground" + /> + </span> + <span + className="euiLoadingContent__singleLine" + key="8" + > + <span + className="euiLoadingContent__singleLineBackground" + /> + </span> + <span + className="euiLoadingContent__singleLine" + key="9" + > + <span + className="euiLoadingContent__singleLineBackground" + /> + </span> + </span> + </EuiLoadingContent> + </ExpandableEvent> </div> - </EuiFlyoutBody> - </Styled(EuiFlyoutBody)> - </EventDetailsPanelComponent> - </div> - </div> - </EuiFocusTrap> + </div> + </div> + </EuiFlyoutBody> + </Styled(EuiFlyoutBody)> + </EventDetailsPanelComponent> + </div> </EuiFlyout>, - .c1 { + .c0 { -webkit-flex: 0 1 auto; -ms-flex: 0 1 auto; flex: 0 1 auto; margin-top: 8px; } -.c2 .euiFlyoutBody__overflow { +.c1 .euiFlyoutBody__overflow { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -807,7 +717,7 @@ Array [ overflow: hidden; } -.c2 .euiFlyoutBody__overflow .euiFlyoutBody__overflowContent { +.c1 .euiFlyoutBody__overflow .euiFlyoutBody__overflowContent { -webkit-flex: 1; -ms-flex: 1; flex: 1; @@ -815,54 +725,16 @@ Array [ padding: 4px 16px 50px; } -.c0 { - z-index: 7000; -} - <div - className="euiFlyout euiFlyout--medium euiFlyout--paddingLarge c0" + data-eui="EuiFlyout" data-test-subj="timeline:details-panel:flyout" role="dialog" - tabIndex={0} > - <EuiI18n - default="Close this dialog" - token="euiFlyout.closeAriaLabel" - > - <EuiButtonIcon - aria-label="Close this dialog" - className="euiFlyout__closeButton" - color="text" - data-test-subj="euiFlyoutCloseButton" - iconType="cross" - onClick={[Function]} - > - <button - aria-label="Close this dialog" - className="euiButtonIcon euiButtonIcon--text euiButtonIcon--empty euiButtonIcon--xSmall euiFlyout__closeButton" - data-test-subj="euiFlyoutCloseButton" - disabled={false} - onClick={[Function]} - type="button" - > - <EuiIcon - aria-hidden="true" - className="euiButtonIcon__icon" - color="inherit" - size="m" - type="cross" - > - <span - aria-hidden="true" - className="euiButtonIcon__icon" - color="inherit" - data-euiicon-type="cross" - size="m" - /> - </EuiIcon> - </button> - </EuiButtonIcon> - </EuiI18n> + <button + data-test-subj="euiFlyoutCloseButton" + onClick={[Function]} + type="button" + /> <EventDetailsPanelComponent browserFields={Object {}} docValueFields={Array []} @@ -893,13 +765,13 @@ Array [ wrap={true} > <EuiFlexGroup - className="c1" + className="c0" gutterSize="none" justifyContent="spaceBetween" wrap={true} > <div - className="euiFlexGroup euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive euiFlexGroup--wrap c1" + className="euiFlexGroup euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive euiFlexGroup--wrap c0" > <EuiFlexItem grow={false} @@ -920,13 +792,14 @@ Array [ </EuiFlyoutHeader> <Styled(EuiFlyoutBody)> <EuiFlyoutBody - className="c2" + className="c1" > <div - className="euiFlyoutBody c2" + className="euiFlyoutBody c1" > <div className="euiFlyoutBody__overflow" + tabIndex={0} > <div className="euiFlyoutBody__overflowContent" diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx index 177cd2e5ded41..629bdcca98640 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx @@ -5,10 +5,10 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, ReactNode } from 'react'; import { useDispatch } from 'react-redux'; import { EuiFlyout, EuiFlyoutProps } from '@elastic/eui'; -import styled from 'styled-components'; +import styled, { StyledComponent } from 'styled-components'; import { timelineActions, timelineSelectors } from '../../store/timeline'; import { timelineDefaults } from '../../store/timeline/defaults'; import { BrowserFields, DocValueFields } from '../../../common/containers/source'; @@ -18,7 +18,11 @@ import { EventDetailsPanel } from './event_details'; import { HostDetailsPanel } from './host_details'; import { NetworkDetailsPanel } from './network_details'; -const StyledEuiFlyout = styled(EuiFlyout)` +// TODO: EUI team follow up on complex types and styled-components `styled` +// https://github.com/elastic/eui/issues/4855 +const StyledEuiFlyout: StyledComponent<typeof EuiFlyout, {}, { children?: ReactNode }> = styled( + EuiFlyout +)` z-index: ${({ theme }) => theme.eui.euiZLevel7}; `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx index 9887563c0fef6..2daebdf37e77f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx @@ -16,41 +16,28 @@ import { import { useDispatch } from 'react-redux'; import styled from 'styled-components'; -import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline'; +import { + HeaderActionProps, + SortDirection, + TimelineId, + TimelineTabs, +} from '../../../../../../common/types/timeline'; import { EXIT_FULL_SCREEN } from '../../../../../common/components/exit_full_screen/translations'; import { FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../../../common/constants'; import { useGlobalFullScreen, useTimelineFullScreen, } from '../../../../../common/containers/use_full_screen'; -import { BrowserFields } from '../../../../../common/containers/source'; -import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; -import { OnSelectAll } from '../../events'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; import { StatefulFieldsBrowser } from '../../../fields_browser'; import { StatefulRowRenderersBrowser } from '../../../row_renderers_browser'; import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from '../../../fields_browser/helpers'; import { EventsTh, EventsThContent } from '../../styles'; -import { Sort, SortDirection } from '../sort'; import { EventsSelect } from '../column_headers/events_select'; import * as i18n from '../column_headers/translations'; import { timelineActions } from '../../../../store/timeline'; import { isFullScreen } from '../column_headers'; -export interface HeaderActionProps { - width: number; - browserFields: BrowserFields; - columnHeaders: ColumnHeaderOptions[]; - isEventViewer?: boolean; - isSelectAllChecked: boolean; - onSelectAll: OnSelectAll; - showEventsSelect: boolean; - showSelectAllCheckbox: boolean; - sort: Sort[]; - tabType: TimelineTabs; - timelineId: string; -} - const SortingColumnsContainer = styled.div` button { color: ${({ theme }) => theme.eui.euiColorPrimary}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx index a186b324cc03a..82d593e80bc44 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx @@ -41,8 +41,6 @@ describe('Actions', () => { eventId="abc" loadingEventIds={[]} onEventDetailsPanelOpened={jest.fn()} - onPinEvent={jest.fn()} - onUnPinEvent={jest.fn()} onRowSelected={jest.fn()} showNotes={false} isEventPinned={false} @@ -74,8 +72,6 @@ describe('Actions', () => { toggleShowNotes={jest.fn()} timelineId={'test'} refetch={jest.fn()} - onPinEvent={jest.fn()} - onUnPinEvent={jest.fn()} columnId={''} index={2} eventId="abc" diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index 2053b9a0da942..0a3a1cd88accc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -6,7 +6,9 @@ */ import React, { useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; import { EuiButtonIcon, EuiCheckbox, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui'; +import { noop } from 'lodash/fp'; import { eventHasNotes, getEventType, @@ -22,45 +24,9 @@ import * as i18n from '../translations'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; import { AddToCaseAction } from '../../../../../cases/components/timeline_actions/add_to_case_action'; -import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline'; -import { timelineSelectors } from '../../../../store/timeline'; +import { TimelineId, ActionProps, OnPinEvent } from '../../../../../../common/types/timeline'; +import { timelineActions, timelineSelectors } from '../../../../store/timeline'; import { timelineDefaults } from '../../../../store/timeline/defaults'; -import { Ecs } from '../../../../../../common/ecs'; -import { inputsModel } from '../../../../../common/store'; -import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; -import { OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events'; -import { RowCellRender } from '../control_columns'; - -interface Props { - ariaRowindex: number; - action?: RowCellRender; - width?: number; - columnId: string; - columnValues: string; - checked: boolean; - onRowSelected: OnRowSelected; - eventId: string; - loadingEventIds: Readonly<string[]>; - onEventDetailsPanelOpened: () => void; - showCheckboxes: boolean; - data: TimelineNonEcsData[]; - ecsData: Ecs; - index: number; - eventIdToNoteIds: Readonly<Record<string, string[]>>; - isEventPinned: boolean; - isEventViewer?: boolean; - onPinEvent: OnPinEvent; - onUnPinEvent: OnUnPinEvent; - refetch: inputsModel.Refetch; - rowIndex: number; - onRuleChange?: () => void; - showNotes: boolean; - tabType?: TimelineTabs; - timelineId: string; - toggleShowNotes: () => void; -} - -export type ActionProps = Props; const ActionsComponent: React.FC<ActionProps> = ({ ariaRowindex, @@ -75,9 +41,7 @@ const ActionsComponent: React.FC<ActionProps> = ({ isEventViewer = false, loadingEventIds, onEventDetailsPanelOpened, - onPinEvent, onRowSelected, - onUnPinEvent, refetch, onRuleChange, showCheckboxes, @@ -85,9 +49,20 @@ const ActionsComponent: React.FC<ActionProps> = ({ timelineId, toggleShowNotes, }) => { + const dispatch = useDispatch(); const emptyNotes: string[] = []; const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const onPinEvent: OnPinEvent = useCallback( + (evtId) => dispatch(timelineActions.pinEvent({ id: timelineId, eventId: evtId })), + [dispatch, timelineId] + ); + + const onUnPinEvent: OnPinEvent = useCallback( + (evtId) => dispatch(timelineActions.unPinEvent({ id: timelineId, eventId: evtId })), + [dispatch, timelineId] + ); + const handleSelectEvent = useCallback( (event: React.ChangeEvent<HTMLInputElement>) => onRowSelected({ @@ -99,7 +74,7 @@ const ActionsComponent: React.FC<ActionProps> = ({ const handlePinClicked = useCallback( () => getPinOnClick({ - allowUnpinning: !eventHasNotes(eventIdToNoteIds[eventId]), + allowUnpinning: eventIdToNoteIds ? !eventHasNotes(eventIdToNoteIds[eventId]) : true, eventId, onPinEvent, onUnPinEvent, @@ -164,12 +139,12 @@ const ActionsComponent: React.FC<ActionProps> = ({ /> )} - {!isEventViewer && ( + {!isEventViewer && toggleShowNotes && ( <> <AddEventNoteAction ariaLabel={i18n.ADD_NOTES_FOR_ROW({ ariaRowindex, columnValues })} key="add-event-note" - showNotes={showNotes} + showNotes={showNotes ?? false} toggleShowNotes={toggleShowNotes} timelineType={timelineType} /> @@ -177,7 +152,7 @@ const ActionsComponent: React.FC<ActionProps> = ({ ariaLabel={i18n.PIN_EVENT_FOR_ROW({ ariaRowindex, columnValues, isEventPinned })} key="pin-event" onPinClicked={handlePinClicked} - noteIds={eventIdToNoteIds[eventId] || emptyNotes} + noteIds={eventIdToNoteIds ? eventIdToNoteIds[eventId] || emptyNotes : emptyNotes} eventIsPinned={isEventPinned} timelineType={timelineType} /> @@ -200,7 +175,7 @@ const ActionsComponent: React.FC<ActionProps> = ({ ecsRowData={ecsData} timelineId={timelineId} disabled={eventType !== 'signal' && !isEventContextMenuEnabled} - refetch={refetch} + refetch={refetch ?? noop} onRuleChange={onRuleChange} /> </> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx index f9eda55c237ae..8795255dfcfd4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/actions/index.tsx @@ -8,7 +8,7 @@ import { EuiButtonIcon } from '@elastic/eui'; import React, { useCallback } from 'react'; -import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model'; +import { ColumnHeaderOptions } from '../../../../../../../common'; import { OnColumnRemoved } from '../../../events'; import { EventsHeadingExtra, EventsLoading } from '../../../styles'; import { Sort } from '../../sort'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx index 3ab4d564391f3..74593e40ddf4c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx @@ -12,16 +12,12 @@ import { Resizable, ResizeCallback } from 're-resizable'; import deepEqual from 'fast-deep-equal'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; +import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid'; -import { useDraggableKeyboardWrapper } from '../../../../../common/components/drag_and_drop/draggable_keyboard_wrapper_hook'; import { DEFAULT_COLUMN_MIN_WIDTH } from '../constants'; -import { - DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME, - getDraggableFieldId, -} from '../../../../../common/components/drag_and_drop/helpers'; -import { TimelineTabs } from '../../../../../../common/types/timeline'; +import { getDraggableFieldId } from '../../../../../common/components/drag_and_drop/helpers'; +import { ColumnHeaderOptions, TimelineTabs } from '../../../../../../common/types/timeline'; import { Direction } from '../../../../../../common/search_strategy'; -import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { OnFilterChange } from '../../events'; import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers'; import { EventsTh, EventsThContent, EventsHeadingHandle } from '../../styles'; @@ -31,6 +27,7 @@ import { Header } from './header'; import { timelineActions } from '../../../../store/timeline'; import * as i18n from './translations'; +import { useKibana } from '../../../../../common/lib/kibana'; const ContextMenu = styled(EuiContextMenu)` width: 115px; @@ -75,6 +72,7 @@ const ColumnHeaderComponent: React.FC<ColumneHeaderProps> = ({ const restoreFocus = useCallback(() => keyboardHandlerRef.current?.focus(), []); const dispatch = useDispatch(); + const { timelines } = useKibana().services; const resizableSize = useMemo( () => ({ width: header.initialWidth ?? DEFAULT_COLUMN_MIN_WIDTH, @@ -247,7 +245,7 @@ const ColumnHeaderComponent: React.FC<ColumneHeaderProps> = ({ setHoverActionsOwnFocus(true); }, []); - const { onBlur, onKeyDown } = useDraggableKeyboardWrapper({ + const { onBlur, onKeyDown } = timelines.getUseDraggableKeyboardWrapper()({ closePopover: handleClosePopOverTrigger, draggableId, fieldName: header.id, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/default_headers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/default_headers.ts index fea65d0499a13..7eb98b7475952 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/default_headers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/default_headers.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { ColumnHeaderOptions, ColumnHeaderType } from '../../../../store/timeline/model'; +import { ColumnHeaderOptions } from '../../../../../../common'; +import { ColumnHeaderType } from '../../../../store/timeline/model'; import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../constants'; export const defaultColumnHeaderType: ColumnHeaderType = 'not-filtered'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/index.tsx index bdf4cc42fa794..828b8d8701188 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/filter/index.tsx @@ -8,9 +8,9 @@ import { noop } from 'lodash/fp'; import React from 'react'; +import { ColumnHeaderOptions } from '../../../../../../../common'; import { DEFAULT_COLUMN_MIN_WIDTH } from '../../constants'; import { OnFilterChange } from '../../../events'; -import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model'; import { TextFilter } from '../text_filter'; interface Props { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx index 484cb78417c2f..ffab38b64bef8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/header_content.tsx @@ -8,8 +8,8 @@ import { EuiToolTip } from '@elastic/eui'; import { noop } from 'lodash/fp'; import React from 'react'; +import { ColumnHeaderOptions } from '../../../../../../../common/types/timeline'; -import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model'; import { TruncatableText } from '../../../../../../common/components/truncatable_text'; import { EventsHeading, EventsHeadingTitleButton, EventsHeadingTitleSpan } from '../../../styles'; import { Sort } from '../../sort'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts index b52fa292413df..257b88944c14e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/helpers.ts @@ -6,9 +6,8 @@ */ import { Direction } from '../../../../../../../common/search_strategy'; -import { assertUnreachable } from '../../../../../../../common/utility_types'; -import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model'; -import { Sort, SortDirection } from '../../sort'; +import { ColumnHeaderOptions, SortDirection } from '../../../../../../../common/types/timeline'; +import { Sort } from '../../sort'; interface GetNewSortDirectionOnClickParams { clickedHeader: ColumnHeaderOptions; @@ -35,7 +34,7 @@ export const getNextSortDirection = (currentSort: Sort): Direction => { case 'none': return Direction.desc; default: - return assertUnreachable(currentSort.sortDirection, 'Unhandled sort direction'); + return Direction.desc; } }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx index f2496484c25ea..4fa72fa5da424 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.test.tsx @@ -18,6 +18,7 @@ import { defaultHeaders } from '../default_headers'; import { HeaderComponent } from '.'; import { getNewSortDirectionOnClick, getNextSortDirection, getSortDirection } from './helpers'; import { Direction } from '../../../../../../../common/search_strategy'; +import { useDeepEqualSelector } from '../../../../../../common/hooks/use_selector'; const mockDispatch = jest.fn(); jest.mock('react-redux', () => { @@ -30,6 +31,11 @@ jest.mock('react-redux', () => { }; }); +jest.mock('../../../../../../common/hooks/use_selector', () => ({ + useShallowEqualSelector: jest.fn(), + useDeepEqualSelector: jest.fn(), +})); + const filteredColumnHeader: ColumnHeaderType = 'text-filter'; describe('Header', () => { @@ -41,7 +47,11 @@ describe('Header', () => { sortDirection: Direction.desc, }, ]; - const timelineId = 'fakeId'; + const timelineId = 'test'; + + beforeEach(() => { + (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: false }); + }); test('renders correctly against snapshot', () => { const wrapper = shallow( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx index ece28faedb951..60a241a340d99 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx @@ -9,16 +9,18 @@ import { noop } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; -import { useShallowEqualSelector } from '../../../../../../common/hooks/use_selector'; -import { timelineActions } from '../../../../../store/timeline'; -import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model'; +import { ColumnHeaderOptions } from '../../../../../../../common'; +import { + useDeepEqualSelector, + useShallowEqualSelector, +} from '../../../../../../common/hooks/use_selector'; +import { timelineActions, timelineSelectors } from '../../../../../store/timeline'; import { OnFilterChange } from '../../../events'; import { Sort } from '../../sort'; import { Actions } from '../actions'; import { Filter } from '../filter'; import { getNewSortDirectionOnClick } from './helpers'; import { HeaderContent } from './header_content'; -import { useManageTimeline } from '../../../../manage_timeline'; import { isEqlOnSelector } from './selectors'; interface Props { @@ -80,12 +82,10 @@ export const HeaderComponent: React.FC<Props> = ({ [dispatch, timelineId] ); - const { getManageTimelineById } = useManageTimeline(); - - const isLoading = useMemo(() => getManageTimelineById(timelineId).isLoading, [ - getManageTimelineById, - timelineId, - ]); + const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []); + const { isLoading } = useDeepEqualSelector( + (state) => getManageTimeline(state, timelineId) || { isLoading: false } + ); const showSortingCapability = !isEqlOn && !(header.subType && header.subType.nested); return ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx index 5b5a8b10591d4..b33e47dd27b96 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx @@ -9,9 +9,8 @@ import { mount, shallow } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; import React from 'react'; -import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model'; +import { ColumnHeaderOptions } from '../../../../../../../common'; import { defaultHeaders } from '../../../../../../common/mock'; - import { HeaderToolTipContent } from '.'; describe('HeaderToolTipContent', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.tsx index f4e7b6459bd14..0ae8dbb537fb8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.tsx @@ -10,7 +10,7 @@ import { isEmpty } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; -import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model'; +import { ColumnHeaderOptions } from '../../../../../../../common'; import { getIconFromType } from '../../../../../../common/components/event_details/helpers'; import * as i18n from '../translations'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts index d19c5689ab049..c49d088d6241d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.ts @@ -6,9 +6,9 @@ */ import { get } from 'lodash/fp'; +import { ColumnHeaderOptions } from '../../../../../../common'; import { BrowserFields } from '../../../../../common/containers/source'; -import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx index 41f9db3f1c25b..378f7fce250fe 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx @@ -24,6 +24,8 @@ import { Direction } from '../../../../../../common/search_strategy'; import { defaultControlColumn } from '../control_columns'; import { testTrailingControlColumns } from '../../../../../common/mock/mock_timeline_control_columns'; +jest.mock('../../../../../common/lib/kibana'); + const mockDispatch = jest.fn(); jest.mock('react-redux', () => { const original = jest.requireActual('react-redux'); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx index 3b0b935bfcff4..25aefd513f806 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx @@ -11,12 +11,17 @@ import { Droppable, DraggableChildrenFn } from 'react-beautiful-dnd'; import { DragEffects } from '../../../../../common/components/drag_and_drop/draggable_wrapper'; import { DraggableFieldBadge } from '../../../../../common/components/draggables/field_badge'; import { BrowserFields } from '../../../../../common/containers/source'; -import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { DRAG_TYPE_FIELD, droppableTimelineColumnsPrefix, } from '../../../../../common/components/drag_and_drop/helpers'; -import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline'; +import { + ColumnHeaderOptions, + ControlColumnProps, + HeaderActionProps, + TimelineId, + TimelineTabs, +} from '../../../../../../common/types/timeline'; import { OnSelectAll } from '../../events'; import { EventsTh, @@ -27,8 +32,6 @@ import { } from '../../styles'; import { Sort } from '../sort'; import { ColumnHeader } from './column_header'; -import { ControlColumnProps } from '../control_columns'; -import { HeaderActionProps } from '../actions/header_actions'; interface Props { actionsColumnWidth: number; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/control_columns/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/control_columns/index.tsx index 8ef69697af1d0..e4f4c26417351 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/control_columns/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/control_columns/index.tsx @@ -5,48 +5,9 @@ * 2.0. */ -import { ComponentType, JSXElementConstructor } from 'react'; -import { EuiDataGridControlColumn, EuiDataGridCellValueElementProps } from '@elastic/eui'; -import { OnRowSelected } from '../../events'; -import { ActionProps, Actions } from '../actions'; -import { HeaderActions, HeaderActionProps } from '../actions/header_actions'; - -export type GenericActionRowCellRenderProps = Pick< - EuiDataGridCellValueElementProps, - 'rowIndex' | 'columnId' ->; - -export type HeaderCellRender = ComponentType | ComponentType<HeaderActionProps>; -export type RowCellRender = - | JSXElementConstructor<GenericActionRowCellRenderProps> - | ((props: GenericActionRowCellRenderProps) => JSX.Element) - | JSXElementConstructor<ActionProps> - | ((props: ActionProps) => JSX.Element); - -interface AdditionalControlColumnProps { - ariaRowindex: number; - actionsColumnWidth: number; - columnValues: string; - checked: boolean; - onRowSelected: OnRowSelected; - eventId: string; - id: string; - columnId: string; - loadingEventIds: Readonly<string[]>; - onEventDetailsPanelOpened: () => void; - showCheckboxes: boolean; - // Override these type definitions to support either a generic custom component or the one used in security_solution today. - headerCellRender: HeaderCellRender; - rowCellRender: RowCellRender; - // If not provided, calculated dynamically - width?: number; -} - -export type ControlColumnProps = Omit< - EuiDataGridControlColumn, - keyof AdditionalControlColumnProps -> & - Partial<AdditionalControlColumnProps>; +import { ControlColumnProps } from '../../../../../../common/types/timeline'; +import { Actions } from '../actions'; +import { HeaderActions } from '../actions/header_actions'; export const defaultControlColumn: ControlColumnProps = { id: 'default-timeline-control-column', diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx index ae6307c0a294b..ecacbc51e395a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx @@ -36,11 +36,9 @@ describe('Columns', () => { timelineId="test" columnValues={'abc def'} showCheckboxes={false} - onPinEvent={jest.fn()} selectedEventIds={{}} loadingEventIds={[]} onEventDetailsPanelOpened={jest.fn()} - onUnPinEvent={jest.fn()} onRowSelected={jest.fn()} showNotes={false} isEventPinned={false} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx index ecabc3eae51c4..11bf88977fe61 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.tsx @@ -8,17 +8,20 @@ import { EuiScreenReaderOnly } from '@elastic/eui'; import React, { useMemo } from 'react'; import { getOr } from 'lodash/fp'; +import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid'; -import { CellValueElementProps } from '../../cell_rendering'; -import { ControlColumnProps, RowCellRender } from '../control_columns'; -import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '../../../../../common/components/drag_and_drop/helpers'; import { Ecs } from '../../../../../../common/ecs'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; -import { TimelineTabs } from '../../../../../../common/types/timeline'; -import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; +import { + ColumnHeaderOptions, + CellValueElementProps, + ActionProps, + ControlColumnProps, + TimelineTabs, + RowCellRender, +} from '../../../../../../common/types/timeline'; import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers'; -import { OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events'; -import { ActionProps } from '../actions'; +import { OnRowSelected } from '../../events'; import { inputsModel } from '../../../../../common/store'; import { EventsTd, @@ -60,9 +63,7 @@ interface DataDrivenColumnProps { loadingEventIds: Readonly<string[]>; notesCount: number; onEventDetailsPanelOpened: () => void; - onPinEvent: OnPinEvent; onRowSelected: OnRowSelected; - onUnPinEvent: OnUnPinEvent; refetch: inputsModel.Refetch; onRuleChange?: () => void; hasRowRenderers: boolean; @@ -137,9 +138,7 @@ const TgridActionTdCell = ({ loadingEventIds, notesCount, onEventDetailsPanelOpened, - onPinEvent, onRowSelected, - onUnPinEvent, refetch, rowIndex, hasRowRenderers, @@ -193,9 +192,7 @@ const TgridActionTdCell = ({ isEventViewer={isEventViewer} loadingEventIds={loadingEventIds} onEventDetailsPanelOpened={onEventDetailsPanelOpened} - onPinEvent={onPinEvent} onRowSelected={onRowSelected} - onUnPinEvent={onUnPinEvent} refetch={refetch} rowIndex={rowIndex} onRuleChange={onRuleChange} @@ -292,9 +289,7 @@ export const DataDrivenColumns = React.memo<DataDrivenColumnProps>( loadingEventIds, notesCount, onEventDetailsPanelOpened, - onPinEvent, onRowSelected, - onUnPinEvent, refetch, hasRowRenderers, onRuleChange, @@ -345,8 +340,6 @@ export const DataDrivenColumns = React.memo<DataDrivenColumnProps>( isEventPinned={isEventPinned} isEventViewer={isEventViewer} notesCount={notesCount} - onPinEvent={onPinEvent} - onUnPinEvent={onUnPinEvent} refetch={refetch} hasRowRenderers={hasRowRenderers} onRuleChange={onRuleChange} @@ -365,7 +358,6 @@ export const DataDrivenColumns = React.memo<DataDrivenColumnProps>( data, ecsData, onRowSelected, - onPinEvent, isEventPinned, isEventViewer, actionsColumnWidth, @@ -378,7 +370,6 @@ export const DataDrivenColumns = React.memo<DataDrivenColumnProps>( notesCount, onEventDetailsPanelOpened, onRuleChange, - onUnPinEvent, refetch, selectedEventIds, showCheckboxes, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx index 3c75bc7fb2649..3e22cba208ca2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.test.tsx @@ -9,11 +9,13 @@ import { mount } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; import React, { useEffect } from 'react'; -import { CellValueElementProps } from '../../cell_rendering'; import { defaultHeaders, mockTimelineData } from '../../../../../common/mock'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; -import { TimelineTabs } from '../../../../../../common/types/timeline'; -import { ColumnHeaderOptions } from '../../../../store/timeline/model'; +import { + ColumnHeaderOptions, + CellValueElementProps, + TimelineTabs, +} from '../../../../../../common/types/timeline'; import { StatefulCell } from './stateful_cell'; import { getMappedNonEcsValue } from '.'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx index a5f8336cc7997..7931e0739aa68 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/stateful_cell.tsx @@ -7,10 +7,12 @@ import React, { HTMLAttributes, useState } from 'react'; -import { CellValueElementProps } from '../../cell_rendering'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; -import { TimelineTabs } from '../../../../../../common/types/timeline'; -import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; +import { + ColumnHeaderOptions, + CellValueElementProps, + TimelineTabs, +} from '../../../../../../common/types/timeline'; export interface CommonProps { className?: string; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx index e56171aae003c..17f231c0fdad9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx @@ -60,9 +60,7 @@ describe('EventColumnView', () => { loadingEventIds: [], notesCount: 0, onEventDetailsPanelOpened: jest.fn(), - onPinEvent: jest.fn(), onRowSelected: jest.fn(), - onUnPinEvent: jest.fn(), refetch: jest.fn(), renderCellValue: DefaultCellRenderer, selectedEventIds: {}, @@ -120,16 +118,6 @@ describe('EventColumnView', () => { expect(wrapper.find('[data-test-subj="pin"]').exists()).toBe(false); }); - test('it invokes onPinClicked when the button for pinning events is clicked', () => { - const wrapper = mount(<EventColumnView {...props} />, { wrappingComponent: TestProviders }); - - expect(props.onPinEvent).not.toHaveBeenCalled(); - - wrapper.find('[data-test-subj="pin"]').first().simulate('click'); - - expect(props.onPinEvent).toHaveBeenCalled(); - }); - test('it render AddToCaseAction if timelineId === TimelineId.detectionsPage', () => { const wrapper = mount(<EventColumnView {...props} timelineId={TimelineId.detectionsPage} />, { wrappingComponent: TestProviders, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx index 5dc718f90a91a..298ce252ba925 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx @@ -7,16 +7,19 @@ import React, { useMemo } from 'react'; -import { CellValueElementProps } from '../../cell_rendering'; -import { ControlColumnProps, RowCellRender } from '../control_columns'; import { Ecs } from '../../../../../../common/ecs'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; -import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; -import { OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events'; +import { OnRowSelected } from '../../events'; import { EventsTrData, EventsTdGroupActions } from '../../styles'; import { DataDrivenColumns, getMappedNonEcsValue } from '../data_driven_columns'; import { inputsModel } from '../../../../../common/store'; -import { TimelineTabs } from '../../../../../../common/types/timeline'; +import { + ColumnHeaderOptions, + CellValueElementProps, + ControlColumnProps, + RowCellRender, + TimelineTabs, +} from '../../../../../../common/types/timeline'; interface Props { id: string; @@ -31,9 +34,7 @@ interface Props { loadingEventIds: Readonly<string[]>; notesCount: number; onEventDetailsPanelOpened: () => void; - onPinEvent: OnPinEvent; onRowSelected: OnRowSelected; - onUnPinEvent: OnUnPinEvent; refetch: inputsModel.Refetch; renderCellValue: (props: CellValueElementProps) => React.ReactNode; onRuleChange?: () => void; @@ -62,9 +63,7 @@ export const EventColumnView = React.memo<Props>( loadingEventIds, notesCount, onEventDetailsPanelOpened, - onPinEvent, onRowSelected, - onUnPinEvent, refetch, hasRowRenderers, onRuleChange, @@ -134,10 +133,8 @@ export const EventColumnView = React.memo<Props>( eventIdToNoteIds={eventIdToNoteIds} isEventPinned={isEventPinned} isEventViewer={isEventViewer} - onPinEvent={onPinEvent} - onUnPinEvent={onUnPinEvent} - refetch={refetch} onRuleChange={onRuleChange} + refetch={refetch} showNotes={showNotes} tabType={tabType} timelineId={timelineId} @@ -161,10 +158,8 @@ export const EventColumnView = React.memo<Props>( leadingControlColumns, loadingEventIds, onEventDetailsPanelOpened, - onPinEvent, onRowSelected, onRuleChange, - onUnPinEvent, refetch, selectedEventIds, showCheckboxes, @@ -201,8 +196,6 @@ export const EventColumnView = React.memo<Props>( eventIdToNoteIds={eventIdToNoteIds} isEventPinned={isEventPinned} isEventViewer={isEventViewer} - onPinEvent={onPinEvent} - onUnPinEvent={onUnPinEvent} refetch={refetch} onRuleChange={onRuleChange} selectedEventIds={selectedEventIds} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx index c3097ad68aba1..c09de87c87f32 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/index.tsx @@ -8,19 +8,21 @@ import React from 'react'; import { isEmpty } from 'lodash'; -import { CellValueElementProps } from '../../cell_rendering'; -import { ControlColumnProps } from '../control_columns'; import { inputsModel } from '../../../../../common/store'; import { BrowserFields } from '../../../../../common/containers/source'; import { TimelineItem, TimelineNonEcsData, } from '../../../../../../common/search_strategy/timeline'; -import { TimelineTabs } from '../../../../../../common/types/timeline'; -import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; +import { + ColumnHeaderOptions, + CellValueElementProps, + ControlColumnProps, + RowRenderer, + TimelineTabs, +} from '../../../../../../common/types/timeline'; import { OnRowSelected } from '../../events'; import { EventsTbody } from '../../styles'; -import { RowRenderer } from '../renderers/row_renderer'; import { StatefulEvent } from './stateful_event'; import { eventIsPinned } from '../helpers'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index 701dc549467e9..b8840a75cc9b4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -8,10 +8,12 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; -import { CellValueElementProps } from '../../cell_rendering'; -import { ControlColumnProps } from '../control_columns'; import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; import { + ColumnHeaderOptions, + CellValueElementProps, + ControlColumnProps, + RowRenderer, TimelineExpandedDetailType, TimelineId, TimelineTabs, @@ -21,11 +23,9 @@ import { TimelineItem, TimelineNonEcsData, } from '../../../../../../common/search_strategy/timeline'; -import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; -import { OnPinEvent, OnRowSelected } from '../../events'; +import { OnRowSelected } from '../../events'; import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../../helpers'; import { EventsTrGroup, EventsTrSupplement, EventsTrSupplementContainer } from '../../styles'; -import { RowRenderer } from '../renderers/row_renderer'; import { isEventBuildingBlockType, getEventType, isEvenEqlSequence } from '../helpers'; import { NoteCards } from '../../../notes/note_cards'; import { useEventDetailsWidthContext } from '../../../../../common/components/events_viewer/event_details_width_context'; @@ -176,16 +176,6 @@ const StatefulEventComponent: React.FC<Props> = ({ }); }, [event]); - const onPinEvent: OnPinEvent = useCallback( - (eventId) => dispatch(timelineActions.pinEvent({ id: timelineId, eventId })), - [dispatch, timelineId] - ); - - const onUnPinEvent: OnPinEvent = useCallback( - (eventId) => dispatch(timelineActions.unPinEvent({ id: timelineId, eventId })), - [dispatch, timelineId] - ); - const handleOnEventDetailPanelOpened = useCallback(() => { const eventId = event._id; const indexName = event._index!; @@ -215,10 +205,10 @@ const StatefulEventComponent: React.FC<Props> = ({ (noteId: string) => { dispatch(timelineActions.addNoteToEvent({ eventId: event._id, id: timelineId, noteId })); if (!isEventPinned) { - onPinEvent(event._id); // pin the event, because it has notes + dispatch(timelineActions.pinEvent({ id: timelineId, eventId: event._id })); } }, - [dispatch, event, isEventPinned, onPinEvent, timelineId] + [dispatch, event, isEventPinned, timelineId] ); const RowRendererContent = useMemo( @@ -273,9 +263,7 @@ const StatefulEventComponent: React.FC<Props> = ({ loadingEventIds={loadingEventIds} notesCount={notes.length} onEventDetailsPanelOpened={handleOnEventDetailPanelOpened} - onPinEvent={onPinEvent} onRowSelected={onRowSelected} - onUnPinEvent={onUnPinEvent} refetch={refetch} renderCellValue={renderCellValue} onRuleChange={onRuleChange} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx index 10a25538c1ba3..19abd6841e7e8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_row_renderer/index.tsx @@ -9,15 +9,15 @@ import { noop } from 'lodash/fp'; import { EuiFocusTrap, EuiOutsideClickDetector, EuiScreenReaderOnly } from '@elastic/eui'; import React, { useMemo } from 'react'; -import { BrowserFields } from '../../../../../../common/containers/source'; import { ARIA_COLINDEX_ATTRIBUTE, ARIA_ROWINDEX_ATTRIBUTE, getRowRendererClassName, -} from '../../../../../../common/components/accessibility/helpers'; +} from '../../../../../../../../timelines/public'; +import { RowRenderer } from '../../../../../../../common'; +import { BrowserFields } from '../../../../../../common/containers/source'; import { TimelineItem } from '../../../../../../../common/search_strategy/timeline'; import { getRowRenderer } from '../../renderers/get_row_renderer'; -import { RowRenderer } from '../../renderers/row_renderer'; import { useStatefulEventFocus } from '../use_stateful_event_focus'; import * as i18n from '../translations'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/use_stateful_event_focus/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/use_stateful_event_focus/index.tsx index 5f3c4dac8b73d..4e8fd7dc48968 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/use_stateful_event_focus/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/use_stateful_event_focus/index.tsx @@ -13,7 +13,7 @@ import { isEscape, focusColumn, OnColumnFocused, -} from '../../../../../../common/components/accessibility/helpers'; +} from '../../../../../../../../timelines/public'; type FocusOwnership = 'not-owned' | 'owned'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index 61601c3921445..19059b5fb4599 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -23,6 +23,8 @@ import { timelineActions } from '../../../store/timeline'; import { TimelineTabs } from '../../../../../common/types/timeline'; import { defaultRowRenderers } from './renderers'; +jest.mock('../../../../common/lib/kibana'); + const mockSort: Sort[] = [ { columnId: '@timestamp', @@ -255,7 +257,7 @@ describe('Body', () => { tabType: 'query', timelineId: 'timeline-test', }, - type: 'x-pack/security_solution/local/timeline/TOGGLE_DETAIL_PANEL', + type: 'x-pack/timelines/t-grid/TOGGLE_DETAIL_PANEL', }); }); @@ -279,7 +281,7 @@ describe('Body', () => { tabType: 'pinned', timelineId: 'timeline-test', }, - type: 'x-pack/security_solution/local/timeline/TOGGLE_DETAIL_PANEL', + type: 'x-pack/timelines/t-grid/TOGGLE_DETAIL_PANEL', }); }); @@ -303,7 +305,7 @@ describe('Body', () => { tabType: 'notes', timelineId: 'timeline-test', }, - type: 'x-pack/security_solution/local/timeline/TOGGLE_DETAIL_PANEL', + type: 'x-pack/timelines/t-grid/TOGGLE_DETAIL_PANEL', }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index 64f61232377e8..fc8bf2086471c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -11,21 +11,26 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; -import { CellValueElementProps } from '../cell_rendering'; -import { DEFAULT_COLUMN_MIN_WIDTH } from './constants'; -import { ControlColumnProps } from './control_columns'; -import { RowRendererId, TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; import { FIRST_ARIA_INDEX, ARIA_COLINDEX_ATTRIBUTE, ARIA_ROWINDEX_ATTRIBUTE, onKeyDownFocusHandler, -} from '../../../../common/components/accessibility/helpers'; +} from '../../../../../../timelines/public'; +import { CellValueElementProps } from '../cell_rendering'; +import { DEFAULT_COLUMN_MIN_WIDTH } from './constants'; +import { + ColumnHeaderOptions, + ControlColumnProps, + RowRendererId, + RowRenderer, + TimelineId, + TimelineTabs, +} from '../../../../../common/types/timeline'; import { BrowserFields } from '../../../../common/containers/source'; import { TimelineItem } from '../../../../../common/search_strategy/timeline'; import { inputsModel, State } from '../../../../common/store'; -import { useManageTimeline } from '../../manage_timeline'; -import { ColumnHeaderOptions, TimelineModel } from '../../../store/timeline/model'; +import { TimelineModel } from '../../../store/timeline/model'; import { timelineDefaults } from '../../../store/timeline/defaults'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; import { OnRowSelected, OnSelectAll } from '../events'; @@ -33,11 +38,11 @@ import { getActionsColumnWidth, getColumnHeaders } from './column_headers/helper import { getEventIdToDataMapping } from './helpers'; import { Sort } from './sort'; import { plainRowRenderer } from './renderers/plain_row_renderer'; -import { RowRenderer } from './renderers/row_renderer'; import { EventsTable, TimelineBody, TimelineBodyGlobalStyle } from '../styles'; import { ColumnHeaders } from './column_headers'; import { Events } from './events'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; interface OwnProps { activePage: number; @@ -99,11 +104,10 @@ export const BodyComponent = React.memo<StatefulBodyProps>( trailingControlColumns = [], }) => { const containerRef = useRef<HTMLDivElement | null>(null); - const { getManageTimelineById } = useManageTimeline(); - const { queryFields, selectAll } = useMemo(() => getManageTimelineById(id), [ - getManageTimelineById, - id, - ]); + const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []); + const { queryFields, selectAll } = useDeepEqualSelector((state) => + getManageTimeline(state, id) + ); const onRowSelected: OnRowSelected = useCallback( ({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/args.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/args.test.tsx index 21c44cb26e2e5..d5ec8b6f94862 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/args.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/args.test.tsx @@ -13,6 +13,8 @@ import { useMountAppended } from '../../../../../common/utils/use_mount_appended import { TestProviders } from '../../../../../common/mock'; import { ArgsComponent } from './args'; +jest.mock('../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_details.test.tsx index f45c049ca137a..2a5764e53756a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_details.test.tsx @@ -15,6 +15,8 @@ import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; import { AuditdGenericDetails, AuditdGenericLine } from './generic_details'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; +jest.mock('../../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.test.tsx index 51676c067cd79..009ffecf28f74 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.test.tsx @@ -15,6 +15,8 @@ import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; import { AuditdGenericFileDetails, AuditdGenericFileLine } from './generic_file_details'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; +jest.mock('../../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx index 31fea6fa25e65..74a5ff472b581 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx @@ -9,17 +9,19 @@ import { shallow } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; import React from 'react'; +import { RowRenderer } from '../../../../../../../common'; import { BrowserFields } from '../../../../../../common/containers/source'; import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; import { Ecs } from '../../../../../../../common/ecs'; import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; -import { RowRenderer } from '../row_renderer'; import { createGenericAuditRowRenderer, createGenericFileRowRenderer, } from './generic_row_renderer'; +jest.mock('../../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx index 9133e500162bc..765bfd3d21351 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx @@ -11,9 +11,9 @@ import { IconType } from '@elastic/eui'; import { get } from 'lodash/fp'; import React from 'react'; -import { RowRendererId } from '../../../../../../../common/types/timeline'; +import { RowRendererId, RowRenderer } from '../../../../../../../common/types/timeline'; -import { RowRenderer, RowRendererContainer } from '../row_renderer'; +import { RowRendererContainer } from '../row_renderer'; import { AuditdGenericDetails } from './generic_details'; import { AuditdGenericFileDetails } from './generic_file_details'; import * as i18n from './translations'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx index 24b9f8d40eb17..d6037a310dc7e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx @@ -13,6 +13,8 @@ import { TestProviders } from '../../../../../../common/mock'; import { PrimarySecondaryUserInfo, nilOrUnSet } from './primary_secondary_user_info'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; +jest.mock('../../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx index 22cd8446a51c0..fa6eda6bce37d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx @@ -14,6 +14,8 @@ import { TestProviders } from '../../../../../../common/mock'; import { SessionUserHostWorkingDir } from './session_user_host_working_dir'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; +jest.mock('../../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.test.tsx index 8b4a9f72b1a45..c7da6f758766e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.test.tsx @@ -14,6 +14,8 @@ import { useMountAppended } from '../../../../../../common/utils/use_mount_appen import { Bytes } from '.'; +jest.mock('../../../../../../common/lib/kibana'); + describe('Bytes', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/column_renderer.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/column_renderer.ts index cb670b53a9679..65bb67458ab2a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/column_renderer.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/column_renderer.ts @@ -6,8 +6,8 @@ */ import type React from 'react'; +import { ColumnHeaderOptions } from '../../../../../../common'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; -import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; export interface ColumnRenderer { isInstance: (columnName: string, data: TimelineNonEcsData[]) => boolean; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx index 7f580642130fe..872ca017d7f7d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx @@ -12,6 +12,8 @@ import { TestProviders } from '../../../../../../common/mock'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { ThreatMatchRowProps, ThreatMatchRowView } from './threat_match_row'; +jest.mock('../../../../../../common/lib/kibana'); + describe('ThreatMatchRowView', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx index 2a7e8ce02d79f..16426bf74aba7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row_renderer.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import { RowRendererId } from '../../../../../../../common/types/timeline'; -import { RowRenderer } from '../row_renderer'; +import { RowRendererId, RowRenderer } from '../../../../../../../common/types/timeline'; import { hasThreatMatchValue } from './helpers'; import { ThreatMatchRows } from './threat_match_rows'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx index cc34f9e63b5e2..f6feb6dd1b126 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_rows.tsx @@ -10,9 +10,10 @@ import { get } from 'lodash'; import React, { Fragment } from 'react'; import styled from 'styled-components'; +import { RowRenderer } from '../../../../../../../common'; import { Fields } from '../../../../../../../common/search_strategy'; import { ID_FIELD_NAME } from '../../../../../../common/components/event_details/event_id'; -import { RowRenderer, RowRendererContainer } from '../row_renderer'; +import { RowRendererContainer } from '../row_renderer'; import { ThreatMatchRow } from './threat_match_row'; const SpacedContainer = styled.div` diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx index d3e870aa92ef0..9e6c5b819a20b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx @@ -15,6 +15,8 @@ import { useMountAppended } from '../../../../../../common/utils/use_mount_appen import { DnsRequestEventDetails } from './dns_request_event_details'; +jest.mock('../../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx index 2809b06c77469..5c0aecf5fbbc7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx @@ -12,6 +12,8 @@ import '../../../../../../common/mock/match_media'; import { DnsRequestEventDetailsLine } from './dns_request_event_details_line'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; +jest.mock('../../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.test.tsx index 034ade75ef2c0..5144705f26174 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.test.tsx @@ -18,6 +18,8 @@ import { getEmptyValue } from '../../../../../common/components/empty_value'; import { deleteItemIdx, findItem } from './helpers'; import { emptyColumnRenderer } from './empty_column_renderer'; +jest.mock('../../../../../common/lib/kibana'); + describe('empty_column_renderer', () => { let mockDatum: TimelineNonEcsData[]; const _id = mockTimelineData[0]._id; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx index 400ccf47201ac..37873df7f4e7b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx @@ -8,9 +8,8 @@ /* eslint-disable react/display-name */ import React from 'react'; - +import { ColumnHeaderOptions } from '../../../../../../common'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; -import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { DraggableWrapper, DragEffects, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx index c1df6d6eb48c8..613d66505601a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx @@ -20,6 +20,8 @@ import { useMountAppended } from '../../../../../../common/utils/use_mount_appen import { EndgameSecurityEventDetails } from './endgame_security_event_details'; +jest.mock('../../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx index 5d08898789821..879862d06b250 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx @@ -13,6 +13,8 @@ import '../../../../../../common/mock/match_media'; import { EndgameSecurityEventDetailsLine } from './endgame_security_event_details_line'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; +jest.mock('../../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.test.tsx index a6f15a9f79f4e..1bf8d1a4a4f51 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.test.tsx @@ -13,6 +13,8 @@ import { useMountAppended } from '../../../../../common/utils/use_mount_appended import { ExitCodeDraggable } from './exit_code_draggable'; +jest.mock('../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_draggable.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_draggable.test.tsx index d7274f0774fc5..cf3fce2c25c0b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_draggable.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_draggable.test.tsx @@ -13,6 +13,8 @@ import { TestProviders } from '../../../../../common/mock'; import { FileDraggable } from './file_draggable'; import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; +jest.mock('../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_hash.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_hash.test.tsx index e7e6274942bea..8ebd3ae8a67c2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_hash.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_hash.test.tsx @@ -13,6 +13,8 @@ import { useMountAppended } from '../../../../../common/utils/use_mount_appended import { FileHash } from './file_hash'; +jest.mock('../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx index 8e54f13ec9cbf..852331aa021dd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx @@ -21,6 +21,8 @@ import { getColumnRenderer } from './get_column_renderer'; import { getValues, findItem, deleteItemIdx } from './helpers'; import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; +jest.mock('../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx index 56dbc99d47c66..104550f138f16 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx @@ -20,6 +20,8 @@ import { useMountAppended } from '../../../../../common/utils/use_mount_appended import { defaultRowRenderers } from '.'; import { getRowRenderer } from './get_row_renderer'; +jest.mock('../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.ts index bfe60a14e042d..2d1be6ee7914a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.ts @@ -5,8 +5,8 @@ * 2.0. */ +import { RowRenderer } from '../../../../../../common'; import { Ecs } from '../../../../../../common/ecs'; -import { RowRenderer } from './row_renderer'; export const getRowRenderer = (ecs: Ecs, rowRenderers: RowRenderer[]): RowRenderer | null => rowRenderers.find((rowRenderer) => rowRenderer.isInstance(ecs)) ?? null; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_working_dir.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_working_dir.test.tsx index 9412ecfd364ba..d650710b25cad 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_working_dir.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_working_dir.test.tsx @@ -13,6 +13,8 @@ import { mockTimelineData, TestProviders } from '../../../../../common/mock'; import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; import { HostWorkingDir } from './host_working_dir'; +jest.mock('../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts index 537a24bbfd953..911dcc8cd2e87 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/index.ts @@ -5,12 +5,12 @@ * 2.0. */ +import { RowRenderer } from '../../../../../../common'; import { auditdRowRenderers } from './auditd/generic_row_renderer'; import { ColumnRenderer } from './column_renderer'; import { emptyColumnRenderer } from './empty_column_renderer'; import { netflowRowRenderer } from './netflow/netflow_row_renderer'; import { plainColumnRenderer } from './plain_column_renderer'; -import { RowRenderer } from './row_renderer'; import { suricataRowRenderer } from './suricata/suricata_row_renderer'; import { unknownColumnRenderer } from './unknown_column_renderer'; import { zeekRowRenderer } from './zeek/zeek_row_renderer'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx index 72e3516827c8a..fc97624dbfc96 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx @@ -26,6 +26,8 @@ export const justIdAndTimestamp: Ecs = { timestamp: '2018-11-12T19:03:25.936Z', }; +jest.mock('../../../../../../common/lib/kibana'); + jest.mock('../../../../../../common/components/link_to'); describe('netflowRowRenderer', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx index 2605670ee8b38..35406dce6ff72 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx @@ -11,7 +11,7 @@ import { get } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; -import { RowRendererId } from '../../../../../../../common/types/timeline'; +import { RowRendererId, RowRenderer } from '../../../../../../../common/types/timeline'; import { asArrayIfExists } from '../../../../../../common/lib/helpers'; import { TLS_CLIENT_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME, @@ -63,7 +63,7 @@ import { SOURCE_BYTES_FIELD_NAME, SOURCE_PACKETS_FIELD_NAME, } from '../../../../../../network/components/source_destination/source_destination_arrows'; -import { RowRenderer, RowRendererContainer } from '../row_renderer'; +import { RowRendererContainer } from '../row_renderer'; const Details = styled.div` margin: 5px 0; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/parent_process_draggable.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/parent_process_draggable.test.tsx index 2402be88dea18..7c28747cc84ef 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/parent_process_draggable.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/parent_process_draggable.test.tsx @@ -13,6 +13,8 @@ import { TestProviders } from '../../../../../common/mock'; import { ParentProcessDraggable } from './parent_process_draggable'; import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; +jest.mock('../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx index a56acbe48685c..e970aaad026b1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx @@ -18,6 +18,8 @@ import { useMountAppended } from '../../../../../common/utils/use_mount_appended import { plainColumnRenderer } from './plain_column_renderer'; import { getValues, deleteItemIdx, findItem } from './helpers'; +jest.mock('../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.tsx index a2b7750d9bb59..77039ddc4a586 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.tsx @@ -8,8 +8,8 @@ import { head } from 'lodash/fp'; import React from 'react'; +import { ColumnHeaderOptions } from '../../../../../../common'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; -import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { getEmptyTagValue } from '../../../../../common/components/empty_value'; import { ColumnRenderer } from './column_renderer'; import { FormattedFieldValue } from './formatted_field'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx index 0b5afd579d08c..15620a7fc04b4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx @@ -7,9 +7,7 @@ import React from 'react'; -import { RowRendererId } from '../../../../../../common/types/timeline'; - -import { RowRenderer } from './row_renderer'; +import { RowRendererId, RowRenderer } from '../../../../../../common/types/timeline'; const PlainRowRenderer = () => <></>; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_draggable.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_draggable.test.tsx index 31a1745fa2a6d..6509808fb0c9f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_draggable.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_draggable.test.tsx @@ -13,6 +13,8 @@ import '../../../../../common/mock/match_media'; import { ProcessDraggable, ProcessDraggableWithNonExistentProcess } from './process_draggable'; import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; +jest.mock('../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_hash.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_hash.test.tsx index 9e90e061e94d5..7135f2a5fed6a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_hash.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_hash.test.tsx @@ -13,6 +13,8 @@ import { useMountAppended } from '../../../../../common/utils/use_mount_appended import { ProcessHash } from './process_hash'; +jest.mock('../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details.test.tsx index f37adef7e73cb..e5bb91c532505 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details.test.tsx @@ -18,6 +18,8 @@ import { MODIFIED_REGISTRY_KEY } from '../system/translations'; import { RegistryEventDetails } from './registry_event_details'; +jest.mock('../../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details_line.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details_line.test.tsx index 6be1529152523..d0287f2b010ae 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details_line.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/registry/registry_event_details_line.test.tsx @@ -13,6 +13,8 @@ import { useMountAppended } from '../../../../../../common/utils/use_mount_appen import { RegistryEventDetailsLine } from './registry_event_details_line'; import { MODIFIED_REGISTRY_KEY } from '../system/translations'; +jest.mock('../../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx index 679da28e622bf..9099f76b8305c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/row_renderer.tsx @@ -7,11 +7,7 @@ import React from 'react'; -import { BrowserFields } from '../../../../../common/containers/source'; -import type { RowRendererId } from '../../../../../../common/types/timeline'; -import { Ecs } from '../../../../../../common/ecs'; import { EventsTrSupplement } from '../../styles'; - interface RowRendererContainerProps { children: React.ReactNode; } @@ -22,17 +18,3 @@ export const RowRendererContainer = React.memo<RowRendererContainerProps>(({ chi </EventsTrSupplement> )); RowRendererContainer.displayName = 'RowRendererContainer'; - -export interface RowRenderer { - id: RowRendererId; - isInstance: (data: Ecs) => boolean; - renderRow: ({ - browserFields, - data, - timelineId, - }: { - browserFields: BrowserFields; - data: Ecs; - timelineId: string; - }) => React.ReactNode; -} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx index 5960f43174b98..355077ee50066 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx @@ -16,6 +16,8 @@ import { TestProviders } from '../../../../../../common/mock/test_providers'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { SuricataDetails } from './suricata_details'; +jest.mock('../../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx index 098d6775cfaa4..998233b2278c9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx @@ -18,6 +18,8 @@ import { TestProviders } from '../../../../../../common/mock/test_providers'; import { suricataRowRenderer } from './suricata_row_renderer'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; +jest.mock('../../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx index 5a68bc6fe28c8..aa482926bf007 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx @@ -10,9 +10,9 @@ import { get } from 'lodash/fp'; import React from 'react'; -import { RowRendererId } from '../../../../../../../common/types/timeline'; +import { RowRendererId, RowRenderer } from '../../../../../../../common/types/timeline'; -import { RowRenderer, RowRendererContainer } from '../row_renderer'; +import { RowRendererContainer } from '../row_renderer'; import { SuricataDetails } from './suricata_details'; export const suricataRowRenderer: RowRenderer = { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.test.tsx index 4a727e4e7bc27..b3911f9eded67 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.test.tsx @@ -18,6 +18,8 @@ import { SURICATA_SIGNATURE_ID_FIELD_NAME, } from './suricata_signature'; +jest.mock('../../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.test.tsx index 001b7f4b68bab..35872d0093f02 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.test.tsx @@ -15,6 +15,8 @@ import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; import { SystemGenericDetails, SystemGenericLine } from './generic_details'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; +jest.mock('../../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx index b660d823954ee..f5dc4c6fdf599 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx @@ -16,6 +16,8 @@ import { mockEndgameCreationEvent } from '../../../../../../common/mock/mock_end import { SystemGenericFileDetails, SystemGenericFileLine } from './generic_file_details'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; +jest.mock('../../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx index 8e8ce9cb2f988..6f5b225f0690b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx @@ -67,7 +67,6 @@ import { mockEndpointSecurityLogOffEvent, } from '../../../../../../common/mock/mock_endgame_ecs_data'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; -import { RowRenderer } from '../row_renderer'; import { createDnsRowRenderer, createEndgameProcessRowRenderer, @@ -82,6 +81,9 @@ import { EndpointAlertCriteria, } from './generic_row_renderer'; import * as i18n from './translations'; +import { RowRenderer } from '../../../../../../../common'; + +jest.mock('../../../../../../common/lib/kibana'); jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx index 211fa9152dc8d..c6845d7d672d2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx @@ -10,13 +10,13 @@ import { get } from 'lodash/fp'; import React from 'react'; -import { RowRendererId } from '../../../../../../../common/types/timeline'; +import { RowRendererId, RowRenderer } from '../../../../../../../common/types/timeline'; import { DnsRequestEventDetails } from '../dns/dns_request_event_details'; import { EndgameSecurityEventDetails } from '../endgame/endgame_security_event_details'; import { isFileEvent, isNillEmptyOrNotFinite } from '../helpers'; import { RegistryEventDetails } from '../registry/registry_event_details'; -import { RowRenderer, RowRendererContainer } from '../row_renderer'; +import { RowRendererContainer } from '../row_renderer'; import { SystemGenericDetails } from './generic_details'; import { SystemGenericFileDetails } from './generic_file_details'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/package.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/package.test.tsx index ac1e4d6748dcd..be11955169bd7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/package.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/package.test.tsx @@ -13,6 +13,8 @@ import { TestProviders } from '../../../../../../common/mock'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { Package } from './package'; +jest.mock('../../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx index dfb9ae69ac2d4..7cff1166cd0de 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx @@ -13,6 +13,8 @@ import '../../../../../common/mock/match_media'; import { UserHostWorkingDir } from './user_host_working_dir'; import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; +jest.mock('../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx index 04150163fb4d4..7f0ec8b7b0b79 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx @@ -14,6 +14,8 @@ import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { ZeekDetails } from './zeek_details'; +jest.mock('../../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx index 749e450b36ae4..6b154d4d32707 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx @@ -17,6 +17,8 @@ import '../../../../../../common/mock/match_media'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { zeekRowRenderer } from './zeek_row_renderer'; +jest.mock('../../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx index 7a8d284d0ec1e..2b6311b8cae83 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx @@ -10,9 +10,9 @@ import { get } from 'lodash/fp'; import React from 'react'; -import { RowRendererId } from '../../../../../../../common/types/timeline'; +import { RowRendererId, RowRenderer } from '../../../../../../../common/types/timeline'; -import { RowRenderer, RowRendererContainer } from '../row_renderer'; +import { RowRendererContainer } from '../row_renderer'; import { ZeekDetails } from './zeek_details'; export const zeekRowRenderer: RowRenderer = { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx index 61155331b1a4b..28034dac8f575 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx @@ -28,6 +28,8 @@ import { defaultStringRenderer, } from './zeek_signature'; +jest.mock('../../../../../../common/lib/kibana'); + jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/index.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/index.ts index e7c69b9229d70..bd05bf0656687 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/index.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/index.ts @@ -5,15 +5,7 @@ * 2.0. */ -import { Direction } from '../../../../../../common/search_strategy'; -import { ColumnId } from '../column_id'; - -/** Specifies a column's sort direction */ -export type SortDirection = 'none' | Direction; +import { SortColumnTimeline } from '../../../../../../common/types/timeline'; /** Specifies which column the timeline is sorted on */ -export interface Sort { - columnId: ColumnId; - columnType: string; - sortDirection: SortDirection; -} +export type Sort = SortColumnTimeline; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx index 6af29793f9373..3e610abe79050 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx @@ -11,8 +11,8 @@ import React from 'react'; import * as i18n from '../translations'; import { SortNumber } from './sort_number'; -import { SortDirection } from '.'; import { Direction } from '../../../../../../common/search_strategy'; +import { SortDirection } from '../../../../../../common/types/timeline'; enum SortDirectionIndicatorEnum { SORT_UP = 'sortUp', diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx index 5ac1dcf8805cf..06d8133a24f6e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/default_cell_renderer.test.tsx @@ -17,6 +17,8 @@ import { mockBrowserFields } from '../../../../common/containers/source/mock'; import { defaultHeaders, mockTimelineData, TestProviders } from '../../../../common/mock'; import { DefaultCellRenderer } from './default_cell_renderer'; +jest.mock('../../../../common/lib/kibana'); + jest.mock('../body/renderers/get_column_renderer'); const getColumnRendererMock = getColumnRenderer as jest.Mock; const mockImplementation = { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/index.tsx index 03e444e3a9afd..2848a850a5227 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/cell_rendering/index.tsx @@ -5,16 +5,4 @@ * 2.0. */ -import { EuiDataGridCellValueElementProps } from '@elastic/eui'; - -import { TimelineNonEcsData } from '../../../../../common/search_strategy/timeline'; -import { ColumnHeaderOptions } from '../../../store/timeline/model'; - -/** The following props are provided to the function called by `renderCellValue` */ -export type CellValueElementProps = EuiDataGridCellValueElementProps & { - data: TimelineNonEcsData[]; - eventId: string; // _id - header: ColumnHeaderOptions; - linkValues: string[] | undefined; - timelineId: string; -}; +export { CellValueElementProps } from '../../../../../common/types/timeline'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.test.tsx index 35595de646126..ef04c1177dcd6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.test.tsx @@ -11,11 +11,6 @@ import { TestProviders } from '../../../../common/mock/test_providers'; import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { DataProviders } from '.'; -import { ManageGlobalTimeline, getTimelineDefaults } from '../../manage_timeline'; -import { FilterManager } from '../../../../../../../../src/plugins/data/public/query/filter_manager'; -import { coreMock } from '../../../../../../../../src/core/public/mocks'; - -const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; jest.mock('../../../../common/hooks/use_selector', () => { const actual = jest.requireActual('../../../../common/hooks/use_selector'); @@ -25,7 +20,6 @@ jest.mock('../../../../common/hooks/use_selector', () => { }; }); -const filterManager = new FilterManager(mockUiSettingsForFilterManager); describe('DataProviders', () => { const mount = useMountAppended(); @@ -33,17 +27,9 @@ describe('DataProviders', () => { const dropMessage = ['Drop', 'query', 'build', 'here']; test('renders correctly against snapshot', () => { - const manageTimelineForTesting = { - foo: { - ...getTimelineDefaults('foo'), - filterManager, - }, - }; const wrapper = mount( <TestProviders> - <ManageGlobalTimeline manageTimelineForTesting={manageTimelineForTesting}> - <DataProviders data-test-subj="dataProviders-container" timelineId="foo" /> - </ManageGlobalTimeline> + <DataProviders data-test-subj="dataProviders-container" timelineId="foo" /> </TestProviders> ); expect(wrapper.find(`[data-test-subj="dataProviders-container"]`)).toBeTruthy(); @@ -73,19 +59,10 @@ describe('DataProviders', () => { }); describe('resizable drop target', () => { - const manageTimelineForTesting = { - foo: { - ...getTimelineDefaults('test'), - filterManager, - }, - }; - test('it may be resized vertically via a resize handle', () => { const wrapper = mount( <TestProviders> - <ManageGlobalTimeline manageTimelineForTesting={manageTimelineForTesting}> - <DataProviders timelineId="test" /> - </ManageGlobalTimeline> + <DataProviders timelineId="test" /> </TestProviders> ); @@ -98,9 +75,7 @@ describe('DataProviders', () => { test('it never grows taller than one third (33%) of the view height', () => { const wrapper = mount( <TestProviders> - <ManageGlobalTimeline manageTimelineForTesting={manageTimelineForTesting}> - <DataProviders timelineId="test" /> - </ManageGlobalTimeline> + <DataProviders timelineId="test" /> </TestProviders> ); @@ -113,9 +88,7 @@ describe('DataProviders', () => { test('it automatically displays scroll bars when the width or height of the data providers exceeds the drop target', () => { const wrapper = mount( <TestProviders> - <ManageGlobalTimeline manageTimelineForTesting={manageTimelineForTesting}> - <DataProviders timelineId="test" /> - </ManageGlobalTimeline> + <DataProviders timelineId="test" /> </TestProviders> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx index bdc0327026488..f642ec35d4306 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx @@ -9,19 +9,16 @@ import { rgba } from 'polished'; import React, { useMemo } from 'react'; import styled from 'styled-components'; import uuid from 'uuid'; +import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; import { useSourcererScope } from '../../../../common/containers/sourcerer'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { DroppableWrapper } from '../../../../common/components/drag_and_drop/droppable_wrapper'; -import { - droppableTimelineProvidersPrefix, - IS_DRAGGING_CLASS_NAME, -} from '../../../../common/components/drag_and_drop/helpers'; +import { droppableTimelineProvidersPrefix } from '../../../../common/components/drag_and_drop/helpers'; import { Empty } from './empty'; import { Providers } from './providers'; -import { useManageTimeline } from '../../manage_timeline'; import { timelineSelectors } from '../../../store/timeline'; import { timelineDefaults } from '../../../store/timeline/defaults'; @@ -89,11 +86,8 @@ const getDroppableId = (id: string): string => */ export const DataProviders = React.memo<Props>(({ timelineId }) => { const { browserFields } = useSourcererScope(SourcererScopeName.timeline); - const { getManageTimelineById } = useManageTimeline(); - const isLoading = useMemo(() => getManageTimelineById(timelineId).isLoading, [ - getManageTimelineById, - timelineId, - ]); + const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []); + const { isLoading } = useDeepEqualSelector((state) => getManageTimeline(state, timelineId)); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const dataProviders = useDeepEqualSelector( (state) => (getTimeline(state, timelineId) ?? timelineDefaults).dataProviders diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx index a3693d5ba2001..e5e5ad5f010fc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx @@ -11,7 +11,10 @@ import { useDispatch } from 'react-redux'; import { TimelineType } from '../../../../../common/types/timeline'; import { BrowserFields } from '../../../../common/containers/source'; -import { useShallowEqualSelector } from '../../../../common/hooks/use_selector'; +import { + useDeepEqualSelector, + useShallowEqualSelector, +} from '../../../../common/hooks/use_selector'; import { timelineSelectors } from '../../../store/timeline'; import { OnDataProviderEdited } from '../events'; @@ -19,7 +22,6 @@ import { ProviderBadge } from './provider_badge'; import { ProviderItemActions } from './provider_item_actions'; import { DataProvidersAnd, DataProviderType, QueryOperator } from './data_provider'; import { dragAndDropActions } from '../../../../common/store/drag_and_drop'; -import { useManageTimeline } from '../../manage_timeline'; interface ProviderItemBadgeProps { andProviderId?: string; @@ -75,11 +77,10 @@ export const ProviderItemBadge = React.memo<ProviderItemBadgeProps>( return getTimeline(state, timelineId)?.timelineType ?? TimelineType.default; }); - const { getManageTimelineById } = useManageTimeline(); - const isLoading = useMemo(() => getManageTimelineById(timelineId ?? '').isLoading, [ - getManageTimelineById, - timelineId, - ]); + const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []); + const { isLoading } = useDeepEqualSelector((state) => + getManageTimeline(state, timelineId ?? '') + ); const togglePopover = useCallback(() => { setIsPopoverOpen(!isPopoverOpen); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx index 7f2133aca7348..a2a91c206521a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx @@ -8,36 +8,30 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { coreMock } from '../../../../../../../../src/core/public/mocks'; import { TestProviders } from '../../../../common/mock/test_providers'; import { DroppableWrapper } from '../../../../common/components/drag_and_drop/droppable_wrapper'; -import { FilterManager } from '../../../../../../../../src/plugins/data/public'; import { timelineActions } from '../../../store/timeline'; import { mockDataProviders } from './mock/mock_data_providers'; import { Providers } from './providers'; import { DELETE_CLASS_NAME, ENABLE_CLASS_NAME, EXCLUDE_CLASS_NAME } from './provider_item_actions'; import { useMountAppended } from '../../../../common/utils/use_mount_appended'; -import { ManageGlobalTimeline, getTimelineDefaults } from '../../manage_timeline'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; -const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; +jest.mock('../../../../common/lib/kibana'); + +jest.mock('../../../../common/hooks/use_selector', () => ({ + useShallowEqualSelector: jest.fn(), + useDeepEqualSelector: jest.fn(), +})); describe('Providers', () => { - const isLoading: boolean = true; const mount = useMountAppended(); - const filterManager = new FilterManager(mockUiSettingsForFilterManager); const mockOnDataProviderRemoved = jest.spyOn(timelineActions, 'removeProvider'); - const manageTimelineForTesting = { - test: { - ...getTimelineDefaults('test'), - filterManager, - isLoading, - }, - }; - beforeEach(() => { jest.clearAllMocks(); + (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: false }); }); describe('rendering', () => { @@ -82,13 +76,12 @@ describe('Providers', () => { }); test('while loading data, it does NOT invoke the onDataProviderRemoved callback when the close button is clicked', () => { + (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true }); const wrapper = mount( <TestProviders> - <ManageGlobalTimeline manageTimelineForTesting={manageTimelineForTesting}> - <DroppableWrapper droppableId="unitTest"> - <Providers browserFields={{}} dataProviders={mockDataProviders} timelineId="test" /> - </DroppableWrapper> - </ManageGlobalTimeline> + <DroppableWrapper droppableId="unitTest"> + <Providers browserFields={{}} dataProviders={mockDataProviders} timelineId="test" /> + </DroppableWrapper> </TestProviders> ); @@ -120,13 +113,12 @@ describe('Providers', () => { }); test('while loading data, it does NOT invoke the onDataProviderRemoved callback when you click on the option "Delete" in the provider menu', () => { + (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true }); const wrapper = mount( <TestProviders> - <ManageGlobalTimeline manageTimelineForTesting={manageTimelineForTesting}> - <DroppableWrapper droppableId="unitTest"> - <Providers browserFields={{}} dataProviders={mockDataProviders} timelineId="test" /> - </DroppableWrapper> - </ManageGlobalTimeline> + <DroppableWrapper droppableId="unitTest"> + <Providers browserFields={{}} dataProviders={mockDataProviders} timelineId="test" /> + </DroppableWrapper> </TestProviders> ); wrapper.find('button[data-test-subj="providerBadge"]').first().simulate('click'); @@ -172,17 +164,16 @@ describe('Providers', () => { }); test('while loading data, it does NOT invoke the onToggleDataProviderEnabled callback when you click on the option "Temporary disable" in the provider menu', () => { + (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true }); const mockOnToggleDataProviderEnabled = jest.spyOn( timelineActions, 'updateDataProviderEnabled' ); const wrapper = mount( <TestProviders> - <ManageGlobalTimeline manageTimelineForTesting={manageTimelineForTesting}> - <DroppableWrapper droppableId="unitTest"> - <Providers browserFields={{}} dataProviders={mockDataProviders} timelineId="test" /> - </DroppableWrapper> - </ManageGlobalTimeline> + <DroppableWrapper droppableId="unitTest"> + <Providers browserFields={{}} dataProviders={mockDataProviders} timelineId="test" /> + </DroppableWrapper> </TestProviders> ); @@ -231,6 +222,7 @@ describe('Providers', () => { }); test('while loading data, it does NOT invoke the onToggleDataProviderExcluded callback when you click on the option "Exclude results" in the provider menu', () => { + (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true }); const mockOnToggleDataProviderExcluded = jest.spyOn( timelineActions, 'updateDataProviderExcluded' @@ -238,11 +230,9 @@ describe('Providers', () => { const wrapper = mount( <TestProviders> - <ManageGlobalTimeline manageTimelineForTesting={manageTimelineForTesting}> - <DroppableWrapper droppableId="unitTest"> - <Providers browserFields={{}} dataProviders={mockDataProviders} timelineId="test" /> - </DroppableWrapper> - </ManageGlobalTimeline> + <DroppableWrapper droppableId="unitTest"> + <Providers browserFields={{}} dataProviders={mockDataProviders} timelineId="test" /> + </DroppableWrapper> </TestProviders> ); @@ -311,16 +301,15 @@ describe('Providers', () => { }); test('while loading data, it does NOT invoke the onDataProviderRemoved callback when you click on the close button is clicked', () => { + (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true }); const dataProviders = mockDataProviders.slice(0, 1); dataProviders[0].and = mockDataProviders.slice(1, 3); const wrapper = mount( <TestProviders> - <ManageGlobalTimeline manageTimelineForTesting={manageTimelineForTesting}> - <DroppableWrapper droppableId="unitTest"> - <Providers browserFields={{}} dataProviders={mockDataProviders} timelineId="test" /> - </DroppableWrapper> - </ManageGlobalTimeline> + <DroppableWrapper droppableId="unitTest"> + <Providers browserFields={{}} dataProviders={mockDataProviders} timelineId="test" /> + </DroppableWrapper> </TestProviders> ); @@ -375,6 +364,7 @@ describe('Providers', () => { }); test('while loading data, it does NOT invoke the onToggleDataProviderEnabled callback when you click on the option "Temporary disable" in the provider menu', () => { + (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true }); const dataProviders = mockDataProviders.slice(0, 1); dataProviders[0].and = mockDataProviders.slice(1, 3); const mockOnToggleDataProviderEnabled = jest.spyOn( @@ -384,11 +374,9 @@ describe('Providers', () => { const wrapper = mount( <TestProviders> - <ManageGlobalTimeline manageTimelineForTesting={manageTimelineForTesting}> - <DroppableWrapper droppableId="unitTest"> - <Providers browserFields={{}} dataProviders={dataProviders} timelineId="test" /> - </DroppableWrapper> - </ManageGlobalTimeline> + <DroppableWrapper droppableId="unitTest"> + <Providers browserFields={{}} dataProviders={dataProviders} timelineId="test" /> + </DroppableWrapper> </TestProviders> ); @@ -448,6 +436,7 @@ describe('Providers', () => { }); test('while loading data, it does NOT invoke the onToggleDataProviderExcluded callback when you click on the option "Exclude results" in the provider menu', () => { + (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true }); const dataProviders = mockDataProviders.slice(0, 1); dataProviders[0].and = mockDataProviders.slice(1, 3); const mockOnToggleDataProviderExcluded = jest.spyOn( @@ -457,11 +446,9 @@ describe('Providers', () => { const wrapper = mount( <TestProviders> - <ManageGlobalTimeline manageTimelineForTesting={manageTimelineForTesting}> - <DroppableWrapper droppableId="unitTest"> - <Providers browserFields={{}} dataProviders={dataProviders} timelineId="test" /> - </DroppableWrapper> - </ManageGlobalTimeline> + <DroppableWrapper droppableId="unitTest"> + <Providers browserFields={{}} dataProviders={dataProviders} timelineId="test" /> + </DroppableWrapper> </TestProviders> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx index d7436d2b891b8..5b982e4e831f7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx @@ -13,24 +13,25 @@ import styled from 'styled-components'; import { useDispatch } from 'react-redux'; import deepEqual from 'fast-deep-equal'; +import { + DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME, + IS_DRAGGING_CLASS_NAME, +} from '@kbn/securitysolution-t-grid'; import { timelineActions } from '../../../store/timeline'; import { AndOrBadge } from '../../../../common/components/and_or_badge'; -import { useDraggableKeyboardWrapper } from '../../../../common/components/drag_and_drop/draggable_keyboard_wrapper_hook'; import { AddDataProviderPopover } from './add_data_provider_popover'; import { BrowserFields } from '../../../../common/containers/source'; import { - DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME, getTimelineProviderDraggableId, getTimelineProviderDroppableId, - IS_DRAGGING_CLASS_NAME, } from '../../../../common/components/drag_and_drop/helpers'; - import { DataProvider, DataProviderType, DataProvidersAnd, IS_OPERATOR } from './data_provider'; import { EMPTY_GROUP, flattenIntoAndGroups } from './helpers'; import { ProviderItemBadge } from './provider_item_badge'; import * as i18n from './translations'; +import { useKibana } from '../../../../common/lib/kibana'; export const EMPTY_PROVIDERS_GROUP_CLASS_NAME = 'empty-providers-group'; @@ -159,6 +160,7 @@ export const DataProvidersGroupItem = React.memo<DataProvidersGroupItem>( const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [, setClosePopOverTrigger] = useState(false); const dispatch = useDispatch(); + const { timelines } = useKibana().services; const handleClosePopOverTrigger = useCallback(() => { setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger); @@ -244,7 +246,7 @@ export const DataProvidersGroupItem = React.memo<DataProvidersGroupItem>( setIsPopoverOpen(true); }, []); - const { onBlur, onKeyDown } = useDraggableKeyboardWrapper({ + const { onBlur, onKeyDown } = timelines.getUseDraggableKeyboardWrapper()({ closePopover: handleClosePopOverTrigger, draggableId, fieldName: dataProvider.queryMatch.field, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx index e13bed1e2eff6..5f08bf5a016f5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.test.tsx @@ -22,6 +22,7 @@ import { useTimelineEvents } from '../../../containers/index'; import { useTimelineEventsDetails } from '../../../containers/details/index'; import { useSourcererScope } from '../../../../common/containers/sourcerer'; import { mockSourcererScope } from '../../../../common/containers/sourcerer/mocks'; +import { useDraggableKeyboardWrapper as mockUseDraggableKeyboardWrapper } from '../../../../../../timelines/public/components'; jest.mock('../../../containers/index', () => ({ useTimelineEvents: jest.fn(), @@ -57,6 +58,10 @@ jest.mock('../../../../common/lib/kibana', () => { savedObjects: { client: {}, }, + timelines: { + getLastUpdated: jest.fn(), + getUseDraggableKeyboardWrapper: () => mockUseDraggableKeyboardWrapper, + }, }, }), useGetUserSavedObjectPermissions: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx index bb2a995ff9fae..b67b9348f51aa 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx @@ -17,7 +17,7 @@ import { isEmpty } from 'lodash/fp'; import React, { useEffect, useCallback } from 'react'; import styled from 'styled-components'; import { Dispatch } from 'redux'; -import { connect, ConnectedProps } from 'react-redux'; +import { connect, ConnectedProps, useDispatch } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import { InPortal } from 'react-reverse-portal'; @@ -27,12 +27,17 @@ import { TimelineItem } from '../../../../../common/search_strategy'; import { useTimelineEvents } from '../../../containers/index'; import { defaultHeaders } from '../body/column_headers/default_headers'; import { StatefulBody } from '../body'; -import { RowRenderer } from '../body/renderers/row_renderer'; import { Footer, footerHeight } from '../footer'; import { calculateTotalPages } from '../helpers'; import { TimelineRefetch } from '../refetch_timeline'; -import { useManageTimeline } from '../../manage_timeline'; -import { TimelineEventsType, TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; +import { + ControlColumnProps, + RowRenderer, + TimelineEventsType, + TimelineId, + TimelineTabs, + ToggleDetailPanel, +} from '../../../../../common/types/timeline'; import { requiredFieldsForActions } from '../../../../detections/components/alerts_table/default_config'; import { ExitFullScreen } from '../../../../common/components/exit_full_screen'; import { SuperDatePicker } from '../../../../common/components/super_date_picker'; @@ -48,10 +53,9 @@ import { TimelineModel } from '../../../../timelines/store/timeline/model'; import { TimelineDatePickerLock } from '../date_picker_lock'; import { useTimelineFullScreen } from '../../../../common/containers/use_full_screen'; import { activeTimeline } from '../../../containers/active_timeline_context'; -import { ToggleDetailPanel } from '../../../store/timeline/actions'; import { DetailsPanel } from '../../side_panel'; import { EqlQueryBarTimeline } from '../query_bar/eql'; -import { defaultControlColumn, ControlColumnProps } from '../body/control_columns'; +import { defaultControlColumn } from '../body/control_columns'; import { Sort } from '../body/sort'; const TimelineHeaderContainer = styled.div` @@ -166,6 +170,7 @@ export const EqlTabContentComponent: React.FC<Props> = ({ timerangeKind, updateEventTypeAndIndexesName, }) => { + const dispatch = useDispatch(); const { query: eqlQuery = '', ...restEqlOption } = eqlOptions; const { portalNode: eqlEventsCountPortalNode } = useEqlEventsCountPortal(); const { setTimelineFullScreen, timelineFullScreen } = useTimelineFullScreen(); @@ -192,12 +197,13 @@ export const EqlTabContentComponent: React.FC<Props> = ({ return [...columnFields, ...requiredFieldsForActions]; }; - const { initializeTimeline, setIsTimelineLoading } = useManageTimeline(); useEffect(() => { - initializeTimeline({ - id: timelineId, - }); - }, [initializeTimeline, timelineId]); + dispatch( + timelineActions.initializeTGridSettings({ + id: timelineId, + }) + ); + }, [dispatch, timelineId]); const [ isQueryLoading, @@ -230,8 +236,13 @@ export const EqlTabContentComponent: React.FC<Props> = ({ }, [onEventClosed, timelineId, expandedDetail, showExpandedDetails]); useEffect(() => { - setIsTimelineLoading({ id: timelineId, isLoading: isQueryLoading || loadingSourcerer }); - }, [loadingSourcerer, timelineId, isQueryLoading, setIsTimelineLoading]); + dispatch( + timelineActions.updateIsLoading({ + id: timelineId, + isLoading: isQueryLoading || loadingSourcerer, + }) + ); + }, [loadingSourcerer, timelineId, isQueryLoading, dispatch]); const leadingControlColumns: ControlColumnProps[] = [defaultControlColumn]; const trailingControlColumns: ControlColumnProps[] = []; @@ -385,7 +396,6 @@ const makeMapStateToProps = () => { }; return mapStateToProps; }; - const mapDispatchToProps = (dispatch: Dispatch, { timelineId }: OwnProps) => ({ updateEventTypeAndIndexesName: (newEventType: TimelineEventsType, newIndexNames: string[]) => { dispatch(timelineActions.updateEventType({ id: timelineId, eventType: newEventType })); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts index 21e213b799535..ca7c3596d13bb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/events.ts @@ -5,10 +5,20 @@ * 2.0. */ -import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { ColumnId } from './body/column_id'; -import { SortDirection } from './body/sort'; import { DataProvider, QueryOperator } from './data_providers/data_provider'; +export { + OnColumnSorted, + OnColumnsSorted, + OnColumnRemoved, + OnColumnResized, + OnChangePage, + OnPinEvent, + OnRowSelected, + OnSelectAll, + OnUnPinEvent, + OnUpdateColumns, +} from '../../../../common/types/timeline'; export type OnDataProviderEdited = ({ andProviderId, @@ -35,38 +45,3 @@ export type OnRangeSelected = (range: string) => void; /** Invoked when a user updates a column's filter */ export type OnFilterChange = (filter: { columnId: ColumnId; filter: string }) => void; - -/** Invoked when a column is sorted */ -export type OnColumnSorted = (sorted: { columnId: ColumnId; sortDirection: SortDirection }) => void; - -export type OnColumnsSorted = ( - sorted: Array<{ columnId: ColumnId; sortDirection: SortDirection }> -) => void; - -export type OnColumnRemoved = (columnId: ColumnId) => void; - -export type OnColumnResized = ({ columnId, delta }: { columnId: ColumnId; delta: number }) => void; - -/** Invoked when a user clicks to load more item */ -export type OnChangePage = (nextPage: number) => void; - -/** Invoked when a user pins an event */ -export type OnPinEvent = (eventId: string) => void; - -/** Invoked when a user checks/un-checks a row */ -export type OnRowSelected = ({ - eventIds, - isSelected, -}: { - eventIds: string[]; - isSelected: boolean; -}) => void; - -/** Invoked when a user checks/un-checks the select all checkbox */ -export type OnSelectAll = ({ isSelected }: { isSelected: boolean }) => void; - -/** Invoked when columns are updated */ -export type OnUpdateColumns = (columns: ColumnHeaderOptions[]) => void; - -/** Invoked when a user unpins an event */ -export type OnUnPinEvent = (eventId: string) => void; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.test.tsx index f0a14e990e1cc..cf8d51546a899 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.test.tsx @@ -12,6 +12,8 @@ import { TestProviders } from '../../../../common/mock/test_providers'; import { FooterComponent, PagingControlComponent } from './index'; +jest.mock('../../../../common/lib/kibana'); + describe('Footer Timeline Component', () => { const loadMore = jest.fn(); const updatedAt = 1546878704036; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx index 4c5432f686c93..ac6f6e52db1e2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx @@ -24,15 +24,14 @@ import React, { FC, useCallback, useEffect, useState, useMemo } from 'react'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; -import { LoadingPanel } from '../../loading'; import { OnChangePage } from '../events'; import { EVENTS_COUNT_BUTTON_CLASS_NAME } from '../helpers'; import * as i18n from './translations'; import { useEventDetailsWidthContext } from '../../../../common/components/events_viewer/event_details_width_context'; -import { useManageTimeline } from '../../manage_timeline'; -import { LastUpdatedAt } from '../../../../common/components/last_updated'; -import { timelineActions } from '../../../store/timeline'; +import { timelineActions, timelineSelectors } from '../../../store/timeline'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { useKibana } from '../../../../common/lib/kibana'; export const isCompactFooter = (width: number): boolean => width < 600; @@ -42,12 +41,13 @@ interface FixedWidthLastUpdatedContainerProps { const FixedWidthLastUpdatedContainer = React.memo<FixedWidthLastUpdatedContainerProps>( ({ updatedAt }) => { + const { timelines } = useKibana().services; const width = useEventDetailsWidthContext(); const compact = useMemo(() => isCompactFooter(width), [width]); return ( <FixedWidthLastUpdated data-test-subj="fixed-width-last-updated" compact={compact}> - <LastUpdatedAt updatedAt={updatedAt} compact={compact} /> + {timelines.getLastUpdated({ updatedAt, compact })} </FixedWidthLastUpdated> ); } @@ -259,14 +259,16 @@ export const FooterComponent = ({ totalCount, }: FooterProps) => { const dispatch = useDispatch(); + const { timelines } = useKibana().services; const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [paginationLoading, setPaginationLoading] = useState(false); - const { getManageTimelineById } = useManageTimeline(); - const { documentType, loadingText, footerText } = useMemo(() => getManageTimelineById(id), [ - getManageTimelineById, - id, - ]); + const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []); + const { + documentType = i18n.TOTAL_COUNT_OF_EVENTS, + loadingText = i18n.LOADING_EVENTS, + footerText = i18n.TOTAL_COUNT_OF_EVENTS, + } = useDeepEqualSelector((state) => getManageTimeline(state, id)); const handleChangePageClick = useCallback( (nextPage: number) => { @@ -322,13 +324,13 @@ export const FooterComponent = ({ if (isLoading && !paginationLoading) { return ( <LoadingPanelContainer> - <LoadingPanel - data-test-subj="LoadingPanelTimeline" - height="35px" - showBorder={false} - text={`${loadingText}...`} - width="100%" - /> + {timelines.getLoadingPanel({ + dataTestSubj: 'LoadingPanelTimeline', + height: '35px', + showBorder: false, + text: loadingText, + width: '100%', + })} </LoadingPanelContainer> ); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/translations.ts index fa8a8b743646d..6736573cac293 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/translations.ts @@ -43,3 +43,10 @@ export const AUTO_REFRESH_ACTIVE = i18n.translate( defaultMessage: 'Auto-Refresh Active', } ); + +export const LOADING_EVENTS = i18n.translate( + 'xpack.securitySolution.footer.loadingEventsDataLabel', + { + defaultMessage: 'Loading Events', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx index 0093ce2f95bdd..f2a4071111602 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx @@ -14,7 +14,7 @@ import { getFocusedAriaColindexCell, getTableSkipFocus, stopPropagationAndPreventDefault, -} from '../../../common/components/accessibility/helpers'; +} from '../../../../../timelines/public'; import { escapeQueryValue, convertToBuildEsQuery } from '../../../common/lib/keury'; import { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index 5e86bf8d75385..e95efdf754418 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -11,16 +11,15 @@ import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; +import { isTab } from '../../../../../timelines/public'; import { timelineActions, timelineSelectors } from '../../store/timeline'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { defaultHeaders } from './body/column_headers/default_headers'; -import { RowRenderer } from './body/renderers/row_renderer'; import { CellValueElementProps } from './cell_rendering'; -import { isTab } from '../../../common/components/accessibility/helpers'; import { useSourcererScope } from '../../../common/containers/sourcerer'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { FlyoutHeader, FlyoutHeaderPanel } from '../flyout/header'; -import { TimelineType, TimelineId } from '../../../../common/types/timeline'; +import { TimelineType, TimelineId, RowRenderer } from '../../../../common/types/timeline'; import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector'; import { activeTimeline } from '../../containers/active_timeline_context'; import { EVENTS_COUNT_BUTTON_CLASS_NAME, onTimelineTabKeyPressed } from './helpers'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx index 0f781b0958d02..f4d5570ce40d3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.test.tsx @@ -23,6 +23,7 @@ import { useSourcererScope } from '../../../../common/containers/sourcerer'; import { mockSourcererScope } from '../../../../common/containers/sourcerer/mocks'; import { PinnedTabContentComponent, Props as PinnedTabContentComponentProps } from '.'; import { Direction } from '../../../../../common/search_strategy'; +import { useDraggableKeyboardWrapper as mockUseDraggableKeyboardWrapper } from '../../../../../../timelines/public/components'; jest.mock('../../../containers/index', () => ({ useTimelineEvents: jest.fn(), @@ -57,6 +58,10 @@ jest.mock('../../../../common/lib/kibana', () => { savedObjects: { client: {}, }, + timelines: { + getLastUpdated: jest.fn(), + getUseDraggableKeyboardWrapper: () => mockUseDraggableKeyboardWrapper, + }, }, }), useGetUserSavedObjectPermissions: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx index c01cf5c8aa0f0..b5e3d853bc81c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pinned_tab_content/index.tsx @@ -19,7 +19,6 @@ import { Direction } from '../../../../../common/search_strategy'; import { useTimelineEvents } from '../../../containers/index'; import { defaultHeaders } from '../body/column_headers/default_headers'; import { StatefulBody } from '../body'; -import { RowRenderer } from '../body/renderers/row_renderer'; import { Footer, footerHeight } from '../footer'; import { requiredFieldsForActions } from '../../../../detections/components/alerts_table/default_config'; import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context'; @@ -29,14 +28,18 @@ import { timelineDefaults } from '../../../store/timeline/defaults'; import { useSourcererScope } from '../../../../common/containers/sourcerer'; import { useTimelineFullScreen } from '../../../../common/containers/use_full_screen'; import { TimelineModel } from '../../../store/timeline/model'; -import { ToggleDetailPanel } from '../../../store/timeline/actions'; import { State } from '../../../../common/store'; import { calculateTotalPages } from '../helpers'; -import { TimelineTabs } from '../../../../../common/types/timeline'; +import { + ControlColumnProps, + RowRenderer, + TimelineTabs, + ToggleDetailPanel, +} from '../../../../../common/types/timeline'; import { DetailsPanel } from '../../side_panel'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { ExitFullScreen } from '../../../../common/components/exit_full_screen'; -import { defaultControlColumn, ControlColumnProps } from '../body/control_columns'; +import { defaultControlColumn } from '../body/control_columns'; const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` overflow-y: hidden; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx index 8790d8c98c161..b2b304e16c4a0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx @@ -22,7 +22,6 @@ import { SavedQueryTimeFilter, } from '../../../../../../../../src/plugins/data/public'; import { convertKueryToElasticSearchQuery } from '../../../../common/lib/keury'; -import { KueryFilterQuery, KueryFilterQueryKind } from '../../../../common/store'; import { KqlMode } from '../../../../timelines/store/timeline/model'; import { useSavedQueryServices } from '../../../../common/utils/saved_query_services'; import { DispatchUpdateReduxTime } from '../../../../common/components/super_date_picker'; @@ -30,6 +29,7 @@ import { QueryBar } from '../../../../common/components/query_bar'; import { DataProvider } from '../data_providers/data_provider'; import { buildGlobalQuery } from '../helpers'; import { timelineActions } from '../../../store/timeline'; +import { KueryFilterQuery, KueryFilterQueryKind } from '../../../../../common/types/timeline'; export interface QueryBarTimelineComponentProps { dataProviders: DataProvider[]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx index acae8c8c53cd0..9bf7ee28f3934 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx @@ -59,6 +59,15 @@ jest.mock('../../../../common/lib/kibana', () => { savedObjects: { client: {}, }, + timelines: { + getLastUpdated: jest.fn(), + getLoadingPanel: jest.fn(), + getUseDraggableKeyboardWrapper: () => + jest.fn().mockReturnValue({ + onBlur: jest.fn(), + onKeyDown: jest.fn(), + }), + }, }, }), useGetUserSavedObjectPermissions: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx index 4298f2ff74517..6f0bbd026cd7b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx @@ -17,12 +17,11 @@ import { isEmpty } from 'lodash/fp'; import React, { useState, useMemo, useEffect, useCallback } from 'react'; import styled from 'styled-components'; import { Dispatch } from 'redux'; -import { connect, ConnectedProps } from 'react-redux'; +import { connect, ConnectedProps, useDispatch } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import { InPortal } from 'react-reverse-portal'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; -import { RowRenderer } from '../body/renderers/row_renderer'; import { CellValueElementProps } from '../cell_rendering'; import { Direction, TimelineItem } from '../../../../../common/search_strategy'; import { useTimelineEvents } from '../../../containers/index'; @@ -34,18 +33,20 @@ import { TimelineHeader } from '../header'; import { calculateTotalPages, combineQueries } from '../helpers'; import { TimelineRefetch } from '../refetch_timeline'; import { esQuery, FilterManager } from '../../../../../../../../src/plugins/data/public'; -import { useManageTimeline } from '../../manage_timeline'; -import { TimelineEventsType, TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; +import { + ControlColumnProps, + KueryFilterQueryKind, + RowRenderer, + TimelineEventsType, + TimelineId, + TimelineTabs, + ToggleDetailPanel, +} from '../../../../../common/types/timeline'; import { requiredFieldsForActions } from '../../../../detections/components/alerts_table/default_config'; import { SuperDatePicker } from '../../../../common/components/super_date_picker'; import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context'; import { PickEventType } from '../search_or_filter/pick_events'; -import { - inputsModel, - inputsSelectors, - KueryFilterQueryKind, - State, -} from '../../../../common/store'; +import { inputsModel, inputsSelectors, State } from '../../../../common/store'; import { sourcererActions } from '../../../../common/store/sourcerer'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; import { timelineDefaults } from '../../../../timelines/store/timeline/defaults'; @@ -55,10 +56,9 @@ import { TimelineModel } from '../../../../timelines/store/timeline/model'; import { TimelineDatePickerLock } from '../date_picker_lock'; import { useTimelineFullScreen } from '../../../../common/containers/use_full_screen'; import { activeTimeline } from '../../../containers/active_timeline_context'; -import { ToggleDetailPanel } from '../../../store/timeline/actions'; import { DetailsPanel } from '../../side_panel'; import { ExitFullScreen } from '../../../../common/components/exit_full_screen'; -import { defaultControlColumn, ControlColumnProps } from '../body/control_columns'; +import { defaultControlColumn } from '../body/control_columns'; const TimelineHeaderContainer = styled.div` margin-top: 6px; @@ -180,6 +180,7 @@ export const QueryTabContentComponent: React.FC<Props> = ({ timerangeKind, updateEventTypeAndIndexesName, }) => { + const dispatch = useDispatch(); const { portalNode: timelineEventsCountPortalNode } = useTimelineEventsCountPortal(); const { setTimelineFullScreen, timelineFullScreen } = useTimelineFullScreen(); const { @@ -231,13 +232,14 @@ export const QueryTabContentComponent: React.FC<Props> = ({ type: columnType, })); - const { initializeTimeline, setIsTimelineLoading } = useManageTimeline(); useEffect(() => { - initializeTimeline({ - filterManager, - id: timelineId, - }); - }, [initializeTimeline, filterManager, timelineId]); + dispatch( + timelineActions.initializeTGridSettings({ + filterManager, + id: timelineId, + }) + ); + }, [filterManager, timelineId, dispatch]); const [ isQueryLoading, @@ -270,8 +272,13 @@ export const QueryTabContentComponent: React.FC<Props> = ({ }, [onEventClosed, timelineId, expandedDetail, showExpandedDetails]); useEffect(() => { - setIsTimelineLoading({ id: timelineId, isLoading: isQueryLoading || loadingSourcerer }); - }, [loadingSourcerer, timelineId, isQueryLoading, setIsTimelineLoading]); + dispatch( + timelineActions.updateIsLoading({ + id: timelineId, + isLoading: isQueryLoading || loadingSourcerer, + }) + ); + }, [loadingSourcerer, timelineId, isQueryLoading, dispatch]); const leadingControlColumns: ControlColumnProps[] = [defaultControlColumn]; const trailingControlColumns: ControlColumnProps[] = []; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx index 4ea4f94abff63..33ab2e0049828 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx @@ -12,17 +12,13 @@ import { Dispatch } from 'redux'; import deepEqual from 'fast-deep-equal'; import { Filter, FilterManager } from '../../../../../../../../src/plugins/data/public'; -import { - SerializedFilterQuery, - State, - inputsModel, - inputsSelectors, -} from '../../../../common/store'; +import { State, inputsModel, inputsSelectors } from '../../../../common/store'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; import { KqlMode, TimelineModel } from '../../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../../timelines/store/timeline/defaults'; import { dispatchUpdateReduxTime } from '../../../../common/components/super_date_picker'; import { SearchOrFilter } from './search_or_filter'; +import { SerializedFilterQuery } from '../../../../../common/types/timeline'; interface OwnProps { filterManager: FilterManager; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx index 262709ed98e5a..f1c4b7c3ef089 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx @@ -10,9 +10,9 @@ import React, { useCallback } from 'react'; import styled, { createGlobalStyle } from 'styled-components'; import { Filter, FilterManager } from '../../../../../../../../src/plugins/data/public'; -import { KueryFilterQuery } from '../../../../common/store'; import { KqlMode } from '../../../../timelines/store/timeline/model'; import { DispatchUpdateReduxTime } from '../../../../common/components/super_date_picker'; +import { KueryFilterQuery } from '../../../../../common/types/timeline'; import { DataProvider } from '../data_providers/data_provider'; import { QueryBarTimeline } from '../query_bar'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx index 0c584a2d62efe..3514766b334a0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx @@ -8,9 +8,9 @@ import { EuiLoadingSpinner } from '@elastic/eui'; import { rgba } from 'polished'; import styled, { createGlobalStyle } from 'styled-components'; +import { IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid'; import { TimelineEventsType } from '../../../../common/types/timeline'; -import { IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME } from '../../../common/components/drag_and_drop/helpers'; import { ACTIONS_COLUMN_ARIA_COL_INDEX } from './helpers'; import { EVENTS_TABLE_ARIA_LABEL } from './translations'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx index adaa5f98c88c4..8cdd7722d7fbd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx @@ -10,7 +10,12 @@ import React, { lazy, memo, Suspense, useCallback, useEffect, useMemo } from 're import { useDispatch } from 'react-redux'; import styled from 'styled-components'; -import { TimelineTabs, TimelineId, TimelineType } from '../../../../../common/types/timeline'; +import { + RowRenderer, + TimelineTabs, + TimelineId, + TimelineType, +} from '../../../../../common/types/timeline'; import { useShallowEqualSelector, useDeepEqualSelector, @@ -20,7 +25,6 @@ import { TimelineEventsCountBadge, } from '../../../../common/hooks/use_timeline_events_count'; import { timelineActions } from '../../../store/timeline'; -import { RowRenderer } from '../body/renderers/row_renderer'; import { CellValueElementProps } from '../cell_rendering'; import { getActiveTabSelector, diff --git a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx index 37fdd5a444b2b..86624ba161a83 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx @@ -69,7 +69,7 @@ export const useTimelineEventsDetails = ({ .search<TimelineEventsDetailsRequestOptions, TimelineEventsDetailsStrategyResponse>( request, { - strategy: 'securitySolutionTimelineSearchStrategy', + strategy: 'timelineSearchStrategy', abortSignal: abortCtrl.current.signal, } ) diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index 17c107899d85a..00df0146e06d5 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -14,7 +14,7 @@ import { Subscription } from 'rxjs'; import { ESQuery } from '../../../common/typed_json'; import { isCompleteResponse, isErrorResponse } from '../../../../../../src/plugins/data/public'; import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; -import { inputsModel, KueryFilterQueryKind } from '../../common/store'; +import { inputsModel } from '../../common/store'; import { useKibana } from '../../common/lib/kibana'; import { createFilter } from '../../common/containers/helpers'; import { timelineActions } from '../../timelines/store/timeline'; @@ -33,7 +33,7 @@ import { } from '../../../common/search_strategy'; import { InspectResponse } from '../../types'; import * as i18n from './translations'; -import { TimelineId } from '../../../common/types/timeline'; +import { KueryFilterQueryKind, TimelineId } from '../../../common/types/timeline'; import { useRouteSpy } from '../../common/utils/route/use_route_spy'; import { activeTimeline } from './active_timeline_context'; import { @@ -214,9 +214,7 @@ export const useTimelineEvents = ({ searchSubscription$.current = data.search .search<TimelineRequest<typeof language>, TimelineResponse<typeof language>>(request, { strategy: - request.language === 'eql' - ? 'securitySolutionTimelineEqlSearchStrategy' - : 'securitySolutionTimelineSearchStrategy', + request.language === 'eql' ? 'timelineEqlSearchStrategy' : 'timelineSearchStrategy', abortSignal: abortCtrl.current.signal, }) .subscribe({ diff --git a/x-pack/plugins/security_solution/public/timelines/containers/kpis/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/kpis/index.tsx index 4a6eab13ba4f1..be93a13ab1c6a 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/kpis/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/kpis/index.tsx @@ -64,7 +64,7 @@ export const useTimelineKpis = ({ searchSubscription$.current = data.search .search<TimelineRequestBasicOptions, TimelineKpiStrategyResponse>(request, { - strategy: 'securitySolutionTimelineSearchStrategy', + strategy: 'timelineSearchStrategy', abortSignal: abortCtrl.current.signal, }) .subscribe({ diff --git a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx index 38eb6d3d222f8..99f45c7d9a4b4 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.tsx @@ -9,8 +9,8 @@ import { isEmpty } from 'lodash/fp'; import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; import { TimelinesStorage } from './types'; import { useKibana } from '../../../common/lib/kibana'; -import { ColumnHeaderOptions, TimelineModel } from '../../store/timeline/model'; -import { TimelineIdLiteral } from '../../../../common/types/timeline'; +import { TimelineModel } from '../../store/timeline/model'; +import { ColumnHeaderOptions, TimelineIdLiteral } from '../../../../common/types/timeline'; export const LOCAL_STORAGE_TIMELINE_KEY = 'timelines'; const EMPTY_TIMELINE = {} as { diff --git a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx index fe8650da7a090..5d2e45b638d59 100644 --- a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx +++ b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx @@ -12,7 +12,7 @@ import { useParams } from 'react-router-dom'; import { TimelineId, TimelineType } from '../../../common/types/timeline'; import { HeaderPage } from '../../common/components/header_page'; -import { WrapperPage } from '../../common/components/wrapper_page'; +import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; import { useKibana } from '../../common/lib/kibana'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { OverviewEmpty } from '../../overview/components/overview_empty'; @@ -45,8 +45,8 @@ export const TimelinesPageComponent: React.FC = () => { <> {indicesExist ? ( <> - <WrapperPage> - <HeaderPage border hideSourcerer={true} title={i18n.PAGE_TITLE}> + <SecuritySolutionPageWrapper> + <HeaderPage hideSourcerer={true} title={i18n.PAGE_TITLE}> <EuiFlexGroup gutterSize="s" alignItems="center"> <EuiFlexItem> {capabilitiesCanUserCRUD && ( @@ -89,13 +89,13 @@ export const TimelinesPageComponent: React.FC = () => { data-test-subj="stateful-open-timeline" /> </TimelinesContainer> - </WrapperPage> + </SecuritySolutionPageWrapper> </> ) : ( - <WrapperPage> + <SecuritySolutionPageWrapper> <HeaderPage hideSourcerer={true} border title={i18n.PAGE_TITLE} /> <OverviewEmpty /> - </WrapperPage> + </SecuritySolutionPageWrapper> )} <SpyRoute pageName={SecurityPageName.timelines} /> diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index 11e9a625d05d0..a3429c9247ffd 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -8,25 +8,42 @@ import actionCreatorFactory from 'typescript-fsa'; import { Filter } from '../../../../../../../src/plugins/data/public'; -import { Sort } from '../../../timelines/components/timeline/body/sort'; import { DataProvider, DataProviderType, QueryOperator, } from '../../../timelines/components/timeline/data_providers/data_provider'; -import { SerializedFilterQuery } from '../../../common/store/types'; -import { KqlMode, TimelineModel, ColumnHeaderOptions } from './model'; -import { FieldsEqlOptions, TimelineNonEcsData } from '../../../../common/search_strategy/timeline'; +import { KqlMode, TimelineModel } from './model'; +import { FieldsEqlOptions } from '../../../../common/search_strategy/timeline'; import { TimelineEventsType, - TimelineExpandedDetail, - TimelineExpandedDetailType, - TimelineTypeLiteral, RowRendererId, TimelineTabs, + TimelinePersistInput, + SerializedFilterQuery, } from '../../../../common/types/timeline'; import { InsertTimeline } from './types'; +import { tGridActions } from '../../../../../timelines/public'; +export const { + applyDeltaToColumnWidth, + clearEventsDeleted, + clearEventsLoading, + clearSelected, + initializeTGridSettings, + removeColumn, + setEventsDeleted, + setEventsLoading, + setSelected, + setTGridSelectAll, + toggleDetailPanel, + updateColumns, + updateIsLoading, + updateItemsPerPage, + updateItemsPerPageOptions, + updateSort, + upsertColumn, +} = tGridActions; const actionCreator = actionCreatorFactory('x-pack/security_solution/local/timeline'); @@ -38,62 +55,14 @@ export const addNoteToEvent = actionCreator<{ id: string; noteId: string; eventI 'ADD_NOTE_TO_EVENT' ); -export type ToggleDetailPanel = TimelineExpandedDetailType & { - tabType?: TimelineTabs; - timelineId: string; -}; - -export const toggleDetailPanel = actionCreator<ToggleDetailPanel>('TOGGLE_DETAIL_PANEL'); - -export const upsertColumn = actionCreator<{ - column: ColumnHeaderOptions; - id: string; - index: number; -}>('UPSERT_COLUMN'); - export const addProvider = actionCreator<{ id: string; provider: DataProvider }>('ADD_PROVIDER'); -export const applyDeltaToColumnWidth = actionCreator<{ - id: string; - columnId: string; - delta: number; -}>('APPLY_DELTA_TO_COLUMN_WIDTH'); - -export interface TimelineInput { - id: string; - dataProviders?: DataProvider[]; - dateRange?: { - start: string; - end: string; - }; - excludedRowRendererIds?: RowRendererId[]; - expandedDetail?: TimelineExpandedDetail; - filters?: Filter[]; - columns: ColumnHeaderOptions[]; - itemsPerPage?: number; - indexNames: string[]; - kqlQuery?: { - filterQuery: SerializedFilterQuery | null; - }; - show?: boolean; - sort?: Sort[]; - showCheckboxes?: boolean; - timelineType?: TimelineTypeLiteral; - templateTimelineId?: string | null; - templateTimelineVersion?: number | null; -} - -export const saveTimeline = actionCreator<TimelineInput>('SAVE_TIMELINE'); +export const saveTimeline = actionCreator<TimelinePersistInput>('SAVE_TIMELINE'); -export const createTimeline = actionCreator<TimelineInput>('CREATE_TIMELINE'); +export const createTimeline = actionCreator<TimelinePersistInput>('CREATE_TIMELINE'); export const pinEvent = actionCreator<{ id: string; eventId: string }>('PIN_EVENT'); -export const removeColumn = actionCreator<{ - id: string; - columnId: string; -}>('REMOVE_COLUMN'); - export const removeProvider = actionCreator<{ id: string; providerId: string; @@ -129,16 +98,6 @@ export const endTimelineSaving = actionCreator<{ id: string; }>('END_TIMELINE_SAVING'); -export const updateIsLoading = actionCreator<{ - id: string; - isLoading: boolean; -}>('UPDATE_LOADING'); - -export const updateColumns = actionCreator<{ - id: string; - columns: ColumnHeaderOptions[]; -}>('UPDATE_COLUMNS'); - export const updateDataProviderEnabled = actionCreator<{ id: string; enabled: boolean; @@ -189,15 +148,6 @@ export const updateIsFavorite = actionCreator<{ id: string; isFavorite: boolean export const updateIsLive = actionCreator<{ id: string; isLive: boolean }>('UPDATE_IS_LIVE'); -export const updateItemsPerPage = actionCreator<{ id: string; itemsPerPage: number }>( - 'UPDATE_ITEMS_PER_PAGE' -); - -export const updateItemsPerPageOptions = actionCreator<{ - id: string; - itemsPerPageOptions: number[]; -}>('UPDATE_ITEMS_PER_PAGE_OPTIONS'); - export const updateTitleAndDescription = actionCreator<{ description: string; id: string; @@ -216,8 +166,6 @@ export const updateRange = actionCreator<{ id: string; start: string; end: strin 'UPDATE_RANGE' ); -export const updateSort = actionCreator<{ id: string; sort: Sort[] }>('UPDATE_SORT'); - export const updateAutoSaveMsg = actionCreator<{ timelineId: string | null; newTimelineModel: TimelineModel | null; @@ -235,37 +183,6 @@ export const setFilters = actionCreator<{ filters: Filter[]; }>('SET_TIMELINE_FILTERS'); -export const setSelected = actionCreator<{ - id: string; - eventIds: Readonly<Record<string, TimelineNonEcsData[]>>; - isSelected: boolean; - isSelectAllChecked: boolean; -}>('SET_TIMELINE_SELECTED'); - -export const clearSelected = actionCreator<{ - id: string; -}>('CLEAR_TIMELINE_SELECTED'); - -export const setEventsLoading = actionCreator<{ - id: string; - eventIds: string[]; - isLoading: boolean; -}>('SET_TIMELINE_EVENTS_LOADING'); - -export const clearEventsLoading = actionCreator<{ - id: string; -}>('CLEAR_TIMELINE_EVENTS_LOADING'); - -export const setEventsDeleted = actionCreator<{ - id: string; - eventIds: string[]; - isDeleted: boolean; -}>('SET_TIMELINE_EVENTS_DELETED'); - -export const clearEventsDeleted = actionCreator<{ - id: string; -}>('CLEAR_TIMELINE_EVENTS_DELETED'); - export const updateEventType = actionCreator<{ id: string; eventType: TimelineEventsType }>( 'UPDATE_EVENT_TYPE' ); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts index 7e76f6035f8b5..d8fd82005dfbe 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts @@ -10,7 +10,6 @@ import { TimelineType, TimelineStatus, TimelineTabs } from '../../../../common/t import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers'; import { normalizeTimeRange } from '../../../common/components/url_state/normalize_time_range'; import { SubsetTimelineModel, TimelineModel } from './model'; -import { Direction } from '../../../../common/search_strategy'; // normalizeTimeRange uses getTimeRangeSettings which cannot be used outside Kibana context if the uiSettings is not false const { from: start, to: end } = normalizeTimeRange({ from: '', to: '' }, false); @@ -66,7 +65,7 @@ export const timelineDefaults: SubsetTimelineModel & { columnId: '@timestamp', columnType: 'number', - sortDirection: Direction.desc, + sortDirection: 'desc', }, ], status: TimelineStatus.draft, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts index 5f5d76990b5ff..8f2631dac6769 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts @@ -41,6 +41,7 @@ import { TimelineType, ResponseTimeline, TimelineResult, + ColumnHeaderOptions, } from '../../../../common/types/timeline'; import { inputsModel } from '../../../common/store/inputs'; import { addError } from '../../../common/store/app/actions'; @@ -81,7 +82,7 @@ import { showCallOutUnauthorizedMsg, saveTimeline, } from './actions'; -import { ColumnHeaderOptions, TimelineModel } from './model'; +import { TimelineModel } from './model'; import { epicPersistNote, timelineNoteActionsType } from './epic_note'; import { epicPersistPinnedEvent, timelinePinnedEventActionsType } from './epic_pinned_event'; import { epicPersistTimelineFavorite, timelineFavoriteActionsType } from './epic_favorite'; @@ -96,13 +97,11 @@ const timelineActionsType = [ addProvider.type, addTimeline.type, dataProviderEdited.type, - removeColumn.type, removeProvider.type, saveTimeline.type, setExcludedRowRendererIds.type, setFilters.type, setSavedQueryId.type, - updateColumns.type, updateDataProviderEnabled.type, updateDataProviderExcluded.type, updateDataProviderKqlQuery.type, @@ -110,10 +109,13 @@ const timelineActionsType = [ updateEqlOptions.type, updateEventType.type, updateKqlMode.type, - updateIndexNames.type, updateProviders.type, - updateSort.type, updateTitleAndDescription.type, + + updateIndexNames.type, + removeColumn.type, + updateColumns.type, + updateSort.type, updateRange.type, upsertColumn.type, ]; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts index 2172cf8562c97..610c394614c32 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts @@ -8,7 +8,6 @@ import { getOr, omit, uniq, isEmpty, isEqualWith, union } from 'lodash/fp'; import uuid from 'uuid'; -import { ToggleDetailPanel } from './actions'; import { Filter } from '../../../../../../../src/plugins/data/public'; import { Sort } from '../../../timelines/components/timeline/body/sort'; @@ -20,22 +19,24 @@ import { IS_OPERATOR, EXISTS_OPERATOR, } from '../../../timelines/components/timeline/data_providers/data_provider'; -import { SerializedFilterQuery } from '../../../common/store/model'; import { TimelineNonEcsData } from '../../../../common/search_strategy/timeline'; import { + ColumnHeaderOptions, TimelineEventsType, - TimelineExpandedDetail, TimelineTypeLiteral, TimelineType, RowRendererId, TimelineStatus, TimelineId, TimelineTabs, + SerializedFilterQuery, + ToggleDetailPanel, + TimelinePersistInput, } from '../../../../common/types/timeline'; import { normalizeTimeRange } from '../../../common/components/url_state/normalize_time_range'; import { timelineDefaults } from './defaults'; -import { ColumnHeaderOptions, KqlMode, TimelineModel } from './model'; +import { KqlMode, TimelineModel } from './model'; import { TimelineById } from './types'; import { DEFAULT_FROM_MOMENT, @@ -168,47 +169,20 @@ export const addTimelineToStore = ({ }; }; -interface AddNewTimelineParams { - columns: ColumnHeaderOptions[]; - dataProviders?: DataProvider[]; - dateRange?: { - start: string; - end: string; - }; - excludedRowRendererIds?: RowRendererId[]; - expandedDetail?: TimelineExpandedDetail; - filters?: Filter[]; - id: string; - itemsPerPage?: number; - indexNames: string[]; - kqlQuery?: { - filterQuery: SerializedFilterQuery | null; - }; - show?: boolean; - sort?: Sort[]; - showCheckboxes?: boolean; +interface AddNewTimelineParams extends TimelinePersistInput { timelineById: TimelineById; timelineType: TimelineTypeLiteral; } /** Adds a new `Timeline` to the provided collection of `TimelineById` */ export const addNewTimeline = ({ - columns, - dataProviders = [], - dateRange: maybeDateRange, - excludedRowRendererIds = [], - expandedDetail = {}, - filters = timelineDefaults.filters, id, - itemsPerPage = timelineDefaults.itemsPerPage, - indexNames, - kqlQuery = { filterQuery: null }, - sort = timelineDefaults.sort, - show = false, - showCheckboxes = false, timelineById, timelineType, + dateRange: maybeDateRange, + ...timelineProps }: AddNewTimelineParams): TimelineById => { + const timeline = timelineById[id]; const { from: startDateRange, to: endDateRange } = normalizeTimeRange({ from: '', to: '' }); const dateRange = maybeDateRange ?? { start: startDateRange, end: endDateRange }; const templateTimelineInfo = @@ -222,23 +196,14 @@ export const addNewTimeline = ({ ...timelineById, [id]: { id, + ...(timeline ? timeline : {}), ...timelineDefaults, - columns, - dataProviders, + ...timelineProps, dateRange, - expandedDetail, - excludedRowRendererIds, - filters, - itemsPerPage, - indexNames, - kqlQuery, - sort, - show, savedObjectId: null, version: null, isSaving: false, isLoading: false, - showCheckboxes, timelineType, ...templateTimelineInfo, }, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts index 559cec57dd55c..a68617536c6af 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts @@ -5,63 +5,29 @@ * 2.0. */ -import { EuiDataGridColumn } from '@elastic/eui'; - -import { Filter, IFieldSubType } from '../../../../../../../src/plugins/data/public'; - import { DataProvider } from '../../components/timeline/data_providers/data_provider'; -import { Sort } from '../../components/timeline/body/sort'; -import { - EqlOptionsSelected, - TimelineNonEcsData, -} from '../../../../common/search_strategy/timeline'; -import { SerializedFilterQuery } from '../../../common/store/types'; +import { EqlOptionsSelected } from '../../../../common/search_strategy/timeline'; import type { TimelineEventsType, - TimelineExpandedDetail, TimelineType, TimelineStatus, - RowRendererId, TimelineTabs, } from '../../../../common/types/timeline'; import { PinnedEvent } from '../../../../common/types/timeline/pinned_event'; +import type { TGridModelForTimeline } from '../../../../../timelines/public'; export const DEFAULT_PAGE_COUNT = 2; // Eui Pager will not render unless this is a minimum of 2 pages export type KqlMode = 'filter' | 'search'; export type ColumnHeaderType = 'not-filtered' | 'text-filter'; -/** Uniquely identifies a column */ -export type ColumnId = string; - -/** The specification of a column header */ -export type ColumnHeaderOptions = Pick< - EuiDataGridColumn, - 'display' | 'displayAsText' | 'id' | 'initialWidth' -> & { - aggregatable?: boolean; - category?: string; - columnHeaderType: ColumnHeaderType; - description?: string; - example?: string; - format?: string; - linkField?: string; - placeholder?: string; - subType?: IFieldSubType; - type?: string; -}; - -export interface TimelineModel { +export type TimelineModel = TGridModelForTimeline & { /** The selected tab to displayed in the timeline */ activeTab: TimelineTabs; prevActiveTab: TimelineTabs; - /** The columns displayed in the timeline */ - columns: ColumnHeaderOptions[]; /** Timeline saved object owner */ createdBy?: string; /** The sources of the event data shown in the timeline */ dataProviders: DataProvider[]; - /** Events to not be rendered **/ - deletedEventIds: string[]; /** A summary of the events and notes in this timeline */ description: string; eqlOptions: EqlOptionsSelected; @@ -69,40 +35,16 @@ export interface TimelineModel { eventType?: TimelineEventsType; /** A map of events in this timeline to the chronologically ordered notes (in this timeline) associated with the event */ eventIdToNoteIds: Record<string, string[]>; - /** A list of Ids of excluded Row Renderers */ - excludedRowRendererIds: RowRendererId[]; - /** This holds the view information for the flyout when viewing timeline in a consuming view (i.e. hosts page) or the side panel in the primary timeline view */ - expandedDetail: TimelineExpandedDetail; - filters?: Filter[]; - /** When non-empty, display a graph view for this event */ - graphEventId?: string; /** The chronological history of actions related to this timeline */ historyIds: string[]; /** The chronological history of actions related to this timeline */ highlightedDropAndProviderId: string; - /** Uniquely identifies the timeline */ - id: string; - /** TO DO sourcerer @X define this */ - indexNames: string[]; - /** If selectAll checkbox in header is checked **/ - isSelectAllChecked: boolean; - /** Events to be rendered as loading **/ - loadingEventIds: string[]; - savedObjectId: string | null; /** When true, this timeline was marked as "favorite" by the user */ isFavorite: boolean; /** When true, the timeline will update as new data arrives */ isLive: boolean; - /** The number of items to show in a single page of results */ - itemsPerPage: number; - /** Displays a series of choices that when selected, become the value of `itemsPerPage` */ - itemsPerPageOptions: number[]; /** determines the behavior of the KQL bar */ kqlMode: KqlMode; - /** the KQL query in the KQL bar */ - kqlQuery: { - filterQuery: SerializedFilterQuery | null; - }; /** Title */ title: string; /** timelineType: default | template */ @@ -116,30 +58,18 @@ export interface TimelineModel { /** Events pinned to this timeline */ pinnedEventIds: Record<string, boolean>; pinnedEventsSaveObject: Record<string, PinnedEvent>; - /** Specifies the granularity of the date range (e.g. 1 Day / Week / Month) applicable to the mini-map */ - dateRange: { - start: string; - end: string; - }; showSaveModal?: boolean; savedQueryId?: string | null; - /** Events selected on this timeline -- eventId to TimelineNonEcsData[] mapping of data required for batch actions **/ - selectedEventIds: Record<string, TimelineNonEcsData[]>; /** When true, show the timeline flyover */ show: boolean; - /** When true, shows checkboxes enabling selection. Selected events store in selectedEventIds **/ - showCheckboxes: boolean; - /** Specifies which column the timeline is sorted on, and the direction (ascending / descending) */ - sort: Sort[]; /** status: active | draft */ status: TimelineStatus; /** updated saved object timestamp */ updated?: number; /** timeline is saving */ isSaving: boolean; - isLoading: boolean; version: string | null; -} +}; export type SubsetTimelineModel = Readonly< Pick< diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts index 1c65c01a0bdfc..8a5c8546d3834 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts @@ -7,6 +7,7 @@ import { cloneDeep } from 'lodash/fp'; import { + ColumnHeaderOptions, TimelineType, TimelineStatus, TimelineTabs, @@ -47,7 +48,7 @@ import { upsertTimelineColumn, updateGraphEventId, } from './helpers'; -import { ColumnHeaderOptions, TimelineModel } from './model'; +import { TimelineModel } from './model'; import { timelineDefaults } from './defaults'; import { TimelineById } from './types'; import { Direction } from '../../../../common/search_strategy'; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts index 80c6d83075719..656784c330e45 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts @@ -13,32 +13,22 @@ import { addNoteToEvent, addProvider, addTimeline, - applyDeltaToColumnWidth, applyKqlFilterQuery, - clearEventsDeleted, - clearEventsLoading, - clearSelected, createTimeline, dataProviderEdited, endTimelineSaving, pinEvent, - removeColumn, removeProvider, - setEventsDeleted, setActiveTabTimeline, - setEventsLoading, setExcludedRowRendererIds, setFilters, setInsertTimeline, setSavedQueryId, - setSelected, showCallOutUnauthorizedMsg, showTimeline, startTimelineSaving, - toggleDetailPanel, unPinEvent, updateAutoSaveMsg, - updateColumns, updateDataProviderEnabled, updateDataProviderExcluded, updateDataProviderKqlQuery, @@ -47,18 +37,13 @@ import { updateIndexNames, updateIsFavorite, updateIsLive, - updateIsLoading, - updateItemsPerPage, - updateItemsPerPageOptions, updateKqlMode, updatePageIndex, updateProviders, updateRange, - updateSort, updateTimeline, updateTimelineGraphEventId, updateTitleAndDescription, - upsertColumn, toggleModalSaveTimeline, updateEqlOptions, } from './actions'; @@ -69,23 +54,15 @@ import { addTimelineNoteToEvent, addTimelineProvider, addTimelineToStore, - applyDeltaToTimelineColumnWidth, applyKqlFilterQueryDraft, pinTimelineEvent, - removeTimelineColumn, removeTimelineProvider, - setDeletedTimelineEvents, - setLoadingTimelineEvents, - setSelectedTimelineEvents, unPinTimelineEvent, updateExcludedRowRenderersIds, - updateTimelineColumns, updateTimelineIsFavorite, updateTimelineIsLive, - updateTimelineItemsPerPage, updateTimelineKqlMode, updateTimelinePageIndex, - updateTimelinePerPageOptions, updateTimelineProviderEnabled, updateTimelineProviderExcluded, updateTimelineProviderProperties, @@ -94,13 +71,10 @@ import { updateTimelineProviders, updateTimelineRange, updateTimelineShowTimeline, - updateTimelineSort, updateTimelineTitleAndDescription, - upsertTimelineColumn, updateSavedQuery, updateGraphEventId, updateFilters, - updateTimelineDetailsPanel, updateTimelineEventType, } from './helpers'; @@ -123,53 +97,17 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) ...state, timelineById: addTimelineToStore({ id, timeline, timelineById: state.timelineById }), })) - .case( - createTimeline, - ( - state, - { + .case(createTimeline, (state, { id, timelineType = TimelineType.default, ...timelineProps }) => { + return { + ...state, + timelineById: addNewTimeline({ id, - dataProviders, - dateRange, - excludedRowRendererIds, - expandedDetail = {}, - show, - columns, - itemsPerPage, - indexNames, - kqlQuery, - sort, - showCheckboxes, - timelineType = TimelineType.default, - filters, - } - ) => { - return { - ...state, - timelineById: addNewTimeline({ - columns, - dataProviders, - dateRange, - excludedRowRendererIds, - expandedDetail, - filters, - id, - itemsPerPage, - indexNames, - kqlQuery, - sort, - show, - showCheckboxes, - timelineById: state.timelineById, - timelineType, - }), - }; - } - ) - .case(upsertColumn, (state, { column, id, index }) => ({ - ...state, - timelineById: upsertTimelineColumn({ column, id, index, timelineById: state.timelineById }), - })) + timelineById: state.timelineById, + timelineType, + ...timelineProps, + }), + }; + }) .case(addHistory, (state, { id, historyId }) => ({ ...state, timelineById: addTimelineHistory({ id, historyId, timelineById: state.timelineById }), @@ -182,19 +120,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) ...state, timelineById: addTimelineNoteToEvent({ id, noteId, eventId, timelineById: state.timelineById }), })) - .case(toggleDetailPanel, (state, action) => ({ - ...state, - timelineById: { - ...state.timelineById, - [action.timelineId]: { - ...state.timelineById[action.timelineId], - expandedDetail: { - ...state.timelineById[action.timelineId].expandedDetail, - ...updateTimelineDetailsPanel(action), - }, - }, - }, - })) .case(addProvider, (state, { id, provider }) => ({ ...state, timelineById: addTimelineProvider({ id, provider, timelineById: state.timelineById }), @@ -215,27 +140,10 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) ...state, timelineById: updateGraphEventId({ id, graphEventId, timelineById: state.timelineById }), })) - .case(applyDeltaToColumnWidth, (state, { id, columnId, delta }) => ({ - ...state, - timelineById: applyDeltaToTimelineColumnWidth({ - id, - columnId, - delta, - timelineById: state.timelineById, - }), - })) .case(pinEvent, (state, { id, eventId }) => ({ ...state, timelineById: pinTimelineEvent({ id, eventId, timelineById: state.timelineById }), })) - .case(removeColumn, (state, { id, columnId }) => ({ - ...state, - timelineById: removeTimelineColumn({ - id, - columnId, - timelineById: state.timelineById, - }), - })) .case(removeProvider, (state, { id, providerId, andProviderId }) => ({ ...state, timelineById: removeTimelineProvider({ @@ -265,44 +173,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) }, }, })) - .case(setEventsDeleted, (state, { id, eventIds, isDeleted }) => ({ - ...state, - timelineById: setDeletedTimelineEvents({ - id, - eventIds, - timelineById: state.timelineById, - isDeleted, - }), - })) - .case(clearEventsDeleted, (state, { id }) => ({ - ...state, - timelineById: { - ...state.timelineById, - [id]: { - ...state.timelineById[id], - deletedEventIds: [], - }, - }, - })) - .case(setEventsLoading, (state, { id, eventIds, isLoading }) => ({ - ...state, - timelineById: setLoadingTimelineEvents({ - id, - eventIds, - timelineById: state.timelineById, - isLoading, - }), - })) - .case(clearEventsLoading, (state, { id }) => ({ - ...state, - timelineById: { - ...state.timelineById, - [id]: { - ...state.timelineById[id], - loadingEventIds: [], - }, - }, - })) .case(setExcludedRowRendererIds, (state, { id, excludedRowRendererIds }) => ({ ...state, timelineById: updateExcludedRowRenderersIds({ @@ -311,37 +181,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) timelineById: state.timelineById, }), })) - .case(setSelected, (state, { id, eventIds, isSelected, isSelectAllChecked }) => ({ - ...state, - timelineById: setSelectedTimelineEvents({ - id, - eventIds, - timelineById: state.timelineById, - isSelected, - isSelectAllChecked, - }), - })) - .case(clearSelected, (state, { id }) => ({ - ...state, - timelineById: { - ...state.timelineById, - [id]: { - ...state.timelineById[id], - selectedEventIds: {}, - isSelectAllChecked: false, - }, - }, - })) - .case(updateIsLoading, (state, { id, isLoading }) => ({ - ...state, - timelineById: { - ...state.timelineById, - [id]: { - ...state.timelineById[id], - isLoading, - }, - }, - })) .case(updateTimeline, (state, { id, timeline }) => ({ ...state, timelineById: { @@ -353,14 +192,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) ...state, timelineById: unPinTimelineEvent({ id, eventId, timelineById: state.timelineById }), })) - .case(updateColumns, (state, { id, columns }) => ({ - ...state, - timelineById: updateTimelineColumns({ - id, - columns, - timelineById: state.timelineById, - }), - })) .case(updateEventType, (state, { id, eventType }) => ({ ...state, timelineById: updateTimelineEventType({ id, eventType, timelineById: state.timelineById }), @@ -394,10 +225,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) ...state, timelineById: updateTimelineRange({ id, start, end, timelineById: state.timelineById }), })) - .case(updateSort, (state, { id, sort }) => ({ - ...state, - timelineById: updateTimelineSort({ id, sort, timelineById: state.timelineById }), - })) .case(updateDataProviderEnabled, (state, { id, enabled, providerId, andProviderId }) => ({ ...state, timelineById: updateTimelineProviderEnabled({ @@ -454,14 +281,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) timelineById: state.timelineById, }), })) - .case(updateItemsPerPage, (state, { id, itemsPerPage }) => ({ - ...state, - timelineById: updateTimelineItemsPerPage({ - id, - itemsPerPage, - timelineById: state.timelineById, - }), - })) .case(updatePageIndex, (state, { id, activePage }) => ({ ...state, timelineById: updateTimelinePageIndex({ @@ -470,14 +289,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) timelineById: state.timelineById, }), })) - .case(updateItemsPerPageOptions, (state, { id, itemsPerPageOptions }) => ({ - ...state, - timelineById: updateTimelinePerPageOptions({ - id, - itemsPerPageOptions, - timelineById: state.timelineById, - }), - })) .case(updateAutoSaveMsg, (state, { timelineId, newTimelineModel }) => ({ ...state, autoSavedWarningMsg: { diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts index b05e6568be6c3..f46b55bcd3345 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts @@ -7,11 +7,14 @@ import { createSelector } from 'reselect'; +import { tGridSelectors } from '../../../../../timelines/public'; import { State } from '../../../common/store/types'; import { TimelineModel } from './model'; import { AutoSavedWarningMsg, InsertTimeline, TimelineById } from './types'; +export const { getManageTimelineById } = tGridSelectors; + const selectTimelineById = (state: State): TimelineById => state.timeline.timelineById; const selectAutoSaveMsg = (state: State): AutoSavedWarningMsg => state.timeline.autoSavedWarningMsg; diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index d4e2601554187..aad685f9fb103 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -23,6 +23,7 @@ import { } from '../../triggers_actions_ui/public'; import { CasesUiStart } from '../../cases/public'; import { SecurityPluginSetup } from '../../security/public'; +import { TimelinesUIStart } from '../../timelines/public'; import { ResolverPluginSetup } from './resolver/types'; import { Inspect } from '../common/search_strategy'; import { MlPluginSetup, MlPluginStart } from '../../ml/public'; @@ -56,6 +57,7 @@ export interface StartPlugins { licensing: LicensingPluginStart; newsfeed?: NewsfeedPublicPluginStart; triggersActionsUi: TriggersActionsStart; + timelines: TimelinesUIStart; uiActions: UiActionsStart; ml?: MlPluginStart; } diff --git a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts index 72f56f13eaddf..66ac744b3a50c 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts @@ -15,9 +15,16 @@ import { KbnClient } from '@kbn/test'; import { AxiosResponse } from 'axios'; import { indexHostsAndAlerts } from '../../common/endpoint/index_data'; import { ANCESTRY_LIMIT, EndpointDocGenerator } from '../../common/endpoint/generate_data'; -import { AGENTS_SETUP_API_ROUTES, SETUP_API_ROUTE } from '../../../fleet/common/constants'; import { + AGENTS_SETUP_API_ROUTES, + EPM_API_ROUTES, + SETUP_API_ROUTE, +} from '../../../fleet/common/constants'; +import { + BulkInstallPackageInfo, + BulkInstallPackagesResponse, CreateFleetSetupResponse, + IBulkInstallPackageHTTPError, PostIngestSetupResponse, } from '../../../fleet/common/types/rest_spec'; import { KbnClientWithApiKeySupport } from './kbn_client_with_api_key_support'; @@ -44,6 +51,12 @@ async function deleteIndices(indices: string[], client: Client) { } } +function isFleetBulkInstallError( + installResponse: BulkInstallPackageInfo | IBulkInstallPackageHTTPError +): installResponse is IBulkInstallPackageHTTPError { + return 'error' in installResponse && installResponse.error !== undefined; +} + async function doIngestSetup(kbnClient: KbnClient) { // Setup Ingest try { @@ -76,6 +89,35 @@ async function doIngestSetup(kbnClient: KbnClient) { console.error(error); throw error; } + + // Install/upgrade the endpoint package + try { + const installEndpointPackageResp = (await kbnClient.request({ + path: EPM_API_ROUTES.BULK_INSTALL_PATTERN, + method: 'POST', + body: { + packages: ['endpoint'], + }, + })) as AxiosResponse<BulkInstallPackagesResponse>; + + const bulkResp = installEndpointPackageResp.data.response; + if (bulkResp.length <= 0) { + throw new Error('Installing the Endpoint package failed, response was empty, existing'); + } + + if (isFleetBulkInstallError(bulkResp[0])) { + if (bulkResp[0].error instanceof Error) { + throw new Error( + `Installing the Endpoint package failed: ${bulkResp[0].error.message}, exiting` + ); + } + + throw new Error(bulkResp[0].error); + } + } catch (error) { + console.error(error); + throw error; + } } async function main() { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/service.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/service.ts index 20b29694a1df1..1a8b17bf19e18 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/service.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/service.ts @@ -56,7 +56,6 @@ export const getAuditLogResponse = async ({ context: SecuritySolutionRequestHandlerContext; logger: Logger; }): Promise<{ - total: number; page: number; pageSize: number; data: Array<{ @@ -96,10 +95,6 @@ export const getAuditLogResponse = async ({ } return { - total: - typeof result.body.hits.total === 'number' - ? result.body.hits.total - : result.body.hits.total.value, page, pageSize, data: result.body.hits.hits.map((e) => ({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/__mocks__/eql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/__mocks__/eql.ts new file mode 100644 index 0000000000000..76389d7376fc8 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/__mocks__/eql.ts @@ -0,0 +1,791 @@ +/* + * 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 { EqlSearchStrategyResponse } from '../../../../../../../../src/plugins/data/common'; +import { EqlSearchResponse } from '../../../../../common/detection_engine/types'; + +export const sequenceResponse = ({ + rawResponse: { + body: { + is_partial: false, + is_running: false, + took: 527, + timed_out: false, + hits: { + total: { + value: 10, + relation: 'eq', + }, + sequences: [ + { + join_keys: ['win2019-endpoint-mr-pedro'], + events: [ + { + _index: '.ds-logs-endpoint.events.security-default-2021.02.05-000005', + _id: 'qhymg3cBX5UUcOOYP3Ec', + _source: { + agent: { + id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2', + type: 'endpoint', + version: '7.10.0', + }, + process: { + Ext: { + ancestry: [ + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTIzODAtMTMyNTUwNzg2ODkuOTY1Nzg1NTAw', + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU2OC0xMzI1NTA3ODY2Ny4zMjk3MDY2MDA=', + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=', + ], + }, + name: 'C:\\Program Files\\OpenSSH-Win64\\sshd.exe', + entity_id: + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTUyODQtMTMyNTcyOTQ2MjMuOTk2NTkxMDAw', + executable: 'C:\\Program Files\\OpenSSH-Win64\\sshd.exe', + }, + message: 'Endpoint security event', + '@timestamp': '2021-02-08T21:50:28.3377092Z', + ecs: { + version: '1.5.0', + }, + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'endpoint.events.security', + }, + elastic: { + agent: { + id: 'f5dec71e-438c-424e-ac9b-0281f10412b9', + }, + }, + host: { + hostname: 'win2019-endpoint-mr-pedro', + os: { + Ext: { + variant: 'Windows Server 2019 Datacenter', + }, + kernel: '1809 (10.0.17763.1697)', + name: 'Windows', + family: 'windows', + version: '1809 (10.0.17763.1697)', + platform: 'windows', + full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)', + }, + ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'], + name: 'win2019-endpoint-mr-pedro', + id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d', + mac: ['42:01:0a:80:00:39'], + architecture: 'x86_64', + }, + event: { + sequence: 3293866, + ingested: '2021-02-08T21:57:26.417559711Z', + created: '2021-02-08T21:50:28.3377092Z', + kind: 'event', + module: 'endpoint', + action: 'log_on', + id: 'LzzWB9jjGmCwGMvk++++FG/O', + category: ['authentication', 'session'], + type: ['start'], + dataset: 'endpoint.events.security', + outcome: 'success', + }, + user: { + domain: 'NT AUTHORITY', + name: 'SYSTEM', + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.security-default-2021.02.05-000005', + _id: 'qxymg3cBX5UUcOOYP3Ec', + _source: { + agent: { + id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2', + type: 'endpoint', + version: '7.10.0', + }, + process: { + Ext: { + ancestry: [ + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=', + ], + }, + entity_id: + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU4MC0xMzI1NTA3ODY2Ny45MTg5Njc1MDA=', + executable: 'C:\\Windows\\System32\\lsass.exe', + }, + message: 'Endpoint security event', + '@timestamp': '2021-02-08T21:50:28.3377142Z', + ecs: { + version: '1.5.0', + }, + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'endpoint.events.security', + }, + elastic: { + agent: { + id: 'f5dec71e-438c-424e-ac9b-0281f10412b9', + }, + }, + host: { + hostname: 'win2019-endpoint-mr-pedro', + os: { + Ext: { + variant: 'Windows Server 2019 Datacenter', + }, + kernel: '1809 (10.0.17763.1697)', + name: 'Windows', + family: 'windows', + version: '1809 (10.0.17763.1697)', + platform: 'windows', + full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)', + }, + ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'], + name: 'win2019-endpoint-mr-pedro', + id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d', + mac: ['42:01:0a:80:00:39'], + architecture: 'x86_64', + }, + event: { + sequence: 3293867, + ingested: '2021-02-08T21:57:26.417596906Z', + created: '2021-02-08T21:50:28.3377142Z', + kind: 'event', + module: 'endpoint', + action: 'log_on', + id: 'LzzWB9jjGmCwGMvk++++FG/P', + category: ['authentication', 'session'], + type: ['start'], + dataset: 'endpoint.events.security', + outcome: 'success', + }, + user: { + domain: 'NT AUTHORITY', + name: 'SYSTEM', + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.security-default-2021.02.05-000005', + _id: 'rBymg3cBX5UUcOOYP3Ec', + _source: { + agent: { + id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2', + type: 'endpoint', + version: '7.10.0', + }, + process: { + Ext: { + ancestry: [ + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=', + ], + }, + entity_id: + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU4MC0xMzI1NTA3ODY2Ny45MTg5Njc1MDA=', + executable: 'C:\\Windows\\System32\\lsass.exe', + }, + message: 'Endpoint security event', + '@timestamp': '2021-02-08T21:50:28.3381013Z', + ecs: { + version: '1.5.0', + }, + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'endpoint.events.security', + }, + elastic: { + agent: { + id: 'f5dec71e-438c-424e-ac9b-0281f10412b9', + }, + }, + host: { + hostname: 'win2019-endpoint-mr-pedro', + os: { + Ext: { + variant: 'Windows Server 2019 Datacenter', + }, + kernel: '1809 (10.0.17763.1697)', + name: 'Windows', + family: 'windows', + version: '1809 (10.0.17763.1697)', + platform: 'windows', + full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)', + }, + ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'], + name: 'win2019-endpoint-mr-pedro', + id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d', + mac: ['42:01:0a:80:00:39'], + architecture: 'x86_64', + }, + event: { + sequence: 3293868, + ingested: '2021-02-08T21:57:26.417632166Z', + created: '2021-02-08T21:50:28.3381013Z', + kind: 'event', + module: 'endpoint', + id: 'LzzWB9jjGmCwGMvk++++FG/Q', + category: [], + type: [], + dataset: 'endpoint.events.security', + }, + user: { + domain: 'NT AUTHORITY', + name: 'SYSTEM', + }, + }, + }, + ], + }, + { + join_keys: ['win2019-endpoint-mr-pedro'], + events: [ + { + _index: '.ds-logs-endpoint.events.security-default-2021.02.05-000005', + _id: 'qxymg3cBX5UUcOOYP3Ec', + _source: { + agent: { + id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2', + type: 'endpoint', + version: '7.10.0', + }, + process: { + Ext: { + ancestry: [ + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=', + ], + }, + entity_id: + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU4MC0xMzI1NTA3ODY2Ny45MTg5Njc1MDA=', + executable: 'C:\\Windows\\System32\\lsass.exe', + }, + message: 'Endpoint security event', + '@timestamp': '2021-02-08T21:50:28.3377142Z', + ecs: { + version: '1.5.0', + }, + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'endpoint.events.security', + }, + elastic: { + agent: { + id: 'f5dec71e-438c-424e-ac9b-0281f10412b9', + }, + }, + host: { + hostname: 'win2019-endpoint-mr-pedro', + os: { + Ext: { + variant: 'Windows Server 2019 Datacenter', + }, + kernel: '1809 (10.0.17763.1697)', + name: 'Windows', + family: 'windows', + version: '1809 (10.0.17763.1697)', + platform: 'windows', + full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)', + }, + ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'], + name: 'win2019-endpoint-mr-pedro', + id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d', + mac: ['42:01:0a:80:00:39'], + architecture: 'x86_64', + }, + event: { + sequence: 3293867, + ingested: '2021-02-08T21:57:26.417596906Z', + created: '2021-02-08T21:50:28.3377142Z', + kind: 'event', + module: 'endpoint', + action: 'log_on', + id: 'LzzWB9jjGmCwGMvk++++FG/P', + category: ['authentication', 'session'], + type: ['start'], + dataset: 'endpoint.events.security', + outcome: 'success', + }, + user: { + domain: 'NT AUTHORITY', + name: 'SYSTEM', + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.security-default-2021.02.05-000005', + _id: 'rBymg3cBX5UUcOOYP3Ec', + _source: { + agent: { + id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2', + type: 'endpoint', + version: '7.10.0', + }, + process: { + Ext: { + ancestry: [ + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=', + ], + }, + entity_id: + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU4MC0xMzI1NTA3ODY2Ny45MTg5Njc1MDA=', + executable: 'C:\\Windows\\System32\\lsass.exe', + }, + message: 'Endpoint security event', + '@timestamp': '2021-02-08T21:50:28.3381013Z', + ecs: { + version: '1.5.0', + }, + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'endpoint.events.security', + }, + elastic: { + agent: { + id: 'f5dec71e-438c-424e-ac9b-0281f10412b9', + }, + }, + host: { + hostname: 'win2019-endpoint-mr-pedro', + os: { + Ext: { + variant: 'Windows Server 2019 Datacenter', + }, + kernel: '1809 (10.0.17763.1697)', + name: 'Windows', + family: 'windows', + version: '1809 (10.0.17763.1697)', + platform: 'windows', + full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)', + }, + ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'], + name: 'win2019-endpoint-mr-pedro', + id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d', + mac: ['42:01:0a:80:00:39'], + architecture: 'x86_64', + }, + event: { + sequence: 3293868, + ingested: '2021-02-08T21:57:26.417632166Z', + created: '2021-02-08T21:50:28.3381013Z', + kind: 'event', + module: 'endpoint', + id: 'LzzWB9jjGmCwGMvk++++FG/Q', + category: [], + type: [], + dataset: 'endpoint.events.security', + }, + user: { + domain: 'NT AUTHORITY', + name: 'SYSTEM', + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2021.02.02-000005', + _id: 'pxymg3cBX5UUcOOYP3Ec', + _source: { + agent: { + id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2', + type: 'endpoint', + version: '7.10.0', + }, + process: { + Ext: { + ancestry: [ + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTUyODQtMTMyNTcyOTQ2MjMuOTk2NTkxMDAw', + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTIzODAtMTMyNTUwNzg2ODkuOTY1Nzg1NTAw', + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU2OC0xMzI1NTA3ODY2Ny4zMjk3MDY2MDA=', + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=', + ], + code_signature: [ + { + trusted: true, + subject_name: 'Microsoft Corporation', + exists: true, + status: 'trusted', + }, + ], + token: { + integrity_level_name: 'high', + elevation_level: 'default', + }, + }, + args: ['C:\\Program Files\\OpenSSH-Win64\\sshd.exe', '-y'], + parent: { + args: ['C:\\Program Files\\OpenSSH-Win64\\sshd.exe', '-R'], + name: 'sshd.exe', + pid: 5284, + args_count: 2, + entity_id: + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTUyODQtMTMyNTcyOTQ2MjMuOTk2NTkxMDAw', + command_line: '"C:\\Program Files\\OpenSSH-Win64\\sshd.exe" -R', + executable: 'C:\\Program Files\\OpenSSH-Win64\\sshd.exe', + }, + code_signature: { + trusted: true, + subject_name: 'Microsoft Corporation', + exists: true, + status: 'trusted', + }, + name: 'sshd.exe', + pid: 6368, + args_count: 2, + entity_id: + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTYzNjgtMTMyNTcyOTQ2MjguMzQ0NjM1NTAw', + command_line: '"C:\\Program Files\\OpenSSH-Win64\\sshd.exe" -y', + executable: 'C:\\Program Files\\OpenSSH-Win64\\sshd.exe', + hash: { + sha1: '631244d731f406394c17c7dfd85203e317c74814', + sha256: 'e6a972f9db27de18be225095b3b3141b945be8aadc4014c8704ae5acafe3e8e0', + md5: '331ba0e529810ef718dd3efbd1242302', + }, + }, + message: 'Endpoint process event', + '@timestamp': '2021-02-08T21:50:28.3446355Z', + ecs: { + version: '1.5.0', + }, + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'endpoint.events.process', + }, + elastic: { + agent: { + id: 'f5dec71e-438c-424e-ac9b-0281f10412b9', + }, + }, + host: { + hostname: 'win2019-endpoint-mr-pedro', + os: { + Ext: { + variant: 'Windows Server 2019 Datacenter', + }, + kernel: '1809 (10.0.17763.1697)', + name: 'Windows', + family: 'windows', + version: '1809 (10.0.17763.1697)', + platform: 'windows', + full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)', + }, + ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'], + name: 'win2019-endpoint-mr-pedro', + id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d', + mac: ['42:01:0a:80:00:39'], + architecture: 'x86_64', + }, + event: { + sequence: 3293863, + ingested: '2021-02-08T21:57:26.417387865Z', + created: '2021-02-08T21:50:28.3446355Z', + kind: 'event', + module: 'endpoint', + action: 'start', + id: 'LzzWB9jjGmCwGMvk++++FG/K', + category: ['process'], + type: ['start'], + dataset: 'endpoint.events.process', + }, + user: { + domain: '', + name: '', + }, + }, + }, + ], + }, + { + join_keys: ['win2019-endpoint-mr-pedro'], + events: [ + { + _index: '.ds-logs-endpoint.events.security-default-2021.02.05-000005', + _id: 'rBymg3cBX5UUcOOYP3Ec', + _source: { + agent: { + id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2', + type: 'endpoint', + version: '7.10.0', + }, + process: { + Ext: { + ancestry: [ + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=', + ], + }, + entity_id: + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU4MC0xMzI1NTA3ODY2Ny45MTg5Njc1MDA=', + executable: 'C:\\Windows\\System32\\lsass.exe', + }, + message: 'Endpoint security event', + '@timestamp': '2021-02-08T21:50:28.3381013Z', + ecs: { + version: '1.5.0', + }, + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'endpoint.events.security', + }, + elastic: { + agent: { + id: 'f5dec71e-438c-424e-ac9b-0281f10412b9', + }, + }, + host: { + hostname: 'win2019-endpoint-mr-pedro', + os: { + Ext: { + variant: 'Windows Server 2019 Datacenter', + }, + kernel: '1809 (10.0.17763.1697)', + name: 'Windows', + family: 'windows', + version: '1809 (10.0.17763.1697)', + platform: 'windows', + full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)', + }, + ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'], + name: 'win2019-endpoint-mr-pedro', + id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d', + mac: ['42:01:0a:80:00:39'], + architecture: 'x86_64', + }, + event: { + sequence: 3293868, + ingested: '2021-02-08T21:57:26.417632166Z', + created: '2021-02-08T21:50:28.3381013Z', + kind: 'event', + module: 'endpoint', + id: 'LzzWB9jjGmCwGMvk++++FG/Q', + category: [], + type: [], + dataset: 'endpoint.events.security', + }, + user: { + domain: 'NT AUTHORITY', + name: 'SYSTEM', + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.process-default-2021.02.02-000005', + _id: 'pxymg3cBX5UUcOOYP3Ec', + _source: { + agent: { + id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2', + type: 'endpoint', + version: '7.10.0', + }, + process: { + Ext: { + ancestry: [ + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTUyODQtMTMyNTcyOTQ2MjMuOTk2NTkxMDAw', + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTIzODAtMTMyNTUwNzg2ODkuOTY1Nzg1NTAw', + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU2OC0xMzI1NTA3ODY2Ny4zMjk3MDY2MDA=', + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=', + ], + code_signature: [ + { + trusted: true, + subject_name: 'Microsoft Corporation', + exists: true, + status: 'trusted', + }, + ], + token: { + integrity_level_name: 'high', + elevation_level: 'default', + }, + }, + args: ['C:\\Program Files\\OpenSSH-Win64\\sshd.exe', '-y'], + parent: { + args: ['C:\\Program Files\\OpenSSH-Win64\\sshd.exe', '-R'], + name: 'sshd.exe', + pid: 5284, + args_count: 2, + entity_id: + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTUyODQtMTMyNTcyOTQ2MjMuOTk2NTkxMDAw', + command_line: '"C:\\Program Files\\OpenSSH-Win64\\sshd.exe" -R', + executable: 'C:\\Program Files\\OpenSSH-Win64\\sshd.exe', + }, + code_signature: { + trusted: true, + subject_name: 'Microsoft Corporation', + exists: true, + status: 'trusted', + }, + name: 'sshd.exe', + pid: 6368, + args_count: 2, + entity_id: + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTYzNjgtMTMyNTcyOTQ2MjguMzQ0NjM1NTAw', + command_line: '"C:\\Program Files\\OpenSSH-Win64\\sshd.exe" -y', + executable: 'C:\\Program Files\\OpenSSH-Win64\\sshd.exe', + hash: { + sha1: '631244d731f406394c17c7dfd85203e317c74814', + sha256: 'e6a972f9db27de18be225095b3b3141b945be8aadc4014c8704ae5acafe3e8e0', + md5: '331ba0e529810ef718dd3efbd1242302', + }, + }, + message: 'Endpoint process event', + '@timestamp': '2021-02-08T21:50:28.3446355Z', + ecs: { + version: '1.5.0', + }, + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'endpoint.events.process', + }, + elastic: { + agent: { + id: 'f5dec71e-438c-424e-ac9b-0281f10412b9', + }, + }, + host: { + hostname: 'win2019-endpoint-mr-pedro', + os: { + Ext: { + variant: 'Windows Server 2019 Datacenter', + }, + kernel: '1809 (10.0.17763.1697)', + name: 'Windows', + family: 'windows', + version: '1809 (10.0.17763.1697)', + platform: 'windows', + full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)', + }, + ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'], + name: 'win2019-endpoint-mr-pedro', + id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d', + mac: ['42:01:0a:80:00:39'], + architecture: 'x86_64', + }, + event: { + sequence: 3293863, + ingested: '2021-02-08T21:57:26.417387865Z', + created: '2021-02-08T21:50:28.3446355Z', + kind: 'event', + module: 'endpoint', + action: 'start', + id: 'LzzWB9jjGmCwGMvk++++FG/K', + category: ['process'], + type: ['start'], + dataset: 'endpoint.events.process', + }, + user: { + domain: '', + name: '', + }, + }, + }, + { + _index: '.ds-logs-endpoint.events.network-default-2021.02.02-000005', + _id: 'qBymg3cBX5UUcOOYP3Ec', + _source: { + agent: { + id: '1d15cf9e-3dc7-5b97-f586-743f7c2518b2', + type: 'endpoint', + version: '7.10.0', + }, + process: { + Ext: { + ancestry: [ + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTU2OC0xMzI1NTA3ODY2Ny4zMjk3MDY2MDA=', + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTQ2OC0xMzI1NTA3ODY2NS42Mzg5MzY1MDA=', + ], + }, + name: 'svchost.exe', + pid: 968, + entity_id: + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTk2OC0xMzI1NTA3ODY3My4yNjQyNDcyMDA=', + executable: 'C:\\Windows\\System32\\svchost.exe', + }, + destination: { + address: '10.128.0.57', + port: 3389, + bytes: 1681, + ip: '10.128.0.57', + }, + source: { + address: '142.202.189.139', + port: 16151, + bytes: 1224, + ip: '142.202.189.139', + }, + message: 'Endpoint network event', + network: { + transport: 'tcp', + type: 'ipv4', + direction: 'incoming', + }, + '@timestamp': '2021-02-08T21:50:28.5553532Z', + ecs: { + version: '1.5.0', + }, + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'endpoint.events.network', + }, + elastic: { + agent: { + id: 'f5dec71e-438c-424e-ac9b-0281f10412b9', + }, + }, + host: { + hostname: 'win2019-endpoint-mr-pedro', + os: { + Ext: { + variant: 'Windows Server 2019 Datacenter', + }, + kernel: '1809 (10.0.17763.1697)', + name: 'Windows', + family: 'windows', + version: '1809 (10.0.17763.1697)', + platform: 'windows', + full: 'Windows Server 2019 Datacenter 1809 (10.0.17763.1697)', + }, + ip: ['10.128.0.57', 'fe80::9ced:8f1c:880b:3e1f', '127.0.0.1', '::1'], + name: 'win2019-endpoint-mr-pedro', + id: 'd8ad572e-d224-4044-a57d-f5a84c0dfe5d', + mac: ['42:01:0a:80:00:39'], + architecture: 'x86_64', + }, + event: { + sequence: 3293864, + ingested: '2021-02-08T21:57:26.417451347Z', + created: '2021-02-08T21:50:28.5553532Z', + kind: 'event', + module: 'endpoint', + action: 'disconnect_received', + id: 'LzzWB9jjGmCwGMvk++++FG/L', + category: ['network'], + type: ['end'], + dataset: 'endpoint.events.network', + }, + user: { + domain: 'NT AUTHORITY', + name: 'NETWORK SERVICE', + }, + }, + }, + ], + }, + ], + }, + }, + statusCode: 200, + headers: {}, + meta: {}, + hits: {}, + }, +} as unknown) as EqlSearchStrategyResponse<EqlSearchResponse<unknown>>; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/eql.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/eql.test.ts index 6529c594dd5a5..da5c89a3102a1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/eql.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/reference_rules/eql.test.ts @@ -7,10 +7,8 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; - -import { sequenceResponse } from '../../../search_strategy/timeline/eql/__mocks__'; - import { createEqlAlertType } from './eql'; +import { sequenceResponse } from './__mocks__/eql'; import { createRuleTypeMocks } from './__mocks__/rule_type'; describe('EQL alerts', () => { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.test.ts index 94e70e4eb001b..3a37a49d03dcd 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.test.ts @@ -7,13 +7,20 @@ import { AuthenticatedUser } from '../../../../../../security/common/model'; -import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline'; +import { TimelineStatus, TimelineType, SavedTimeline } from '../../../../../common/types/timeline'; +import { NoteSavedObject } from '../../../../../common/types/timeline/note'; import { pickSavedTimeline } from './pick_saved_timeline'; describe('pickSavedTimeline', () => { const mockDateNow = new Date('2020-04-03T23:00:00.000Z').valueOf(); - const getMockSavedTimeline = () => ({ + const getMockSavedTimeline = (): SavedTimeline & { + savedObjectId?: string | null; + version?: string; + eventNotes?: NoteSavedObject[]; + globalNotes?: NoteSavedObject[]; + pinnedEventIds?: []; + } => ({ savedObjectId: '7af80430-03f4-11eb-9d9d-ffba20fabba8', version: 'WzQ0ODgsMV0=', created: 1601563413330, @@ -91,7 +98,7 @@ describe('pickSavedTimeline', () => { test('Updating a timeline', () => { const savedTimeline = getMockSavedTimeline(); - const timelineId = savedTimeline.savedObjectId; + const timelineId = savedTimeline.savedObjectId ?? null; const userInfo = { username: 'elastic' } as AuthenticatedUser; const result = pickSavedTimeline(timelineId, savedTimeline, userInfo); @@ -113,7 +120,7 @@ describe('pickSavedTimeline', () => { test('Updating a timeline', () => { const savedTimeline = getMockSavedTimeline(); - const timelineId = savedTimeline.savedObjectId; + const timelineId = savedTimeline.savedObjectId ?? null; const userInfo = { username: 'elastic' } as AuthenticatedUser; const result = pickSavedTimeline(timelineId, savedTimeline, userInfo); @@ -143,7 +150,7 @@ describe('pickSavedTimeline', () => { test('Updating a timeline with a new title', () => { const savedTimeline = getMockSavedTimeline(); - const timelineId = savedTimeline.savedObjectId; + const timelineId = savedTimeline.savedObjectId ?? null; const userInfo = { username: 'elastic' } as AuthenticatedUser; const result = pickSavedTimeline(timelineId, savedTimeline, userInfo); @@ -152,7 +159,7 @@ describe('pickSavedTimeline', () => { test('Updating a timeline without title', () => { const savedTimeline = getMockSavedTimeline(); - const timelineId = savedTimeline.savedObjectId; + const timelineId = savedTimeline.savedObjectId ?? null; const userInfo = { username: 'elastic' } as AuthenticatedUser; const result = pickSavedTimeline(timelineId, savedTimeline, userInfo); @@ -161,7 +168,7 @@ describe('pickSavedTimeline', () => { test('Updating an immutable timeline with a new title', () => { const savedTimeline = { ...getMockSavedTimeline(), status: TimelineStatus.immutable }; - const timelineId = savedTimeline.savedObjectId; + const timelineId = savedTimeline.savedObjectId ?? null; const userInfo = { username: 'elastic' } as AuthenticatedUser; const result = pickSavedTimeline(timelineId, savedTimeline, userInfo); @@ -192,7 +199,7 @@ describe('pickSavedTimeline', () => { test('Updating an untitled draft timeline with a title', () => { const savedTimeline = { ...getMockSavedTimeline(), status: TimelineStatus.draft }; - const timelineId = savedTimeline.savedObjectId; + const timelineId = savedTimeline.savedObjectId ?? null; const userInfo = { username: 'elastic' } as AuthenticatedUser; const result = pickSavedTimeline(timelineId, savedTimeline, userInfo); @@ -201,7 +208,7 @@ describe('pickSavedTimeline', () => { test('Updating a draft timeline with a new title', () => { const savedTimeline = { ...getMockSavedTimeline(), status: TimelineStatus.draft }; - const timelineId = savedTimeline.savedObjectId; + const timelineId = savedTimeline.savedObjectId ?? null; const userInfo = { username: 'elastic' } as AuthenticatedUser; const result = pickSavedTimeline(timelineId, savedTimeline, userInfo); @@ -210,7 +217,7 @@ describe('pickSavedTimeline', () => { test('Updating a draft timeline without title', () => { const savedTimeline = { ...getMockSavedTimeline(), status: TimelineStatus.draft }; - const timelineId = savedTimeline.savedObjectId; + const timelineId = savedTimeline.savedObjectId ?? null; const userInfo = { username: 'elastic' } as AuthenticatedUser; const result = pickSavedTimeline(timelineId, savedTimeline, userInfo); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.ts index a28084cd78154..3e00a33966f17 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object/timelines/pick_saved_timeline.ts @@ -12,10 +12,9 @@ import { SavedTimeline, TimelineType, TimelineStatus } from '../../../../../comm export const pickSavedTimeline = ( timelineId: string | null, - savedTimeline: SavedTimeline, + savedTimeline: SavedTimeline & { savedObjectId?: string | null }, userInfo: AuthenticatedUser | null - // eslint-disable-next-line @typescript-eslint/no-explicit-any -): any => { +): SavedTimeline & { savedObjectId?: string | null } => { const dateNow = new Date().valueOf(); if (timelineId == null) { diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index ac9d854f18211..4bcbcb71d048c 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -83,8 +83,6 @@ import { initUsageCollectors } from './usage'; import type { SecuritySolutionRequestHandlerContext } from './types'; import { registerTrustedAppsRoutes } from './endpoint/routes/trusted_apps'; import { securitySolutionSearchStrategyProvider } from './search_strategy/security_solution'; -import { securitySolutionIndexFieldsProvider } from './search_strategy/index_fields'; -import { securitySolutionTimelineSearchStrategyProvider } from './search_strategy/timeline'; import { TelemetryEventsSender } from './lib/telemetry/sender'; import { TelemetryPluginStart, @@ -92,7 +90,6 @@ import { } from '../../../../src/plugins/telemetry/server'; import { licenseService } from './lib/license'; import { PolicyWatcher } from './endpoint/lib/policy/license_watch'; -import { securitySolutionTimelineEqlSearchStrategyProvider } from './search_strategy/timeline/eql'; import { parseExperimentalConfigValue } from '../common/experimental_features'; import { migrateArtifactsToFleet } from './endpoint/lib/artifacts/migrate_artifacts_to_fleet'; @@ -451,30 +448,10 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S depsStart.data, endpointContext ); - const securitySolutionTimelineSearchStrategy = securitySolutionTimelineSearchStrategyProvider( - depsStart.data - ); - const securitySolutionTimelineEqlSearchStrategy = securitySolutionTimelineEqlSearchStrategyProvider( - depsStart.data - ); - const securitySolutionIndexFields = securitySolutionIndexFieldsProvider(); - plugins.data.search.registerSearchStrategy( 'securitySolutionSearchStrategy', securitySolutionSearchStrategy ); - plugins.data.search.registerSearchStrategy( - 'securitySolutionIndexFields', - securitySolutionIndexFields - ); - plugins.data.search.registerSearchStrategy( - 'securitySolutionTimelineSearchStrategy', - securitySolutionTimelineSearchStrategy - ); - plugins.data.search.registerSearchStrategy( - 'securitySolutionTimelineEqlSearchStrategy', - securitySolutionTimelineEqlSearchStrategy - ); }); this.telemetryEventsSender.setup(plugins.telemetry, plugins.taskManager); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx index b50623bf9090a..a6cc14ec7e103 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx @@ -33,6 +33,7 @@ const mockDeps = { trustedAppsByPolicyEnabled: false, metricsEntitiesEnabled: false, ruleRegistryEnabled: false, + tGridEnabled: false, }, service: {} as EndpointAppContextService, } as EndpointAppContext, diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts deleted file mode 100644 index 5f732d2bacdac..0000000000000 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts +++ /dev/null @@ -1,1515 +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 { eventHit } from '../../../../../../common/utils/mock_event_details'; -import { EventHit } from '../../../../../../common/search_strategy'; -import { TIMELINE_EVENTS_FIELDS } from './constants'; -import { buildFieldsRequest, buildObjectForFieldPath, formatTimelineData } from './helpers'; - -describe('search strategy timeline helpers', () => { - describe('#formatTimelineData', () => { - it('happy path', async () => { - const res = await formatTimelineData( - [ - '@timestamp', - 'host.name', - 'destination.ip', - 'source.ip', - 'source.geo.location', - 'threat.indicator.matched.field', - ], - TIMELINE_EVENTS_FIELDS, - eventHit - ); - expect(res).toEqual({ - cursor: { - tiebreaker: 'beats-ci-immutable-ubuntu-1804-1605624279743236239', - value: '1605624488922', - }, - node: { - _id: 'tkCt1nUBaEgqnrVSZ8R_', - _index: 'auditbeat-7.8.0-2020.11.05-000003', - data: [ - { - field: '@timestamp', - value: ['2020-11-17T14:48:08.922Z'], - }, - { - field: 'host.name', - value: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], - }, - { - field: 'threat.indicator.matched.field', - value: ['matched_field', 'other_matched_field', 'matched_field_2'], - }, - { - field: 'source.geo.location', - value: [`{"lon":118.7778,"lat":32.0617}`], - }, - ], - ecs: { - '@timestamp': ['2020-11-17T14:48:08.922Z'], - _id: 'tkCt1nUBaEgqnrVSZ8R_', - _index: 'auditbeat-7.8.0-2020.11.05-000003', - agent: { - type: ['auditbeat'], - }, - event: { - action: ['process_started'], - category: ['process'], - dataset: ['process'], - kind: ['event'], - module: ['system'], - type: ['start'], - }, - host: { - id: ['e59991e835905c65ed3e455b33e13bd6'], - ip: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], - name: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], - os: { - family: ['debian'], - }, - }, - message: ['Process go (PID: 4313) by user jenkins STARTED'], - process: { - args: ['go', 'vet', './...'], - entity_id: ['Z59cIkAAIw8ZoK0H'], - executable: [ - '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', - ], - hash: { - sha1: ['1eac22336a41e0660fb302add9d97daa2bcc7040'], - }, - name: ['go'], - pid: ['4313'], - ppid: ['3977'], - working_directory: [ - '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', - ], - }, - timestamp: '2020-11-17T14:48:08.922Z', - user: { - name: ['jenkins'], - }, - threat: { - indicator: [ - { - event: { - dataset: [], - reference: [], - }, - matched: { - atomic: ['matched_atomic'], - field: ['matched_field', 'other_matched_field'], - type: [], - }, - provider: ['yourself'], - }, - { - event: { - dataset: [], - reference: [], - }, - matched: { - atomic: ['matched_atomic_2'], - field: ['matched_field_2'], - type: [], - }, - provider: ['other_you'], - }, - ], - }, - }, - }, - }); - }); - - it('rule signal results', async () => { - const response: EventHit = { - _index: '.siem-signals-patrykkopycinski-default-000007', - _id: 'a77040f198355793c35bf22b900902371309be615381f0a2ec92c208b6132562', - _score: 0, - _source: { - signal: { - threshold_result: { - count: 10000, - value: '2a990c11-f61b-4c8e-b210-da2574e9f9db', - }, - parent: { - depth: 0, - index: - 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*', - id: '0268af90-d8da-576a-9747-2a191519416a', - type: 'event', - }, - depth: 1, - _meta: { - version: 14, - }, - rule: { - note: null, - throttle: null, - references: [], - severity_mapping: [], - description: 'asdasd', - created_at: '2021-01-09T11:25:45.046Z', - language: 'kuery', - threshold: { - field: '', - value: 200, - }, - building_block_type: null, - output_index: '.siem-signals-patrykkopycinski-default', - type: 'threshold', - rule_name_override: null, - enabled: true, - exceptions_list: [], - updated_at: '2021-01-09T13:36:39.204Z', - timestamp_override: null, - from: 'now-360s', - id: '696c24e0-526d-11eb-836c-e1620268b945', - timeline_id: null, - max_signals: 100, - severity: 'low', - risk_score: 21, - risk_score_mapping: [], - author: [], - query: '_id :*', - index: [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'logs-*', - 'packetbeat-*', - 'winlogbeat-*', - ], - filters: [ - { - $state: { - store: 'appState', - }, - meta: { - negate: false, - alias: null, - disabled: false, - type: 'exists', - value: 'exists', - key: '_index', - }, - exists: { - field: '_index', - }, - }, - { - $state: { - store: 'appState', - }, - meta: { - negate: false, - alias: 'id_exists', - disabled: false, - type: 'exists', - value: 'exists', - key: '_id', - }, - exists: { - field: '_id', - }, - }, - ], - created_by: 'patryk_test_user', - version: 1, - saved_id: null, - tags: [], - rule_id: '2a990c11-f61b-4c8e-b210-da2574e9f9db', - license: '', - immutable: false, - timeline_title: null, - meta: { - from: '1m', - kibana_siem_app_url: 'http://localhost:5601/app/security', - }, - name: 'Threshold test', - updated_by: 'patryk_test_user', - interval: '5m', - false_positives: [], - to: 'now', - threat: [], - actions: [], - }, - original_time: '2021-01-09T13:39:32.595Z', - ancestors: [ - { - depth: 0, - index: - 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*', - id: '0268af90-d8da-576a-9747-2a191519416a', - type: 'event', - }, - ], - parents: [ - { - depth: 0, - index: - 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*', - id: '0268af90-d8da-576a-9747-2a191519416a', - type: 'event', - }, - ], - status: 'open', - }, - }, - fields: { - 'signal.rule.output_index': ['.siem-signals-patrykkopycinski-default'], - 'signal.rule.from': ['now-360s'], - 'signal.rule.language': ['kuery'], - '@timestamp': ['2021-01-09T13:41:40.517Z'], - 'signal.rule.query': ['_id :*'], - 'signal.rule.type': ['threshold'], - 'signal.rule.id': ['696c24e0-526d-11eb-836c-e1620268b945'], - 'signal.rule.risk_score': [21], - 'signal.status': ['open'], - 'event.kind': ['signal'], - 'signal.original_time': ['2021-01-09T13:39:32.595Z'], - 'signal.rule.severity': ['low'], - 'signal.rule.version': ['1'], - 'signal.rule.index': [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'logs-*', - 'packetbeat-*', - 'winlogbeat-*', - ], - 'signal.rule.name': ['Threshold test'], - 'signal.rule.to': ['now'], - }, - _type: '', - sort: ['1610199700517'], - aggregations: {}, - }; - - expect( - await formatTimelineData( - ['@timestamp', 'host.name', 'destination.ip', 'source.ip'], - TIMELINE_EVENTS_FIELDS, - response - ) - ).toEqual({ - cursor: { - tiebreaker: null, - value: '', - }, - node: { - _id: 'a77040f198355793c35bf22b900902371309be615381f0a2ec92c208b6132562', - _index: '.siem-signals-patrykkopycinski-default-000007', - data: [ - { - field: '@timestamp', - value: ['2021-01-09T13:41:40.517Z'], - }, - ], - ecs: { - '@timestamp': ['2021-01-09T13:41:40.517Z'], - timestamp: '2021-01-09T13:41:40.517Z', - _id: 'a77040f198355793c35bf22b900902371309be615381f0a2ec92c208b6132562', - _index: '.siem-signals-patrykkopycinski-default-000007', - event: { - kind: ['signal'], - }, - signal: { - original_time: ['2021-01-09T13:39:32.595Z'], - status: ['open'], - threshold_result: ['{"count":10000,"value":"2a990c11-f61b-4c8e-b210-da2574e9f9db"}'], - rule: { - building_block_type: [], - exceptions_list: [], - from: ['now-360s'], - id: ['696c24e0-526d-11eb-836c-e1620268b945'], - index: [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'logs-*', - 'packetbeat-*', - 'winlogbeat-*', - ], - language: ['kuery'], - name: ['Threshold test'], - output_index: ['.siem-signals-patrykkopycinski-default'], - risk_score: ['21'], - query: ['_id :*'], - severity: ['low'], - to: ['now'], - type: ['threshold'], - version: ['1'], - timeline_id: [], - timeline_title: [], - saved_id: [], - note: [], - threshold: [ - JSON.stringify({ - field: '', - value: 200, - }), - ], - filters: [ - JSON.stringify({ - $state: { - store: 'appState', - }, - meta: { - negate: false, - alias: null, - disabled: false, - type: 'exists', - value: 'exists', - key: '_index', - }, - exists: { - field: '_index', - }, - }), - JSON.stringify({ - $state: { - store: 'appState', - }, - meta: { - negate: false, - alias: 'id_exists', - disabled: false, - type: 'exists', - value: 'exists', - key: '_id', - }, - exists: { - field: '_id', - }, - }), - ], - }, - }, - }, - }, - }); - }); - }); - - describe('#buildObjectForFieldPath', () => { - it('builds an object from a single non-nested field', () => { - expect(buildObjectForFieldPath('@timestamp', eventHit)).toEqual({ - '@timestamp': ['2020-11-17T14:48:08.922Z'], - }); - }); - - it('builds an object with no fields response', () => { - const { fields, ...fieldLessHit } = eventHit; - // @ts-expect-error fieldLessHit is intentionally missing fields - expect(buildObjectForFieldPath('@timestamp', fieldLessHit)).toEqual({ - '@timestamp': [], - }); - }); - - it('does not misinterpret non-nested fields with a common prefix', () => { - // @ts-expect-error hit is minimal - const hit: EventHit = { - fields: { - 'foo.bar': ['baz'], - 'foo.barBaz': ['foo'], - }, - }; - - expect(buildObjectForFieldPath('foo.barBaz', hit)).toEqual({ - foo: { barBaz: ['foo'] }, - }); - }); - - it('builds an array of objects from a nested field', () => { - // @ts-expect-error hit is minimal - const hit: EventHit = { - fields: { - foo: [{ bar: ['baz'] }], - }, - }; - expect(buildObjectForFieldPath('foo.bar', hit)).toEqual({ - foo: [{ bar: ['baz'] }], - }); - }); - - it('builds intermediate objects for nested fields', () => { - // @ts-expect-error nestedHit is minimal - const nestedHit: EventHit = { - fields: { - 'foo.bar': [ - { - baz: ['host.name'], - }, - ], - }, - }; - expect(buildObjectForFieldPath('foo.bar.baz', nestedHit)).toEqual({ - foo: { - bar: [ - { - baz: ['host.name'], - }, - ], - }, - }); - }); - - it('builds intermediate objects at multiple levels', () => { - expect(buildObjectForFieldPath('threat.indicator.matched.atomic', eventHit)).toEqual({ - threat: { - indicator: [ - { - matched: { - atomic: ['matched_atomic'], - }, - }, - { - matched: { - atomic: ['matched_atomic_2'], - }, - }, - ], - }, - }); - }); - - it('preserves multiple values for a single leaf', () => { - expect(buildObjectForFieldPath('threat.indicator.matched.field', eventHit)).toEqual({ - threat: { - indicator: [ - { - matched: { - field: ['matched_field', 'other_matched_field'], - }, - }, - { - matched: { - field: ['matched_field_2'], - }, - }, - ], - }, - }); - }); - - describe('multiple levels of nested fields', () => { - let nestedHit: EventHit; - - beforeEach(() => { - // @ts-expect-error nestedHit is minimal - nestedHit = { - fields: { - 'nested_1.foo': [ - { - 'nested_2.bar': [ - { leaf: ['leaf_value'], leaf_2: ['leaf_2_value'] }, - { leaf_2: ['leaf_2_value_2', 'leaf_2_value_3'] }, - ], - }, - { - 'nested_2.bar': [ - { leaf: ['leaf_value_2'], leaf_2: ['leaf_2_value_4'] }, - { leaf: ['leaf_value_3'], leaf_2: ['leaf_2_value_5'] }, - ], - }, - ], - }, - }; - }); - - it('includes objects without the field', () => { - expect(buildObjectForFieldPath('nested_1.foo.nested_2.bar.leaf', nestedHit)).toEqual({ - nested_1: { - foo: [ - { - nested_2: { - bar: [{ leaf: ['leaf_value'] }, { leaf: [] }], - }, - }, - { - nested_2: { - bar: [{ leaf: ['leaf_value_2'] }, { leaf: ['leaf_value_3'] }], - }, - }, - ], - }, - }); - }); - - it('groups multiple leaf values', () => { - expect(buildObjectForFieldPath('nested_1.foo.nested_2.bar.leaf_2', nestedHit)).toEqual({ - nested_1: { - foo: [ - { - nested_2: { - bar: [ - { leaf_2: ['leaf_2_value'] }, - { leaf_2: ['leaf_2_value_2', 'leaf_2_value_3'] }, - ], - }, - }, - { - nested_2: { - bar: [{ leaf_2: ['leaf_2_value_4'] }, { leaf_2: ['leaf_2_value_5'] }], - }, - }, - ], - }, - }); - }); - }); - }); - - describe('#buildFieldsRequest', () => { - it('happy path', async () => { - const res = await buildFieldsRequest([ - '@timestamp', - 'host.name', - 'destination.ip', - 'source.ip', - 'source.geo.location', - 'threat.indicator.matched.field', - ]); - expect(res).toEqual([ - { - field: '@timestamp', - include_unmapped: true, - }, - { - field: 'host.name', - include_unmapped: true, - }, - { - field: 'destination.ip', - include_unmapped: true, - }, - { - field: 'source.ip', - include_unmapped: true, - }, - { - field: 'source.geo.location', - include_unmapped: true, - }, - { - field: 'threat.indicator.matched.field', - include_unmapped: true, - }, - { - field: 'signal.status', - include_unmapped: true, - }, - { - field: 'signal.group.id', - include_unmapped: true, - }, - { - field: 'signal.original_time', - include_unmapped: true, - }, - { - field: 'signal.rule.filters', - include_unmapped: true, - }, - { - field: 'signal.rule.from', - include_unmapped: true, - }, - { - field: 'signal.rule.language', - include_unmapped: true, - }, - { - field: 'signal.rule.query', - include_unmapped: true, - }, - { - field: 'signal.rule.name', - include_unmapped: true, - }, - { - field: 'signal.rule.to', - include_unmapped: true, - }, - { - field: 'signal.rule.id', - include_unmapped: true, - }, - { - field: 'signal.rule.index', - include_unmapped: true, - }, - { - field: 'signal.rule.type', - include_unmapped: true, - }, - { - field: 'signal.original_event.kind', - include_unmapped: true, - }, - { - field: 'signal.original_event.module', - include_unmapped: true, - }, - { - field: 'signal.rule.version', - include_unmapped: true, - }, - { - field: 'signal.rule.severity', - include_unmapped: true, - }, - { - field: 'signal.rule.risk_score', - include_unmapped: true, - }, - { - field: 'signal.threshold_result', - include_unmapped: true, - }, - { - field: 'event.code', - include_unmapped: true, - }, - { - field: 'event.module', - include_unmapped: true, - }, - { - field: 'event.action', - include_unmapped: true, - }, - { - field: 'event.category', - include_unmapped: true, - }, - { - field: 'user.name', - include_unmapped: true, - }, - { - field: 'message', - include_unmapped: true, - }, - { - field: 'system.auth.ssh.signature', - include_unmapped: true, - }, - { - field: 'system.auth.ssh.method', - include_unmapped: true, - }, - { - field: 'system.audit.package.arch', - include_unmapped: true, - }, - { - field: 'system.audit.package.entity_id', - include_unmapped: true, - }, - { - field: 'system.audit.package.name', - include_unmapped: true, - }, - { - field: 'system.audit.package.size', - include_unmapped: true, - }, - { - field: 'system.audit.package.summary', - include_unmapped: true, - }, - { - field: 'system.audit.package.version', - include_unmapped: true, - }, - { - field: 'event.created', - include_unmapped: true, - }, - { - field: 'event.dataset', - include_unmapped: true, - }, - { - field: 'event.duration', - include_unmapped: true, - }, - { - field: 'event.end', - include_unmapped: true, - }, - { - field: 'event.hash', - include_unmapped: true, - }, - { - field: 'event.id', - include_unmapped: true, - }, - { - field: 'event.kind', - include_unmapped: true, - }, - { - field: 'event.original', - include_unmapped: true, - }, - { - field: 'event.outcome', - include_unmapped: true, - }, - { - field: 'event.risk_score', - include_unmapped: true, - }, - { - field: 'event.risk_score_norm', - include_unmapped: true, - }, - { - field: 'event.severity', - include_unmapped: true, - }, - { - field: 'event.start', - include_unmapped: true, - }, - { - field: 'event.timezone', - include_unmapped: true, - }, - { - field: 'event.type', - include_unmapped: true, - }, - { - field: 'agent.type', - include_unmapped: true, - }, - { - field: 'auditd.result', - include_unmapped: true, - }, - { - field: 'auditd.session', - include_unmapped: true, - }, - { - field: 'auditd.data.acct', - include_unmapped: true, - }, - { - field: 'auditd.data.terminal', - include_unmapped: true, - }, - { - field: 'auditd.data.op', - include_unmapped: true, - }, - { - field: 'auditd.summary.actor.primary', - include_unmapped: true, - }, - { - field: 'auditd.summary.actor.secondary', - include_unmapped: true, - }, - { - field: 'auditd.summary.object.primary', - include_unmapped: true, - }, - { - field: 'auditd.summary.object.secondary', - include_unmapped: true, - }, - { - field: 'auditd.summary.object.type', - include_unmapped: true, - }, - { - field: 'auditd.summary.how', - include_unmapped: true, - }, - { - field: 'auditd.summary.message_type', - include_unmapped: true, - }, - { - field: 'auditd.summary.sequence', - include_unmapped: true, - }, - { - field: 'file.Ext.original.path', - include_unmapped: true, - }, - { - field: 'file.name', - include_unmapped: true, - }, - { - field: 'file.target_path', - include_unmapped: true, - }, - { - field: 'file.extension', - include_unmapped: true, - }, - { - field: 'file.type', - include_unmapped: true, - }, - { - field: 'file.device', - include_unmapped: true, - }, - { - field: 'file.inode', - include_unmapped: true, - }, - { - field: 'file.uid', - include_unmapped: true, - }, - { - field: 'file.owner', - include_unmapped: true, - }, - { - field: 'file.gid', - include_unmapped: true, - }, - { - field: 'file.group', - include_unmapped: true, - }, - { - field: 'file.mode', - include_unmapped: true, - }, - { - field: 'file.size', - include_unmapped: true, - }, - { - field: 'file.mtime', - include_unmapped: true, - }, - { - field: 'file.ctime', - include_unmapped: true, - }, - { - field: 'file.path', - include_unmapped: true, - }, - { - field: 'file.Ext.code_signature', - include_unmapped: true, - }, - { - field: 'file.Ext.code_signature.subject_name', - include_unmapped: true, - }, - { - field: 'file.Ext.code_signature.trusted', - include_unmapped: true, - }, - { - field: 'file.hash.sha256', - include_unmapped: true, - }, - { - field: 'host.os.family', - include_unmapped: true, - }, - { - field: 'host.id', - include_unmapped: true, - }, - { - field: 'host.ip', - include_unmapped: true, - }, - { - field: 'registry.key', - include_unmapped: true, - }, - { - field: 'registry.path', - include_unmapped: true, - }, - { - field: 'rule.reference', - include_unmapped: true, - }, - { - field: 'source.bytes', - include_unmapped: true, - }, - { - field: 'source.packets', - include_unmapped: true, - }, - { - field: 'source.port', - include_unmapped: true, - }, - { - field: 'source.geo.continent_name', - include_unmapped: true, - }, - { - field: 'source.geo.country_name', - include_unmapped: true, - }, - { - field: 'source.geo.country_iso_code', - include_unmapped: true, - }, - { - field: 'source.geo.city_name', - include_unmapped: true, - }, - { - field: 'source.geo.region_iso_code', - include_unmapped: true, - }, - { - field: 'source.geo.region_name', - include_unmapped: true, - }, - { - field: 'destination.bytes', - include_unmapped: true, - }, - { - field: 'destination.packets', - include_unmapped: true, - }, - { - field: 'destination.port', - include_unmapped: true, - }, - { - field: 'destination.geo.continent_name', - include_unmapped: true, - }, - { - field: 'destination.geo.country_name', - include_unmapped: true, - }, - { - field: 'destination.geo.country_iso_code', - include_unmapped: true, - }, - { - field: 'destination.geo.city_name', - include_unmapped: true, - }, - { - field: 'destination.geo.region_iso_code', - include_unmapped: true, - }, - { - field: 'destination.geo.region_name', - include_unmapped: true, - }, - { - field: 'dns.question.name', - include_unmapped: true, - }, - { - field: 'dns.question.type', - include_unmapped: true, - }, - { - field: 'dns.resolved_ip', - include_unmapped: true, - }, - { - field: 'dns.response_code', - include_unmapped: true, - }, - { - field: 'endgame.exit_code', - include_unmapped: true, - }, - { - field: 'endgame.file_name', - include_unmapped: true, - }, - { - field: 'endgame.file_path', - include_unmapped: true, - }, - { - field: 'endgame.logon_type', - include_unmapped: true, - }, - { - field: 'endgame.parent_process_name', - include_unmapped: true, - }, - { - field: 'endgame.pid', - include_unmapped: true, - }, - { - field: 'endgame.process_name', - include_unmapped: true, - }, - { - field: 'endgame.subject_domain_name', - include_unmapped: true, - }, - { - field: 'endgame.subject_logon_id', - include_unmapped: true, - }, - { - field: 'endgame.subject_user_name', - include_unmapped: true, - }, - { - field: 'endgame.target_domain_name', - include_unmapped: true, - }, - { - field: 'endgame.target_logon_id', - include_unmapped: true, - }, - { - field: 'endgame.target_user_name', - include_unmapped: true, - }, - { - field: 'signal.rule.saved_id', - include_unmapped: true, - }, - { - field: 'signal.rule.timeline_id', - include_unmapped: true, - }, - { - field: 'signal.rule.timeline_title', - include_unmapped: true, - }, - { - field: 'signal.rule.output_index', - include_unmapped: true, - }, - { - field: 'signal.rule.note', - include_unmapped: true, - }, - { - field: 'signal.rule.threshold', - include_unmapped: true, - }, - { - field: 'signal.rule.exceptions_list', - include_unmapped: true, - }, - { - field: 'signal.rule.building_block_type', - include_unmapped: true, - }, - { - field: 'suricata.eve.proto', - include_unmapped: true, - }, - { - field: 'suricata.eve.flow_id', - include_unmapped: true, - }, - { - field: 'suricata.eve.alert.signature', - include_unmapped: true, - }, - { - field: 'suricata.eve.alert.signature_id', - include_unmapped: true, - }, - { - field: 'network.bytes', - include_unmapped: true, - }, - { - field: 'network.community_id', - include_unmapped: true, - }, - { - field: 'network.direction', - include_unmapped: true, - }, - { - field: 'network.packets', - include_unmapped: true, - }, - { - field: 'network.protocol', - include_unmapped: true, - }, - { - field: 'network.transport', - include_unmapped: true, - }, - { - field: 'http.version', - include_unmapped: true, - }, - { - field: 'http.request.method', - include_unmapped: true, - }, - { - field: 'http.request.body.bytes', - include_unmapped: true, - }, - { - field: 'http.request.body.content', - include_unmapped: true, - }, - { - field: 'http.request.referrer', - include_unmapped: true, - }, - { - field: 'http.response.status_code', - include_unmapped: true, - }, - { - field: 'http.response.body.bytes', - include_unmapped: true, - }, - { - field: 'http.response.body.content', - include_unmapped: true, - }, - { - field: 'tls.client_certificate.fingerprint.sha1', - include_unmapped: true, - }, - { - field: 'tls.fingerprints.ja3.hash', - include_unmapped: true, - }, - { - field: 'tls.server_certificate.fingerprint.sha1', - include_unmapped: true, - }, - { - field: 'user.domain', - include_unmapped: true, - }, - { - field: 'winlog.event_id', - include_unmapped: true, - }, - { - field: 'process.exit_code', - include_unmapped: true, - }, - { - field: 'process.hash.md5', - include_unmapped: true, - }, - { - field: 'process.hash.sha1', - include_unmapped: true, - }, - { - field: 'process.hash.sha256', - include_unmapped: true, - }, - { - field: 'process.parent.name', - include_unmapped: true, - }, - { - field: 'process.parent.pid', - include_unmapped: true, - }, - { - field: 'process.pid', - include_unmapped: true, - }, - { - field: 'process.name', - include_unmapped: true, - }, - { - field: 'process.ppid', - include_unmapped: true, - }, - { - field: 'process.args', - include_unmapped: true, - }, - { - field: 'process.entity_id', - include_unmapped: true, - }, - { - field: 'process.executable', - include_unmapped: true, - }, - { - field: 'process.title', - include_unmapped: true, - }, - { - field: 'process.working_directory', - include_unmapped: true, - }, - { - field: 'zeek.session_id', - include_unmapped: true, - }, - { - field: 'zeek.connection.local_resp', - include_unmapped: true, - }, - { - field: 'zeek.connection.local_orig', - include_unmapped: true, - }, - { - field: 'zeek.connection.missed_bytes', - include_unmapped: true, - }, - { - field: 'zeek.connection.state', - include_unmapped: true, - }, - { - field: 'zeek.connection.history', - include_unmapped: true, - }, - { - field: 'zeek.notice.suppress_for', - include_unmapped: true, - }, - { - field: 'zeek.notice.msg', - include_unmapped: true, - }, - { - field: 'zeek.notice.note', - include_unmapped: true, - }, - { - field: 'zeek.notice.sub', - include_unmapped: true, - }, - { - field: 'zeek.notice.dst', - include_unmapped: true, - }, - { - field: 'zeek.notice.dropped', - include_unmapped: true, - }, - { - field: 'zeek.notice.peer_descr', - include_unmapped: true, - }, - { - field: 'zeek.dns.AA', - include_unmapped: true, - }, - { - field: 'zeek.dns.qclass_name', - include_unmapped: true, - }, - { - field: 'zeek.dns.RD', - include_unmapped: true, - }, - { - field: 'zeek.dns.qtype_name', - include_unmapped: true, - }, - { - field: 'zeek.dns.qtype', - include_unmapped: true, - }, - { - field: 'zeek.dns.query', - include_unmapped: true, - }, - { - field: 'zeek.dns.trans_id', - include_unmapped: true, - }, - { - field: 'zeek.dns.qclass', - include_unmapped: true, - }, - { - field: 'zeek.dns.RA', - include_unmapped: true, - }, - { - field: 'zeek.dns.TC', - include_unmapped: true, - }, - { - field: 'zeek.http.resp_mime_types', - include_unmapped: true, - }, - { - field: 'zeek.http.trans_depth', - include_unmapped: true, - }, - { - field: 'zeek.http.status_msg', - include_unmapped: true, - }, - { - field: 'zeek.http.resp_fuids', - include_unmapped: true, - }, - { - field: 'zeek.http.tags', - include_unmapped: true, - }, - { - field: 'zeek.files.session_ids', - include_unmapped: true, - }, - { - field: 'zeek.files.timedout', - include_unmapped: true, - }, - { - field: 'zeek.files.local_orig', - include_unmapped: true, - }, - { - field: 'zeek.files.tx_host', - include_unmapped: true, - }, - { - field: 'zeek.files.source', - include_unmapped: true, - }, - { - field: 'zeek.files.is_orig', - include_unmapped: true, - }, - { - field: 'zeek.files.overflow_bytes', - include_unmapped: true, - }, - { - field: 'zeek.files.sha1', - include_unmapped: true, - }, - { - field: 'zeek.files.duration', - include_unmapped: true, - }, - { - field: 'zeek.files.depth', - include_unmapped: true, - }, - { - field: 'zeek.files.analyzers', - include_unmapped: true, - }, - { - field: 'zeek.files.mime_type', - include_unmapped: true, - }, - { - field: 'zeek.files.rx_host', - include_unmapped: true, - }, - { - field: 'zeek.files.total_bytes', - include_unmapped: true, - }, - { - field: 'zeek.files.fuid', - include_unmapped: true, - }, - { - field: 'zeek.files.seen_bytes', - include_unmapped: true, - }, - { - field: 'zeek.files.missing_bytes', - include_unmapped: true, - }, - { - field: 'zeek.files.md5', - include_unmapped: true, - }, - { - field: 'zeek.ssl.cipher', - include_unmapped: true, - }, - { - field: 'zeek.ssl.established', - include_unmapped: true, - }, - { - field: 'zeek.ssl.resumed', - include_unmapped: true, - }, - { - field: 'zeek.ssl.version', - include_unmapped: true, - }, - { - field: 'threat.indicator.matched.atomic', - include_unmapped: true, - }, - { - field: 'threat.indicator.matched.type', - include_unmapped: true, - }, - { - field: 'threat.indicator.event.dataset', - include_unmapped: true, - }, - { - field: 'threat.indicator.event.reference', - include_unmapped: true, - }, - { - field: 'threat.indicator.provider', - include_unmapped: true, - }, - ]); - }); - - it('remove internal attributes starting with _', async () => { - const res = await buildFieldsRequest([ - '@timestamp', - '_id', - 'host.name', - 'destination.ip', - 'source.ip', - 'source.geo.location', - '_type', - 'threat.indicator.matched.field', - ]); - expect(res.some((f) => f.field === '_id')).toEqual(false); - expect(res.some((f) => f.field === '_type')).toEqual(false); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index bebfd9ca88c23..0df41b9f988b7 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -42,5 +42,6 @@ { "path": "../ml/tsconfig.json" }, { "path": "../spaces/tsconfig.json" }, { "path": "../security/tsconfig.json"}, + { "path": "../timelines/tsconfig.json"}, ] } diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index fa387ddc151fc..39852ebaeb46b 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -31,6 +31,9 @@ "__index": { "type": "long" }, + "__swimlane": { + "type": "long" + }, "__pagerduty": { "type": "long" }, @@ -68,6 +71,9 @@ "__index": { "type": "long" }, + "__swimlane": { + "type": "long" + }, "__pagerduty": { "type": "long" }, @@ -1924,9 +1930,6 @@ "create_first_engine_button": { "type": "long" }, - "header_launch_button": { - "type": "long" - }, "engine_table_link": { "type": "long" } diff --git a/x-pack/plugins/timelines/README.md b/x-pack/plugins/timelines/README.md index 441a505903698..0c14953837d02 100644 --- a/x-pack/plugins/timelines/README.md +++ b/x-pack/plugins/timelines/README.md @@ -3,9 +3,9 @@ Timelines is a plugin that provides a grid component with accompanying server si ## Using timelines in another plugin -- Add `TimelinesPluginSetup` to Kibana plugin `SetupServices` dependencies: +- Add `TimelinesPluginUI` to Kibana plugin `SetupServices` dependencies: ```ts -timelines: TimelinesPluginSetup; +timelines: TimelinesPluginUI; ``` - Once `timelines` is added as a required plugin in the consuming plugin's kibana.json, timeline functionality will be available as any other kibana plugin, ie PluginSetupDependencies.timelines.getTimeline() diff --git a/x-pack/plugins/timelines/common/constants.ts b/x-pack/plugins/timelines/common/constants.ts new file mode 100644 index 0000000000000..86ff9d501f148 --- /dev/null +++ b/x-pack/plugins/timelines/common/constants.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 const DEFAULT_MAX_TABLE_QUERY_SIZE = 10000; diff --git a/x-pack/plugins/timelines/common/ecs/agent/index.ts b/x-pack/plugins/timelines/common/ecs/agent/index.ts new file mode 100644 index 0000000000000..2332b60f1a3ca --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/agent/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 interface AgentEcs { + type?: string[]; +} diff --git a/x-pack/plugins/timelines/common/ecs/auditd/index.ts b/x-pack/plugins/timelines/common/ecs/auditd/index.ts new file mode 100644 index 0000000000000..f210f8862dc44 --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/auditd/index.ts @@ -0,0 +1,46 @@ +/* + * 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 interface AuditdEcs { + result?: string[]; + + session?: string[]; + + data?: AuditdDataEcs; + + summary?: SummaryEcs; + + sequence?: string[]; +} + +export interface AuditdDataEcs { + acct?: string[]; + + terminal?: string[]; + + op?: string[]; +} + +export interface SummaryEcs { + actor?: PrimarySecondaryEcs; + + object?: PrimarySecondaryEcs; + + how?: string[]; + + message_type?: string[]; + + sequence?: string[]; +} + +export interface PrimarySecondaryEcs { + primary?: string[]; + + secondary?: string[]; + + type?: string[]; +} diff --git a/x-pack/plugins/timelines/common/ecs/cloud/index.ts b/x-pack/plugins/timelines/common/ecs/cloud/index.ts new file mode 100644 index 0000000000000..a169e5561c6b6 --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/cloud/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. + */ + +export interface CloudEcs { + instance?: CloudInstanceEcs; + machine?: CloudMachineEcs; + provider?: string[]; + region?: string[]; +} + +export interface CloudMachineEcs { + type?: string[]; +} + +export interface CloudInstanceEcs { + id?: string[]; +} diff --git a/x-pack/plugins/timelines/common/ecs/destination/index.ts b/x-pack/plugins/timelines/common/ecs/destination/index.ts new file mode 100644 index 0000000000000..2d3b6154276b9 --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/destination/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { GeoEcs } from '../geo'; + +export interface DestinationEcs { + bytes?: number[]; + + ip?: string[]; + + port?: number[]; + + domain?: string[]; + + geo?: GeoEcs; + + packets?: number[]; +} diff --git a/x-pack/plugins/timelines/common/ecs/dns/index.ts b/x-pack/plugins/timelines/common/ecs/dns/index.ts new file mode 100644 index 0000000000000..e0f142d9cf57a --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/dns/index.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. + */ + +export interface DnsEcs { + question?: DnsQuestionEcs; + + resolved_ip?: string[]; + + response_code?: string[]; +} + +export interface DnsQuestionEcs { + name?: string[]; + + type?: string[]; +} diff --git a/x-pack/plugins/timelines/common/ecs/ecs_fields/extend_map.test.ts b/x-pack/plugins/timelines/common/ecs/ecs_fields/extend_map.test.ts new file mode 100644 index 0000000000000..e27b15f021257 --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/ecs_fields/extend_map.test.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { extendMap } from './extend_map'; + +describe('ecs_fields test', () => { + describe('extendMap', () => { + test('it should extend a record', () => { + const osFieldsMap: Readonly<Record<string, string>> = { + 'os.platform': 'os.platform', + 'os.full': 'os.full', + 'os.family': 'os.family', + 'os.version': 'os.version', + 'os.kernel': 'os.kernel', + }; + const expected: Record<string, string> = { + 'host.os.family': 'host.os.family', + 'host.os.full': 'host.os.full', + 'host.os.kernel': 'host.os.kernel', + 'host.os.platform': 'host.os.platform', + 'host.os.version': 'host.os.version', + }; + expect(extendMap('host', osFieldsMap)).toEqual(expected); + }); + + test('it should extend a sample hosts record', () => { + const hostMap: Record<string, string> = { + 'host.id': 'host.id', + 'host.ip': 'host.ip', + 'host.name': 'host.name', + }; + const osFieldsMap: Readonly<Record<string, string>> = { + 'os.platform': 'os.platform', + 'os.full': 'os.full', + 'os.family': 'os.family', + 'os.version': 'os.version', + 'os.kernel': 'os.kernel', + }; + const expected: Record<string, string> = { + 'host.id': 'host.id', + 'host.ip': 'host.ip', + 'host.name': 'host.name', + 'host.os.family': 'host.os.family', + 'host.os.full': 'host.os.full', + 'host.os.kernel': 'host.os.kernel', + 'host.os.platform': 'host.os.platform', + 'host.os.version': 'host.os.version', + }; + const output = { ...hostMap, ...extendMap('host', osFieldsMap) }; + expect(output).toEqual(expected); + }); + }); +}); diff --git a/x-pack/plugins/timelines/common/ecs/ecs_fields/extend_map.ts b/x-pack/plugins/timelines/common/ecs/ecs_fields/extend_map.ts new file mode 100644 index 0000000000000..184e6b4f32566 --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/ecs_fields/extend_map.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 const extendMap = ( + path: string, + map: Readonly<Record<string, string>> +): Readonly<Record<string, string>> => + Object.entries(map).reduce<Record<string, string>>((accum, [key, value]) => { + accum[`${path}.${key}`] = `${path}.${value}`; + return accum; + }, {}); diff --git a/x-pack/plugins/timelines/common/ecs/ecs_fields/index.ts b/x-pack/plugins/timelines/common/ecs/ecs_fields/index.ts new file mode 100644 index 0000000000000..292822019fc9c --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/ecs_fields/index.ts @@ -0,0 +1,359 @@ +/* + * 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 { extendMap } from './extend_map'; + +export const auditdMap: Readonly<Record<string, string>> = { + 'auditd.result': 'auditd.result', + 'auditd.session': 'auditd.session', + 'auditd.data.acct': 'auditd.data.acct', + 'auditd.data.terminal': 'auditd.data.terminal', + 'auditd.data.op': 'auditd.data.op', + 'auditd.summary.actor.primary': 'auditd.summary.actor.primary', + 'auditd.summary.actor.secondary': 'auditd.summary.actor.secondary', + 'auditd.summary.object.primary': 'auditd.summary.object.primary', + 'auditd.summary.object.secondary': 'auditd.summary.object.secondary', + 'auditd.summary.object.type': 'auditd.summary.object.type', + 'auditd.summary.how': 'auditd.summary.how', + 'auditd.summary.message_type': 'auditd.summary.message_type', + 'auditd.summary.sequence': 'auditd.summary.sequence', +}; + +export const cloudFieldsMap: Readonly<Record<string, string>> = { + 'cloud.account.id': 'cloud.account.id', + 'cloud.availability_zone': 'cloud.availability_zone', + 'cloud.instance.id': 'cloud.instance.id', + 'cloud.instance.name': 'cloud.instance.name', + 'cloud.machine.type': 'cloud.machine.type', + 'cloud.provider': 'cloud.provider', + 'cloud.region': 'cloud.region', +}; + +export const fileMap: Readonly<Record<string, string>> = { + 'file.name': 'file.name', + 'file.path': 'file.path', + 'file.target_path': 'file.target_path', + 'file.extension': 'file.extension', + 'file.type': 'file.type', + 'file.device': 'file.device', + 'file.inode': 'file.inode', + 'file.uid': 'file.uid', + 'file.owner': 'file.owner', + 'file.gid': 'file.gid', + 'file.group': 'file.group', + 'file.mode': 'file.mode', + 'file.size': 'file.size', + 'file.mtime': 'file.mtime', + 'file.ctime': 'file.ctime', +}; + +export const osFieldsMap: Readonly<Record<string, string>> = { + 'os.platform': 'os.platform', + 'os.name': 'os.name', + 'os.full': 'os.full', + 'os.family': 'os.family', + 'os.version': 'os.version', + 'os.kernel': 'os.kernel', +}; + +export const hostFieldsMap: Readonly<Record<string, string>> = { + 'host.architecture': 'host.architecture', + 'host.id': 'host.id', + 'host.ip': 'host.ip', + 'host.mac': 'host.mac', + 'host.name': 'host.name', + ...extendMap('host', osFieldsMap), +}; + +export const processFieldsMap: Readonly<Record<string, string>> = { + 'process.hash.md5': 'process.hash.md5', + 'process.hash.sha1': 'process.hash.sha1', + 'process.hash.sha256': 'process.hash.sha256', + 'process.pid': 'process.pid', + 'process.name': 'process.name', + 'process.ppid': 'process.ppid', + 'process.args': 'process.args', + 'process.entity_id': 'process.entity_id', + 'process.executable': 'process.executable', + 'process.title': 'process.title', + 'process.thread': 'process.thread', + 'process.working_directory': 'process.working_directory', +}; + +export const agentFieldsMap: Readonly<Record<string, string>> = { + 'agent.type': 'agent.type', +}; + +export const userFieldsMap: Readonly<Record<string, string>> = { + 'user.domain': 'user.domain', + 'user.id': 'user.id', + 'user.name': 'user.name', + // NOTE: This field is not tested and available from ECS. Please remove this tag once it is + 'user.full_name': 'user.full_name', + // NOTE: This field is not tested and available from ECS. Please remove this tag once it is + 'user.email': 'user.email', + // NOTE: This field is not tested and available from ECS. Please remove this tag once it is + 'user.hash': 'user.hash', + // NOTE: This field is not tested and available from ECS. Please remove this tag once it is + 'user.group': 'user.group', +}; + +export const winlogFieldsMap: Readonly<Record<string, string>> = { + 'winlog.event_id': 'winlog.event_id', +}; + +export const suricataFieldsMap: Readonly<Record<string, string>> = { + 'suricata.eve.flow_id': 'suricata.eve.flow_id', + 'suricata.eve.proto': 'suricata.eve.proto', + 'suricata.eve.alert.signature': 'suricata.eve.alert.signature', + 'suricata.eve.alert.signature_id': 'suricata.eve.alert.signature_id', +}; + +export const tlsFieldsMap: Readonly<Record<string, string>> = { + 'tls.client_certificate.fingerprint.sha1': 'tls.client_certificate.fingerprint.sha1', + 'tls.fingerprints.ja3.hash': 'tls.fingerprints.ja3.hash', + 'tls.server_certificate.fingerprint.sha1': 'tls.server_certificate.fingerprint.sha1', +}; + +export const urlFieldsMap: Readonly<Record<string, string>> = { + 'url.original': 'url.original', + 'url.domain': 'url.domain', + 'user.username': 'user.username', + 'user.password': 'user.password', +}; + +export const httpFieldsMap: Readonly<Record<string, string>> = { + 'http.version': 'http.version', + 'http.request': 'http.request', + 'http.request.method': 'http.request.method', + 'http.request.body.bytes': 'http.request.body.bytes', + 'http.request.body.content': 'http.request.body.content', + 'http.request.referrer': 'http.request.referrer', + 'http.response.status_code': 'http.response.status_code', + 'http.response.body': 'http.response.body', + 'http.response.body.bytes': 'http.response.body.bytes', + 'http.response.body.content': 'http.response.body.content', +}; + +export const zeekFieldsMap: Readonly<Record<string, string>> = { + 'zeek.session_id': 'zeek.session_id', + 'zeek.connection.local_resp': 'zeek.connection.local_resp', + 'zeek.connection.local_orig': 'zeek.connection.local_orig', + 'zeek.connection.missed_bytes': 'zeek.connection.missed_bytes', + 'zeek.connection.state': 'zeek.connection.state', + 'zeek.connection.history': 'zeek.connection.history', + 'zeek.notice.suppress_for': 'zeek.notice.suppress_for', + 'zeek.notice.msg': 'zeek.notice.msg', + 'zeek.notice.note': 'zeek.notice.note', + 'zeek.notice.sub': 'zeek.notice.sub', + 'zeek.notice.dst': 'zeek.notice.dst', + 'zeek.notice.dropped': 'zeek.notice.dropped', + 'zeek.notice.peer_descr': 'zeek.notice.peer_descr', + 'zeek.dns.AA': 'zeek.dns.AA', + 'zeek.dns.qclass_name': 'zeek.dns.qclass_name', + 'zeek.dns.RD': 'zeek.dns.RD', + 'zeek.dns.qtype_name': 'zeek.dns.qtype_name', + 'zeek.dns.qtype': 'zeek.dns.qtype', + 'zeek.dns.query': 'zeek.dns.query', + 'zeek.dns.trans_id': 'zeek.dns.trans_id', + 'zeek.dns.qclass': 'zeek.dns.qclass', + 'zeek.dns.RA': 'zeek.dns.RA', + 'zeek.dns.TC': 'zeek.dns.TC', + 'zeek.http.resp_mime_types': 'zeek.http.resp_mime_types', + 'zeek.http.trans_depth': 'zeek.http.trans_depth', + 'zeek.http.status_msg': 'zeek.http.status_msg', + 'zeek.http.resp_fuids': 'zeek.http.resp_fuids', + 'zeek.http.tags': 'zeek.http.tags', + 'zeek.files.session_ids': 'zeek.files.session_ids', + 'zeek.files.timedout': 'zeek.files.timedout', + 'zeek.files.local_orig': 'zeek.files.local_orig', + 'zeek.files.tx_host': 'zeek.files.tx_host', + 'zeek.files.source': 'zeek.files.source', + 'zeek.files.is_orig': 'zeek.files.is_orig', + 'zeek.files.overflow_bytes': 'zeek.files.overflow_bytes', + 'zeek.files.sha1': 'zeek.files.sha1', + 'zeek.files.duration': 'zeek.files.duration', + 'zeek.files.depth': 'zeek.files.depth', + 'zeek.files.analyzers': 'zeek.files.analyzers', + 'zeek.files.mime_type': 'zeek.files.mime_type', + 'zeek.files.rx_host': 'zeek.files.rx_host', + 'zeek.files.total_bytes': 'zeek.files.total_bytes', + 'zeek.files.fuid': 'zeek.files.fuid', + 'zeek.files.seen_bytes': 'zeek.files.seen_bytes', + 'zeek.files.missing_bytes': 'zeek.files.missing_bytes', + 'zeek.files.md5': 'zeek.files.md5', + 'zeek.ssl.cipher': 'zeek.ssl.cipher', + 'zeek.ssl.established': 'zeek.ssl.established', + 'zeek.ssl.resumed': 'zeek.ssl.resumed', + 'zeek.ssl.version': 'zeek.ssl.version', +}; + +export const sourceFieldsMap: Readonly<Record<string, string>> = { + 'source.bytes': 'source.bytes', + 'source.ip': 'source.ip', + 'source.packets': 'source.packets', + 'source.port': 'source.port', + 'source.domain': 'source.domain', + 'source.geo.continent_name': 'source.geo.continent_name', + 'source.geo.country_name': 'source.geo.country_name', + 'source.geo.country_iso_code': 'source.geo.country_iso_code', + 'source.geo.city_name': 'source.geo.city_name', + 'source.geo.region_iso_code': 'source.geo.region_iso_code', + 'source.geo.region_name': 'source.geo.region_name', +}; + +export const destinationFieldsMap: Readonly<Record<string, string>> = { + 'destination.bytes': 'destination.bytes', + 'destination.ip': 'destination.ip', + 'destination.packets': 'destination.packets', + 'destination.port': 'destination.port', + 'destination.domain': 'destination.domain', + 'destination.geo.continent_name': 'destination.geo.continent_name', + 'destination.geo.country_name': 'destination.geo.country_name', + 'destination.geo.country_iso_code': 'destination.geo.country_iso_code', + 'destination.geo.city_name': 'destination.geo.city_name', + 'destination.geo.region_iso_code': 'destination.geo.region_iso_code', + 'destination.geo.region_name': 'destination.geo.region_name', +}; + +export const networkFieldsMap: Readonly<Record<string, string>> = { + 'network.bytes': 'network.bytes', + 'network.community_id': 'network.community_id', + 'network.direction': 'network.direction', + 'network.packets': 'network.packets', + 'network.protocol': 'network.protocol', + 'network.transport': 'network.transport', +}; + +export const geoFieldsMap: Readonly<Record<string, string>> = { + 'geo.region_name': 'destination.geo.region_name', + 'geo.country_iso_code': 'destination.geo.country_iso_code', +}; + +export const dnsFieldsMap: Readonly<Record<string, string>> = { + 'dns.question.name': 'dns.question.name', + 'dns.question.type': 'dns.question.type', + 'dns.resolved_ip': 'dns.resolved_ip', + 'dns.response_code': 'dns.response_code', +}; + +export const endgameFieldsMap: Readonly<Record<string, string>> = { + 'endgame.exit_code': 'endgame.exit_code', + 'endgame.file_name': 'endgame.file_name', + 'endgame.file_path': 'endgame.file_path', + 'endgame.logon_type': 'endgame.logon_type', + 'endgame.parent_process_name': 'endgame.parent_process_name', + 'endgame.pid': 'endgame.pid', + 'endgame.process_name': 'endgame.process_name', + 'endgame.subject_domain_name': 'endgame.subject_domain_name', + 'endgame.subject_logon_id': 'endgame.subject_logon_id', + 'endgame.subject_user_name': 'endgame.subject_user_name', + 'endgame.target_domain_name': 'endgame.target_domain_name', + 'endgame.target_logon_id': 'endgame.target_logon_id', + 'endgame.target_user_name': 'endgame.target_user_name', +}; + +export const eventBaseFieldsMap: Readonly<Record<string, string>> = { + 'event.action': 'event.action', + 'event.category': 'event.category', + 'event.code': 'event.code', + 'event.created': 'event.created', + 'event.dataset': 'event.dataset', + 'event.duration': 'event.duration', + 'event.end': 'event.end', + 'event.hash': 'event.hash', + 'event.id': 'event.id', + 'event.kind': 'event.kind', + 'event.module': 'event.module', + 'event.original': 'event.original', + 'event.outcome': 'event.outcome', + 'event.risk_score': 'event.risk_score', + 'event.risk_score_norm': 'event.risk_score_norm', + 'event.severity': 'event.severity', + 'event.start': 'event.start', + 'event.timezone': 'event.timezone', + 'event.type': 'event.type', +}; + +export const systemFieldsMap: Readonly<Record<string, string>> = { + 'system.audit.package.arch': 'system.audit.package.arch', + 'system.audit.package.entity_id': 'system.audit.package.entity_id', + 'system.audit.package.name': 'system.audit.package.name', + 'system.audit.package.size': 'system.audit.package.size', + 'system.audit.package.summary': 'system.audit.package.summary', + 'system.audit.package.version': 'system.audit.package.version', + 'system.auth.ssh.signature': 'system.auth.ssh.signature', + 'system.auth.ssh.method': 'system.auth.ssh.method', +}; + +export const signalFieldsMap: Readonly<Record<string, string>> = { + 'signal.original_time': 'signal.original_time', + 'signal.rule.id': 'signal.rule.id', + 'signal.rule.saved_id': 'signal.rule.saved_id', + 'signal.rule.timeline_id': 'signal.rule.timeline_id', + 'signal.rule.timeline_title': 'signal.rule.timeline_title', + 'signal.rule.output_index': 'signal.rule.output_index', + 'signal.rule.from': 'signal.rule.from', + 'signal.rule.index': 'signal.rule.index', + 'signal.rule.language': 'signal.rule.language', + 'signal.rule.query': 'signal.rule.query', + 'signal.rule.to': 'signal.rule.to', + 'signal.rule.filters': 'signal.rule.filters', + 'signal.rule.rule_id': 'signal.rule.rule_id', + 'signal.rule.false_positives': 'signal.rule.false_positives', + 'signal.rule.max_signals': 'signal.rule.max_signals', + 'signal.rule.risk_score': 'signal.rule.risk_score', + 'signal.rule.description': 'signal.rule.description', + 'signal.rule.name': 'signal.rule.name', + 'signal.rule.immutable': 'signal.rule.immutable', + 'signal.rule.references': 'signal.rule.references', + 'signal.rule.severity': 'signal.rule.severity', + 'signal.rule.tags': 'signal.rule.tags', + 'signal.rule.threat': 'signal.rule.threat', + 'signal.rule.type': 'signal.rule.type', + 'signal.rule.size': 'signal.rule.size', + 'signal.rule.enabled': 'signal.rule.enabled', + 'signal.rule.created_at': 'signal.rule.created_at', + 'signal.rule.updated_at': 'signal.rule.updated_at', + 'signal.rule.created_by': 'signal.rule.created_by', + 'signal.rule.updated_by': 'signal.rule.updated_by', + 'signal.rule.version': 'signal.rule.version', + 'signal.rule.note': 'signal.rule.note', + 'signal.rule.threshold': 'signal.rule.threshold', + 'signal.rule.exceptions_list': 'signal.rule.exceptions_list', +}; + +export const ruleFieldsMap: Readonly<Record<string, string>> = { + 'rule.reference': 'rule.reference', +}; + +export const eventFieldsMap: Readonly<Record<string, string>> = { + timestamp: '@timestamp', + '@timestamp': '@timestamp', + message: 'message', + ...{ ...agentFieldsMap }, + ...{ ...auditdMap }, + ...{ ...destinationFieldsMap }, + ...{ ...dnsFieldsMap }, + ...{ ...endgameFieldsMap }, + ...{ ...eventBaseFieldsMap }, + ...{ ...fileMap }, + ...{ ...geoFieldsMap }, + ...{ ...hostFieldsMap }, + ...{ ...networkFieldsMap }, + ...{ ...ruleFieldsMap }, + ...{ ...signalFieldsMap }, + ...{ ...sourceFieldsMap }, + ...{ ...suricataFieldsMap }, + ...{ ...systemFieldsMap }, + ...{ ...tlsFieldsMap }, + ...{ ...zeekFieldsMap }, + ...{ ...httpFieldsMap }, + ...{ ...userFieldsMap }, + ...{ ...winlogFieldsMap }, + ...{ ...processFieldsMap }, +}; diff --git a/x-pack/plugins/timelines/common/ecs/endgame/index.ts b/x-pack/plugins/timelines/common/ecs/endgame/index.ts new file mode 100644 index 0000000000000..f82a9587c75c3 --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/endgame/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface EndgameEcs { + exit_code?: number[]; + file_name?: string[]; + file_path?: string[]; + logon_type?: number[]; + parent_process_name?: string[]; + pid?: number[]; + process_name?: string[]; + subject_domain_name?: string[]; + subject_logon_id?: string[]; + subject_user_name?: string[]; + target_domain_name?: string[]; + target_logon_id?: string[]; + target_user_name?: string[]; +} diff --git a/x-pack/plugins/timelines/common/ecs/event/index.ts b/x-pack/plugins/timelines/common/ecs/event/index.ts new file mode 100644 index 0000000000000..4e38bacefd351 --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/event/index.ts @@ -0,0 +1,46 @@ +/* + * 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 interface EventEcs { + action?: string[]; + + category?: string[]; + + code?: string[]; + + created?: string[]; + + dataset?: string[]; + + duration?: number[]; + + end?: string[]; + + hash?: string[]; + + id?: string[]; + + kind?: string[]; + + module?: string[]; + + original?: string[]; + + outcome?: string[]; + + risk_score?: number[]; + + risk_score_norm?: number[]; + + severity?: number[]; + + start?: string[]; + + timezone?: string[]; + + type?: string[]; +} diff --git a/x-pack/plugins/timelines/common/ecs/file/index.ts b/x-pack/plugins/timelines/common/ecs/file/index.ts new file mode 100644 index 0000000000000..5e409b1095cf5 --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/file/index.ts @@ -0,0 +1,61 @@ +/* + * 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. + */ + +interface Original { + name?: string[]; + path?: string[]; +} + +export interface CodeSignature { + subject_name: string[]; + trusted: string[]; +} +export interface Ext { + code_signature?: CodeSignature[] | CodeSignature; + original?: Original; +} +export interface Hash { + md5?: string[]; + sha1?: string[]; + sha256: string[]; +} + +export interface FileEcs { + name?: string[]; + + path?: string[]; + + target_path?: string[]; + + extension?: string[]; + + Ext?: Ext; + + type?: string[]; + + device?: string[]; + + inode?: string[]; + + uid?: string[]; + + owner?: string[]; + + gid?: string[]; + + group?: string[]; + + mode?: string[]; + + size?: number[]; + + mtime?: string[]; + + ctime?: string[]; + + hash?: Hash; +} diff --git a/x-pack/plugins/timelines/common/ecs/geo/index.ts b/x-pack/plugins/timelines/common/ecs/geo/index.ts new file mode 100644 index 0000000000000..b6bf0f7b8aaad --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/geo/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. + */ + +export interface GeoEcs { + city_name?: string[]; + continent_name?: string[]; + country_iso_code?: string[]; + country_name?: string[]; + location?: Location; + region_iso_code?: string[]; + region_name?: string[]; +} + +export interface Location { + lon?: number[]; + lat?: number[]; +} diff --git a/x-pack/plugins/timelines/common/ecs/host/index.ts b/x-pack/plugins/timelines/common/ecs/host/index.ts new file mode 100644 index 0000000000000..37032c91fc312 --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/host/index.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. + */ + +export interface HostEcs { + architecture?: string[]; + + id?: string[]; + + ip?: string[]; + + mac?: string[]; + + name?: string[]; + + os?: OsEcs; + + type?: string[]; +} + +export interface OsEcs { + platform?: string[]; + + name?: string[]; + + full?: string[]; + + family?: string[]; + + version?: string[]; + + kernel?: string[]; +} diff --git a/x-pack/plugins/timelines/common/ecs/http/index.ts b/x-pack/plugins/timelines/common/ecs/http/index.ts new file mode 100644 index 0000000000000..89ce6b678181b --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/http/index.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. + */ + +export interface HttpEcs { + version?: string[]; + + request?: HttpRequestData; + + response?: HttpResponseData; +} + +export interface HttpRequestData { + method?: string[]; + + body?: HttpBodyData; + + referrer?: string[]; + + bytes?: number[]; +} + +export interface HttpBodyData { + content?: string[]; + + bytes?: number[]; +} + +export interface HttpResponseData { + status_code?: number[]; + + body?: HttpBodyData; + + bytes?: number[]; +} diff --git a/x-pack/plugins/timelines/common/ecs/index.ts b/x-pack/plugins/timelines/common/ecs/index.ts new file mode 100644 index 0000000000000..8054b3c8521db --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/index.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AgentEcs } from './agent'; +import { AuditdEcs } from './auditd'; +import { DestinationEcs } from './destination'; +import { DnsEcs } from './dns'; +import { EndgameEcs } from './endgame'; +import { EventEcs } from './event'; +import { FileEcs } from './file'; +import { GeoEcs } from './geo'; +import { HostEcs } from './host'; +import { NetworkEcs } from './network'; +import { RegistryEcs } from './registry'; +import { RuleEcs } from './rule'; +import { SignalEcs } from './signal'; +import { SourceEcs } from './source'; +import { SuricataEcs } from './suricata'; +import { TlsEcs } from './tls'; +import { ZeekEcs } from './zeek'; +import { HttpEcs } from './http'; +import { UrlEcs } from './url'; +import { UserEcs } from './user'; +import { WinlogEcs } from './winlog'; +import { ProcessEcs } from './process'; +import { SystemEcs } from './system'; +import { ThreatEcs } from './threat'; +import { Ransomware } from './ransomware'; + +export interface Ecs { + _id: string; + _index?: string; + agent?: AgentEcs; + auditd?: AuditdEcs; + destination?: DestinationEcs; + dns?: DnsEcs; + endgame?: EndgameEcs; + event?: EventEcs; + geo?: GeoEcs; + host?: HostEcs; + network?: NetworkEcs; + registry?: RegistryEcs; + rule?: RuleEcs; + signal?: SignalEcs; + source?: SourceEcs; + suricata?: SuricataEcs; + tls?: TlsEcs; + zeek?: ZeekEcs; + http?: HttpEcs; + url?: UrlEcs; + timestamp?: string; + message?: string[]; + user?: UserEcs; + winlog?: WinlogEcs; + process?: ProcessEcs; + file?: FileEcs; + system?: SystemEcs; + threat?: ThreatEcs; + // This should be temporary + eql?: { parentId: string; sequenceNumber: string }; + Ransomware?: Ransomware; +} diff --git a/x-pack/plugins/timelines/common/ecs/network/index.ts b/x-pack/plugins/timelines/common/ecs/network/index.ts new file mode 100644 index 0000000000000..6cc5dacab1e53 --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/network/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 interface NetworkEcs { + bytes?: number[]; + community_id?: string[]; + direction?: string[]; + packets?: number[]; + protocol?: string[]; + transport?: string[]; +} diff --git a/x-pack/plugins/timelines/common/ecs/process/index.ts b/x-pack/plugins/timelines/common/ecs/process/index.ts new file mode 100644 index 0000000000000..820ecc5560e6c --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/process/index.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 { Ext } from '../file'; + +export interface ProcessEcs { + Ext?: Ext; + entity_id?: string[]; + exit_code?: number[]; + hash?: ProcessHashData; + parent?: ProcessParentData; + pid?: number[]; + name?: string[]; + ppid?: number[]; + args?: string[]; + executable?: string[]; + title?: string[]; + thread?: Thread; + working_directory?: string[]; +} + +export interface ProcessHashData { + md5?: string[]; + sha1?: string[]; + sha256?: string[]; +} + +export interface ProcessParentData { + name?: string[]; + pid?: number[]; +} + +export interface Thread { + id?: number[]; + start?: string[]; +} diff --git a/x-pack/plugins/timelines/common/ecs/ransomware/index.ts b/x-pack/plugins/timelines/common/ecs/ransomware/index.ts new file mode 100644 index 0000000000000..1724a264f8a4c --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/ransomware/index.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface Ransomware { + feature?: string[]; + score?: string[]; + version?: number[]; + child_pids?: string[]; + files?: RansomwareFiles; +} + +export interface RansomwareFiles { + operation?: string[]; + entropy?: number[]; + metrics?: string[]; + extension?: string[]; + original?: OriginalRansomwareFiles; + path?: string[]; + data?: string[]; + score?: number[]; +} + +export interface OriginalRansomwareFiles { + path?: string[]; + extension?: string[]; +} diff --git a/x-pack/plugins/timelines/common/ecs/registry/index.ts b/x-pack/plugins/timelines/common/ecs/registry/index.ts new file mode 100644 index 0000000000000..c756fb139199e --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/registry/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface RegistryEcs { + hive?: string[]; + key?: string[]; + path?: string[]; + value?: string[]; +} diff --git a/x-pack/plugins/timelines/common/ecs/rule/index.ts b/x-pack/plugins/timelines/common/ecs/rule/index.ts new file mode 100644 index 0000000000000..ae7e5064a8ece --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/rule/index.ts @@ -0,0 +1,43 @@ +/* + * 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 interface RuleEcs { + id?: string[]; + rule_id?: string[]; + name?: string[]; + false_positives?: string[]; + saved_id?: string[]; + timeline_id?: string[]; + timeline_title?: string[]; + max_signals?: number[]; + risk_score?: string[]; + output_index?: string[]; + description?: string[]; + from?: string[]; + immutable?: boolean[]; + index?: string[]; + interval?: string[]; + language?: string[]; + query?: string[]; + references?: string[]; + severity?: string[]; + tags?: string[]; + threat?: unknown; + threshold?: unknown; + type?: string[]; + size?: string[]; + to?: string[]; + enabled?: boolean[]; + filters?: unknown; + created_at?: string[]; + updated_at?: string[]; + created_by?: string[]; + updated_by?: string[]; + version?: string[]; + note?: string[]; + building_block_type?: string[]; +} diff --git a/x-pack/plugins/timelines/common/ecs/signal/index.ts b/x-pack/plugins/timelines/common/ecs/signal/index.ts new file mode 100644 index 0000000000000..45e1f04d2b405 --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/signal/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RuleEcs } from '../rule'; + +export interface SignalEcs { + rule?: RuleEcs; + original_time?: string[]; + status?: string[]; + group?: { + id?: string[]; + }; + threshold_result?: unknown; +} diff --git a/x-pack/plugins/timelines/common/ecs/source/index.ts b/x-pack/plugins/timelines/common/ecs/source/index.ts new file mode 100644 index 0000000000000..10a2025eb43ec --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/source/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. + */ + +import { GeoEcs } from '../geo'; + +export interface SourceEcs { + bytes?: number[]; + ip?: string[]; + port?: number[]; + domain?: string[]; + geo?: GeoEcs; + packets?: number[]; +} diff --git a/x-pack/plugins/timelines/common/ecs/suricata/index.ts b/x-pack/plugins/timelines/common/ecs/suricata/index.ts new file mode 100644 index 0000000000000..5555a40188432 --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/suricata/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. + */ + +export interface SuricataEcs { + eve?: SuricataEveData; +} + +export interface SuricataEveData { + alert?: SuricataAlertData; + + flow_id?: number[]; + + proto?: string[]; +} + +export interface SuricataAlertData { + signature?: string[]; + + signature_id?: number[]; +} diff --git a/x-pack/plugins/timelines/common/ecs/system/index.ts b/x-pack/plugins/timelines/common/ecs/system/index.ts new file mode 100644 index 0000000000000..f2313c7884511 --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/system/index.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. + */ + +export interface SystemEcs { + audit?: AuditEcs; + + auth?: AuthEcs; +} + +export interface AuditEcs { + package?: PackageEcs; +} + +export interface PackageEcs { + arch?: string[]; + + entity_id?: string[]; + + name?: string[]; + + size?: number[]; + + summary?: string[]; + + version?: string[]; +} + +export interface AuthEcs { + ssh?: SshEcs; +} + +export interface SshEcs { + method?: string[]; + + signature?: string[]; +} diff --git a/x-pack/plugins/timelines/common/ecs/threat/index.ts b/x-pack/plugins/timelines/common/ecs/threat/index.ts new file mode 100644 index 0000000000000..19923a82dc846 --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/threat/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EventEcs } from '../event'; + +interface ThreatMatchEcs { + atomic?: string[]; + field?: string[]; + type?: string[]; +} + +export interface ThreatIndicatorEcs { + matched?: ThreatMatchEcs; + event?: EventEcs & { reference?: string[] }; + provider?: string[]; + type?: string[]; +} + +export interface ThreatEcs { + indicator: ThreatIndicatorEcs[]; +} diff --git a/x-pack/plugins/timelines/common/ecs/tls/index.ts b/x-pack/plugins/timelines/common/ecs/tls/index.ts new file mode 100644 index 0000000000000..f2e6b3d36653d --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/tls/index.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface TlsEcs { + client_certificate?: TlsClientCertificateData; + + fingerprints?: TlsFingerprintsData; + + server_certificate?: TlsServerCertificateData; +} + +export interface TlsClientCertificateData { + fingerprint?: FingerprintData; +} + +export interface FingerprintData { + sha1?: string[]; +} + +export interface TlsFingerprintsData { + ja3?: TlsJa3Data; +} + +export interface TlsJa3Data { + hash?: string[]; +} + +export interface TlsServerCertificateData { + fingerprint?: FingerprintData; +} diff --git a/x-pack/plugins/timelines/common/ecs/url/index.ts b/x-pack/plugins/timelines/common/ecs/url/index.ts new file mode 100644 index 0000000000000..ea9dc303108e3 --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/url/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface UrlEcs { + domain?: string[]; + + original?: string[]; + + username?: string[]; + + password?: string[]; +} diff --git a/x-pack/plugins/timelines/common/ecs/user/index.ts b/x-pack/plugins/timelines/common/ecs/user/index.ts new file mode 100644 index 0000000000000..b03a8e5e96b41 --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/user/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface UserEcs { + domain?: string[]; + + id?: string[]; + + name?: string[]; + + full_name?: string[]; + + email?: string[]; + + hash?: string[]; + + group?: string[]; +} diff --git a/x-pack/plugins/timelines/common/ecs/winlog/index.ts b/x-pack/plugins/timelines/common/ecs/winlog/index.ts new file mode 100644 index 0000000000000..27757d05ba6ec --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/winlog/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 interface WinlogEcs { + event_id?: number[]; +} diff --git a/x-pack/plugins/timelines/common/ecs/zeek/index.ts b/x-pack/plugins/timelines/common/ecs/zeek/index.ts new file mode 100644 index 0000000000000..b1a3786ae74aa --- /dev/null +++ b/x-pack/plugins/timelines/common/ecs/zeek/index.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface ZeekEcs { + session_id?: string[]; + + connection?: ZeekConnectionData; + + notice?: ZeekNoticeData; + + dns?: ZeekDnsData; + + http?: ZeekHttpData; + + files?: ZeekFileData; + + ssl?: ZeekSslData; +} + +export interface ZeekConnectionData { + local_resp?: boolean[]; + + local_orig?: boolean[]; + + missed_bytes?: number[]; + + state?: string[]; + + history?: string[]; +} + +export interface ZeekNoticeData { + suppress_for?: number[]; + + msg?: string[]; + + note?: string[]; + + sub?: string[]; + + dst?: string[]; + + dropped?: boolean[]; + + peer_descr?: string[]; +} + +export interface ZeekDnsData { + AA?: boolean[]; + + qclass_name?: string[]; + + RD?: boolean[]; + + qtype_name?: string[]; + + rejected?: boolean[]; + + qtype?: string[]; + + query?: string[]; + + trans_id?: number[]; + + qclass?: string[]; + + RA?: boolean[]; + + TC?: boolean[]; +} + +export interface ZeekHttpData { + resp_mime_types?: string[]; + + trans_depth?: string[]; + + status_msg?: string[]; + + resp_fuids?: string[]; + + tags?: string[]; +} + +export interface ZeekFileData { + session_ids?: string[]; + + timedout?: boolean[]; + + local_orig?: boolean[]; + + tx_host?: string[]; + + source?: string[]; + + is_orig?: boolean[]; + + overflow_bytes?: number[]; + + sha1?: string[]; + + duration?: number[]; + + depth?: number[]; + + analyzers?: string[]; + + mime_type?: string[]; + + rx_host?: string[]; + + total_bytes?: number[]; + + fuid?: string[]; + + seen_bytes?: number[]; + + missing_bytes?: number[]; + + md5?: string[]; +} + +export interface ZeekSslData { + cipher?: string[]; + + established?: boolean[]; + + resumed?: boolean[]; + + version?: string[]; +} diff --git a/x-pack/plugins/timelines/common/index.ts b/x-pack/plugins/timelines/common/index.ts index c095b6c89627e..05174235c20db 100644 --- a/x-pack/plugins/timelines/common/index.ts +++ b/x-pack/plugins/timelines/common/index.ts @@ -5,5 +5,9 @@ * 2.0. */ +export * from './types'; +export * from './search_strategy'; +export * from './utils/accessibility'; + export const PLUGIN_ID = 'timelines'; export const PLUGIN_NAME = 'timelines'; diff --git a/x-pack/plugins/timelines/common/search_strategy/common/index.ts b/x-pack/plugins/timelines/common/search_strategy/common/index.ts new file mode 100644 index 0000000000000..62c2187e267fa --- /dev/null +++ b/x-pack/plugins/timelines/common/search_strategy/common/index.ts @@ -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 type { estypes } from '@elastic/elasticsearch'; + +export type Maybe<T> = T | null; + +export interface TotalValue { + value: number; + relation: string; +} + +export interface CursorType { + value?: Maybe<string>; + tiebreaker?: Maybe<string>; +} + +export interface Inspect { + dsl: string[]; +} + +export enum Direction { + asc = 'asc', + desc = 'desc', +} + +export interface SortField<Field = string> { + field: Field; + direction: Direction; +} + +export interface TimerangeInput { + /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */ + interval: string; + /** The end of the timerange */ + to: string; + /** The beginning of the timerange */ + from: string; +} + +export interface PaginationInputPaginated { + /** The activePage parameter defines the page of results you want to fetch */ + activePage: number; + /** The cursorStart parameter defines the start of the results to be displayed */ + cursorStart: number; + /** The fakePossibleCount parameter determines the total count in order to show 5 additional pages */ + fakePossibleCount: number; + /** The querySize parameter is the number of items to be returned */ + querySize: number; +} + +export type DocValueFields = estypes.SearchDocValueField; + +export interface TimerangeFilter { + range: { + [timestamp: string]: { + gte: string; + lte: string; + format: string; + }; + }; +} + +export interface Fields<T = unknown[]> { + [x: string]: T | Array<Fields<T>>; +} + +export interface EventSource { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [field: string]: any; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface EventHit extends estypes.SearchHit<Record<string, any>> { + sort: string[]; + fields: Fields; +} diff --git a/x-pack/plugins/timelines/common/search_strategy/eql/index.ts b/x-pack/plugins/timelines/common/search_strategy/eql/index.ts new file mode 100644 index 0000000000000..4a361bed64890 --- /dev/null +++ b/x-pack/plugins/timelines/common/search_strategy/eql/index.ts @@ -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 { TotalValue } from '../common'; + +export * from './validation'; + +export type SearchTypes = + | string + | string[] + | number + | number[] + | boolean + | boolean[] + | object + | object[] + | undefined; + +export interface BaseHit<T> { + _index: string; + _id: string; + _source: T; + fields?: Record<string, SearchTypes[]>; +} + +export interface EqlSequence<T> { + join_keys: SearchTypes[]; + events: Array<BaseHit<T>>; +} + +export interface EqlSearchResponse<T> { + is_partial: boolean; + is_running: boolean; + took: number; + timed_out: boolean; + hits: { + total: TotalValue; + sequences?: Array<EqlSequence<T>>; + events?: Array<BaseHit<T>>; + }; +} diff --git a/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.mock.ts b/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.mock.ts new file mode 100644 index 0000000000000..b3a2c9c9a3f62 --- /dev/null +++ b/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.mock.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ApiResponse } from '@elastic/elasticsearch'; +import { ErrorResponse } from './helpers'; + +export const getValidEqlResponse = (): ApiResponse['body'] => ({ + is_partial: false, + is_running: false, + took: 162, + timed_out: false, + hits: { + total: { + value: 1, + relation: 'eq', + }, + sequences: [], + }, +}); + +export const getEqlResponseWithValidationError = (): ErrorResponse => ({ + error: { + root_cause: [ + { + type: 'verification_exception', + reason: + 'Found 2 problems\nline 1:1: Unknown column [event.category]\nline 1:13: Unknown column [event.name]', + }, + ], + type: 'verification_exception', + reason: + 'Found 2 problems\nline 1:1: Unknown column [event.category]\nline 1:13: Unknown column [event.name]', + }, +}); + +export const getEqlResponseWithValidationErrors = (): ErrorResponse => ({ + error: { + root_cause: [ + { + type: 'verification_exception', + reason: + 'Found 2 problems\nline 1:1: Unknown column [event.category]\nline 1:13: Unknown column [event.name]', + }, + { + type: 'parsing_exception', + reason: "line 1:4: mismatched input '<EOF>' expecting 'where'", + }, + ], + type: 'verification_exception', + reason: + 'Found 2 problems\nline 1:1: Unknown column [event.category]\nline 1:13: Unknown column [event.name]', + }, +}); + +export const getEqlResponseWithNonValidationError = (): ApiResponse['body'] => ({ + error: { + root_cause: [ + { + type: 'other_error', + reason: 'some other reason', + }, + ], + type: 'other_error', + reason: 'some other reason', + }, +}); diff --git a/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.test.ts b/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.test.ts new file mode 100644 index 0000000000000..de75cf6ac6dc7 --- /dev/null +++ b/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.test.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getValidationErrors, isErrorResponse, isValidationErrorResponse } from './helpers'; +import { + getEqlResponseWithNonValidationError, + getEqlResponseWithValidationError, + getEqlResponseWithValidationErrors, + getValidEqlResponse, +} from './helpers.mock'; + +describe('eql validation helpers', () => { + describe('isErrorResponse', () => { + it('is false for a regular response', () => { + expect(isErrorResponse(getValidEqlResponse())).toEqual(false); + }); + + it('is true for a response with non-validation errors', () => { + expect(isErrorResponse(getEqlResponseWithNonValidationError())).toEqual(true); + }); + + it('is true for a response with validation errors', () => { + expect(isErrorResponse(getEqlResponseWithValidationError())).toEqual(true); + }); + }); + + describe('isValidationErrorResponse', () => { + it('is false for a regular response', () => { + expect(isValidationErrorResponse(getValidEqlResponse())).toEqual(false); + }); + + it('is false for a response with non-validation errors', () => { + expect(isValidationErrorResponse(getEqlResponseWithNonValidationError())).toEqual(false); + }); + + it('is true for a response with validation errors', () => { + expect(isValidationErrorResponse(getEqlResponseWithValidationError())).toEqual(true); + }); + }); + + describe('getValidationErrors', () => { + it('returns a single error for a single root cause', () => { + expect(getValidationErrors(getEqlResponseWithValidationError())).toEqual([ + 'Found 2 problems\nline 1:1: Unknown column [event.category]\nline 1:13: Unknown column [event.name]', + ]); + }); + + it('returns multiple errors for multiple root causes', () => { + expect(getValidationErrors(getEqlResponseWithValidationErrors())).toEqual([ + 'Found 2 problems\nline 1:1: Unknown column [event.category]\nline 1:13: Unknown column [event.name]', + "line 1:4: mismatched input '<EOF>' expecting 'where'", + ]); + }); + }); +}); diff --git a/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.ts b/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.ts new file mode 100644 index 0000000000000..63a812cad759a --- /dev/null +++ b/x-pack/plugins/timelines/common/search_strategy/eql/validation/helpers.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { get, has } from 'lodash'; + +const PARSING_ERROR_TYPE = 'parsing_exception'; +const VERIFICATION_ERROR_TYPE = 'verification_exception'; +const MAPPING_ERROR_TYPE = 'mapping_exception'; + +interface ErrorCause { + type: string; + reason: string; +} + +export interface ErrorResponse { + error: ErrorCause & { root_cause: ErrorCause[] }; +} + +const isValidationErrorType = (type: unknown): boolean => + type === PARSING_ERROR_TYPE || type === VERIFICATION_ERROR_TYPE || type === MAPPING_ERROR_TYPE; + +export const isErrorResponse = (response: unknown): response is ErrorResponse => + has(response, 'error.type'); + +export const isValidationErrorResponse = (response: unknown): response is ErrorResponse => + isErrorResponse(response) && isValidationErrorType(get(response, 'error.type')); + +export const getValidationErrors = (response: ErrorResponse): string[] => + response.error.root_cause + .filter((cause) => isValidationErrorType(cause.type)) + .map((cause) => cause.reason); diff --git a/x-pack/plugins/timelines/common/search_strategy/eql/validation/index.ts b/x-pack/plugins/timelines/common/search_strategy/eql/validation/index.ts new file mode 100644 index 0000000000000..6c315f929b9bb --- /dev/null +++ b/x-pack/plugins/timelines/common/search_strategy/eql/validation/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 './helpers'; diff --git a/x-pack/plugins/timelines/common/search_strategy/index.ts b/x-pack/plugins/timelines/common/search_strategy/index.ts new file mode 100644 index 0000000000000..155306327ee0c --- /dev/null +++ b/x-pack/plugins/timelines/common/search_strategy/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 * from './common'; +export * from './timeline'; +export * from './index_fields'; +export * from './eql'; diff --git a/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts b/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts new file mode 100644 index 0000000000000..76ab48a8243db --- /dev/null +++ b/x-pack/plugins/timelines/common/search_strategy/index_fields/index.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 { IIndexPattern } from 'src/plugins/data/public'; +import { + IEsSearchRequest, + IEsSearchResponse, + IFieldSubType, +} from '../../../../../../src/plugins/data/common'; +import { DocValueFields, Maybe } from '../common'; + +export type BeatFieldsFactoryQueryType = 'beatFields'; + +interface FieldInfo { + category: string; + description?: string; + example?: string | number; + format?: string; + name: string; + type?: string; +} + +export interface IndexField { + /** Where the field belong */ + category: string; + /** Example of field's value */ + example?: Maybe<string | number>; + /** whether the field's belong to an alias index */ + indexes: Array<Maybe<string>>; + /** The name of the field */ + name: string; + /** The type of the field's values as recognized by Kibana */ + type: string; + /** Whether the field's values can be efficiently searched for */ + searchable: boolean; + /** Whether the field's values can be aggregated */ + aggregatable: boolean; + /** Description of the field */ + description?: Maybe<string>; + format?: Maybe<string>; + /** the elastic type as mapped in the index */ + esTypes?: string[]; + subType?: IFieldSubType; + readFromDocValues: boolean; +} + +export type BeatFields = Record<string, FieldInfo>; + +export interface IndexFieldsStrategyRequest extends IEsSearchRequest { + indices: string[]; + onlyCheckIfIndicesExist: boolean; +} + +export interface IndexFieldsStrategyResponse extends IEsSearchResponse { + indexFields: IndexField[]; + indicesExist: string[]; +} + +export interface BrowserField { + aggregatable: boolean; + category: string; + description: string | null; + example: string | number | null; + fields: Readonly<Record<string, Partial<BrowserField>>>; + format: string; + indexes: string[]; + name: string; + searchable: boolean; + type: string; + subType?: { + [key: string]: unknown; + nested?: { + path: string; + }; + }; +} + +export type BrowserFields = Readonly<Record<string, Partial<BrowserField>>>; + +export const EMPTY_BROWSER_FIELDS = {}; +export const EMPTY_DOCVALUE_FIELD: DocValueFields[] = []; +export const EMPTY_INDEX_PATTERN: IIndexPattern = { + fields: [], + title: '', +}; diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/events/all/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/events/all/index.ts new file mode 100644 index 0000000000000..94f7bc617e2f2 --- /dev/null +++ b/x-pack/plugins/timelines/common/search_strategy/timeline/events/all/index.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; +import type { Ecs } from '../../../../ecs'; +import type { CursorType, Inspect, Maybe, PaginationInputPaginated } from '../../../common'; +import type { TimelineRequestOptionsPaginated } from '../..'; + +export interface TimelineEdges { + node: TimelineItem; + cursor: CursorType; +} + +export interface TimelineItem { + _id: string; + _index?: Maybe<string>; + data: TimelineNonEcsData[]; + ecs: Ecs; +} + +export interface TimelineNonEcsData { + field: string; + value?: Maybe<string[]>; +} + +export interface TimelineEventsAllStrategyResponse extends IEsSearchResponse { + edges: TimelineEdges[]; + totalCount: number; + pageInfo: Pick<PaginationInputPaginated, 'activePage' | 'querySize'>; + inspect?: Maybe<Inspect>; +} + +export interface TimelineEventsAllRequestOptions extends TimelineRequestOptionsPaginated { + fields: string[] | Array<{ field: string; include_unmapped: boolean }>; + fieldRequested: string[]; + language: 'eql' | 'kuery' | 'lucene'; + excludeEcsData?: boolean; +} diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/events/common/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/events/common/index.ts new file mode 100644 index 0000000000000..4a5bd2c99a0eb --- /dev/null +++ b/x-pack/plugins/timelines/common/search_strategy/timeline/events/common/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 { Ecs } from '../../../../ecs'; +import { CursorType, Maybe } from '../../../common'; + +export interface TimelineEdges { + node: TimelineItem; + cursor: CursorType; +} + +export interface TimelineItem { + _id: string; + _index?: Maybe<string>; + data: TimelineNonEcsData[]; + ecs: Ecs; +} + +export interface TimelineNonEcsData { + field: string; + value?: Maybe<string[] | string>; +} diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/events/details/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/events/details/index.ts new file mode 100644 index 0000000000000..1f9820f8e5c2b --- /dev/null +++ b/x-pack/plugins/timelines/common/search_strategy/timeline/events/details/index.ts @@ -0,0 +1,31 @@ +/* + * 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 { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; +import { Inspect, Maybe } from '../../../common'; +import { TimelineRequestOptionsPaginated } from '../..'; + +export interface TimelineEventsDetailsItem { + ariaRowindex?: Maybe<number>; + category?: string; + field: string; + values?: Maybe<string[]>; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + originalValue?: Maybe<any>; + isObjectArray: boolean; +} + +export interface TimelineEventsDetailsStrategyResponse extends IEsSearchResponse { + data?: Maybe<TimelineEventsDetailsItem[]>; + inspect?: Maybe<Inspect>; +} + +export interface TimelineEventsDetailsRequestOptions + extends Partial<TimelineRequestOptionsPaginated> { + indexName: string; + eventId: string; +} diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/events/eql/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/events/eql/index.ts new file mode 100644 index 0000000000000..1e5164684bf6e --- /dev/null +++ b/x-pack/plugins/timelines/common/search_strategy/timeline/events/eql/index.ts @@ -0,0 +1,46 @@ +/* + * 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 { EuiComboBoxOptionOption } from '@elastic/eui'; +import { + EqlSearchStrategyRequest, + EqlSearchStrategyResponse, +} from '../../../../../../../../src/plugins/data/common'; +import { EqlSearchResponse, Inspect, Maybe, PaginationInputPaginated } from '../../..'; +import { TimelineEdges, TimelineEventsAllRequestOptions } from '../..'; + +export interface TimelineEqlRequestOptions + extends EqlSearchStrategyRequest, + Omit<TimelineEventsAllRequestOptions, 'params'> { + eventCategoryField?: string; + tiebreakerField?: string; + timestampField?: string; + size?: number; +} + +export interface TimelineEqlResponse extends EqlSearchStrategyResponse<EqlSearchResponse<unknown>> { + edges: TimelineEdges[]; + totalCount: number; + pageInfo: Pick<PaginationInputPaginated, 'activePage' | 'querySize'>; + inspect: Maybe<Inspect>; +} + +export interface EqlOptionsData { + keywordFields: EuiComboBoxOptionOption[]; + dateFields: EuiComboBoxOptionOption[]; + nonDateFields: EuiComboBoxOptionOption[]; +} + +export interface EqlOptionsSelected { + eventCategoryField?: string; + tiebreakerField?: string; + timestampField?: string; + query?: string; + size?: number; +} + +export type FieldsEqlOptions = keyof EqlOptionsSelected; diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/events/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/events/index.ts new file mode 100644 index 0000000000000..c4d6f70a27587 --- /dev/null +++ b/x-pack/plugins/timelines/common/search_strategy/timeline/events/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './all'; +export * from './details'; +export * from './last_event_time'; +export * from './eql'; + +export enum TimelineEventsQueries { + all = 'eventsAll', + details = 'eventsDetails', + kpi = 'eventsKpi', + lastEventTime = 'eventsLastEventTime', +} diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/events/last_event_time/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/events/last_event_time/index.ts new file mode 100644 index 0000000000000..f29dc4a3c7450 --- /dev/null +++ b/x-pack/plugins/timelines/common/search_strategy/timeline/events/last_event_time/index.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; +import { Inspect, Maybe } from '../../../common'; +import { TimelineRequestBasicOptions } from '../..'; + +export enum LastEventIndexKey { + hostDetails = 'hostDetails', + hosts = 'hosts', + ipDetails = 'ipDetails', + network = 'network', +} + +export interface LastTimeDetails { + hostName?: Maybe<string>; + ip?: Maybe<string>; +} + +export interface TimelineEventsLastEventTimeStrategyResponse extends IEsSearchResponse { + lastSeen: Maybe<string>; + inspect?: Maybe<Inspect>; +} + +export interface TimelineKpiStrategyResponse extends IEsSearchResponse { + destinationIpCount: number; + inspect?: Maybe<Inspect>; + hostCount: number; + processCount: number; + sourceIpCount: number; + userCount: number; +} + +export interface TimelineEventsLastEventTimeRequestOptions + extends Omit<TimelineRequestBasicOptions, 'filterQuery' | 'timerange'> { + indexKey: LastEventIndexKey; + details: LastTimeDetails; +} diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/index.ts new file mode 100644 index 0000000000000..7064ef033fc5a --- /dev/null +++ b/x-pack/plugins/timelines/common/search_strategy/timeline/index.ts @@ -0,0 +1,197 @@ +/* + * 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 { IEsSearchRequest } from '../../../../../../src/plugins/data/common'; +import { ESQuery } from '../../typed_json'; +import { + TimelineEventsQueries, + TimelineEventsAllRequestOptions, + TimelineEventsAllStrategyResponse, + TimelineEventsDetailsRequestOptions, + TimelineEventsDetailsStrategyResponse, + TimelineEventsLastEventTimeRequestOptions, + TimelineEventsLastEventTimeStrategyResponse, + TimelineKpiStrategyResponse, +} from './events'; +import { + DocValueFields, + PaginationInputPaginated, + TimerangeInput, + SortField, + Maybe, +} from '../common'; +import { + DataProviderType, + TimelineType, + TimelineStatus, + RowRendererId, +} from '../../types/timeline'; + +export * from './events'; + +export type TimelineFactoryQueryTypes = TimelineEventsQueries; + +export interface TimelineRequestBasicOptions extends IEsSearchRequest { + timerange: TimerangeInput; + filterQuery: ESQuery | string | undefined; + defaultIndex: string[]; + docValueFields?: DocValueFields[]; + factoryQueryType?: TimelineFactoryQueryTypes; +} + +export interface TimelineRequestSortField<Field = string> extends SortField<Field> { + type: string; +} + +export interface TimelineRequestOptionsPaginated<Field = string> + extends TimelineRequestBasicOptions { + pagination: Pick<PaginationInputPaginated, 'activePage' | 'querySize'>; + sort: Array<TimelineRequestSortField<Field>>; +} + +export type TimelineStrategyResponseType< + T extends TimelineFactoryQueryTypes +> = T extends TimelineEventsQueries.all + ? TimelineEventsAllStrategyResponse + : T extends TimelineEventsQueries.details + ? TimelineEventsDetailsStrategyResponse + : T extends TimelineEventsQueries.kpi + ? TimelineKpiStrategyResponse + : T extends TimelineEventsQueries.lastEventTime + ? TimelineEventsLastEventTimeStrategyResponse + : never; + +export type TimelineStrategyRequestType< + T extends TimelineFactoryQueryTypes +> = T extends TimelineEventsQueries.all + ? TimelineEventsAllRequestOptions + : T extends TimelineEventsQueries.details + ? TimelineEventsDetailsRequestOptions + : T extends TimelineEventsQueries.kpi + ? TimelineRequestBasicOptions + : T extends TimelineEventsQueries.lastEventTime + ? TimelineEventsLastEventTimeRequestOptions + : never; + +export interface ColumnHeaderInput { + aggregatable?: Maybe<boolean>; + category?: Maybe<string>; + columnHeaderType?: Maybe<string>; + description?: Maybe<string>; + example?: Maybe<string>; + indexes?: Maybe<string[]>; + id?: Maybe<string>; + name?: Maybe<string>; + placeholder?: Maybe<string>; + searchable?: Maybe<boolean>; + type?: Maybe<string>; +} + +export interface QueryMatchInput { + field?: Maybe<string>; + + displayField?: Maybe<string>; + + value?: Maybe<string>; + + displayValue?: Maybe<string>; + + operator?: Maybe<string>; +} + +export interface DataProviderInput { + id?: Maybe<string>; + name?: Maybe<string>; + enabled?: Maybe<boolean>; + excluded?: Maybe<boolean>; + kqlQuery?: Maybe<string>; + queryMatch?: Maybe<QueryMatchInput>; + and?: Maybe<DataProviderInput[]>; + type?: Maybe<DataProviderType>; +} + +export interface EqlOptionsInput { + eventCategoryField?: Maybe<string>; + tiebreakerField?: Maybe<string>; + timestampField?: Maybe<string>; + query?: Maybe<string>; + size?: Maybe<number>; +} + +export interface FilterMetaTimelineInput { + alias?: Maybe<string>; + controlledBy?: Maybe<string>; + disabled?: Maybe<boolean>; + field?: Maybe<string>; + formattedValue?: Maybe<string>; + index?: Maybe<string>; + key?: Maybe<string>; + negate?: Maybe<boolean>; + params?: Maybe<string>; + type?: Maybe<string>; + value?: Maybe<string>; +} + +export interface FilterTimelineInput { + exists?: Maybe<string>; + meta?: Maybe<FilterMetaTimelineInput>; + match_all?: Maybe<string>; + missing?: Maybe<string>; + query?: Maybe<string>; + range?: Maybe<string>; + script?: Maybe<string>; +} + +export interface SerializedFilterQueryInput { + filterQuery?: Maybe<SerializedKueryQueryInput>; +} + +export interface SerializedKueryQueryInput { + kuery?: Maybe<KueryFilterQueryInput>; + serializedQuery?: Maybe<string>; +} + +export interface KueryFilterQueryInput { + kind?: Maybe<string>; + expression?: Maybe<string>; +} + +export interface DateRangePickerInput { + start?: Maybe<number>; + end?: Maybe<number>; +} + +export interface SortTimelineInput { + columnId?: Maybe<string>; + sortDirection?: Maybe<string>; +} + +export interface TimelineInput { + columns?: Maybe<ColumnHeaderInput[]>; + dataProviders?: Maybe<DataProviderInput[]>; + description?: Maybe<string>; + eqlOptions?: Maybe<EqlOptionsInput>; + eventType?: Maybe<string>; + excludedRowRendererIds?: Maybe<RowRendererId[]>; + filters?: Maybe<FilterTimelineInput[]>; + kqlMode?: Maybe<string>; + kqlQuery?: Maybe<SerializedFilterQueryInput>; + indexNames?: Maybe<string[]>; + title?: Maybe<string>; + templateTimelineId?: Maybe<string>; + templateTimelineVersion?: Maybe<number>; + timelineType?: Maybe<TimelineType>; + dateRange?: Maybe<DateRangePickerInput>; + savedQueryId?: Maybe<string>; + sort?: Maybe<SortTimelineInput[]>; + status?: Maybe<TimelineStatus>; +} + +export enum FlowDirection { + uniDirectional = 'uniDirectional', + biDirectional = 'biDirectional', +} diff --git a/x-pack/plugins/timelines/common/typed_json.ts b/x-pack/plugins/timelines/common/typed_json.ts new file mode 100644 index 0000000000000..71ece54777871 --- /dev/null +++ b/x-pack/plugins/timelines/common/typed_json.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { JsonObject } from '@kbn/common-utils'; + +import { DslQuery, Filter } from 'src/plugins/data/common'; + +export type ESQuery = + | ESRangeQuery + | ESQueryStringQuery + | ESMatchQuery + | ESTermQuery + | ESBoolQuery + | JsonObject; + +export interface ESRangeQuery { + range: { + [name: string]: { + gte: number; + lte: number; + format: string; + }; + }; +} + +export interface ESMatchQuery { + match: { + [name: string]: { + query: string; + operator: string; + zero_terms_query: string; + }; + }; +} + +export interface ESQueryStringQuery { + query_string: { + query: string; + analyze_wildcard: boolean; + }; +} + +export interface ESTermQuery { + term: Record<string, string>; +} + +export interface ESBoolQuery { + bool: { + must: DslQuery[]; + filter: Filter[]; + should: never[]; + must_not: Filter[]; + }; +} diff --git a/x-pack/plugins/timelines/common/types/index.ts b/x-pack/plugins/timelines/common/types/index.ts new file mode 100644 index 0000000000000..9464a33082a49 --- /dev/null +++ b/x-pack/plugins/timelines/common/types/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 './timeline'; diff --git a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts new file mode 100644 index 0000000000000..8d3f212fd6bcc --- /dev/null +++ b/x-pack/plugins/timelines/common/types/timeline/actions/index.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 { ComponentType, JSXElementConstructor } from 'react'; +import { EuiDataGridControlColumn, EuiDataGridCellValueElementProps } from '@elastic/eui'; + +import { OnRowSelected, SortColumnTimeline, TimelineTabs } from '..'; +import { BrowserFields } from '../../../search_strategy/index_fields'; +import { ColumnHeaderOptions } from '../columns'; +import { TimelineNonEcsData } from '../../../search_strategy'; +import { Ecs } from '../../../ecs'; + +export interface ActionProps { + ariaRowindex: number; + action?: RowCellRender; + width?: number; + columnId: string; + columnValues: string; + checked: boolean; + onRowSelected: OnRowSelected; + eventId: string; + loadingEventIds: Readonly<string[]>; + onEventDetailsPanelOpened: () => void; + showCheckboxes: boolean; + data: TimelineNonEcsData[]; + ecsData: Ecs; + index: number; + eventIdToNoteIds?: Readonly<Record<string, string[]>>; + isEventPinned?: boolean; + isEventViewer?: boolean; + rowIndex: number; + refetch?: () => void; + onRuleChange?: () => void; + showNotes?: boolean; + tabType?: TimelineTabs; + timelineId: string; + toggleShowNotes?: () => void; +} + +export interface HeaderActionProps { + width: number; + browserFields: BrowserFields; + columnHeaders: ColumnHeaderOptions[]; + isEventViewer?: boolean; + isSelectAllChecked: boolean; + onSelectAll: ({ isSelected }: { isSelected: boolean }) => void; + showEventsSelect: boolean; + showSelectAllCheckbox: boolean; + sort: SortColumnTimeline[]; + tabType: TimelineTabs; + timelineId: string; +} + +export type GenericActionRowCellRenderProps = Pick< + EuiDataGridCellValueElementProps, + 'rowIndex' | 'columnId' +>; + +export type HeaderCellRender = ComponentType | ComponentType<HeaderActionProps>; +export type RowCellRender = + | JSXElementConstructor<GenericActionRowCellRenderProps> + | ((props: GenericActionRowCellRenderProps) => JSX.Element) + | JSXElementConstructor<ActionProps> + | ((props: ActionProps) => JSX.Element); + +interface AdditionalControlColumnProps { + ariaRowindex: number; + actionsColumnWidth: number; + columnValues: string; + checked: boolean; + onRowSelected: OnRowSelected; + eventId: string; + id: string; + columnId: string; + loadingEventIds: Readonly<string[]>; + onEventDetailsPanelOpened: () => void; + showCheckboxes: boolean; + // Override these type definitions to support either a generic custom component or the one used in security_solution today. + headerCellRender: HeaderCellRender; + rowCellRender: RowCellRender; + // If not provided, calculated dynamically + width?: number; +} + +export type ControlColumnProps = Omit< + EuiDataGridControlColumn, + keyof AdditionalControlColumnProps +> & + Partial<AdditionalControlColumnProps>; diff --git a/x-pack/plugins/timelines/common/types/timeline/cells/index.ts b/x-pack/plugins/timelines/common/types/timeline/cells/index.ts new file mode 100644 index 0000000000000..ad70d8bba82fd --- /dev/null +++ b/x-pack/plugins/timelines/common/types/timeline/cells/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 { EuiDataGridCellValueElementProps } from '@elastic/eui'; +import { TimelineNonEcsData } from '../../../search_strategy'; +import { ColumnHeaderOptions } from '../columns'; + +/** The following props are provided to the function called by `renderCellValue` */ +export type CellValueElementProps = EuiDataGridCellValueElementProps & { + data: TimelineNonEcsData[]; + eventId: string; // _id + header: ColumnHeaderOptions; + linkValues: string[] | undefined; + timelineId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + setFlyoutAlert?: (data: any) => void; +}; diff --git a/x-pack/plugins/timelines/common/types/timeline/columns/index.ts b/x-pack/plugins/timelines/common/types/timeline/columns/index.ts new file mode 100644 index 0000000000000..61f0c6a0b8f23 --- /dev/null +++ b/x-pack/plugins/timelines/common/types/timeline/columns/index.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiDataGridColumn } from '@elastic/eui'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IFieldSubType } from '../../../../../../../src/plugins/data/public'; +import { TimelineNonEcsData } from '../../../search_strategy/timeline'; + +export type ColumnHeaderType = 'not-filtered' | 'text-filter'; + +/** Uniquely identifies a column */ +export type ColumnId = string; + +/** The specification of a column header */ +export type ColumnHeaderOptions = Pick< + EuiDataGridColumn, + 'display' | 'displayAsText' | 'id' | 'initialWidth' +> & { + aggregatable?: boolean; + category?: string; + columnHeaderType: ColumnHeaderType; + description?: string; + example?: string; + format?: string; + linkField?: string; + placeholder?: string; + subType?: IFieldSubType; + type?: string; +}; + +export interface ColumnRenderer { + isInstance: (columnName: string, data: TimelineNonEcsData[]) => boolean; + renderColumn: ({ + columnName, + eventId, + field, + timelineId, + truncate, + values, + linkValues, + }: { + columnName: string; + eventId: string; + field: ColumnHeaderOptions; + timelineId: string; + truncate?: boolean; + values: string[] | null | undefined; + linkValues?: string[] | null | undefined; + }) => React.ReactNode; +} diff --git a/x-pack/plugins/timelines/common/types/timeline/data_provider/index.ts b/x-pack/plugins/timelines/common/types/timeline/data_provider/index.ts new file mode 100644 index 0000000000000..d706aff6f6aa7 --- /dev/null +++ b/x-pack/plugins/timelines/common/types/timeline/data_provider/index.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** Represents the Timeline data providers */ + +/** The `is` operator in a KQL query */ +export const IS_OPERATOR = ':'; + +/** The `exists` operator in a KQL query */ +export const EXISTS_OPERATOR = ':*'; + +/** The operator applied to a field */ +export type QueryOperator = ':' | ':*'; + +export enum DataProviderType { + default = 'default', + template = 'template', +} + +export interface QueryMatch { + field: string; + displayField?: string; + value: string | number; + displayValue?: string | number; + operator: QueryOperator; +} + +export interface DataProvider { + /** Uniquely identifies a data provider */ + id: string; + /** Human readable */ + name: string; + /** + * When `false`, a data provider is temporarily disabled, but not removed from + * the timeline. default: `true` + */ + enabled: boolean; + /** + * When `true`, a data provider is excluding the match, but not removed from + * the timeline. default: `false` + */ + excluded: boolean; + /** + * Returns the KQL query who have been added by user + */ + kqlQuery: string; + /** + * Returns a query properties that, when executed, returns the data for this provider + */ + queryMatch: QueryMatch; + /** + * Additional query clauses that are ANDed with this query to narrow results + */ + and: DataProvidersAnd[]; + /** + * Returns a DataProviderType + */ + type?: DataProviderType.default | DataProviderType.template; +} + +export type DataProvidersAnd = Pick<DataProvider, Exclude<keyof DataProvider, 'and'>>; diff --git a/x-pack/plugins/timelines/common/types/timeline/index.ts b/x-pack/plugins/timelines/common/types/timeline/index.ts new file mode 100644 index 0000000000000..c0bc1c305b970 --- /dev/null +++ b/x-pack/plugins/timelines/common/types/timeline/index.ts @@ -0,0 +1,744 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as runtimeTypes from 'io-ts'; + +import { stringEnum, unionWithNullType } from '../../utility_types'; +import { NoteResult, NoteSavedObject, NoteSavedObjectToReturnRuntimeType } from './note'; +import { + PinnedEventToReturnSavedObjectRuntimeType, + PinnedEventSavedObject, + PinnedEvent, +} from './pinned_event'; +import { Direction, Maybe } from '../../search_strategy'; + +export * from './actions'; +export * from './cells'; +export * from './columns'; +export * from './data_provider'; +export * from './rows'; +export * from './store'; + +const errorSchema = runtimeTypes.exact( + runtimeTypes.type({ + error: runtimeTypes.type({ + status_code: runtimeTypes.number, + message: runtimeTypes.string, + }), + }) +); + +export type ErrorSchema = runtimeTypes.TypeOf<typeof errorSchema>; + +/* + * ColumnHeader Types + */ +const SavedColumnHeaderRuntimeType = runtimeTypes.partial({ + aggregatable: unionWithNullType(runtimeTypes.boolean), + category: unionWithNullType(runtimeTypes.string), + columnHeaderType: unionWithNullType(runtimeTypes.string), + description: unionWithNullType(runtimeTypes.string), + example: unionWithNullType(runtimeTypes.string), + indexes: unionWithNullType(runtimeTypes.array(runtimeTypes.string)), + id: unionWithNullType(runtimeTypes.string), + name: unionWithNullType(runtimeTypes.string), + placeholder: unionWithNullType(runtimeTypes.string), + searchable: unionWithNullType(runtimeTypes.boolean), + type: unionWithNullType(runtimeTypes.string), +}); + +/* + * DataProvider Types + */ +const SavedDataProviderQueryMatchBasicRuntimeType = runtimeTypes.partial({ + field: unionWithNullType(runtimeTypes.string), + displayField: unionWithNullType(runtimeTypes.string), + value: unionWithNullType(runtimeTypes.string), + displayValue: unionWithNullType(runtimeTypes.string), + operator: unionWithNullType(runtimeTypes.string), +}); + +const SavedDataProviderQueryMatchRuntimeType = runtimeTypes.partial({ + id: unionWithNullType(runtimeTypes.string), + name: unionWithNullType(runtimeTypes.string), + enabled: unionWithNullType(runtimeTypes.boolean), + excluded: unionWithNullType(runtimeTypes.boolean), + kqlQuery: unionWithNullType(runtimeTypes.string), + queryMatch: unionWithNullType(SavedDataProviderQueryMatchBasicRuntimeType), +}); + +export enum DataProviderType { + default = 'default', + template = 'template', +} + +export const DataProviderTypeLiteralRt = runtimeTypes.union([ + runtimeTypes.literal(DataProviderType.default), + runtimeTypes.literal(DataProviderType.template), +]); + +const SavedDataProviderRuntimeType = runtimeTypes.partial({ + id: unionWithNullType(runtimeTypes.string), + name: unionWithNullType(runtimeTypes.string), + enabled: unionWithNullType(runtimeTypes.boolean), + excluded: unionWithNullType(runtimeTypes.boolean), + kqlQuery: unionWithNullType(runtimeTypes.string), + queryMatch: unionWithNullType(SavedDataProviderQueryMatchBasicRuntimeType), + and: unionWithNullType(runtimeTypes.array(SavedDataProviderQueryMatchRuntimeType)), + type: unionWithNullType(DataProviderTypeLiteralRt), +}); + +/* + * Filters Types + */ +const SavedFilterMetaRuntimeType = runtimeTypes.partial({ + alias: unionWithNullType(runtimeTypes.string), + controlledBy: unionWithNullType(runtimeTypes.string), + disabled: unionWithNullType(runtimeTypes.boolean), + field: unionWithNullType(runtimeTypes.string), + formattedValue: unionWithNullType(runtimeTypes.string), + index: unionWithNullType(runtimeTypes.string), + key: unionWithNullType(runtimeTypes.string), + negate: unionWithNullType(runtimeTypes.boolean), + params: unionWithNullType(runtimeTypes.string), + type: unionWithNullType(runtimeTypes.string), + value: unionWithNullType(runtimeTypes.string), +}); + +const SavedFilterRuntimeType = runtimeTypes.partial({ + exists: unionWithNullType(runtimeTypes.string), + meta: unionWithNullType(SavedFilterMetaRuntimeType), + match_all: unionWithNullType(runtimeTypes.string), + missing: unionWithNullType(runtimeTypes.string), + query: unionWithNullType(runtimeTypes.string), + range: unionWithNullType(runtimeTypes.string), + script: unionWithNullType(runtimeTypes.string), +}); + +/* + * eqlOptionsQuery -> filterQuery Types + */ +const EqlOptionsRuntimeType = runtimeTypes.partial({ + eventCategoryField: unionWithNullType(runtimeTypes.string), + query: unionWithNullType(runtimeTypes.string), + tiebreakerField: unionWithNullType(runtimeTypes.string), + timestampField: unionWithNullType(runtimeTypes.string), + size: unionWithNullType(runtimeTypes.union([runtimeTypes.string, runtimeTypes.number])), +}); + +/* + * kqlQuery -> filterQuery Types + */ +const SavedKueryFilterQueryRuntimeType = runtimeTypes.partial({ + kind: unionWithNullType(runtimeTypes.string), + expression: unionWithNullType(runtimeTypes.string), +}); + +const SavedSerializedFilterQueryQueryRuntimeType = runtimeTypes.partial({ + kuery: unionWithNullType(SavedKueryFilterQueryRuntimeType), + serializedQuery: unionWithNullType(runtimeTypes.string), +}); + +const SavedFilterQueryQueryRuntimeType = runtimeTypes.partial({ + filterQuery: unionWithNullType(SavedSerializedFilterQueryQueryRuntimeType), +}); + +/* + * DatePicker Range Types + */ +const SavedDateRangePickerRuntimeType = runtimeTypes.partial({ + /* Before the change of all timestamp to ISO string the values of start and from + * attributes where a number. Specifically UNIX timestamps. + * To support old timeline's saved object we need to add the number io-ts type + */ + start: unionWithNullType(runtimeTypes.union([runtimeTypes.string, runtimeTypes.number])), + end: unionWithNullType(runtimeTypes.union([runtimeTypes.string, runtimeTypes.number])), +}); + +/* + * Favorite Types + */ +const SavedFavoriteRuntimeType = runtimeTypes.partial({ + keySearch: unionWithNullType(runtimeTypes.string), + favoriteDate: unionWithNullType(runtimeTypes.number), + fullName: unionWithNullType(runtimeTypes.string), + userName: unionWithNullType(runtimeTypes.string), +}); + +/* + * Sort Types + */ + +const SavedSortObject = runtimeTypes.partial({ + columnId: unionWithNullType(runtimeTypes.string), + columnType: unionWithNullType(runtimeTypes.string), + sortDirection: unionWithNullType(runtimeTypes.string), +}); +const SavedSortRuntimeType = runtimeTypes.union([ + runtimeTypes.array(SavedSortObject), + SavedSortObject, +]); + +export type Sort = runtimeTypes.TypeOf<typeof SavedSortRuntimeType>; + +/* + * Timeline Statuses + */ + +export enum TimelineStatus { + active = 'active', + draft = 'draft', + immutable = 'immutable', +} + +export const TimelineStatusLiteralRt = runtimeTypes.union([ + runtimeTypes.literal(TimelineStatus.active), + runtimeTypes.literal(TimelineStatus.draft), + runtimeTypes.literal(TimelineStatus.immutable), +]); + +const TimelineStatusLiteralWithNullRt = unionWithNullType(TimelineStatusLiteralRt); + +export type TimelineStatusLiteral = runtimeTypes.TypeOf<typeof TimelineStatusLiteralRt>; +export type TimelineStatusLiteralWithNull = runtimeTypes.TypeOf< + typeof TimelineStatusLiteralWithNullRt +>; + +export enum RowRendererId { + alerts = 'alerts', + auditd = 'auditd', + auditd_file = 'auditd_file', + library = 'library', + netflow = 'netflow', + plain = 'plain', + registry = 'registry', + suricata = 'suricata', + system = 'system', + system_dns = 'system_dns', + system_endgame_process = 'system_endgame_process', + system_file = 'system_file', + system_fim = 'system_fim', + system_security_event = 'system_security_event', + system_socket = 'system_socket', + threat_match = 'threat_match', + zeek = 'zeek', +} + +export const RowRendererIdRuntimeType = stringEnum(RowRendererId, 'RowRendererId'); + +/** + * Timeline template type + */ + +export enum TemplateTimelineType { + elastic = 'elastic', + custom = 'custom', +} + +export const TemplateTimelineTypeLiteralRt = runtimeTypes.union([ + runtimeTypes.literal(TemplateTimelineType.elastic), + runtimeTypes.literal(TemplateTimelineType.custom), +]); + +export const TemplateTimelineTypeLiteralWithNullRt = unionWithNullType( + TemplateTimelineTypeLiteralRt +); + +export type TemplateTimelineTypeLiteral = runtimeTypes.TypeOf<typeof TemplateTimelineTypeLiteralRt>; +export type TemplateTimelineTypeLiteralWithNull = runtimeTypes.TypeOf< + typeof TemplateTimelineTypeLiteralWithNullRt +>; + +/* + * Timeline Types + */ + +export enum TimelineType { + default = 'default', + template = 'template', + test = 'test', +} + +export const TimelineTypeLiteralRt = runtimeTypes.union([ + runtimeTypes.literal(TimelineType.template), + runtimeTypes.literal(TimelineType.default), + runtimeTypes.literal(TimelineType.test), +]); + +export const TimelineTypeLiteralWithNullRt = unionWithNullType(TimelineTypeLiteralRt); + +export type TimelineTypeLiteral = runtimeTypes.TypeOf<typeof TimelineTypeLiteralRt>; +export type TimelineTypeLiteralWithNull = runtimeTypes.TypeOf<typeof TimelineTypeLiteralWithNullRt>; + +export const SavedTimelineRuntimeType = runtimeTypes.partial({ + columns: unionWithNullType(runtimeTypes.array(SavedColumnHeaderRuntimeType)), + dataProviders: unionWithNullType(runtimeTypes.array(SavedDataProviderRuntimeType)), + description: unionWithNullType(runtimeTypes.string), + eqlOptions: unionWithNullType(EqlOptionsRuntimeType), + eventType: unionWithNullType(runtimeTypes.string), + excludedRowRendererIds: unionWithNullType(runtimeTypes.array(RowRendererIdRuntimeType)), + favorite: unionWithNullType(runtimeTypes.array(SavedFavoriteRuntimeType)), + filters: unionWithNullType(runtimeTypes.array(SavedFilterRuntimeType)), + indexNames: unionWithNullType(runtimeTypes.array(runtimeTypes.string)), + kqlMode: unionWithNullType(runtimeTypes.string), + kqlQuery: unionWithNullType(SavedFilterQueryQueryRuntimeType), + title: unionWithNullType(runtimeTypes.string), + templateTimelineId: unionWithNullType(runtimeTypes.string), + templateTimelineVersion: unionWithNullType(runtimeTypes.number), + timelineType: unionWithNullType(TimelineTypeLiteralRt), + dateRange: unionWithNullType(SavedDateRangePickerRuntimeType), + savedQueryId: unionWithNullType(runtimeTypes.string), + sort: unionWithNullType(SavedSortRuntimeType), + status: unionWithNullType(TimelineStatusLiteralRt), + created: unionWithNullType(runtimeTypes.number), + createdBy: unionWithNullType(runtimeTypes.string), + updated: unionWithNullType(runtimeTypes.number), + updatedBy: unionWithNullType(runtimeTypes.string), +}); + +export type SavedTimeline = runtimeTypes.TypeOf<typeof SavedTimelineRuntimeType>; + +export type SavedTimelineNote = runtimeTypes.TypeOf<typeof SavedTimelineRuntimeType>; + +/* + * Timeline IDs + */ + +export enum TimelineId { + hostsPageEvents = 'hosts-page-events', + hostsPageExternalAlerts = 'hosts-page-external-alerts', + detectionsRulesDetailsPage = 'detections-rules-details-page', + detectionsPage = 'detections-page', + networkPageExternalAlerts = 'network-page-external-alerts', + active = 'timeline-1', + casePage = 'timeline-case', + test = 'test', // Reserved for testing purposes + alternateTest = 'alternateTest', +} + +export const TimelineIdLiteralRt = runtimeTypes.union([ + runtimeTypes.literal(TimelineId.hostsPageEvents), + runtimeTypes.literal(TimelineId.hostsPageExternalAlerts), + runtimeTypes.literal(TimelineId.detectionsRulesDetailsPage), + runtimeTypes.literal(TimelineId.detectionsPage), + runtimeTypes.literal(TimelineId.networkPageExternalAlerts), + runtimeTypes.literal(TimelineId.active), + runtimeTypes.literal(TimelineId.test), +]); + +export type TimelineIdLiteral = runtimeTypes.TypeOf<typeof TimelineIdLiteralRt>; + +/** + * Timeline Saved object type with metadata + */ + +export const TimelineSavedObjectRuntimeType = runtimeTypes.intersection([ + runtimeTypes.type({ + id: runtimeTypes.string, + attributes: SavedTimelineRuntimeType, + version: runtimeTypes.string, + }), + runtimeTypes.partial({ + savedObjectId: runtimeTypes.string, + }), +]); + +export const TimelineSavedToReturnObjectRuntimeType = runtimeTypes.intersection([ + SavedTimelineRuntimeType, + runtimeTypes.type({ + savedObjectId: runtimeTypes.string, + version: runtimeTypes.string, + }), + runtimeTypes.partial({ + eventIdToNoteIds: runtimeTypes.array(NoteSavedObjectToReturnRuntimeType), + noteIds: runtimeTypes.array(runtimeTypes.string), + notes: runtimeTypes.array(NoteSavedObjectToReturnRuntimeType), + pinnedEventIds: runtimeTypes.array(runtimeTypes.string), + pinnedEventsSaveObject: runtimeTypes.array(PinnedEventToReturnSavedObjectRuntimeType), + }), +]); + +export type TimelineSavedObject = runtimeTypes.TypeOf< + typeof TimelineSavedToReturnObjectRuntimeType +>; + +export const SingleTimelineResponseType = runtimeTypes.type({ + data: runtimeTypes.type({ + getOneTimeline: TimelineSavedToReturnObjectRuntimeType, + }), +}); + +export type SingleTimelineResponse = runtimeTypes.TypeOf<typeof SingleTimelineResponseType>; + +/** + * All Timeline Saved object type with metadata + */ +export const TimelineResponseType = runtimeTypes.type({ + data: runtimeTypes.type({ + persistTimeline: runtimeTypes.intersection([ + runtimeTypes.partial({ + code: unionWithNullType(runtimeTypes.number), + message: unionWithNullType(runtimeTypes.string), + }), + runtimeTypes.type({ + timeline: TimelineSavedToReturnObjectRuntimeType, + }), + ]), + }), +}); + +export const TimelineErrorResponseType = runtimeTypes.type({ + status_code: runtimeTypes.number, + message: runtimeTypes.string, +}); + +export type TimelineErrorResponse = runtimeTypes.TypeOf<typeof TimelineErrorResponseType>; +export type TimelineResponse = runtimeTypes.TypeOf<typeof TimelineResponseType>; + +/** + * All Timeline Saved object type with metadata + */ + +export const AllTimelineSavedObjectRuntimeType = runtimeTypes.type({ + total: runtimeTypes.number, + data: TimelineSavedToReturnObjectRuntimeType, +}); + +export type AllTimelineSavedObject = runtimeTypes.TypeOf<typeof AllTimelineSavedObjectRuntimeType>; + +/** + * Import/export timelines + */ + +export type ExportedGlobalNotes = Array<Exclude<NoteSavedObject, 'eventId'>>; +export type ExportedEventNotes = NoteSavedObject[]; + +export interface ExportedNotes { + eventNotes: ExportedEventNotes; + globalNotes: ExportedGlobalNotes; +} + +export type ExportedTimelines = TimelineSavedObject & + ExportedNotes & { + pinnedEventIds: string[]; + }; + +export interface ExportTimelineNotFoundError { + statusCode: number; + message: string; +} + +export interface BulkGetInput { + type: string; + id: string; +} + +export type NotesAndPinnedEventsByTimelineId = Record< + string, + { notes: NoteSavedObject[]; pinnedEvents: PinnedEventSavedObject[] } +>; + +export const importTimelineResultSchema = runtimeTypes.exact( + runtimeTypes.type({ + success: runtimeTypes.boolean, + success_count: runtimeTypes.number, + timelines_installed: runtimeTypes.number, + timelines_updated: runtimeTypes.number, + errors: runtimeTypes.array(errorSchema), + }) +); + +export type ImportTimelineResultSchema = runtimeTypes.TypeOf<typeof importTimelineResultSchema>; + +export type TimelineEventsType = 'all' | 'raw' | 'alert' | 'signal' | 'custom' | 'eql'; + +export enum TimelineTabs { + query = 'query', + graph = 'graph', + notes = 'notes', + pinned = 'pinned', + eql = 'eql', +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type EmptyObject = Record<any, never>; + +export type TimelineExpandedEventType = + | { + panelView?: 'eventDetail'; + params?: { + eventId: string; + indexName: string; + }; + } + | EmptyObject; + +export type TimelineExpandedHostType = + | { + panelView?: 'hostDetail'; + params?: { + hostName: string; + }; + } + | EmptyObject; + +enum FlowTarget { + client = 'client', + destination = 'destination', + server = 'server', + source = 'source', +} + +export type TimelineExpandedNetworkType = + | { + panelView?: 'networkDetail'; + params?: { + ip: string; + flowTarget: FlowTarget; + }; + } + | EmptyObject; + +export type TimelineExpandedDetailType = + | TimelineExpandedEventType + | TimelineExpandedHostType + | TimelineExpandedNetworkType; + +export type TimelineExpandedDetail = { + [tab in TimelineTabs]?: TimelineExpandedDetailType; +}; + +export type ToggleDetailPanel = TimelineExpandedDetailType & { + tabType?: TimelineTabs; + timelineId: string; +}; + +export const pageInfoTimeline = runtimeTypes.type({ + pageIndex: runtimeTypes.number, + pageSize: runtimeTypes.number, +}); + +export enum SortFieldTimeline { + title = 'title', + description = 'description', + updated = 'updated', + created = 'created', +} + +export const sortFieldTimeline = runtimeTypes.union([ + runtimeTypes.literal(SortFieldTimeline.title), + runtimeTypes.literal(SortFieldTimeline.description), + runtimeTypes.literal(SortFieldTimeline.updated), + runtimeTypes.literal(SortFieldTimeline.created), +]); + +export const direction = runtimeTypes.union([ + runtimeTypes.literal(Direction.asc), + runtimeTypes.literal(Direction.desc), +]); + +export const sortTimeline = runtimeTypes.type({ + sortField: sortFieldTimeline, + sortOrder: direction, +}); + +const favoriteTimelineResult = runtimeTypes.partial({ + fullName: unionWithNullType(runtimeTypes.string), + userName: unionWithNullType(runtimeTypes.string), + favoriteDate: unionWithNullType(runtimeTypes.number), +}); + +export type FavoriteTimelineResult = runtimeTypes.TypeOf<typeof favoriteTimelineResult>; + +export const responseFavoriteTimeline = runtimeTypes.partial({ + savedObjectId: runtimeTypes.string, + version: runtimeTypes.string, + code: unionWithNullType(runtimeTypes.number), + message: unionWithNullType(runtimeTypes.string), + templateTimelineId: unionWithNullType(runtimeTypes.string), + templateTimelineVersion: unionWithNullType(runtimeTypes.number), + timelineType: unionWithNullType(TimelineTypeLiteralRt), + favorite: unionWithNullType(runtimeTypes.array(favoriteTimelineResult)), +}); + +export type ResponseFavoriteTimeline = runtimeTypes.TypeOf<typeof responseFavoriteTimeline>; + +export const getTimelinesArgs = runtimeTypes.partial({ + onlyUserFavorite: unionWithNullType(runtimeTypes.boolean), + pageInfo: unionWithNullType(pageInfoTimeline), + search: unionWithNullType(runtimeTypes.string), + sort: unionWithNullType(sortTimeline), + status: unionWithNullType(TimelineStatusLiteralRt), + timelineType: unionWithNullType(TimelineTypeLiteralRt), +}); + +export type GetTimelinesArgs = runtimeTypes.TypeOf<typeof getTimelinesArgs>; + +const responseTimelines = runtimeTypes.type({ + timeline: runtimeTypes.array(TimelineSavedToReturnObjectRuntimeType), + totalCount: runtimeTypes.number, +}); + +export type ResponseTimelines = runtimeTypes.TypeOf<typeof responseTimelines>; + +export const allTimelinesResponse = runtimeTypes.intersection([ + responseTimelines, + runtimeTypes.type({ + defaultTimelineCount: runtimeTypes.number, + templateTimelineCount: runtimeTypes.number, + elasticTemplateTimelineCount: runtimeTypes.number, + customTemplateTimelineCount: runtimeTypes.number, + favoriteCount: runtimeTypes.number, + }), +]); + +export type AllTimelinesResponse = runtimeTypes.TypeOf<typeof allTimelinesResponse>; + +export interface PageInfoTimeline { + pageIndex: number; + + pageSize: number; +} + +export interface ColumnHeaderResult { + aggregatable?: Maybe<boolean>; + category?: Maybe<string>; + columnHeaderType?: Maybe<string>; + description?: Maybe<string>; + example?: Maybe<string>; + indexes?: Maybe<string[]>; + id?: Maybe<string>; + name?: Maybe<string>; + placeholder?: Maybe<string>; + searchable?: Maybe<boolean>; + type?: Maybe<string>; +} + +export interface DataProviderResult { + id?: Maybe<string>; + name?: Maybe<string>; + enabled?: Maybe<boolean>; + excluded?: Maybe<boolean>; + kqlQuery?: Maybe<string>; + queryMatch?: Maybe<QueryMatchResult>; + type?: Maybe<DataProviderType>; + and?: Maybe<DataProviderResult[]>; +} + +export interface QueryMatchResult { + field?: Maybe<string>; + displayField?: Maybe<string>; + value?: Maybe<string>; + displayValue?: Maybe<string>; + operator?: Maybe<string>; +} + +export interface DateRangePickerResult { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + start?: Maybe<any>; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + end?: Maybe<any>; +} + +export interface EqlOptionsResult { + eventCategoryField?: Maybe<string>; + tiebreakerField?: Maybe<string>; + timestampField?: Maybe<string>; + query?: Maybe<string>; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + size?: Maybe<any>; +} + +export interface FilterTimelineResult { + exists?: Maybe<string>; + meta?: Maybe<FilterMetaTimelineResult>; + match_all?: Maybe<string>; + missing?: Maybe<string>; + query?: Maybe<string>; + range?: Maybe<string>; + script?: Maybe<string>; +} + +export interface FilterMetaTimelineResult { + alias?: Maybe<string>; + controlledBy?: Maybe<string>; + disabled?: Maybe<boolean>; + field?: Maybe<string>; + formattedValue?: Maybe<string>; + index?: Maybe<string>; + key?: Maybe<string>; + negate?: Maybe<boolean>; + params?: Maybe<string>; + type?: Maybe<string>; + value?: Maybe<string>; +} + +export interface SerializedFilterQueryResult { + filterQuery?: Maybe<SerializedKueryQueryResult>; +} + +export interface SerializedKueryQueryResult { + kuery?: Maybe<KueryFilterQueryResult>; + serializedQuery?: Maybe<string>; +} + +export interface KueryFilterQueryResult { + kind?: Maybe<string>; + expression?: Maybe<string>; +} + +export interface TimelineResult { + columns?: Maybe<ColumnHeaderResult[]>; + created?: Maybe<number>; + createdBy?: Maybe<string>; + dataProviders?: Maybe<DataProviderResult[]>; + dateRange?: Maybe<DateRangePickerResult>; + description?: Maybe<string>; + eqlOptions?: Maybe<EqlOptionsResult>; + eventIdToNoteIds?: Maybe<NoteResult[]>; + eventType?: Maybe<string>; + excludedRowRendererIds?: Maybe<RowRendererId[]>; + favorite?: Maybe<FavoriteTimelineResult[]>; + filters?: Maybe<FilterTimelineResult[]>; + kqlMode?: Maybe<string>; + kqlQuery?: Maybe<SerializedFilterQueryResult>; + indexNames?: Maybe<string[]>; + notes?: Maybe<NoteResult[]>; + noteIds?: Maybe<string[]>; + pinnedEventIds?: Maybe<string[]>; + pinnedEventsSaveObject?: Maybe<PinnedEvent[]>; + savedQueryId?: Maybe<string>; + savedObjectId: string; + sort?: Maybe<Sort>; + status?: Maybe<TimelineStatus>; + title?: Maybe<string>; + templateTimelineId?: Maybe<string>; + templateTimelineVersion?: Maybe<number>; + timelineType?: Maybe<TimelineType>; + updated?: Maybe<number>; + updatedBy?: Maybe<string>; + version: string; +} + +export interface ResponseTimeline { + code?: Maybe<number>; + message?: Maybe<string>; + timeline: TimelineResult; +} +export interface SortTimeline { + sortField: SortFieldTimeline; + sortOrder: Direction; +} + +export interface GetAllTimelineVariables { + pageInfo: PageInfoTimeline; + search?: Maybe<string>; + sort?: Maybe<SortTimeline>; + onlyUserFavorite?: Maybe<boolean>; + timelineType?: Maybe<TimelineType>; + status?: Maybe<TimelineStatus>; +} diff --git a/x-pack/plugins/timelines/common/types/timeline/note/index.ts b/x-pack/plugins/timelines/common/types/timeline/note/index.ts new file mode 100644 index 0000000000000..074e4132efdff --- /dev/null +++ b/x-pack/plugins/timelines/common/types/timeline/note/index.ts @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import * as runtimeTypes from 'io-ts'; +import { Direction, Maybe } from '../../../search_strategy/common'; + +import { unionWithNullType } from '../../../utility_types'; + +/* + * Note Types + */ +export const SavedNoteRuntimeType = runtimeTypes.intersection([ + runtimeTypes.type({ + timelineId: unionWithNullType(runtimeTypes.string), + }), + runtimeTypes.partial({ + eventId: unionWithNullType(runtimeTypes.string), + note: unionWithNullType(runtimeTypes.string), + created: unionWithNullType(runtimeTypes.number), + createdBy: unionWithNullType(runtimeTypes.string), + updated: unionWithNullType(runtimeTypes.number), + updatedBy: unionWithNullType(runtimeTypes.string), + }), +]); + +export interface SavedNote extends runtimeTypes.TypeOf<typeof SavedNoteRuntimeType> {} + +/** + * Note Saved object type with metadata + */ + +export const NoteSavedObjectRuntimeType = runtimeTypes.intersection([ + runtimeTypes.type({ + id: runtimeTypes.string, + attributes: SavedNoteRuntimeType, + version: runtimeTypes.string, + }), + runtimeTypes.partial({ + noteId: runtimeTypes.string, + timelineVersion: runtimeTypes.union([ + runtimeTypes.string, + runtimeTypes.null, + runtimeTypes.undefined, + ]), + }), +]); + +export const NoteSavedObjectToReturnRuntimeType = runtimeTypes.intersection([ + SavedNoteRuntimeType, + runtimeTypes.type({ + noteId: runtimeTypes.string, + version: runtimeTypes.string, + }), + runtimeTypes.partial({ + timelineVersion: unionWithNullType(runtimeTypes.string), + }), +]); + +export interface NoteSavedObject + extends runtimeTypes.TypeOf<typeof NoteSavedObjectToReturnRuntimeType> {} + +export enum SortFieldNote { + updatedBy = 'updatedBy', + updated = 'updated', +} + +export const pageInfoNoteRt = runtimeTypes.type({ + pageIndex: runtimeTypes.number, + pageSize: runtimeTypes.number, +}); + +export type PageInfoNote = runtimeTypes.TypeOf<typeof pageInfoNoteRt>; + +export const sortNoteRt = runtimeTypes.type({ + sortField: runtimeTypes.union([ + runtimeTypes.literal(SortFieldNote.updatedBy), + runtimeTypes.literal(SortFieldNote.updated), + ]), + sortOrder: runtimeTypes.union([ + runtimeTypes.literal(Direction.asc), + runtimeTypes.literal(Direction.desc), + ]), +}); + +export type SortNote = runtimeTypes.TypeOf<typeof sortNoteRt>; + +export interface NoteResult { + eventId?: Maybe<string>; + + note?: Maybe<string>; + + timelineId?: Maybe<string>; + + noteId: string; + + created?: Maybe<number>; + + createdBy?: Maybe<string>; + + timelineVersion?: Maybe<string>; + + updated?: Maybe<number>; + + updatedBy?: Maybe<string>; + + version?: Maybe<string>; +} + +export interface ResponseNotes { + notes: NoteResult[]; + + totalCount?: Maybe<number>; +} + +export interface ResponseNote { + code?: Maybe<number>; + + message?: Maybe<string>; + + note: NoteResult; +} diff --git a/x-pack/plugins/timelines/common/types/timeline/pinned_event/index.ts b/x-pack/plugins/timelines/common/types/timeline/pinned_event/index.ts new file mode 100644 index 0000000000000..dbb19df7a6b05 --- /dev/null +++ b/x-pack/plugins/timelines/common/types/timeline/pinned_event/index.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import * as runtimeTypes from 'io-ts'; +import { Maybe } from '../../../search_strategy/common'; + +import { unionWithNullType } from '../../../utility_types'; + +/* + * Note Types + */ +export const SavedPinnedEventRuntimeType = runtimeTypes.intersection([ + runtimeTypes.type({ + timelineId: runtimeTypes.string, + eventId: runtimeTypes.string, + }), + runtimeTypes.partial({ + created: unionWithNullType(runtimeTypes.number), + createdBy: unionWithNullType(runtimeTypes.string), + updated: unionWithNullType(runtimeTypes.number), + updatedBy: unionWithNullType(runtimeTypes.string), + }), +]); + +export interface SavedPinnedEvent extends runtimeTypes.TypeOf<typeof SavedPinnedEventRuntimeType> {} + +/** + * Note Saved object type with metadata + */ + +export const PinnedEventSavedObjectRuntimeType = runtimeTypes.intersection([ + runtimeTypes.type({ + id: runtimeTypes.string, + attributes: SavedPinnedEventRuntimeType, + version: runtimeTypes.string, + }), + runtimeTypes.partial({ + pinnedEventId: unionWithNullType(runtimeTypes.string), + timelineVersion: unionWithNullType(runtimeTypes.string), + }), +]); + +export const PinnedEventToReturnSavedObjectRuntimeType = runtimeTypes.intersection([ + runtimeTypes.type({ + pinnedEventId: runtimeTypes.string, + version: runtimeTypes.string, + }), + SavedPinnedEventRuntimeType, + runtimeTypes.partial({ + timelineVersion: unionWithNullType(runtimeTypes.string), + }), +]); + +export interface PinnedEventSavedObject + extends runtimeTypes.TypeOf<typeof PinnedEventToReturnSavedObjectRuntimeType> {} + +export interface PinnedEvent { + code?: Maybe<number>; + + message?: Maybe<string>; + + pinnedEventId: string; + + eventId?: Maybe<string>; + + timelineId?: Maybe<string>; + + timelineVersion?: Maybe<string>; + + created?: Maybe<number>; + + createdBy?: Maybe<string>; + + updated?: Maybe<number>; + + updatedBy?: Maybe<string>; + + version?: Maybe<string>; +} diff --git a/x-pack/plugins/timelines/common/types/timeline/rows/index.ts b/x-pack/plugins/timelines/common/types/timeline/rows/index.ts new file mode 100644 index 0000000000000..b598d13273798 --- /dev/null +++ b/x-pack/plugins/timelines/common/types/timeline/rows/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 { RowRendererId } from '..'; +import { Ecs } from '../../../ecs'; +import { BrowserFields } from '../../../search_strategy/index_fields'; + +export interface RowRenderer { + id: RowRendererId; + isInstance: (data: Ecs) => boolean; + renderRow: ({ + browserFields, + data, + timelineId, + }: { + browserFields: BrowserFields; + data: Ecs; + timelineId: string; + }) => React.ReactNode; +} diff --git a/x-pack/plugins/timelines/common/types/timeline/store.ts b/x-pack/plugins/timelines/common/types/timeline/store.ts new file mode 100644 index 0000000000000..8e3a9fda9475c --- /dev/null +++ b/x-pack/plugins/timelines/common/types/timeline/store.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + ColumnHeaderOptions, + ColumnId, + RowRendererId, + Sort, + TimelineExpandedDetail, + TimelineTypeLiteral, +} from '.'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { Filter } from '../../../../../../src/plugins/data/public'; + +import { Direction } from '../../search_strategy'; +import { DataProvider } from './data_provider'; + +export type KueryFilterQueryKind = 'kuery' | 'lucene' | 'eql'; + +export interface KueryFilterQuery { + kind: KueryFilterQueryKind; + expression: string; +} + +export interface SerializedFilterQuery { + kuery: KueryFilterQuery | null; + serializedQuery: string; +} + +export type SortDirection = 'none' | 'asc' | 'desc' | Direction; +export interface SortColumnTimeline { + columnId: string; + columnType: string; + sortDirection: SortDirection; +} + +export interface TimelinePersistInput { + id: string; + dataProviders?: DataProvider[]; + dateRange?: { + start: string; + end: string; + }; + excludedRowRendererIds?: RowRendererId[]; + expandedDetail?: TimelineExpandedDetail; + filters?: Filter[]; + columns: ColumnHeaderOptions[]; + itemsPerPage?: number; + indexNames: string[]; + kqlQuery?: { + filterQuery: SerializedFilterQuery | null; + }; + show?: boolean; + sort?: Sort[]; + showCheckboxes?: boolean; + timelineType?: TimelineTypeLiteral; + templateTimelineId?: string | null; + templateTimelineVersion?: number | null; +} + +/** Invoked when a column is sorted */ +export type OnColumnSorted = (sorted: { columnId: ColumnId; sortDirection: SortDirection }) => void; + +export type OnColumnsSorted = ( + sorted: Array<{ columnId: ColumnId; sortDirection: SortDirection }> +) => void; + +export type OnColumnRemoved = (columnId: ColumnId) => void; + +export type OnColumnResized = ({ columnId, delta }: { columnId: ColumnId; delta: number }) => void; + +/** Invoked when a user clicks to load more item */ +export type OnChangePage = (nextPage: number) => void; + +/** Invoked when a user checks/un-checks a row */ +export type OnRowSelected = ({ + eventIds, + isSelected, +}: { + eventIds: string[]; + isSelected: boolean; +}) => void; + +/** Invoked when a user checks/un-checks the select all checkbox */ +export type OnSelectAll = ({ isSelected }: { isSelected: boolean }) => void; + +/** Invoked when columns are updated */ +export type OnUpdateColumns = (columns: ColumnHeaderOptions[]) => void; + +/** Invoked when a user pins an event */ +export type OnPinEvent = (eventId: string) => void; + +/** Invoked when a user unpins an event */ +export type OnUnPinEvent = (eventId: string) => void; diff --git a/x-pack/plugins/timelines/common/utility_types.ts b/x-pack/plugins/timelines/common/utility_types.ts new file mode 100644 index 0000000000000..498b18dccaca5 --- /dev/null +++ b/x-pack/plugins/timelines/common/utility_types.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as runtimeTypes from 'io-ts'; +import { ReactNode } from 'react'; + +// This type is for typing EuiDescriptionList +export interface DescriptionList { + title: NonNullable<ReactNode>; + description: NonNullable<ReactNode>; +} + +export const unionWithNullType = <T extends runtimeTypes.Mixed>(type: T) => + runtimeTypes.union([type, runtimeTypes.null]); + +export const stringEnum = <T>(enumObj: T, enumName = 'enum') => + new runtimeTypes.Type<T[keyof T], string>( + enumName, + (u): u is T[keyof T] => Object.values(enumObj).includes(u), + (u, c) => + Object.values(enumObj).includes(u) + ? runtimeTypes.success(u as T[keyof T]) + : runtimeTypes.failure(u, c), + (a) => (a as unknown) as string + ); + +/** + * Unreachable Assertion helper for scenarios like exhaustive switches. + * For references see: https://stackoverflow.com/questions/39419170/how-do-i-check-that-a-switch-block-is-exhaustive-in-typescript + * This "x" should _always_ be a type of "never" and not change to "unknown" or any other type. See above link or the generic + * concept of exhaustive checks in switch blocks. + * + * Optionally you can avoid the use of this by using early returns and TypeScript will clear your type checking without complaints + * but there are situations and times where this function might still be needed. + * + * If you see an error, DO NOT cast "as never" such as: + * assertUnreachable(x as never) // BUG IN YOUR CODE NOW AND IT WILL THROW DURING RUNTIME + * If you see code like that remove it, as that deactivates the intent of this utility. + * If you need to do that, then you should remove assertUnreachable from your code and + * use a default at the end of the switch instead. + * @param x Unreachable field + * @param message Message of error thrown + */ +export const assertUnreachable = ( + x: never, // This should always be a type of "never" + message = 'Unknown Field in switch statement' +): never => { + throw new Error(`${message}: ${x}`); +}; diff --git a/x-pack/plugins/security_solution/public/common/components/accessibility/helpers.test.tsx b/x-pack/plugins/timelines/common/utils/accessibility/helpers.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/accessibility/helpers.test.tsx rename to x-pack/plugins/timelines/common/utils/accessibility/helpers.test.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/accessibility/helpers.ts b/x-pack/plugins/timelines/common/utils/accessibility/helpers.ts similarity index 99% rename from x-pack/plugins/security_solution/public/common/components/accessibility/helpers.ts rename to x-pack/plugins/timelines/common/utils/accessibility/helpers.ts index a1ee9c3cc3bd5..e877edd28458b 100644 --- a/x-pack/plugins/security_solution/public/common/components/accessibility/helpers.ts +++ b/x-pack/plugins/timelines/common/utils/accessibility/helpers.ts @@ -5,14 +5,14 @@ * 2.0. */ -import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '../drag_and_drop/helpers'; +import React from 'react'; import { + DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME, + HOVER_ACTIONS_ALWAYS_SHOW_CLASS_NAME, NOTES_CONTAINER_CLASS_NAME, NOTE_CONTENT_CLASS_NAME, ROW_RENDERER_CLASS_NAME, -} from '../../../timelines/components/timeline/body/helpers'; -import { HOVER_ACTIONS_ALWAYS_SHOW_CLASS_NAME } from '../with_hover_actions'; - +} from '@kbn/securitysolution-t-grid'; /** * The name of the ARIA attribute representing a column, used in conjunction with * the ARIA: grid role https://www.w3.org/TR/wai-aria-practices-1.1/examples/grid/dataGrids.html diff --git a/x-pack/plugins/timelines/common/utils/accessibility/index.ts b/x-pack/plugins/timelines/common/utils/accessibility/index.ts new file mode 100644 index 0000000000000..6c315f929b9bb --- /dev/null +++ b/x-pack/plugins/timelines/common/utils/accessibility/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 './helpers'; diff --git a/x-pack/plugins/security_solution/public/common/utils/api/index.ts b/x-pack/plugins/timelines/common/utils/api.ts similarity index 100% rename from x-pack/plugins/security_solution/public/common/utils/api/index.ts rename to x-pack/plugins/timelines/common/utils/api.ts diff --git a/x-pack/plugins/timelines/common/utils/field_formatters.test.ts b/x-pack/plugins/timelines/common/utils/field_formatters.test.ts new file mode 100644 index 0000000000000..50a3117e53b9b --- /dev/null +++ b/x-pack/plugins/timelines/common/utils/field_formatters.test.ts @@ -0,0 +1,196 @@ +/* + * 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 { eventDetailsFormattedFields, eventHit } from '@kbn/securitysolution-t-grid'; +import { EventHit, EventSource } from '../search_strategy'; +import { getDataFromFieldsHits, getDataFromSourceHits, getDataSafety } from './field_formatters'; + +describe('Events Details Helpers', () => { + const fields: EventHit['fields'] = eventHit.fields; + const resultFields = eventDetailsFormattedFields; + describe('#getDataFromFieldsHits', () => { + it('happy path', () => { + const result = getDataFromFieldsHits(fields); + expect(result).toEqual(resultFields); + }); + it('lets get weird', () => { + const whackFields = { + 'crazy.pants': [ + { + 'matched.field': ['matched_field'], + first_seen: ['2021-02-22T17:29:25.195Z'], + provider: ['yourself'], + type: ['custom'], + 'matched.atomic': ['matched_atomic'], + lazer: [ + { + 'great.field': ['grrrrr'], + lazer: [ + { + lazer: [ + { + cool: true, + lazer: [ + { + lazer: [ + { + lazer: [ + { + lazer: [ + { + whoa: false, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + lazer: [ + { + cool: false, + }, + ], + }, + ], + }, + { + 'great.field': ['grrrrr_2'], + }, + ], + }, + ], + }; + const whackResultFields = [ + { + category: 'crazy', + field: 'crazy.pants.matched.field', + values: ['matched_field'], + originalValue: ['matched_field'], + isObjectArray: false, + }, + { + category: 'crazy', + field: 'crazy.pants.first_seen', + values: ['2021-02-22T17:29:25.195Z'], + originalValue: ['2021-02-22T17:29:25.195Z'], + isObjectArray: false, + }, + { + category: 'crazy', + field: 'crazy.pants.provider', + values: ['yourself'], + originalValue: ['yourself'], + isObjectArray: false, + }, + { + category: 'crazy', + field: 'crazy.pants.type', + values: ['custom'], + originalValue: ['custom'], + isObjectArray: false, + }, + { + category: 'crazy', + field: 'crazy.pants.matched.atomic', + values: ['matched_atomic'], + originalValue: ['matched_atomic'], + isObjectArray: false, + }, + { + category: 'crazy', + field: 'crazy.pants.lazer.great.field', + values: ['grrrrr', 'grrrrr_2'], + originalValue: ['grrrrr', 'grrrrr_2'], + isObjectArray: false, + }, + { + category: 'crazy', + field: 'crazy.pants.lazer.lazer.lazer.cool', + values: ['true', 'false'], + originalValue: ['true', 'false'], + isObjectArray: false, + }, + { + category: 'crazy', + field: 'crazy.pants.lazer.lazer.lazer.lazer.lazer.lazer.lazer.whoa', + values: ['false'], + originalValue: ['false'], + isObjectArray: false, + }, + ]; + const result = getDataFromFieldsHits(whackFields); + expect(result).toEqual(whackResultFields); + }); + }); + it('#getDataFromSourceHits', () => { + const _source: EventSource = { + '@timestamp': '2021-02-24T00:41:06.527Z', + 'signal.status': 'open', + 'signal.rule.name': 'Rawr', + 'threat.indicator': [ + { + provider: 'yourself', + type: 'custom', + first_seen: ['2021-02-22T17:29:25.195Z'], + matched: { atomic: 'atom', field: 'field', type: 'type' }, + }, + { + provider: 'other_you', + type: 'custom', + first_seen: '2021-02-22T17:29:25.195Z', + matched: { atomic: 'atom', field: 'field', type: 'type' }, + }, + ], + }; + expect(getDataFromSourceHits(_source)).toEqual([ + { + category: 'base', + field: '@timestamp', + values: ['2021-02-24T00:41:06.527Z'], + originalValue: ['2021-02-24T00:41:06.527Z'], + isObjectArray: false, + }, + { + category: 'signal', + field: 'signal.status', + values: ['open'], + originalValue: ['open'], + isObjectArray: false, + }, + { + category: 'signal', + field: 'signal.rule.name', + values: ['Rawr'], + originalValue: ['Rawr'], + isObjectArray: false, + }, + { + category: 'threat', + field: 'threat.indicator', + values: [ + '{"provider":"yourself","type":"custom","first_seen":["2021-02-22T17:29:25.195Z"],"matched":{"atomic":"atom","field":"field","type":"type"}}', + '{"provider":"other_you","type":"custom","first_seen":"2021-02-22T17:29:25.195Z","matched":{"atomic":"atom","field":"field","type":"type"}}', + ], + originalValue: [ + '{"provider":"yourself","type":"custom","first_seen":["2021-02-22T17:29:25.195Z"],"matched":{"atomic":"atom","field":"field","type":"type"}}', + '{"provider":"other_you","type":"custom","first_seen":"2021-02-22T17:29:25.195Z","matched":{"atomic":"atom","field":"field","type":"type"}}', + ], + isObjectArray: true, + }, + ]); + }); + it('#getDataSafety', async () => { + const result = await getDataSafety(getDataFromFieldsHits, fields); + expect(result).toEqual(resultFields); + }); +}); diff --git a/x-pack/plugins/timelines/common/utils/field_formatters.ts b/x-pack/plugins/timelines/common/utils/field_formatters.ts new file mode 100644 index 0000000000000..b436f8e616122 --- /dev/null +++ b/x-pack/plugins/timelines/common/utils/field_formatters.ts @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { get, isEmpty, isNumber, isObject, isString } from 'lodash/fp'; + +import { EventHit, EventSource, TimelineEventsDetailsItem } from '../search_strategy'; +import { toObjectArrayOfStrings, toStringArray } from './to_array'; + +export const baseCategoryFields = ['@timestamp', 'labels', 'message', 'tags']; + +export const getFieldCategory = (field: string): string => { + const fieldCategory = field.split('.')[0]; + if (!isEmpty(fieldCategory) && baseCategoryFields.includes(fieldCategory)) { + return 'base'; + } + return fieldCategory; +}; + +export const formatGeoLocation = (item: unknown[]) => { + const itemGeo = item.length > 0 ? (item[0] as { coordinates: number[] }) : null; + if (itemGeo != null && !isEmpty(itemGeo.coordinates)) { + try { + return toStringArray({ + lon: itemGeo.coordinates[0], + lat: itemGeo.coordinates[1], + }); + } catch { + return toStringArray(item); + } + } + return toStringArray(item); +}; + +export const isGeoField = (field: string) => + field.includes('geo.location') || field.includes('geoip.location'); + +export const getDataFromSourceHits = ( + sources: EventSource, + category?: string, + path?: string +): TimelineEventsDetailsItem[] => + Object.keys(sources).reduce<TimelineEventsDetailsItem[]>((accumulator, source) => { + const item: EventSource = get(source, sources); + if (Array.isArray(item) || isString(item) || isNumber(item)) { + const field = path ? `${path}.${source}` : source; + const fieldCategory = getFieldCategory(field); + + const objArrStr = toObjectArrayOfStrings(item); + const strArr = objArrStr.map(({ str }) => str); + const isObjectArray = objArrStr.some((o) => o.isObjectArray); + + return [ + ...accumulator, + { + category: fieldCategory, + field, + values: strArr, + originalValue: strArr, + isObjectArray, + } as TimelineEventsDetailsItem, + ]; + } else if (isObject(item)) { + return [ + ...accumulator, + ...getDataFromSourceHits(item, category || source, path ? `${path}.${source}` : source), + ]; + } + return accumulator; + }, []); + +export const getDataFromFieldsHits = ( + fields: EventHit['fields'], + prependField?: string, + prependFieldCategory?: string +): TimelineEventsDetailsItem[] => + Object.keys(fields).reduce<TimelineEventsDetailsItem[]>((accumulator, field) => { + const item: unknown[] = fields[field]; + + const fieldCategory = + prependFieldCategory != null ? prependFieldCategory : getFieldCategory(field); + if (isGeoField(field)) { + return [ + ...accumulator, + { + category: fieldCategory, + field, + values: formatGeoLocation(item), + originalValue: formatGeoLocation(item), + isObjectArray: true, // important for UI + }, + ]; + } + const objArrStr = toObjectArrayOfStrings(item); + const strArr = objArrStr.map(({ str }) => str); + const isObjectArray = objArrStr.some((o) => o.isObjectArray); + const dotField = prependField ? `${prependField}.${field}` : field; + + // return simple field value (non-object, non-array) + if (!isObjectArray) { + return [ + ...accumulator, + { + category: fieldCategory, + field: dotField, + values: strArr, + originalValue: strArr, + isObjectArray, + }, + ]; + } + + // format nested fields + const nestedFields = Array.isArray(item) + ? item + .reduce((acc, i) => [...acc, getDataFromFieldsHits(i, dotField, fieldCategory)], []) + .flat() + : getDataFromFieldsHits(item, prependField, fieldCategory); + + // combine duplicate fields + const flat: Record<string, TimelineEventsDetailsItem> = [ + ...accumulator, + ...nestedFields, + ].reduce( + (acc, f) => ({ + ...acc, + // acc/flat is hashmap to determine if we already have the field or not without an array iteration + // its converted back to array in return with Object.values + ...(acc[f.field] != null + ? { + [f.field]: { + ...f, + originalValue: acc[f.field].originalValue.includes(f.originalValue[0]) + ? acc[f.field].originalValue + : [...acc[f.field].originalValue, ...f.originalValue], + values: acc[f.field].values.includes(f.values[0]) + ? acc[f.field].values + : [...acc[f.field].values, ...f.values], + }, + } + : { [f.field]: f }), + }), + {} + ); + + return Object.values(flat); + }, []); + +export const getDataSafety = <A, T>(fn: (args: A) => T, args: A): Promise<T> => + new Promise((resolve) => setTimeout(() => resolve(fn(args)))); diff --git a/x-pack/plugins/timelines/common/utils/to_array.ts b/x-pack/plugins/timelines/common/utils/to_array.ts new file mode 100644 index 0000000000000..fbb2b8d48a250 --- /dev/null +++ b/x-pack/plugins/timelines/common/utils/to_array.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const toArray = <T = string>(value: T | T[] | null): T[] => + Array.isArray(value) ? value : value == null ? [] : [value]; +export const toStringArray = <T = string>(value: T | T[] | null): string[] => { + if (Array.isArray(value)) { + return value.reduce<string[]>((acc, v) => { + if (v != null) { + switch (typeof v) { + case 'number': + case 'boolean': + return [...acc, v.toString()]; + case 'object': + try { + return [...acc, JSON.stringify(v)]; + } catch { + return [...acc, 'Invalid Object']; + } + case 'string': + return [...acc, v]; + default: + return [...acc, `${v}`]; + } + } + return acc; + }, []); + } else if (value == null) { + return []; + } else if (!Array.isArray(value) && typeof value === 'object') { + try { + return [JSON.stringify(value)]; + } catch { + return ['Invalid Object']; + } + } else { + return [`${value}`]; + } +}; +export const toObjectArrayOfStrings = <T = string>( + value: T | T[] | null +): Array<{ + str: string; + isObjectArray?: boolean; +}> => { + if (Array.isArray(value)) { + return value.reduce< + Array<{ + str: string; + isObjectArray?: boolean; + }> + >((acc, v) => { + if (v != null) { + switch (typeof v) { + case 'number': + case 'boolean': + return [...acc, { str: v.toString() }]; + case 'object': + try { + return [...acc, { str: JSON.stringify(v), isObjectArray: true }]; // need to track when string is not a simple value + } catch { + return [...acc, { str: 'Invalid Object' }]; + } + case 'string': + return [...acc, { str: v }]; + default: + return [...acc, { str: `${v}` }]; + } + } + return acc; + }, []); + } else if (value == null) { + return []; + } else if (!Array.isArray(value) && typeof value === 'object') { + try { + return [{ str: JSON.stringify(value), isObjectArray: true }]; + } catch { + return [{ str: 'Invalid Object' }]; + } + } else { + return [{ str: `${value}` }]; + } +}; diff --git a/x-pack/plugins/timelines/jest.config.js b/x-pack/plugins/timelines/jest.config.js new file mode 100644 index 0000000000000..12bc67dbb2f07 --- /dev/null +++ b/x-pack/plugins/timelines/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: ['<rootDir>/x-pack/plugins/timelines'], +}; diff --git a/x-pack/plugins/timelines/kibana.json b/x-pack/plugins/timelines/kibana.json index 552ddfd25ce73..5cc05a5996f74 100644 --- a/x-pack/plugins/timelines/kibana.json +++ b/x-pack/plugins/timelines/kibana.json @@ -3,8 +3,9 @@ "version": "1.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "timelines"], + "extraPublicDirs": ["common"], "server": true, "ui": true, - "requiredPlugins": [], + "requiredPlugins": ["data", "dataEnhanced", "kibanaReact", "kibanaUtils"], "optionalPlugins": [] } diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_keyboard_wrapper_hook/index.tsx b/x-pack/plugins/timelines/public/components/drag_and_drop/draggable_keyboard_wrapper_hook/index.tsx similarity index 92% rename from x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_keyboard_wrapper_hook/index.tsx rename to x-pack/plugins/timelines/public/components/drag_and_drop/draggable_keyboard_wrapper_hook/index.tsx index ac08fbe63e7c9..9eb5d7dc640c7 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_keyboard_wrapper_hook/index.tsx +++ b/x-pack/plugins/timelines/public/components/drag_and_drop/draggable_keyboard_wrapper_hook/index.tsx @@ -6,13 +6,13 @@ */ import React, { useCallback, useMemo, useState } from 'react'; -import { FluidDragActions } from 'react-beautiful-dnd'; +import type { FluidDragActions } from 'react-beautiful-dnd'; import { useAddToTimeline } from '../../../hooks/use_add_to_timeline'; import { draggableKeyDownHandler } from '../helpers'; -interface Props { +export interface UseDraggableKeyboardWrapperProps { closePopover?: () => void; draggableId: string; fieldName: string; @@ -31,7 +31,7 @@ export const useDraggableKeyboardWrapper = ({ fieldName, keyboardHandlerRef, openPopover, -}: Props): UseDraggableKeyboardWrapper => { +}: UseDraggableKeyboardWrapperProps): UseDraggableKeyboardWrapper => { const { beginDrag, cancelDrag, dragToLocation, endDrag, hasDraggableLock } = useAddToTimeline({ draggableId, fieldName, @@ -44,7 +44,7 @@ export const useDraggableKeyboardWrapper = ({ cancelDrag(prevDragAction); return null; } - return prevDragAction; + return null; }); }, [cancelDrag]); diff --git a/x-pack/plugins/timelines/public/components/drag_and_drop/helpers.ts b/x-pack/plugins/timelines/public/components/drag_and_drop/helpers.ts new file mode 100644 index 0000000000000..aaf4499cf5ad8 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/drag_and_drop/helpers.ts @@ -0,0 +1,211 @@ +/* + * 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 { DropResult, FluidDragActions, Position } from 'react-beautiful-dnd'; +import { KEYBOARD_DRAG_OFFSET, getFieldIdFromDraggable } from '@kbn/securitysolution-t-grid'; +import { Dispatch } from 'redux'; +import { isString, keyBy } from 'lodash/fp'; + +import { stopPropagationAndPreventDefault, TimelineId } from '../../../common'; +// eslint-disable-next-line no-duplicate-imports +import type { BrowserField, BrowserFields, ColumnHeaderOptions } from '../../../common'; +import { tGridActions } from '../../store/t_grid'; +import { DEFAULT_COLUMN_MIN_WIDTH } from '../t_grid/body/constants'; + +/** + * Temporarily disables tab focus on child links of the draggable to work + * around an issue where tab focus becomes stuck on the interactive children + * + * NOTE: This function is (intentionally) only effective when used in a key + * event handler, because it automatically restores focus capabilities on + * the next tick. + */ +export const temporarilyDisableInteractiveChildTabIndexes = (draggableElement: HTMLDivElement) => { + const interactiveChildren = draggableElement.querySelectorAll('a, button'); + interactiveChildren.forEach((interactiveChild) => { + interactiveChild.setAttribute('tabindex', '-1'); // DOM mutation + }); + + // restore the default tabindexs on the next tick: + setTimeout(() => { + interactiveChildren.forEach((interactiveChild) => { + interactiveChild.setAttribute('tabindex', '0'); // DOM mutation + }); + }, 0); +}; + +export interface DraggableKeyDownHandlerProps { + beginDrag: () => FluidDragActions | null; + cancelDragActions: () => void; + closePopover?: () => void; + draggableElement: HTMLDivElement; + dragActions: FluidDragActions | null; + dragToLocation: ({ + dragActions, + position, + }: { + dragActions: FluidDragActions | null; + position: Position; + }) => void; + keyboardEvent: React.KeyboardEvent; + endDrag: (dragActions: FluidDragActions | null) => void; + openPopover?: () => void; + setDragActions: (value: React.SetStateAction<FluidDragActions | null>) => void; +} + +export const draggableKeyDownHandler = ({ + beginDrag, + cancelDragActions, + closePopover, + draggableElement, + dragActions, + dragToLocation, + endDrag, + keyboardEvent, + openPopover, + setDragActions, +}: DraggableKeyDownHandlerProps) => { + let currentPosition: DOMRect | null = null; + + switch (keyboardEvent.key) { + case ' ': + if (!dragActions) { + // start dragging, because space was pressed + if (closePopover != null) { + closePopover(); + } + setDragActions(beginDrag()); + } else { + // end dragging, because space was pressed + endDrag(dragActions); + setDragActions(null); + } + break; + case 'Escape': + cancelDragActions(); + break; + case 'Tab': + // IMPORTANT: we do NOT want to stop propagation and prevent default when Tab is pressed + temporarilyDisableInteractiveChildTabIndexes(draggableElement); + break; + case 'ArrowUp': + currentPosition = draggableElement.getBoundingClientRect(); + dragToLocation({ + dragActions, + position: { x: currentPosition.x, y: currentPosition.y - KEYBOARD_DRAG_OFFSET }, + }); + break; + case 'ArrowDown': + currentPosition = draggableElement.getBoundingClientRect(); + dragToLocation({ + dragActions, + position: { x: currentPosition.x, y: currentPosition.y + KEYBOARD_DRAG_OFFSET }, + }); + break; + case 'ArrowLeft': + currentPosition = draggableElement.getBoundingClientRect(); + dragToLocation({ + dragActions, + position: { x: currentPosition.x - KEYBOARD_DRAG_OFFSET, y: currentPosition.y }, + }); + break; + case 'ArrowRight': + currentPosition = draggableElement.getBoundingClientRect(); + dragToLocation({ + dragActions, + position: { x: currentPosition.x + KEYBOARD_DRAG_OFFSET, y: currentPosition.y }, + }); + break; + case 'Enter': + stopPropagationAndPreventDefault(keyboardEvent); // prevents the first item in the popover from getting an errant ENTER + if (!dragActions && openPopover != null) { + openPopover(); + } + break; + default: + break; + } +}; +const getAllBrowserFields = (browserFields: BrowserFields): Array<Partial<BrowserField>> => + Object.values(browserFields).reduce<Array<Partial<BrowserField>>>( + (acc, namespace) => [ + ...acc, + ...Object.values(namespace.fields != null ? namespace.fields : {}), + ], + [] + ); + +const getAllFieldsByName = ( + browserFields: BrowserFields +): { [fieldName: string]: Partial<BrowserField> } => + keyBy('name', getAllBrowserFields(browserFields)); + +const linkFields: Record<string, string> = { + 'signal.rule.name': 'signal.rule.id', + 'event.module': 'rule.reference', +}; + +interface AddFieldToTimelineColumnsParams { + defaultsHeader: ColumnHeaderOptions[]; + browserFields: BrowserFields; + dispatch: Dispatch; + result: DropResult; + timelineId: string; +} + +export const addFieldToTimelineColumns = ({ + browserFields, + dispatch, + result, + timelineId, + defaultsHeader, +}: AddFieldToTimelineColumnsParams): void => { + const fieldId = getFieldIdFromDraggable(result); + const allColumns = getAllFieldsByName(browserFields); + const column = allColumns[fieldId]; + const initColumnHeader = + timelineId === TimelineId.detectionsPage || timelineId === TimelineId.detectionsRulesDetailsPage + ? defaultsHeader.find((c) => c.id === fieldId) ?? {} + : {}; + + if (column != null) { + dispatch( + tGridActions.upsertColumn({ + column: { + category: column.category, + columnHeaderType: 'not-filtered', + description: isString(column.description) ? column.description : undefined, + example: isString(column.example) ? column.example : undefined, + id: fieldId, + linkField: linkFields[fieldId] ?? undefined, + type: column.type, + aggregatable: column.aggregatable, + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + ...initColumnHeader, + }, + id: timelineId, + index: result.destination != null ? result.destination.index : 0, + }) + ); + } else { + // create a column definition, because it doesn't exist in the browserFields: + dispatch( + tGridActions.upsertColumn({ + column: { + columnHeaderType: 'not-filtered', + id: fieldId, + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, + id: timelineId, + index: result.destination != null ? result.destination.index : 0, + }) + ); + } +}; + +export const getTimelineIdFromColumnDroppableId = (droppableId: string) => + droppableId.slice(droppableId.lastIndexOf('.') + 1); diff --git a/x-pack/plugins/timelines/public/components/drag_and_drop/index.tsx b/x-pack/plugins/timelines/public/components/drag_and_drop/index.tsx new file mode 100644 index 0000000000000..65ec238ea4d40 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/drag_and_drop/index.tsx @@ -0,0 +1,93 @@ +/* + * 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 { + IS_DRAGGING_CLASS_NAME, + draggableIsField, + fieldWasDroppedOnTimelineColumns, + IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME, +} from '@kbn/securitysolution-t-grid'; +import { noop } from 'lodash/fp'; +import deepEqual from 'fast-deep-equal'; +import React, { useCallback } from 'react'; +import { DropResult, DragDropContext, BeforeCapture } from 'react-beautiful-dnd'; +import { useDispatch } from 'react-redux'; + +import type { ColumnHeaderOptions, BrowserFields } from '../../../common'; +import { useAddToTimelineSensor } from '../../hooks/use_add_to_timeline'; +import { addFieldToTimelineColumns, getTimelineIdFromColumnDroppableId } from './helpers'; + +export * from './draggable_keyboard_wrapper_hook'; +export * from './helpers'; + +interface Props { + browserFields: BrowserFields; + defaultsHeader: ColumnHeaderOptions[]; + children: React.ReactNode; +} + +const sensors = [useAddToTimelineSensor]; + +const DragDropContextWrapperComponent: React.FC<Props> = ({ + browserFields, + defaultsHeader, + children, +}) => { + const dispatch = useDispatch(); + + const onDragEnd = useCallback( + (result: DropResult) => { + try { + enableScrolling(); + + if (fieldWasDroppedOnTimelineColumns(result)) { + addFieldToTimelineColumns({ + browserFields, + defaultsHeader, + dispatch, + result, + timelineId: getTimelineIdFromColumnDroppableId(result.destination?.droppableId ?? ''), + }); + } + } finally { + document.body.classList.remove(IS_DRAGGING_CLASS_NAME); + + if (draggableIsField(result)) { + document.body.classList.remove(IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME); + } + } + }, + [browserFields, defaultsHeader, dispatch] + ); + return ( + <DragDropContext onDragEnd={onDragEnd} onBeforeCapture={onBeforeCapture} sensors={sensors}> + {children} + </DragDropContext> + ); +}; + +DragDropContextWrapperComponent.displayName = 'DragDropContextWrapperComponent'; + +export const DragDropContextWrapper = React.memo( + DragDropContextWrapperComponent, + // prevent re-renders when data providers are added or removed, but all other props are the same + (prevProps, nextProps) => deepEqual(prevProps.children, nextProps.children) +); + +DragDropContextWrapper.displayName = 'DragDropContextWrapper'; + +const onBeforeCapture = (before: BeforeCapture) => { + if (!draggableIsField(before)) { + document.body.classList.add(IS_DRAGGING_CLASS_NAME); + } + + if (draggableIsField(before)) { + document.body.classList.add(IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME); + } +}; + +const enableScrolling = () => (window.onscroll = () => noop); diff --git a/x-pack/plugins/timelines/public/components/draggables/field_badge/index.tsx b/x-pack/plugins/timelines/public/components/draggables/field_badge/index.tsx new file mode 100644 index 0000000000000..62f7e091fae9c --- /dev/null +++ b/x-pack/plugins/timelines/public/components/draggables/field_badge/index.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 { rgba } from 'polished'; +import React from 'react'; +import styled from 'styled-components'; + +interface WidthProp { + width?: number; +} + +const Field = styled.div.attrs<WidthProp>(({ width }) => { + if (width) { + return { + style: { + width: `${width}px`, + }, + }; + } +})<WidthProp>` + background-color: ${({ theme }) => theme.eui.euiColorEmptyShade}; + border: ${({ theme }) => theme.eui.euiBorderThin}; + box-shadow: 0 2px 2px -1px ${({ theme }) => rgba(theme.eui.euiColorMediumShade, 0.3)}, + 0 1px 5px -2px ${({ theme }) => rgba(theme.eui.euiColorMediumShade, 0.3)}; + font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; + font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold}; + line-height: ${({ theme }) => theme.eui.euiLineHeight}; + padding: ${({ theme }) => theme.eui.paddingSizes.xs}; +`; +Field.displayName = 'Field'; + +/** + * Renders a field (e.g. `event.action`) as a draggable badge + */ + +export const DraggableFieldBadge = React.memo<{ fieldId: string; fieldWidth?: number }>( + ({ fieldId, fieldWidth }) => ( + <Field data-test-subj="field" width={fieldWidth}> + {fieldId} + </Field> + ) +); + +DraggableFieldBadge.displayName = 'DraggableFieldBadge'; diff --git a/x-pack/plugins/timelines/public/components/draggables/field_badge/translations.ts b/x-pack/plugins/timelines/public/components/draggables/field_badge/translations.ts new file mode 100644 index 0000000000000..6c8143c228e14 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/draggables/field_badge/translations.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const CATEGORY = i18n.translate('xpack.timelines.draggables.field.categoryLabel', { + defaultMessage: 'Category', +}); + +export const COPY_TO_CLIPBOARD = i18n.translate( + 'xpack.timelines.eventDetails.copyToClipboardTooltip', + { + defaultMessage: 'Copy to Clipboard', + } +); + +export const FIELD = i18n.translate('xpack.timelines.draggables.field.fieldLabel', { + defaultMessage: 'Field', +}); + +export const TYPE = i18n.translate('xpack.timelines.draggables.field.typeLabel', { + defaultMessage: 'Type', +}); + +export const VIEW_CATEGORY = i18n.translate( + 'xpack.timelines.draggables.field.viewCategoryTooltip', + { + defaultMessage: 'View Category', + } +); diff --git a/x-pack/plugins/timelines/public/components/draggables/index.tsx b/x-pack/plugins/timelines/public/components/draggables/index.tsx new file mode 100644 index 0000000000000..a87d97b7ea74a --- /dev/null +++ b/x-pack/plugins/timelines/public/components/draggables/index.tsx @@ -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 './field_badge'; diff --git a/x-pack/plugins/timelines/public/components/exit_full_screen/index.test.tsx b/x-pack/plugins/timelines/public/components/exit_full_screen/index.test.tsx new file mode 100644 index 0000000000000..b60bdafd0835f --- /dev/null +++ b/x-pack/plugins/timelines/public/components/exit_full_screen/index.test.tsx @@ -0,0 +1,60 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; + +import { TestProviders } from '../../mock/test_providers'; +import * as i18n from './translations'; +import { ExitFullScreen, EXIT_FULL_SCREEN_CLASS_NAME } from '.'; + +describe('ExitFullScreen', () => { + test('it returns null when fullScreen is false', () => { + const exitFullScreen = mount( + <TestProviders> + <ExitFullScreen fullScreen={false} setFullScreen={jest.fn()} /> + </TestProviders> + ); + + expect(exitFullScreen.find('[data-test-subj="exit-full-screen"]').exists()).toBe(false); + }); + + test('it renders a button with the exported EXIT_FULL_SCREEN_CLASS_NAME class when fullScreen is true', () => { + const exitFullScreen = mount( + <TestProviders> + <ExitFullScreen fullScreen={true} setFullScreen={jest.fn()} /> + </TestProviders> + ); + + expect(exitFullScreen.find(`button.${EXIT_FULL_SCREEN_CLASS_NAME}`).exists()).toBe(true); + }); + + test('it renders the expected button text when fullScreen is true', () => { + const exitFullScreen = mount( + <TestProviders> + <ExitFullScreen fullScreen={true} setFullScreen={jest.fn()} /> + </TestProviders> + ); + + expect(exitFullScreen.find('[data-test-subj="exit-full-screen"]').first().text()).toBe( + i18n.EXIT_FULL_SCREEN + ); + }); + + test('it invokes setFullScreen with a value of false when the button is clicked', () => { + const setFullScreen = jest.fn(); + + const exitFullScreen = mount( + <TestProviders> + <ExitFullScreen fullScreen={true} setFullScreen={setFullScreen} /> + </TestProviders> + ); + + exitFullScreen.find('[data-test-subj="exit-full-screen"]').first().simulate('click'); + expect(setFullScreen).toBeCalledWith(false); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/exit_full_screen/index.tsx b/x-pack/plugins/timelines/public/components/exit_full_screen/index.tsx new file mode 100644 index 0000000000000..5ae537128bee6 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/exit_full_screen/index.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiButton, EuiWindowEvent } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import styled from 'styled-components'; + +import * as i18n from './translations'; + +export const EXIT_FULL_SCREEN_CLASS_NAME = 'exit-full-screen'; + +const StyledEuiButton = styled(EuiButton)` + margin: ${({ theme }) => theme.eui.paddingSizes.s}; +`; + +interface Props { + fullScreen: boolean; + setFullScreen: (fullScreen: boolean) => void; +} + +const ExitFullScreenComponent: React.FC<Props> = ({ fullScreen, setFullScreen }) => { + const exitFullScreen = useCallback(() => { + setFullScreen(false); + }, [setFullScreen]); + + const onKeyDown = useCallback( + (event: KeyboardEvent) => { + if (event.key === 'Escape') { + event.preventDefault(); + + exitFullScreen(); + } + }, + [exitFullScreen] + ); + + if (!fullScreen) { + return null; + } + + return ( + <> + <EuiWindowEvent event="keydown" handler={onKeyDown} /> + <StyledEuiButton + className={EXIT_FULL_SCREEN_CLASS_NAME} + data-test-subj="exit-full-screen" + fullWidth={false} + iconType="fullScreen" + isDisabled={!fullScreen} + onClick={exitFullScreen} + > + {i18n.EXIT_FULL_SCREEN} + </StyledEuiButton> + </> + ); +}; + +ExitFullScreenComponent.displayName = 'ExitFullScreenComponent'; + +export const ExitFullScreen = React.memo(ExitFullScreenComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/translations.ts b/x-pack/plugins/timelines/public/components/exit_full_screen/translations.ts similarity index 50% rename from x-pack/plugins/security_solution/public/common/components/header_global/translations.ts rename to x-pack/plugins/timelines/public/components/exit_full_screen/translations.ts index a2a22dfe31eb9..22aecebf12a07 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_global/translations.ts +++ b/x-pack/plugins/timelines/public/components/exit_full_screen/translations.ts @@ -7,13 +7,6 @@ import { i18n } from '@kbn/i18n'; -export const SECURITY_SOLUTION = i18n.translate( - 'xpack.securitySolution.headerGlobal.securitySolution', - { - defaultMessage: 'Security solution', - } -); - -export const BUTTON_ADD_DATA = i18n.translate('xpack.securitySolution.headerGlobal.buttonAddData', { - defaultMessage: 'Add data', +export const EXIT_FULL_SCREEN = i18n.translate('xpack.timelines.exitFullScreenButton', { + defaultMessage: 'Exit full screen', }); diff --git a/x-pack/plugins/timelines/public/components/index.tsx b/x-pack/plugins/timelines/public/components/index.tsx index f44ad8052917f..b242c0ec2a4a7 100644 --- a/x-pack/plugins/timelines/public/components/index.tsx +++ b/x-pack/plugins/timelines/public/components/index.tsx @@ -6,24 +6,53 @@ */ import React from 'react'; -import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { Provider } from 'react-redux'; +import { I18nProvider } from '@kbn/i18n/react'; +import { Store } from 'redux'; -import { PLUGIN_NAME } from '../../common'; -import { TimelineProps } from '../types'; +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; +import { createStore } from '../store/t_grid'; -export const Timeline = (props: TimelineProps) => { +import { TGrid as TGridComponent } from './tgrid'; +import { TGridProps } from '../types'; +import { DragDropContextWrapper } from './drag_and_drop'; +import { initialTGridState } from '../store/t_grid/reducer'; +import { TGridIntegratedProps } from './t_grid/integrated'; + +const EMPTY_BROWSER_FIELDS = {}; + +type TGridComponent = TGridProps & { + store?: Store; + storage: Storage; + data?: DataPublicPluginStart; +}; + +export const TGrid = (props: TGridComponent) => { + const { store, storage, ...tGridProps } = props; + let tGridStore = store; + if (!tGridStore && props.type === 'standalone') { + tGridStore = createStore(initialTGridState, storage); + } + let browserFields = EMPTY_BROWSER_FIELDS; + if ((tGridProps as TGridIntegratedProps).browserFields != null) { + browserFields = (tGridProps as TGridIntegratedProps).browserFields; + } return ( - <I18nProvider> - <div data-test-subj="timeline-wrapper"> - <FormattedMessage - id="xpack.timelines.placeholder" - defaultMessage="Plugin: {name} Timeline: {timelineId}" - values={{ name: PLUGIN_NAME, timelineId: props.timelineId }} - /> - </div> - </I18nProvider> + <Provider store={tGridStore!}> + <I18nProvider> + <DragDropContextWrapper browserFields={browserFields} defaultsHeader={props.columns}> + <TGridComponent {...tGridProps} /> + </DragDropContextWrapper> + </I18nProvider> + </Provider> ); }; // eslint-disable-next-line import/no-default-export -export { Timeline as default }; +export { TGrid as default }; + +export * from './drag_and_drop'; +export * from './draggables'; +export * from './last_updated'; +export * from './loading'; diff --git a/x-pack/plugins/timelines/public/components/inspect/index.test.tsx b/x-pack/plugins/timelines/public/components/inspect/index.test.tsx new file mode 100644 index 0000000000000..5d8af0a0653bd --- /dev/null +++ b/x-pack/plugins/timelines/public/components/inspect/index.test.tsx @@ -0,0 +1,105 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; +import { cloneDeep } from 'lodash/fp'; + +import { InspectButton, InspectButtonContainer, BUTTON_CLASS, InspectButtonProps } from '.'; + +describe('Inspect Button', () => { + const newQuery: InspectButtonProps = { + inspect: null, + loading: false, + title: 'My title', + }; + + describe('Render', () => { + test('Eui Icon Button', () => { + const wrapper = mount(<InspectButton {...newQuery} />); + expect(wrapper.find('button[data-test-subj="inspect-icon-button"]').first().exists()).toBe( + true + ); + }); + + test('Eui Icon Button disabled', () => { + const wrapper = mount(<InspectButton isDisabled={true} {...newQuery} />); + expect(wrapper.find('.euiButtonIcon').get(0).props.disabled).toBe(true); + }); + + describe('InspectButtonContainer', () => { + test('it renders a transparent inspect button by default', async () => { + const wrapper = mount( + <InspectButtonContainer> + <InspectButton {...newQuery} /> + </InspectButtonContainer> + ); + + expect(wrapper.find(`InspectButtonContainer`)).toHaveStyleRule('opacity', '0', { + modifier: `.${BUTTON_CLASS}`, + }); + }); + + test('it renders an opaque inspect button when it has mouse focus', async () => { + const wrapper = mount( + <InspectButtonContainer> + <InspectButton {...newQuery} /> + </InspectButtonContainer> + ); + + expect(wrapper.find(`InspectButtonContainer`)).toHaveStyleRule('opacity', '1', { + modifier: `:hover .${BUTTON_CLASS}`, + }); + }); + }); + }); + + describe('Modal Inspect - happy path', () => { + const myQuery = cloneDeep(newQuery); + beforeEach(() => { + myQuery.inspect = { + dsl: ['my dsl'], + response: ['my response'], + }; + }); + test('Open Inspect Modal', () => { + const wrapper = mount(<InspectButton {...myQuery} />); + + wrapper.find('button[data-test-subj="inspect-icon-button"]').first().simulate('click'); + wrapper.update(); + expect(wrapper.find('button[data-test-subj="modal-inspect-close"]').first().exists()).toBe( + true + ); + }); + + test('Close Inspect Modal', () => { + const wrapper = mount(<InspectButton {...myQuery} />); + wrapper.find('button[data-test-subj="inspect-icon-button"]').first().simulate('click'); + + wrapper.update(); + wrapper.find('button[data-test-subj="modal-inspect-close"]').first().simulate('click'); + + wrapper.update(); + expect(wrapper.find('button[data-test-subj="modal-inspect-close"]').first().exists()).toBe( + false + ); + }); + + test('Do not Open Inspect Modal if it is loading', () => { + const wrapper = mount( + <InspectButton title={myQuery.title} inspect={myQuery.inspect} loading={true} /> + ); + wrapper.find('button[data-test-subj="inspect-icon-button"]').first().simulate('click'); + + wrapper.update(); + + expect(wrapper.find('button[data-test-subj="modal-inspect-close"]').first().exists()).toBe( + false + ); + }); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/inspect/index.tsx b/x-pack/plugins/timelines/public/components/inspect/index.tsx new file mode 100644 index 0000000000000..a174cc08a83ee --- /dev/null +++ b/x-pack/plugins/timelines/public/components/inspect/index.tsx @@ -0,0 +1,114 @@ +/* + * 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 { EuiButtonIcon } from '@elastic/eui'; +import { getOr } from 'lodash/fp'; +import React, { useCallback, useState } from 'react'; +import styled, { css } from 'styled-components'; + +import { ModalInspectQuery } from './modal'; +import * as i18n from './translations'; +import { InspectQuery } from '../../store/t_grid/inputs'; + +export const BUTTON_CLASS = 'inspectButtonComponent'; + +export const InspectButtonContainer = styled.div<{ show?: boolean }>` + width: 100%; + display: flex; + flex-grow: 1; + + > * { + max-width: 100%; + } + + .${BUTTON_CLASS} { + pointer-events: none; + opacity: 0; + transition: opacity ${(props) => getOr(250, 'theme.eui.euiAnimSpeedNormal', props)} ease; + } + + ${({ show }) => + show && + css` + &:hover .${BUTTON_CLASS} { + pointer-events: auto; + opacity: 1; + } + `} +`; + +InspectButtonContainer.displayName = 'InspectButtonContainer'; + +InspectButtonContainer.defaultProps = { + show: true, +}; + +interface OwnProps { + inspect: InspectQuery | null; + isDisabled?: boolean; + loading: boolean; + onCloseInspect?: () => void; + title: string | React.ReactElement | React.ReactNode; +} + +export type InspectButtonProps = OwnProps; + +const InspectButtonComponent: React.FC<InspectButtonProps> = ({ + inspect, + isDisabled, + loading, + onCloseInspect, + title = '', +}) => { + const [isInspected, setIsInspected] = useState(false); + const isShowingModal = !loading && isInspected; + const handleClick = useCallback(() => { + setIsInspected(true); + }, []); + + const handleCloseModal = useCallback(() => { + if (onCloseInspect != null) { + onCloseInspect(); + } + setIsInspected(false); + }, [onCloseInspect, setIsInspected]); + + let request: string | null = null; + if (inspect != null && inspect.dsl.length > 0) { + request = inspect.dsl[0]; + } + + let response: string | null = null; + if (inspect != null && inspect.response.length > 0) { + response = inspect.response[0]; + } + + return ( + <> + <EuiButtonIcon + className={BUTTON_CLASS} + aria-label={i18n.INSPECT} + data-test-subj="inspect-icon-button" + iconSize="m" + iconType="inspect" + isDisabled={loading || isDisabled || false} + title={i18n.INSPECT} + onClick={handleClick} + /> + <ModalInspectQuery + closeModal={handleCloseModal} + isShowing={isShowingModal} + request={request} + response={response} + title={title} + data-test-subj="inspect-modal" + /> + </> + ); +}; + +export const InspectButton = React.memo(InspectButtonComponent); diff --git a/x-pack/plugins/timelines/public/components/inspect/modal.test.tsx b/x-pack/plugins/timelines/public/components/inspect/modal.test.tsx new file mode 100644 index 0000000000000..5ac75f92ea45f --- /dev/null +++ b/x-pack/plugins/timelines/public/components/inspect/modal.test.tsx @@ -0,0 +1,282 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { getMockTheme } from '../../mock/kibana_react.mock'; + +import { ModalInspectQuery, formatIndexPatternRequested, NO_ALERT_INDEX } from './modal'; + +const mockTheme = getMockTheme({ + eui: { + euiBreakpoints: { + l: '1200px', + }, + }, +}); + +const request = + '{"index": ["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"allowNoIndices": true, "ignoreUnavailable": true, "body": { "aggregations": {"hosts": {"cardinality": {"field": "host.name" } }, "hosts_histogram": {"auto_date_histogram": {"field": "@timestamp","buckets": "6"},"aggs": { "count": {"cardinality": {"field": "host.name" }}}}}, "query": {"bool": {"filter": [{"range": { "@timestamp": {"gte": 1562290224506,"lte": 1562376624506 }}}]}}, "size": 0, "track_total_hits": false}}'; +const response = + '{"took": 880,"timed_out": false,"_shards": {"total": 26,"successful": 26,"skipped": 0,"failed": 0},"hits": {"max_score": null,"hits": []},"aggregations": {"hosts": {"value": 541},"hosts_histogram": {"buckets": [{"key_as_string": "2019 - 07 - 05T01: 00: 00.000Z", "key": 1562288400000, "doc_count": 1492321, "count": { "value": 105 }}, {"key_as_string": "2019 - 07 - 05T13: 00: 00.000Z", "key": 1562331600000, "doc_count": 2412761, "count": { "value": 453}},{"key_as_string": "2019 - 07 - 06T01: 00: 00.000Z", "key": 1562374800000, "doc_count": 111658, "count": { "value": 15}}],"interval": "12h"}},"status": 200}'; + +describe('Modal Inspect', () => { + const closeModal = jest.fn(); + + describe('rendering', () => { + test('when isShowing is positive and request and response are not null', () => { + const wrapper = mount( + <ThemeProvider theme={mockTheme}> + <ModalInspectQuery + closeModal={closeModal} + isShowing={true} + request={request} + response={response} + title="My title" + /> + </ThemeProvider> + ); + expect(wrapper.find('[data-test-subj="modal-inspect-euiModal"]').first().exists()).toBe(true); + expect(wrapper.find('.euiModalHeader__title').first().text()).toBe('Inspect My title'); + }); + + test('when isShowing is negative and request and response are not null', () => { + const wrapper = mount( + <ModalInspectQuery + closeModal={closeModal} + isShowing={false} + request={request} + response={response} + title="My title" + /> + ); + expect(wrapper.find('[data-test-subj="modal-inspect-euiModal"]').first().exists()).toBe( + false + ); + }); + + test('when isShowing is positive and request is null and response is not null', () => { + const wrapper = mount( + <ModalInspectQuery + closeModal={closeModal} + isShowing={true} + request={null} + response={response} + title="My title" + /> + ); + expect(wrapper.find('[data-test-subj="modal-inspect-euiModal"]').first().exists()).toBe( + false + ); + }); + + test('when isShowing is positive and request is not null and response is null', () => { + const wrapper = mount( + <ModalInspectQuery + closeModal={closeModal} + isShowing={true} + request={request} + response={null} + title="My title" + /> + ); + expect(wrapper.find('[data-test-subj="modal-inspect-euiModal"]').first().exists()).toBe( + false + ); + }); + }); + + describe('functionality from tab statistics/request/response', () => { + test('Click on statistic Tab', () => { + const wrapper = mount( + <ThemeProvider theme={mockTheme}> + <ModalInspectQuery + closeModal={closeModal} + isShowing={true} + request={request} + response={response} + title="My title" + /> + </ThemeProvider> + ); + + wrapper.find('.euiTab').first().simulate('click'); + wrapper.update(); + + expect( + wrapper.find('.euiDescriptionList__title span[data-test-subj="index-pattern-title"]').text() + ).toBe('Index pattern '); + expect( + wrapper + .find('.euiDescriptionList__description span[data-test-subj="index-pattern-description"]') + .text() + ).toBe('auditbeat-*, filebeat-*, packetbeat-*, winlogbeat-*'); + expect( + wrapper.find('.euiDescriptionList__title span[data-test-subj="query-time-title"]').text() + ).toBe('Query time '); + expect( + wrapper + .find('.euiDescriptionList__description span[data-test-subj="query-time-description"]') + .text() + ).toBe('880ms'); + expect( + wrapper + .find('.euiDescriptionList__title span[data-test-subj="request-timestamp-title"]') + .text() + ).toBe('Request timestamp '); + }); + + test('Click on request Tab', () => { + const wrapper = mount( + <ThemeProvider theme={mockTheme}> + <ModalInspectQuery + closeModal={closeModal} + isShowing={true} + request={request} + response={response} + title="My title" + /> + </ThemeProvider> + ); + + wrapper.find('.euiTab').at(2).simulate('click'); + wrapper.update(); + + expect(JSON.parse(wrapper.find('EuiCodeBlock').first().text())).toEqual({ + took: 880, + timed_out: false, + _shards: { + total: 26, + successful: 26, + skipped: 0, + failed: 0, + }, + hits: { + max_score: null, + hits: [], + }, + aggregations: { + hosts: { + value: 541, + }, + hosts_histogram: { + buckets: [ + { + key_as_string: '2019 - 07 - 05T01: 00: 00.000Z', + key: 1562288400000, + doc_count: 1492321, + count: { + value: 105, + }, + }, + { + key_as_string: '2019 - 07 - 05T13: 00: 00.000Z', + key: 1562331600000, + doc_count: 2412761, + count: { + value: 453, + }, + }, + { + key_as_string: '2019 - 07 - 06T01: 00: 00.000Z', + key: 1562374800000, + doc_count: 111658, + count: { + value: 15, + }, + }, + ], + interval: '12h', + }, + }, + status: 200, + }); + }); + + test('Click on response Tab', () => { + const wrapper = mount( + <ThemeProvider theme={mockTheme}> + <ModalInspectQuery + closeModal={closeModal} + isShowing={true} + request={request} + response={response} + title="My title" + /> + </ThemeProvider> + ); + + wrapper.find('.euiTab').at(1).simulate('click'); + wrapper.update(); + + expect(JSON.parse(wrapper.find('EuiCodeBlock').first().text())).toEqual({ + aggregations: { + hosts: { cardinality: { field: 'host.name' } }, + hosts_histogram: { + aggs: { count: { cardinality: { field: 'host.name' } } }, + auto_date_histogram: { buckets: '6', field: '@timestamp' }, + }, + }, + query: { + bool: { + filter: [{ range: { '@timestamp': { gte: 1562290224506, lte: 1562376624506 } } }], + }, + }, + size: 0, + track_total_hits: false, + }); + }); + }); + + describe('events', () => { + test('Make sure that toggle function has been called when you click on the close button', () => { + const wrapper = mount( + <ThemeProvider theme={mockTheme}> + <ModalInspectQuery + closeModal={closeModal} + isShowing={true} + request={request} + response={response} + title="My title" + /> + </ThemeProvider> + ); + + wrapper.find('button[data-test-subj="modal-inspect-close"]').simulate('click'); + wrapper.update(); + expect(closeModal).toHaveBeenCalled(); + }); + }); + + describe('formatIndexPatternRequested', () => { + test('Return specific messages to NO_ALERT_INDEX if we only have one index and we match the index name `NO_ALERT_INDEX`', () => { + const expected = formatIndexPatternRequested([NO_ALERT_INDEX]); + expect(expected).toEqual(<i>{'No alert index found'}</i>); + }); + + test('Ignore NO_ALERT_INDEX if you have more than one indices', () => { + const expected = formatIndexPatternRequested([NO_ALERT_INDEX, 'indice-1']); + expect(expected).toEqual('indice-1'); + }); + + test('Happy path', () => { + const expected = formatIndexPatternRequested(['indice-1, indice-2']); + expect(expected).toEqual('indice-1, indice-2'); + }); + + test('Empty array with no indices', () => { + const expected = formatIndexPatternRequested([]); + expect(expected).toEqual('Sorry about that, something went wrong.'); + }); + + test('Undefined indices', () => { + const expected = formatIndexPatternRequested(undefined); + expect(expected).toEqual('Sorry about that, something went wrong.'); + }); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/inspect/modal.tsx b/x-pack/plugins/timelines/public/components/inspect/modal.tsx new file mode 100644 index 0000000000000..54cfc9827bb5f --- /dev/null +++ b/x-pack/plugins/timelines/public/components/inspect/modal.tsx @@ -0,0 +1,253 @@ +/* + * 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, + EuiCodeBlock, + EuiDescriptionList, + EuiIconTip, + EuiModal, + EuiModalBody, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalFooter, + EuiSpacer, + EuiTabbedContent, +} from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import React, { Fragment, ReactNode } from 'react'; +import styled from 'styled-components'; + +import * as i18n from './translations'; + +export const NO_ALERT_INDEX = 'no-alert-index-049FC71A-4C2C-446F-9901-37XMC5024C51'; + +const DescriptionListStyled = styled(EuiDescriptionList)` + @media only screen and (min-width: ${(props) => + props?.theme?.eui?.euiBreakpoints?.s ?? '600px'}) { + .euiDescriptionList__title { + width: 30% !important; + } + + .euiDescriptionList__description { + width: 70% !important; + } + } +`; + +DescriptionListStyled.displayName = 'DescriptionListStyled'; + +interface ModalInspectProps { + closeModal: () => void; + isShowing: boolean; + request: string | null; + response: string | null; + additionalRequests?: string[] | null; + additionalResponses?: string[] | null; + title: string | React.ReactElement | React.ReactNode; +} + +interface Request { + index: string[]; + allowNoIndices: boolean; + ignoreUnavailable: boolean; + body: Record<string, unknown>; +} + +interface Response { + took: number; + timed_out: boolean; + _shards: Record<string, unknown>; + hits: Record<string, unknown>; + aggregations: Record<string, unknown>; +} + +const MyEuiModal = styled(EuiModal)` + .euiModal__flex { + width: 60vw; + } + .euiCodeBlock { + height: auto !important; + max-width: 718px; + } +`; + +MyEuiModal.displayName = 'MyEuiModal'; +const parseInspectStrings = function <T>(stringsArray: string[]): T[] { + try { + return stringsArray.map((objectStringify) => JSON.parse(objectStringify)); + } catch { + return []; + } +}; + +const manageStringify = (object: Record<string, unknown> | Response): string => { + try { + return JSON.stringify(object, null, 2); + } catch { + return i18n.SOMETHING_WENT_WRONG; + } +}; + +export const formatIndexPatternRequested = (indices: string[] = []) => { + if (indices.length === 1 && indices[0] === NO_ALERT_INDEX) { + return <i>{i18n.NO_ALERT_INDEX_FOUND}</i>; + } + return indices.length > 0 + ? indices.filter((i) => i !== NO_ALERT_INDEX).join(', ') + : i18n.SOMETHING_WENT_WRONG; +}; + +export const ModalInspectQuery = ({ + closeModal, + isShowing = false, + request, + response, + additionalRequests, + additionalResponses, + title, +}: ModalInspectProps) => { + if (!isShowing || request == null || response == null) { + return null; + } + + const requests: string[] = [request, ...(additionalRequests != null ? additionalRequests : [])]; + const responses: string[] = [ + response, + ...(additionalResponses != null ? additionalResponses : []), + ]; + + const inspectRequests: Request[] = parseInspectStrings(requests); + const inspectResponses: Response[] = parseInspectStrings(responses); + + const statistics: Array<{ + title: NonNullable<ReactNode | string>; + description: NonNullable<ReactNode | string>; + }> = [ + { + title: ( + <span data-test-subj="index-pattern-title"> + {i18n.INDEX_PATTERN}{' '} + <EuiIconTip color="subdued" content={i18n.INDEX_PATTERN_DESC} type="iInCircle" /> + </span> + ), + description: ( + <span data-test-subj="index-pattern-description"> + {formatIndexPatternRequested(inspectRequests[0]?.index ?? [])} + </span> + ), + }, + + { + title: ( + <span data-test-subj="query-time-title"> + {i18n.QUERY_TIME}{' '} + <EuiIconTip color="subdued" content={i18n.QUERY_TIME_DESC} type="iInCircle" /> + </span> + ), + description: ( + <span data-test-subj="query-time-description"> + {inspectResponses[0]?.took + ? `${numeral(inspectResponses[0].took).format('0,0')}ms` + : i18n.SOMETHING_WENT_WRONG} + </span> + ), + }, + { + title: ( + <span data-test-subj="request-timestamp-title"> + {i18n.REQUEST_TIMESTAMP}{' '} + <EuiIconTip color="subdued" content={i18n.REQUEST_TIMESTAMP_DESC} type="iInCircle" /> + </span> + ), + description: ( + <span data-test-subj="request-timestamp-description">{new Date().toISOString()}</span> + ), + }, + ]; + + const tabs = [ + { + id: 'statistics', + name: 'Statistics', + content: ( + <> + <EuiSpacer /> + <DescriptionListStyled listItems={statistics} type="column" /> + </> + ), + }, + { + id: 'request', + name: 'Request', + content: + inspectRequests.length > 0 ? ( + inspectRequests.map((inspectRequest, index) => ( + <Fragment key={index}> + <EuiSpacer /> + <EuiCodeBlock + language="js" + fontSize="m" + paddingSize="m" + color="dark" + overflowHeight={300} + isCopyable + > + {manageStringify(inspectRequest.body)} + </EuiCodeBlock> + </Fragment> + )) + ) : ( + <EuiCodeBlock>{i18n.SOMETHING_WENT_WRONG}</EuiCodeBlock> + ), + }, + { + id: 'response', + name: 'Response', + content: + inspectResponses.length > 0 ? ( + responses.map((responseText, index) => ( + <Fragment key={index}> + <EuiSpacer /> + <EuiCodeBlock + language="js" + fontSize="m" + paddingSize="m" + color="dark" + overflowHeight={300} + isCopyable + > + {responseText} + </EuiCodeBlock> + </Fragment> + )) + ) : ( + <EuiCodeBlock>{i18n.SOMETHING_WENT_WRONG}</EuiCodeBlock> + ), + }, + ]; + + return ( + <MyEuiModal onClose={closeModal} data-test-subj="modal-inspect-euiModal"> + <EuiModalHeader> + <EuiModalHeaderTitle> + {i18n.INSPECT} {title} + </EuiModalHeaderTitle> + </EuiModalHeader> + + <EuiModalBody> + <EuiTabbedContent tabs={tabs} initialSelectedTab={tabs[0]} autoFocus="selected" /> + </EuiModalBody> + + <EuiModalFooter> + <EuiButton onClick={closeModal} fill data-test-subj="modal-inspect-close"> + {i18n.CLOSE} + </EuiButton> + </EuiModalFooter> + </MyEuiModal> + ); +}; diff --git a/x-pack/plugins/timelines/public/components/inspect/translations.ts b/x-pack/plugins/timelines/public/components/inspect/translations.ts new file mode 100644 index 0000000000000..286ec9d10c287 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/inspect/translations.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. + */ + +import { i18n } from '@kbn/i18n'; + +export const INSPECT = i18n.translate('xpack.timelines.inspectDescription', { + defaultMessage: 'Inspect', +}); + +export const CLOSE = i18n.translate('xpack.timelines.inspect.modal.closeTitle', { + defaultMessage: 'Close', +}); + +export const SOMETHING_WENT_WRONG = i18n.translate( + 'xpack.timelines.inspect.modal.somethingWentWrongDescription', + { + defaultMessage: 'Sorry about that, something went wrong.', + } +); +export const INDEX_PATTERN = i18n.translate('xpack.timelines.inspect.modal.indexPatternLabel', { + defaultMessage: 'Index pattern', +}); + +export const INDEX_PATTERN_DESC = i18n.translate( + 'xpack.timelines.inspect.modal.indexPatternDescription', + { + defaultMessage: + 'The index pattern that connected to the Elasticsearch indices. These indices can be configured in Kibana > Advanced Settings.', + } +); + +export const QUERY_TIME = i18n.translate('xpack.timelines.inspect.modal.queryTimeLabel', { + defaultMessage: 'Query time', +}); + +export const QUERY_TIME_DESC = i18n.translate( + 'xpack.timelines.inspect.modal.queryTimeDescription', + { + defaultMessage: + 'The time it took to process the query. Does not include the time to send the request or parse it in the browser.', + } +); + +export const REQUEST_TIMESTAMP = i18n.translate('xpack.timelines.inspect.modal.reqTimestampLabel', { + defaultMessage: 'Request timestamp', +}); + +export const REQUEST_TIMESTAMP_DESC = i18n.translate( + 'xpack.timelines.inspect.modal.reqTimestampDescription', + { + defaultMessage: 'Time when the start of the request has been logged', + } +); + +export const NO_ALERT_INDEX_FOUND = i18n.translate( + 'xpack.timelines.inspect.modal.noAlertIndexFound', + { + defaultMessage: 'No alert index found', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/last_updated/index.test.tsx b/x-pack/plugins/timelines/public/components/last_updated/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/last_updated/index.test.tsx rename to x-pack/plugins/timelines/public/components/last_updated/index.test.tsx index 71807eb71776a..f7d81db670983 100644 --- a/x-pack/plugins/security_solution/public/common/components/last_updated/index.test.tsx +++ b/x-pack/plugins/timelines/public/components/last_updated/index.test.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { mount } from 'enzyme'; import { I18nProvider } from '@kbn/i18n/react'; - import { LastUpdatedAt } from './'; + jest.mock('@kbn/i18n/react', () => { const originalModule = jest.requireActual('@kbn/i18n/react'); const FormattedRelative = jest.fn(); diff --git a/x-pack/plugins/security_solution/public/common/components/last_updated/index.tsx b/x-pack/plugins/timelines/public/components/last_updated/index.tsx similarity index 94% rename from x-pack/plugins/security_solution/public/common/components/last_updated/index.tsx rename to x-pack/plugins/timelines/public/components/last_updated/index.tsx index 90c21eb82d8b7..344cb36791dd5 100644 --- a/x-pack/plugins/security_solution/public/common/components/last_updated/index.tsx +++ b/x-pack/plugins/timelines/public/components/last_updated/index.tsx @@ -11,7 +11,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import * as i18n from './translations'; -interface LastUpdatedAtProps { +export interface LastUpdatedAtProps { compact?: boolean; updatedAt: number; showUpdating?: boolean; @@ -82,3 +82,6 @@ export const LastUpdatedAt = React.memo<LastUpdatedAtProps>( ); LastUpdatedAt.displayName = 'LastUpdatedAt'; + +// eslint-disable-next-line import/no-default-export +export { LastUpdatedAt as default }; diff --git a/x-pack/plugins/security_solution/public/common/components/last_updated/translations.ts b/x-pack/plugins/timelines/public/components/last_updated/translations.ts similarity index 67% rename from x-pack/plugins/security_solution/public/common/components/last_updated/translations.ts rename to x-pack/plugins/timelines/public/components/last_updated/translations.ts index 7d1cfc9537239..975c6972e90cd 100644 --- a/x-pack/plugins/security_solution/public/common/components/last_updated/translations.ts +++ b/x-pack/plugins/timelines/public/components/last_updated/translations.ts @@ -7,10 +7,10 @@ import { i18n } from '@kbn/i18n'; -export const UPDATING = i18n.translate('xpack.securitySolution.lastUpdated.updating', { +export const UPDATING = i18n.translate('xpack.timelines.lastUpdated.updating', { defaultMessage: 'Updating...', }); -export const UPDATED = i18n.translate('xpack.securitySolution.lastUpdated.updated', { +export const UPDATED = i18n.translate('xpack.timelines.lastUpdated.updated', { defaultMessage: 'Updated', }); diff --git a/x-pack/plugins/timelines/public/components/loading/index.tsx b/x-pack/plugins/timelines/public/components/loading/index.tsx new file mode 100644 index 0000000000000..59cc18767af21 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/loading/index.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiPanel, EuiText } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +const SpinnerFlexItem = styled(EuiFlexItem)` + margin-right: 5px; +`; + +SpinnerFlexItem.displayName = 'SpinnerFlexItem'; + +export interface LoadingPanelProps { + dataTestSubj?: string; + text: string; + height: number | string; + showBorder?: boolean; + width: number | string; + zIndex?: number | string; + position?: string; +} + +export const LoadingPanel = React.memo<LoadingPanelProps>( + ({ + dataTestSubj = '', + height = 'auto', + showBorder = true, + text, + width, + position = 'relative', + zIndex = 'inherit', + }) => ( + <LoadingStaticPanel + className="app-loading" + height={height} + width={width} + position={position} + zIndex={zIndex} + > + <LoadingStaticContentPanel> + <EuiPanel + data-test-subj={dataTestSubj} + className={showBorder ? '' : 'euiPanel-loading-hide-border'} + > + <EuiFlexGroup alignItems="center" direction="row" gutterSize="none"> + <SpinnerFlexItem grow={false}> + <EuiLoadingSpinner size="m" /> + </SpinnerFlexItem> + + <EuiFlexItem grow={false}> + <EuiText>{text}</EuiText> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPanel> + </LoadingStaticContentPanel> + </LoadingStaticPanel> + ) +); + +LoadingPanel.displayName = 'LoadingPanel'; + +export const LoadingStaticPanel = styled.div<{ + height: number | string; + position: string; + width: number | string; + zIndex: number | string; +}>` + height: ${({ height }) => height}; + position: ${({ position }) => position}; + width: ${({ width }) => width}; + overflow: hidden; + display: flex; + flex-direction: column; + justify-content: center; + z-index: ${({ zIndex }) => zIndex}; +`; + +LoadingStaticPanel.displayName = 'LoadingStaticPanel'; + +export const LoadingStaticContentPanel = styled.div` + flex: 0 0 auto; + align-self: center; + text-align: center; + height: fit-content; + .euiPanel.euiPanel--paddingMedium { + padding: 10px; + } +`; + +LoadingStaticContentPanel.displayName = 'LoadingStaticContentPanel'; + +// eslint-disable-next-line import/no-default-export +export { LoadingPanel as default }; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..9ee08bcd966f3 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/__snapshots__/index.test.tsx.snap @@ -0,0 +1,526 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = ` +<ColumnHeadersComponent + actionsColumnWidth={120} + browserFields={ + Object { + "agent": Object { + "fields": Object { + "agent.ephemeral_id": Object { + "aggregatable": true, + "category": "agent", + "description": "Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but \`agent.id\` does not.", + "example": "8a4f500f", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "agent.ephemeral_id", + "searchable": true, + "type": "string", + }, + "agent.hostname": Object { + "aggregatable": true, + "category": "agent", + "description": null, + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "agent.hostname", + "searchable": true, + "type": "string", + }, + "agent.id": Object { + "aggregatable": true, + "category": "agent", + "description": "Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.", + "example": "8a4f500d", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "agent.id", + "searchable": true, + "type": "string", + }, + "agent.name": Object { + "aggregatable": true, + "category": "agent", + "description": "Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.", + "example": "foo", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "agent.name", + "searchable": true, + "type": "string", + }, + }, + }, + "auditd": Object { + "fields": Object { + "auditd.data.a0": Object { + "aggregatable": true, + "category": "auditd", + "description": null, + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + ], + "name": "auditd.data.a0", + "searchable": true, + "type": "string", + }, + "auditd.data.a1": Object { + "aggregatable": true, + "category": "auditd", + "description": null, + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + ], + "name": "auditd.data.a1", + "searchable": true, + "type": "string", + }, + "auditd.data.a2": Object { + "aggregatable": true, + "category": "auditd", + "description": null, + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + ], + "name": "auditd.data.a2", + "searchable": true, + "type": "string", + }, + }, + }, + "base": Object { + "fields": Object { + "@timestamp": Object { + "aggregatable": true, + "category": "base", + "description": "Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.", + "example": "2016-05-23T08:05:34.853Z", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "@timestamp", + "searchable": true, + "type": "date", + }, + }, + }, + "client": Object { + "fields": Object { + "client.address": Object { + "aggregatable": true, + "category": "client", + "description": "Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the \`.address\` field. Then it should be duplicated to \`.ip\` or \`.domain\`, depending on which one it is.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "client.address", + "searchable": true, + "type": "string", + }, + "client.bytes": Object { + "aggregatable": true, + "category": "client", + "description": "Bytes sent from the client to the server.", + "example": "184", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "client.bytes", + "searchable": true, + "type": "number", + }, + "client.domain": Object { + "aggregatable": true, + "category": "client", + "description": "Client domain.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "client.domain", + "searchable": true, + "type": "string", + }, + "client.geo.country_iso_code": Object { + "aggregatable": true, + "category": "client", + "description": "Country ISO code.", + "example": "CA", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "client.geo.country_iso_code", + "searchable": true, + "type": "string", + }, + }, + }, + "cloud": Object { + "fields": Object { + "cloud.account.id": Object { + "aggregatable": true, + "category": "cloud", + "description": "The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.", + "example": "666777888999", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "cloud.account.id", + "searchable": true, + "type": "string", + }, + "cloud.availability_zone": Object { + "aggregatable": true, + "category": "cloud", + "description": "Availability zone in which this host is running.", + "example": "us-east-1c", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "cloud.availability_zone", + "searchable": true, + "type": "string", + }, + }, + }, + "container": Object { + "fields": Object { + "container.id": Object { + "aggregatable": true, + "category": "container", + "description": "Unique container id.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "container.id", + "searchable": true, + "type": "string", + }, + "container.image.name": Object { + "aggregatable": true, + "category": "container", + "description": "Name of the image the container was built on.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "container.image.name", + "searchable": true, + "type": "string", + }, + "container.image.tag": Object { + "aggregatable": true, + "category": "container", + "description": "Container image tag.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "container.image.tag", + "searchable": true, + "type": "string", + }, + }, + }, + "destination": Object { + "fields": Object { + "destination.address": Object { + "aggregatable": true, + "category": "destination", + "description": "Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the \`.address\` field. Then it should be duplicated to \`.ip\` or \`.domain\`, depending on which one it is.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.address", + "searchable": true, + "type": "string", + }, + "destination.bytes": Object { + "aggregatable": true, + "category": "destination", + "description": "Bytes sent from the destination to the source.", + "example": "184", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.bytes", + "searchable": true, + "type": "number", + }, + "destination.domain": Object { + "aggregatable": true, + "category": "destination", + "description": "Destination domain.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.domain", + "searchable": true, + "type": "string", + }, + "destination.ip": Object { + "aggregatable": true, + "category": "destination", + "description": "IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.ip", + "searchable": true, + "type": "ip", + }, + "destination.port": Object { + "aggregatable": true, + "category": "destination", + "description": "Port of the destination.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.port", + "searchable": true, + "type": "long", + }, + }, + }, + "event": Object { + "fields": Object { + "event.end": Object { + "aggregatable": true, + "category": "event", + "description": "event.end contains the date when the event ended or when the activity was last observed.", + "example": null, + "format": "", + "indexes": Array [ + "apm-*-transaction*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*", + ], + "name": "event.end", + "searchable": true, + "type": "date", + }, + }, + }, + "nestedField": Object { + "fields": Object { + "nestedField.firstAttributes": Object { + "aggregatable": false, + "category": "nestedField", + "description": "", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "nestedField.firstAttributes", + "searchable": true, + "subType": Object { + "nested": Object { + "path": "nestedField", + }, + }, + "type": "string", + }, + "nestedField.secondAttributes": Object { + "aggregatable": false, + "category": "nestedField", + "description": "", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "nestedField.secondAttributes", + "searchable": true, + "subType": Object { + "nested": Object { + "path": "nestedField", + }, + }, + "type": "string", + }, + }, + }, + "source": Object { + "fields": Object { + "source.ip": Object { + "aggregatable": true, + "category": "source", + "description": "IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "source.ip", + "searchable": true, + "type": "ip", + }, + "source.port": Object { + "aggregatable": true, + "category": "source", + "description": "Port of the source.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "source.port", + "searchable": true, + "type": "long", + }, + }, + }, + } + } + columnHeaders={ + Array [ + Object { + "columnHeaderType": "not-filtered", + "id": "@timestamp", + "initialWidth": 190, + "type": "number", + }, + Object { + "columnHeaderType": "not-filtered", + "id": "message", + "initialWidth": 180, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "event.category", + "initialWidth": 180, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "event.action", + "initialWidth": 180, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "host.name", + "initialWidth": 180, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "source.ip", + "initialWidth": 180, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "destination.ip", + "initialWidth": 180, + }, + Object { + "columnHeaderType": "not-filtered", + "id": "user.name", + "initialWidth": 180, + }, + ] + } + isSelectAllChecked={false} + leadingControlColumns={Array []} + onSelectAll={[Function]} + showEventsSelect={false} + showSelectAllCheckbox={false} + sort={ + Array [ + Object { + "columnId": "@timestamp", + "columnType": "number", + "sortDirection": "desc", + }, + ] + } + tabType="query" + timelineId="test" + trailingControlColumns={Array []} +/> +`; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/actions/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/actions/index.tsx new file mode 100644 index 0000000000000..322059576d2b7 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/actions/index.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiButtonIcon } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import type { ColumnHeaderOptions } from '../../../../../../common/types/timeline'; +import { EventsHeadingExtra, EventsLoading } from '../../../styles'; +import type { OnColumnRemoved } from '../../../types'; +import type { Sort } from '../../sort'; + +import * as i18n from '../translations'; + +interface Props { + header: ColumnHeaderOptions; + isLoading: boolean; + onColumnRemoved: OnColumnRemoved; + sort: Sort[]; +} + +/** Given a `header`, returns the `SortDirection` applicable to it */ + +export const CloseButton = React.memo<{ + columnId: string; + onColumnRemoved: OnColumnRemoved; +}>(({ columnId, onColumnRemoved }) => { + const handleClick = useCallback( + (event: React.MouseEvent<HTMLButtonElement>) => { + // To avoid a re-sorting when you delete a column + event.preventDefault(); + event.stopPropagation(); + onColumnRemoved(columnId); + }, + [columnId, onColumnRemoved] + ); + + return ( + <EuiButtonIcon + aria-label={i18n.REMOVE_COLUMN} + color="text" + data-test-subj="remove-column" + iconType="cross" + onClick={handleClick} + /> + ); +}); + +CloseButton.displayName = 'CloseButton'; + +export const Actions = React.memo<Props>(({ header, onColumnRemoved, sort, isLoading }) => { + return ( + <> + {sort.some((i) => i.columnId === header.id) && isLoading ? ( + <EventsHeadingExtra className="siemEventsHeading__extra--loading"> + <EventsLoading data-test-subj="timeline-loading-spinner" /> + </EventsHeadingExtra> + ) : ( + <EventsHeadingExtra className="siemEventsHeading__extra--close"> + <CloseButton columnId={header.id} onColumnRemoved={onColumnRemoved} /> + </EventsHeadingExtra> + )} + </> + ); +}); + +Actions.displayName = 'Actions'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/column_header.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/column_header.tsx new file mode 100644 index 0000000000000..bd8e9508de859 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/column_header.tsx @@ -0,0 +1,310 @@ +/* + * 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 { EuiContextMenu, EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover } from '@elastic/eui'; +import { + DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME, + getDraggableFieldId, +} from '@kbn/securitysolution-t-grid'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import { Draggable } from 'react-beautiful-dnd'; +import { Resizable, ResizeCallback } from 're-resizable'; +import deepEqual from 'fast-deep-equal'; +import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; + +import { DEFAULT_COLUMN_MIN_WIDTH } from '../constants'; + +import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers'; +import { EventsTh, EventsThContent, EventsHeadingHandle } from '../../styles'; +import { Sort } from '../sort'; + +import { Header } from './header'; + +import * as i18n from './translations'; +import { tGridActions } from '../../../../store/t_grid'; +import { TimelineTabs } from '../../../../../common/types/timeline'; +// eslint-disable-next-line no-duplicate-imports +import type { ColumnHeaderOptions } from '../../../../../common/types/timeline'; + +import { Direction } from '../../../../../common/search_strategy'; +import { useDraggableKeyboardWrapper } from '../../../drag_and_drop'; + +const ContextMenu = styled(EuiContextMenu)` + width: 115px; + + & .euiContextMenuItem { + font-size: 12px; + padding: 4px 8px; + width: 115px; + } +`; + +const PopoverContainer = styled.div<{ $width: number }>` + & .euiPopover__anchor { + padding-right: 8px; + width: ${({ $width }) => $width}px; + } +`; + +const RESIZABLE_ENABLE = { right: true }; + +interface ColumneHeaderProps { + draggableIndex: number; + header: ColumnHeaderOptions; + isDragging: boolean; + sort: Sort[]; + tabType: TimelineTabs; + timelineId: string; +} + +const ColumnHeaderComponent: React.FC<ColumneHeaderProps> = ({ + draggableIndex, + header, + timelineId, + isDragging, + sort, + tabType, +}) => { + const keyboardHandlerRef = useRef<HTMLDivElement | null>(null); + const [hoverActionsOwnFocus, setHoverActionsOwnFocus] = useState<boolean>(false); + const restoreFocus = useCallback(() => keyboardHandlerRef.current?.focus(), []); + + const dispatch = useDispatch(); + const resizableSize = useMemo( + () => ({ + width: header.initialWidth ?? DEFAULT_COLUMN_MIN_WIDTH, + height: 'auto', + }), + [header.initialWidth] + ); + const resizableStyle: { + position: 'absolute' | 'relative'; + } = useMemo( + () => ({ + position: isDragging ? 'absolute' : 'relative', + }), + [isDragging] + ); + const resizableHandleComponent = useMemo( + () => ({ + right: <EventsHeadingHandle />, + }), + [] + ); + const handleResizeStop: ResizeCallback = useCallback( + (e, direction, ref, delta) => { + dispatch( + tGridActions.applyDeltaToColumnWidth({ + columnId: header.id, + delta: delta.width, + id: timelineId, + }) + ); + }, + [dispatch, header.id, timelineId] + ); + const draggableId = useMemo( + () => + getDraggableFieldId({ + contextId: `timeline-column-headers-${tabType}-${timelineId}`, + fieldId: header.id, + }), + [tabType, timelineId, header.id] + ); + + const onColumnSort = useCallback( + (sortDirection: Direction) => { + const columnId = header.id; + const headerIndex = sort.findIndex((col) => col.columnId === columnId); + const newSort = + headerIndex === -1 + ? [ + ...sort, + { + columnId, + columnType: `${header.type}`, + sortDirection, + }, + ] + : [ + ...sort.slice(0, headerIndex), + { + columnId, + columnType: `${header.type}`, + sortDirection, + }, + ...sort.slice(headerIndex + 1), + ]; + + dispatch( + tGridActions.updateSort({ + id: timelineId, + sort: newSort, + }) + ); + }, + [dispatch, header, sort, timelineId] + ); + + const handleClosePopOverTrigger = useCallback(() => { + setHoverActionsOwnFocus(false); + restoreFocus(); + }, [restoreFocus]); + + const panels: EuiContextMenuPanelDescriptor[] = useMemo( + () => [ + { + id: 0, + items: [ + { + icon: <EuiIcon type="eyeClosed" size="s" />, + name: i18n.HIDE_COLUMN, + onClick: () => { + dispatch(tGridActions.removeColumn({ id: timelineId, columnId: header.id })); + handleClosePopOverTrigger(); + }, + }, + ...(tabType !== TimelineTabs.eql + ? [ + { + disabled: !header.aggregatable, + icon: <EuiIcon type="sortUp" size="s" />, + name: i18n.SORT_AZ, + onClick: () => { + onColumnSort(Direction.asc); + handleClosePopOverTrigger(); + }, + }, + { + disabled: !header.aggregatable, + icon: <EuiIcon type="sortDown" size="s" />, + name: i18n.SORT_ZA, + onClick: () => { + onColumnSort(Direction.desc); + handleClosePopOverTrigger(); + }, + }, + ] + : []), + ], + }, + ], + [ + dispatch, + handleClosePopOverTrigger, + header.aggregatable, + header.id, + onColumnSort, + tabType, + timelineId, + ] + ); + + const headerButton = useMemo( + () => <Header timelineId={timelineId} header={header} sort={sort} />, + [header, sort, timelineId] + ); + + const DraggableContent = useCallback( + (dragProvided) => ( + <EventsTh + data-test-subj="draggable-header" + {...dragProvided.draggableProps} + {...dragProvided.dragHandleProps} + ref={dragProvided.innerRef} + > + <EventsThContent> + <PopoverContainer $width={header.initialWidth ?? DEFAULT_COLUMN_MIN_WIDTH}> + <EuiPopover + anchorPosition="downLeft" + button={headerButton} + closePopover={handleClosePopOverTrigger} + isOpen={hoverActionsOwnFocus} + ownFocus + panelPaddingSize="none" + > + <ContextMenu initialPanelId={0} panels={panels} /> + </EuiPopover> + </PopoverContainer> + </EventsThContent> + </EventsTh> + ), + [handleClosePopOverTrigger, headerButton, header.initialWidth, hoverActionsOwnFocus, panels] + ); + + const onFocus = useCallback(() => { + keyboardHandlerRef.current?.focus(); + }, []); + + const openPopover = useCallback(() => { + setHoverActionsOwnFocus(true); + }, []); + + const { onBlur, onKeyDown } = useDraggableKeyboardWrapper({ + closePopover: handleClosePopOverTrigger, + draggableId, + fieldName: header.id, + keyboardHandlerRef, + openPopover, + }); + + const keyDownHandler = useCallback( + (keyboardEvent: React.KeyboardEvent) => { + if (!hoverActionsOwnFocus) { + onKeyDown(keyboardEvent); + } + }, + [hoverActionsOwnFocus, onKeyDown] + ); + + return ( + <Resizable + enable={RESIZABLE_ENABLE} + size={resizableSize} + style={resizableStyle} + handleComponent={resizableHandleComponent} + onResizeStop={handleResizeStop} + > + <div + aria-colindex={ + draggableIndex != null ? draggableIndex + ARIA_COLUMN_INDEX_OFFSET : undefined + } + className={DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME} + data-test-subj="draggableWrapperKeyboardHandler" + onClick={onFocus} + onBlur={onBlur} + onKeyDown={keyDownHandler} + ref={keyboardHandlerRef} + role="columnheader" + tabIndex={0} + > + <Draggable + data-test-subj="draggable" + // Required for drag events while hovering the sort button to work: https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/draggable.md#interactive-child-elements-within-a-draggable- + disableInteractiveElementBlocking + draggableId={draggableId} + index={draggableIndex} + key={header.id} + > + {DraggableContent} + </Draggable> + </div> + </Resizable> + ); +}; + +export const ColumnHeader = React.memo( + ColumnHeaderComponent, + (prevProps, nextProps) => + prevProps.draggableIndex === nextProps.draggableIndex && + prevProps.tabType === nextProps.tabType && + prevProps.timelineId === nextProps.timelineId && + prevProps.isDragging === nextProps.isDragging && + deepEqual(prevProps.sort, nextProps.sort) && + deepEqual(prevProps.header, nextProps.header) +); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/common/dragging_container.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/common/dragging_container.tsx new file mode 100644 index 0000000000000..0d7ed0a91121e --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/common/dragging_container.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FC, memo, useEffect } from 'react'; + +interface DraggingContainerProps { + children: JSX.Element; + onDragging: Function; +} + +const DraggingContainerComponent: FC<DraggingContainerProps> = ({ children, onDragging }) => { + useEffect(() => { + onDragging(true); + + return () => onDragging(false); + }); + + return children; +}; + +export const DraggingContainer = memo(DraggingContainerComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/common/styles.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/common/styles.tsx new file mode 100644 index 0000000000000..254c7076fcf5a --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/common/styles.tsx @@ -0,0 +1,19 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; + +export const FullHeightFlexGroup = styled(EuiFlexGroup)` + height: 100%; +`; +FullHeightFlexGroup.displayName = 'FullHeightFlexGroup'; + +export const FullHeightFlexItem = styled(EuiFlexItem)` + height: 100%; +`; +FullHeightFlexItem.displayName = 'FullHeightFlexItem'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/default_headers.ts b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/default_headers.ts new file mode 100644 index 0000000000000..9a32c514e7064 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/default_headers.ts @@ -0,0 +1,58 @@ +/* + * 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 { ColumnHeaderOptions, ColumnHeaderType } from '../../../../../common/types/timeline'; +import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../constants'; + +export const defaultColumnHeaderType: ColumnHeaderType = 'not-filtered'; + +export const defaultHeaders: ColumnHeaderOptions[] = [ + { + columnHeaderType: defaultColumnHeaderType, + id: '@timestamp', + type: 'number', + initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'message', + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'event.category', + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'event.action', + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'host.name', + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'source.ip', + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'destination.ip', + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'user.name', + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, +]; + +/** The default category of fields shown in the Timeline */ +export const DEFAULT_CATEGORY_NAME = 'default ECS'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/__snapshots__/index.test.tsx.snap b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..ff2bdf2f643a0 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/__snapshots__/index.test.tsx.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Header renders correctly against snapshot 1`] = ` +<Fragment> + <Memo(HeaderContentComponent) + header={ + Object { + "columnHeaderType": "not-filtered", + "id": "@timestamp", + "initialWidth": 190, + "type": "number", + } + } + isLoading={false} + isResizing={false} + onClick={[Function]} + showSortingCapability={true} + sort={ + Array [ + Object { + "columnId": "@timestamp", + "columnType": "number", + "sortDirection": "desc", + }, + ] + } + > + <Actions + header={ + Object { + "columnHeaderType": "not-filtered", + "id": "@timestamp", + "initialWidth": 190, + "type": "number", + } + } + isLoading={false} + onColumnRemoved={[Function]} + sort={ + Array [ + Object { + "columnId": "@timestamp", + "columnType": "number", + "sortDirection": "desc", + }, + ] + } + /> + </Memo(HeaderContentComponent)> +</Fragment> +`; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/header_content.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/header_content.tsx new file mode 100644 index 0000000000000..04004b3e90314 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/header_content.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiToolTip } from '@elastic/eui'; +import { noop } from 'lodash/fp'; +import React from 'react'; + +import type { ColumnHeaderOptions } from '../../../../../../common/types/timeline'; +import { TruncatableText } from '../../../../truncatable_text'; + +import { EventsHeading, EventsHeadingTitleButton, EventsHeadingTitleSpan } from '../../../styles'; +import { Sort } from '../../sort'; +import { SortIndicator } from '../../sort/sort_indicator'; +import { HeaderToolTipContent } from '../header_tooltip_content'; +import { getSortDirection, getSortIndex } from './helpers'; +interface HeaderContentProps { + children: React.ReactNode; + header: ColumnHeaderOptions; + isLoading: boolean; + isResizing: boolean; + onClick: () => void; + showSortingCapability: boolean; + sort: Sort[]; +} + +const HeaderContentComponent: React.FC<HeaderContentProps> = ({ + children, + header, + isLoading, + isResizing, + onClick, + showSortingCapability, + sort, +}) => ( + <EventsHeading data-test-subj={`header-${header.id}`} isLoading={isLoading}> + {header.aggregatable && showSortingCapability ? ( + <EventsHeadingTitleButton + data-test-subj="header-sort-button" + onClick={!isResizing && !isLoading ? onClick : noop} + > + <TruncatableText data-test-subj={`header-text-${header.id}`}> + <EuiToolTip + data-test-subj="header-tooltip" + content={<HeaderToolTipContent header={header} />} + > + <> + {React.isValidElement(header.display) + ? header.display + : header.displayAsText ?? header.id} + </> + </EuiToolTip> + </TruncatableText> + + <SortIndicator + data-test-subj="header-sort-indicator" + sortDirection={getSortDirection({ header, sort })} + sortNumber={getSortIndex({ header, sort })} + /> + </EventsHeadingTitleButton> + ) : ( + <EventsHeadingTitleSpan> + <TruncatableText data-test-subj={`header-text-${header.id}`}> + <EuiToolTip + data-test-subj="header-tooltip" + content={<HeaderToolTipContent header={header} />} + > + <> + {React.isValidElement(header.display) + ? header.display + : header.displayAsText ?? header.id} + </> + </EuiToolTip> + </TruncatableText> + </EventsHeadingTitleSpan> + )} + + {children} + </EventsHeading> +); + +export const HeaderContent = React.memo(HeaderContentComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/helpers.ts b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/helpers.ts new file mode 100644 index 0000000000000..84c7155aba8c0 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/helpers.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 { Direction } from '../../../../../../common'; +// eslint-disable-next-line no-duplicate-imports +import type { ColumnHeaderOptions } from '../../../../../../common'; +import { assertUnreachable } from '../../../../../../common/utility_types'; +import { Sort, SortDirection } from '../../sort'; + +interface GetNewSortDirectionOnClickParams { + clickedHeader: ColumnHeaderOptions; + currentSort: Sort[]; +} + +/** Given a `header`, returns the `SortDirection` applicable to it */ +export const getNewSortDirectionOnClick = ({ + clickedHeader, + currentSort, +}: GetNewSortDirectionOnClickParams): Direction => + currentSort.reduce<Direction>( + (acc, item) => (clickedHeader.id === item.columnId ? getNextSortDirection(item) : acc), + Direction.desc + ); + +/** Given a current sort direction, it returns the next sort direction */ +export const getNextSortDirection = (currentSort: Sort): Direction => { + switch (currentSort.sortDirection) { + case Direction.desc: + return Direction.asc; + case Direction.asc: + return Direction.desc; + case 'none': + return Direction.desc; + default: + return assertUnreachable(currentSort.sortDirection as never, 'Unhandled sort direction'); + } +}; + +interface GetSortDirectionParams { + header: ColumnHeaderOptions; + sort: Sort[]; +} + +export const getSortDirection = ({ header, sort }: GetSortDirectionParams): SortDirection => + sort.reduce<SortDirection>( + (acc, item) => (header.id === item.columnId ? item.sortDirection : acc), + 'none' + ); + +export const getSortIndex = ({ header, sort }: GetSortDirectionParams): number => + sort.findIndex((s) => s.columnId === header.id); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/index.test.tsx new file mode 100644 index 0000000000000..4685af483c21e --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/index.test.tsx @@ -0,0 +1,331 @@ +/* + * 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 { mount, shallow } from 'enzyme'; +import React from 'react'; + +import { Sort } from '../../sort'; +import { CloseButton } from '../actions'; +import { defaultHeaders } from '../default_headers'; + +import { HeaderComponent } from '.'; +import { getNewSortDirectionOnClick, getNextSortDirection, getSortDirection } from './helpers'; +import { Direction } from '../../../../../../common/search_strategy'; +import { TestProviders } from '../../../../../mock'; +import { tGridActions } from '../../../../../store/t_grid'; +import { mockGlobalState } from '../../../../../mock/global_state'; + +const mockDispatch = jest.fn(); +jest.mock('../../../../../hooks/use_selector', () => ({ + useShallowEqualSelector: () => mockGlobalState.timelineById.test, + useDeepEqualSelector: () => mockGlobalState.timelineById.test, +})); + +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + + return { + ...original, + useSelector: jest.fn(), + useDispatch: () => mockDispatch, + }; +}); + +describe('Header', () => { + const columnHeader = defaultHeaders[0]; + const sort: Sort[] = [ + { + columnId: columnHeader.id, + columnType: columnHeader.type ?? 'number', + sortDirection: Direction.desc, + }, + ]; + const timelineId = 'test'; + + test('renders correctly against snapshot', () => { + const wrapper = shallow( + <TestProviders> + <HeaderComponent header={columnHeader} sort={sort} timelineId={timelineId} /> + </TestProviders> + ); + expect(wrapper.find('HeaderComponent').dive()).toMatchSnapshot(); + }); + + describe('rendering', () => { + test('it renders the header text', () => { + const wrapper = mount( + <TestProviders> + <HeaderComponent header={columnHeader} sort={sort} timelineId={timelineId} /> + </TestProviders> + ); + + expect( + wrapper.find(`[data-test-subj="header-text-${columnHeader.id}"]`).first().text() + ).toEqual(columnHeader.id); + }); + + test('it renders the header text alias when displayAsText is provided', () => { + const displayAsText = 'Timestamp'; + const headerWithLabel = { ...columnHeader, displayAsText }; + const wrapper = mount( + <TestProviders> + <HeaderComponent header={headerWithLabel} sort={sort} timelineId={timelineId} /> + </TestProviders> + ); + + expect( + wrapper.find(`[data-test-subj="header-text-${columnHeader.id}"]`).first().text() + ).toEqual(displayAsText); + }); + + test('it renders the header as a `ReactNode` when `display` is provided', () => { + const display: React.ReactNode = ( + <div data-test-subj="rendered-via-display"> + {'The display property renders the column heading as a ReactNode'} + </div> + ); + const headerWithLabel = { ...columnHeader, display }; + const wrapper = mount( + <TestProviders> + <HeaderComponent header={headerWithLabel} sort={sort} timelineId={timelineId} /> + </TestProviders> + ); + + expect(wrapper.find(`[data-test-subj="rendered-via-display"]`).exists()).toBe(true); + }); + + test('it prefers to render `display` instead of `displayAsText` when both are provided', () => { + const displayAsText = 'this text should NOT be rendered'; + const display: React.ReactNode = ( + <div data-test-subj="rendered-via-display">{'this text is rendered via display'}</div> + ); + const headerWithLabel = { ...columnHeader, display, displayAsText }; + const wrapper = mount( + <TestProviders> + <HeaderComponent header={headerWithLabel} sort={sort} timelineId={timelineId} /> + </TestProviders> + ); + + expect(wrapper.text()).toBe('this text is rendered via display'); + }); + + test('it falls back to rendering header.id when `display` is not a valid React node', () => { + const display = {}; // a plain object is NOT a `ReactNode` + const headerWithLabel = { ...columnHeader, display }; + const wrapper = mount( + <TestProviders> + <HeaderComponent header={headerWithLabel} sort={sort} timelineId={timelineId} /> + </TestProviders> + ); + + expect( + wrapper.find(`[data-test-subj="header-text-${columnHeader.id}"]`).first().text() + ).toEqual(columnHeader.id); + }); + + test('it renders a sort indicator', () => { + const headerSortable = { ...columnHeader, aggregatable: true }; + const wrapper = mount( + <TestProviders> + <HeaderComponent header={headerSortable} sort={sort} timelineId={timelineId} /> + </TestProviders> + ); + + expect(wrapper.find('[data-test-subj="header-sort-indicator"]').first().exists()).toEqual( + true + ); + }); + }); + + describe('onColumnSorted', () => { + test('it invokes the onColumnSorted callback when the header sort button is clicked', () => { + const headerSortable = { ...columnHeader, aggregatable: true }; + const wrapper = mount( + <TestProviders> + <HeaderComponent header={headerSortable} sort={sort} timelineId={timelineId} /> + </TestProviders> + ); + + wrapper.find('[data-test-subj="header-sort-button"]').first().simulate('click'); + + expect(mockDispatch).toBeCalledWith( + tGridActions.updateSort({ + id: timelineId, + sort: [ + { + columnId: columnHeader.id, + columnType: columnHeader.type ?? 'number', + sortDirection: Direction.asc, // (because the previous state was Direction.desc) + }, + ], + }) + ); + }); + + test('it does NOT render the header sort button when aggregatable is false', () => { + const headerSortable = { ...columnHeader, aggregatable: false }; + const wrapper = mount( + <TestProviders> + <HeaderComponent header={headerSortable} sort={sort} timelineId={timelineId} /> + </TestProviders> + ); + + expect(wrapper.find('[data-test-subj="header-sort-button"]').length).toEqual(0); + }); + + test('it does NOT render the header sort button when aggregatable is missing', () => { + const headerSortable = { ...columnHeader }; + const wrapper = mount( + <TestProviders> + <HeaderComponent header={headerSortable} sort={sort} timelineId={timelineId} /> + </TestProviders> + ); + + expect(wrapper.find('[data-test-subj="header-sort-button"]').length).toEqual(0); + }); + + test('it does NOT invoke the onColumnSorted callback when the header is clicked and aggregatable is undefined', () => { + const mockOnColumnSorted = jest.fn(); + const headerSortable = { ...columnHeader, aggregatable: undefined }; + const wrapper = mount( + <TestProviders> + <HeaderComponent header={headerSortable} sort={sort} timelineId={timelineId} /> + </TestProviders> + ); + + wrapper.find(`[data-test-subj="header-${columnHeader.id}"]`).first().simulate('click'); + + expect(mockOnColumnSorted).not.toHaveBeenCalled(); + }); + }); + + describe('CloseButton', () => { + test('it invokes the onColumnRemoved callback with the column ID when the close button is clicked', () => { + const mockOnColumnRemoved = jest.fn(); + + const wrapper = mount( + <CloseButton columnId={columnHeader.id} onColumnRemoved={mockOnColumnRemoved} /> + ); + + wrapper.find('[data-test-subj="remove-column"]').first().simulate('click'); + + expect(mockOnColumnRemoved).toBeCalledWith(columnHeader.id); + }); + }); + + describe('getSortDirection', () => { + test('it returns the sort direction when the header id matches the sort column id', () => { + expect(getSortDirection({ header: columnHeader, sort })).toEqual(sort[0].sortDirection); + }); + + test('it returns "none" when sort direction when the header id does NOT match the sort column id', () => { + const nonMatching: Sort[] = [ + { + columnId: 'differentSocks', + columnType: columnHeader.type ?? 'number', + sortDirection: Direction.desc, + }, + ]; + + expect(getSortDirection({ header: columnHeader, sort: nonMatching })).toEqual('none'); + }); + }); + + describe('getNextSortDirection', () => { + test('it returns "asc" when the current direction is "desc"', () => { + const sortDescending: Sort = { + columnId: columnHeader.id, + columnType: columnHeader.type ?? 'number', + sortDirection: Direction.desc, + }; + + expect(getNextSortDirection(sortDescending)).toEqual('asc'); + }); + + test('it returns "desc" when the current direction is "asc"', () => { + const sortAscending: Sort = { + columnId: columnHeader.id, + columnType: columnHeader.type ?? 'number', + sortDirection: Direction.asc, + }; + + expect(getNextSortDirection(sortAscending)).toEqual(Direction.desc); + }); + + test('it returns "desc" by default', () => { + const sortNone: Sort = { + columnId: columnHeader.id, + columnType: columnHeader.type ?? 'number', + sortDirection: 'none', + }; + + expect(getNextSortDirection(sortNone)).toEqual(Direction.desc); + }); + }); + + describe('getNewSortDirectionOnClick', () => { + test('it returns the expected new sort direction when the header id matches the sort column id', () => { + const sortMatches: Sort[] = [ + { + columnId: columnHeader.id, + columnType: columnHeader.type ?? 'number', + sortDirection: Direction.desc, + }, + ]; + + expect( + getNewSortDirectionOnClick({ + clickedHeader: columnHeader, + currentSort: sortMatches, + }) + ).toEqual(Direction.asc); + }); + + test('it returns the expected new sort direction when the header id does NOT match the sort column id', () => { + const sortDoesNotMatch: Sort[] = [ + { + columnId: 'someOtherColumn', + columnType: columnHeader.type ?? 'number', + sortDirection: 'none', + }, + ]; + + expect( + getNewSortDirectionOnClick({ + clickedHeader: columnHeader, + currentSort: sortDoesNotMatch, + }) + ).toEqual(Direction.desc); + }); + }); + + describe('text truncation styling', () => { + test('truncates the header text with an ellipsis', () => { + const wrapper = mount( + <TestProviders> + <HeaderComponent header={columnHeader} sort={sort} timelineId={timelineId} /> + </TestProviders> + ); + + expect( + wrapper.find(`[data-test-subj="header-text-${columnHeader.id}"]`).at(1) + ).toHaveStyleRule('text-overflow', 'ellipsis'); + }); + }); + + describe('header tooltip', () => { + test('it has a tooltip to display the properties of the field', () => { + const wrapper = mount( + <TestProviders> + <HeaderComponent header={columnHeader} sort={sort} timelineId={timelineId} /> + </TestProviders> + ); + + expect(wrapper.find('[data-test-subj="header-tooltip"]').exists()).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/index.tsx new file mode 100644 index 0000000000000..1b0f44e686501 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/index.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; + +import type { ColumnHeaderOptions } from '../../../../../../common/types/timeline'; +import type { Sort } from '../../sort'; +import { Actions } from '../actions'; +import { getNewSortDirectionOnClick } from './helpers'; +import { HeaderContent } from './header_content'; +import { tGridActions, tGridSelectors } from '../../../../../store/t_grid'; +import { useDeepEqualSelector } from '../../../../../hooks/use_selector'; +interface Props { + header: ColumnHeaderOptions; + sort: Sort[]; + timelineId: string; +} + +export const HeaderComponent: React.FC<Props> = ({ header, sort, timelineId }) => { + const dispatch = useDispatch(); + + const onColumnSort = useCallback(() => { + const columnId = header.id; + const columnType = header.type ?? 'text'; + const sortDirection = getNewSortDirectionOnClick({ + clickedHeader: header, + currentSort: sort, + }); + const headerIndex = sort.findIndex((col) => col.columnId === columnId); + let newSort = []; + if (headerIndex === -1) { + newSort = [ + ...sort, + { + columnId, + columnType, + sortDirection, + }, + ]; + } else { + newSort = [ + ...sort.slice(0, headerIndex), + { + columnId, + columnType, + sortDirection, + }, + ...sort.slice(headerIndex + 1), + ]; + } + dispatch( + tGridActions.updateSort({ + id: timelineId, + sort: newSort, + }) + ); + }, [dispatch, header, sort, timelineId]); + + const onColumnRemoved = useCallback( + (columnId) => dispatch(tGridActions.removeColumn({ id: timelineId, columnId })), + [dispatch, timelineId] + ); + + const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []); + const { isLoading } = useDeepEqualSelector((state) => getManageTimeline(state, timelineId ?? '')); + const showSortingCapability = !(header.subType && header.subType.nested); + + return ( + <> + <HeaderContent + header={header} + isLoading={isLoading} + isResizing={false} + onClick={onColumnSort} + showSortingCapability={showSortingCapability} + sort={sort} + > + <Actions + header={header} + isLoading={isLoading} + onColumnRemoved={onColumnRemoved} + sort={sort} + /> + </HeaderContent> + </> + ); +}; + +export const Header = React.memo(HeaderComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..945a9a7aee698 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap @@ -0,0 +1,66 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`HeaderToolTipContent it renders the expected table content 1`] = ` +<Fragment> + <P> + <ToolTipTableMetadata + data-test-subj="category" + > + Category + : + </ToolTipTableMetadata> + <ToolTipTableValue + data-test-subj="category-value" + > + base + </ToolTipTableValue> + </P> + <P> + <ToolTipTableMetadata + data-test-subj="field" + > + Field + : + </ToolTipTableMetadata> + <ToolTipTableValue + data-test-subj="field-value" + > + @timestamp + </ToolTipTableValue> + </P> + <P> + <ToolTipTableMetadata + data-test-subj="type" + > + Type + : + </ToolTipTableMetadata> + <ToolTipTableValue> + <IconType + data-test-subj="type-icon" + type="clock" + /> + <span + data-test-subj="type-value" + > + date + </span> + </ToolTipTableValue> + </P> + <P> + <ToolTipTableMetadata + data-test-subj="description" + > + Description + : + </ToolTipTableMetadata> + <ToolTipTableValue + data-test-subj="description-value" + > + Date/time when the event originated. +For log events this is the date/time when the event was generated, and not when it was read. +Required field for all events. + </ToolTipTableValue> + </P> +</Fragment> +`; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header_tooltip_content/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header_tooltip_content/index.test.tsx new file mode 100644 index 0000000000000..a38261994267c --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header_tooltip_content/index.test.tsx @@ -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 { mount, shallow } from 'enzyme'; +import { cloneDeep } from 'lodash/fp'; +import React from 'react'; + +import { ColumnHeaderOptions } from '../../../../../../common/types/timeline'; +import { HeaderToolTipContent } from '.'; +import { defaultHeaders } from '../../../../../mock/header'; + +describe('HeaderToolTipContent', () => { + let header: ColumnHeaderOptions; + beforeEach(() => { + header = cloneDeep(defaultHeaders[0]); + }); + + test('it renders the category', () => { + const wrapper = mount(<HeaderToolTipContent header={header} />); + + expect(wrapper.find('[data-test-subj="category-value"]').first().text()).toEqual( + header.category + ); + }); + + test('it renders the name of the field', () => { + const wrapper = mount(<HeaderToolTipContent header={header} />); + + expect(wrapper.find('[data-test-subj="field-value"]').first().text()).toEqual(header.id); + }); + + test('it renders the expected icon for the header type', () => { + const wrapper = mount(<HeaderToolTipContent header={header} />); + + expect(wrapper.find('[data-test-subj="type-icon"]').first().props().type).toEqual('clock'); + }); + + test('it renders the type of the field', () => { + const wrapper = mount(<HeaderToolTipContent header={header} />); + + expect(wrapper.find('[data-test-subj="type-value"]').first().text()).toEqual(header.type); + }); + + test('it renders the description of the field', () => { + const wrapper = mount(<HeaderToolTipContent header={header} />); + + expect(wrapper.find('[data-test-subj="description-value"]').first().text()).toEqual( + header.description + ); + }); + + test('it does NOT render the description column when the field does NOT contain a description', () => { + const noDescription = { + ...header, + description: '', + }; + + const wrapper = mount(<HeaderToolTipContent header={noDescription} />); + + expect(wrapper.find('[data-test-subj="description"]').exists()).toEqual(false); + }); + + test('it renders the expected table content', () => { + const wrapper = shallow(<HeaderToolTipContent header={header} />); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header_tooltip_content/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header_tooltip_content/index.tsx new file mode 100644 index 0000000000000..b973d99584d61 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header_tooltip_content/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 { EuiIcon } from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import React from 'react'; +import styled from 'styled-components'; + +import type { ColumnHeaderOptions } from '../../../../../../common/types/timeline'; +import { getIconFromType } from '../../../../utils/helpers'; +import * as i18n from '../translations'; + +const IconType = styled(EuiIcon)` + margin-right: 3px; + position: relative; + top: -2px; +`; +IconType.displayName = 'IconType'; + +const P = styled.span` + margin-bottom: 5px; +`; +P.displayName = 'P'; + +const ToolTipTableMetadata = styled.span` + margin-right: 5px; + display: block; +`; +ToolTipTableMetadata.displayName = 'ToolTipTableMetadata'; + +const ToolTipTableValue = styled.span` + word-wrap: break-word; +`; +ToolTipTableValue.displayName = 'ToolTipTableValue'; + +export const HeaderToolTipContent = React.memo<{ header: ColumnHeaderOptions }>(({ header }) => ( + <> + {!isEmpty(header.category) && ( + <P> + <ToolTipTableMetadata data-test-subj="category"> + {i18n.CATEGORY} + {':'} + </ToolTipTableMetadata> + <ToolTipTableValue data-test-subj="category-value">{header.category}</ToolTipTableValue> + </P> + )} + <P> + <ToolTipTableMetadata data-test-subj="field"> + {i18n.FIELD} + {':'} + </ToolTipTableMetadata> + <ToolTipTableValue data-test-subj="field-value">{header.id}</ToolTipTableValue> + </P> + <P> + <ToolTipTableMetadata data-test-subj="type"> + {i18n.TYPE} + {':'} + </ToolTipTableMetadata> + <ToolTipTableValue> + <IconType data-test-subj="type-icon" type={getIconFromType(header.type!)} /> + <span data-test-subj="type-value">{header.type}</span> + </ToolTipTableValue> + </P> + {!isEmpty(header.description) && ( + <P> + <ToolTipTableMetadata data-test-subj="description"> + {i18n.DESCRIPTION} + {':'} + </ToolTipTableMetadata> + <ToolTipTableValue data-test-subj="description-value"> + {header.description} + </ToolTipTableValue> + </P> + )} + </> +)); +HeaderToolTipContent.displayName = 'HeaderToolTipContent'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.ts b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.ts new file mode 100644 index 0000000000000..d19f221966e55 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.ts @@ -0,0 +1,116 @@ +/* + * 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 { defaultHeaders } from './default_headers'; +import { getActionsColumnWidth, getColumnWidthFromType, getColumnHeaders } from './helpers'; +import { + DEFAULT_COLUMN_MIN_WIDTH, + DEFAULT_DATE_COLUMN_MIN_WIDTH, + DEFAULT_ACTIONS_COLUMN_WIDTH, + EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH, + SHOW_CHECK_BOXES_COLUMN_WIDTH, +} from '../constants'; +import { mockBrowserFields } from '../../../../mock/browser_fields'; + +window.matchMedia = jest.fn().mockImplementation((query) => { + return { + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + }; +}); + +describe('helpers', () => { + describe('getColumnWidthFromType', () => { + test('it returns the expected width for a non-date column', () => { + expect(getColumnWidthFromType('keyword')).toEqual(DEFAULT_COLUMN_MIN_WIDTH); + }); + + test('it returns the expected width for a date column', () => { + expect(getColumnWidthFromType('date')).toEqual(DEFAULT_DATE_COLUMN_MIN_WIDTH); + }); + }); + + describe('getActionsColumnWidth', () => { + test('returns the default actions column width when isEventViewer is false', () => { + expect(getActionsColumnWidth(false)).toEqual(DEFAULT_ACTIONS_COLUMN_WIDTH); + }); + + test('returns the default actions column width + checkbox width when isEventViewer is false and showCheckboxes is true', () => { + expect(getActionsColumnWidth(false, true)).toEqual( + DEFAULT_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH + ); + }); + + test('returns the events viewer actions column width when isEventViewer is true', () => { + expect(getActionsColumnWidth(true)).toEqual(EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH); + }); + + test('returns the events viewer actions column width + checkbox width when isEventViewer is true and showCheckboxes is true', () => { + expect(getActionsColumnWidth(true, true)).toEqual( + EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH + ); + }); + }); + + describe('getColumnHeaders', () => { + test('should return a full object of ColumnHeader from the default header', () => { + const expectedData = [ + { + aggregatable: true, + category: 'base', + columnHeaderType: 'not-filtered', + description: + 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', + example: '2016-05-23T08:05:34.853Z', + format: '', + id: '@timestamp', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: '@timestamp', + searchable: true, + type: 'date', + initialWidth: 190, + }, + { + aggregatable: true, + category: 'source', + columnHeaderType: 'not-filtered', + description: 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', + example: '', + format: '', + id: 'source.ip', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'source.ip', + searchable: true, + type: 'ip', + initialWidth: 180, + }, + { + aggregatable: true, + category: 'destination', + columnHeaderType: 'not-filtered', + description: + 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', + example: '', + format: '', + id: 'destination.ip', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.ip', + searchable: true, + type: 'ip', + initialWidth: 180, + }, + ]; + const mockHeader = defaultHeaders.filter((h) => + ['@timestamp', 'source.ip', 'destination.ip'].includes(h.id) + ); + expect(getColumnHeaders(mockHeader, mockBrowserFields)).toEqual(expectedData); + }); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.ts b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.ts new file mode 100644 index 0000000000000..fc566da8c58a2 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { get } from 'lodash/fp'; +import type { BrowserFields } from '../../../../../common/search_strategy/index_fields'; +import type { ColumnHeaderOptions } from '../../../../../common/types/timeline'; + +import { + DEFAULT_COLUMN_MIN_WIDTH, + DEFAULT_DATE_COLUMN_MIN_WIDTH, + SHOW_CHECK_BOXES_COLUMN_WIDTH, + EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH, + DEFAULT_ACTIONS_COLUMN_WIDTH, + MINIMUM_ACTIONS_COLUMN_WIDTH, +} from '../constants'; + +/** Enriches the column headers with field details from the specified browserFields */ +export const getColumnHeaders = ( + headers: ColumnHeaderOptions[], + browserFields: BrowserFields +): ColumnHeaderOptions[] => { + return headers.map((header) => { + const splitHeader = header.id.split('.'); // source.geo.city_name -> [source, geo, city_name] + + return { + ...header, + ...get( + [splitHeader.length > 1 ? splitHeader[0] : 'base', 'fields', header.id], + browserFields + ), + }; + }); +}; + +export const getColumnWidthFromType = (type: string): number => + type !== 'date' ? DEFAULT_COLUMN_MIN_WIDTH : DEFAULT_DATE_COLUMN_MIN_WIDTH; + +/** Returns the (fixed) width of the Actions column */ +export const getActionsColumnWidth = ( + isEventViewer: boolean, + showCheckboxes = false, + additionalActionWidth = 0 +): number => { + const checkboxesWidth = showCheckboxes ? SHOW_CHECK_BOXES_COLUMN_WIDTH : 0; + const actionsColumnWidth = + checkboxesWidth + + (isEventViewer ? EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH : DEFAULT_ACTIONS_COLUMN_WIDTH) + + additionalActionWidth; + + return actionsColumnWidth > MINIMUM_ACTIONS_COLUMN_WIDTH + checkboxesWidth + ? actionsColumnWidth + : MINIMUM_ACTIONS_COLUMN_WIDTH + checkboxesWidth; +}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/index.test.tsx new file mode 100644 index 0000000000000..1466b06f8ed25 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/index.test.tsx @@ -0,0 +1,316 @@ +/* + * 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 { shallow } from 'enzyme'; +import React from 'react'; + +import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants'; +import { defaultHeaders } from './default_headers'; +import { Sort } from '../sort'; + +import { ColumnHeadersComponent } from '.'; +import { cloneDeep } from 'lodash/fp'; +import { useMountAppended } from '../../../utils/use_mount_appended'; +import { mockBrowserFields } from '../../../../mock/browser_fields'; +import { Direction } from '../../../../../common/search_strategy'; +import { TimelineTabs } from '../../../../../common/types/timeline'; +import { tGridActions } from '../../../../store/t_grid'; +import { testTrailingControlColumns } from '../../../../mock/mock_timeline_control_columns'; +import { TestProviders } from '../../../../mock'; +import { mockGlobalState } from '../../../../mock/global_state'; + +const mockDispatch = jest.fn(); +jest.mock('../../../../hooks/use_selector', () => ({ + useShallowEqualSelector: () => mockGlobalState.timelineById.test, + useDeepEqualSelector: () => mockGlobalState.timelineById.test, +})); + +window.matchMedia = jest.fn().mockImplementation((query) => { + return { + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + }; +}); + +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); +const timelineId = 'test'; + +describe('ColumnHeaders', () => { + const mount = useMountAppended(); + + describe('rendering', () => { + const sort: Sort[] = [ + { + columnId: '@timestamp', + columnType: 'number', + sortDirection: Direction.desc, + }, + ]; + + test('renders correctly against snapshot', () => { + const wrapper = shallow( + <TestProviders> + <ColumnHeadersComponent + actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH} + browserFields={mockBrowserFields} + columnHeaders={defaultHeaders} + isSelectAllChecked={false} + onSelectAll={jest.fn} + showEventsSelect={false} + showSelectAllCheckbox={false} + sort={sort} + tabType={TimelineTabs.query} + timelineId={timelineId} + leadingControlColumns={[]} + trailingControlColumns={[]} + /> + </TestProviders> + ); + expect(wrapper.find('ColumnHeadersComponent')).toMatchSnapshot(); + }); + + // TODO BrowserField When we bring back browser fields unskip + test.skip('it renders the field browser', () => { + const wrapper = mount( + <TestProviders> + <ColumnHeadersComponent + actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH} + browserFields={mockBrowserFields} + columnHeaders={defaultHeaders} + isSelectAllChecked={false} + onSelectAll={jest.fn} + showEventsSelect={false} + showSelectAllCheckbox={false} + sort={sort} + tabType={TimelineTabs.query} + timelineId={timelineId} + leadingControlColumns={[]} + trailingControlColumns={[]} + /> + </TestProviders> + ); + + expect(wrapper.find('[data-test-subj="field-browser"]').first().exists()).toEqual(true); + }); + + test('it renders every column header', () => { + const wrapper = mount( + <TestProviders> + <ColumnHeadersComponent + actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH} + browserFields={mockBrowserFields} + columnHeaders={defaultHeaders} + isSelectAllChecked={false} + onSelectAll={jest.fn} + showEventsSelect={false} + showSelectAllCheckbox={false} + sort={sort} + tabType={TimelineTabs.query} + timelineId={timelineId} + leadingControlColumns={[]} + trailingControlColumns={[]} + /> + </TestProviders> + ); + + defaultHeaders.forEach((h) => { + expect(wrapper.find('[data-test-subj="headers-group"]').first().text()).toContain(h.id); + }); + }); + }); + + describe('#onColumnsSorted', () => { + let mockSort: Sort[] = [ + { + columnId: '@timestamp', + columnType: 'number', + sortDirection: Direction.desc, + }, + { + columnId: 'host.name', + columnType: 'text', + sortDirection: Direction.asc, + }, + ]; + let mockDefaultHeaders = cloneDeep( + defaultHeaders.map((h) => (h.id === 'message' ? h : { ...h, aggregatable: true })) + ); + + beforeEach(() => { + mockDefaultHeaders = cloneDeep( + defaultHeaders.map((h) => (h.id === 'message' ? h : { ...h, aggregatable: true })) + ); + mockSort = [ + { + columnId: '@timestamp', + columnType: 'number', + sortDirection: Direction.desc, + }, + { + columnId: 'host.name', + columnType: 'text', + sortDirection: Direction.asc, + }, + ]; + }); + + test('Add column `event.category` as desc sorting', () => { + const wrapper = mount( + <TestProviders> + <ColumnHeadersComponent + actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH} + browserFields={mockBrowserFields} + columnHeaders={mockDefaultHeaders} + isSelectAllChecked={false} + onSelectAll={jest.fn} + showEventsSelect={false} + showSelectAllCheckbox={false} + sort={mockSort} + tabType={TimelineTabs.query} + timelineId={timelineId} + leadingControlColumns={[]} + trailingControlColumns={[]} + /> + </TestProviders> + ); + + wrapper + .find('[data-test-subj="header-event.category"] [data-test-subj="header-sort-button"]') + .first() + .simulate('click'); + expect(mockDispatch).toHaveBeenCalledWith( + tGridActions.updateSort({ + id: timelineId, + sort: [ + { + columnId: '@timestamp', + columnType: 'number', + sortDirection: Direction.desc, + }, + { + columnId: 'host.name', + columnType: 'text', + sortDirection: Direction.asc, + }, + { columnId: 'event.category', columnType: 'text', sortDirection: Direction.desc }, + ], + }) + ); + }); + + test('Change order of column `@timestamp` from desc to asc without changing index position', () => { + const wrapper = mount( + <TestProviders> + <ColumnHeadersComponent + actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH} + browserFields={mockBrowserFields} + columnHeaders={mockDefaultHeaders} + isSelectAllChecked={false} + onSelectAll={jest.fn()} + showEventsSelect={false} + showSelectAllCheckbox={false} + sort={mockSort} + tabType={TimelineTabs.query} + timelineId={timelineId} + leadingControlColumns={[]} + trailingControlColumns={[]} + /> + </TestProviders> + ); + + wrapper + .find('[data-test-subj="header-@timestamp"] [data-test-subj="header-sort-button"]') + .first() + .simulate('click'); + expect(mockDispatch).toHaveBeenCalledWith( + tGridActions.updateSort({ + id: timelineId, + sort: [ + { + columnId: '@timestamp', + columnType: 'number', + sortDirection: Direction.asc, + }, + { columnId: 'host.name', columnType: 'text', sortDirection: Direction.asc }, + ], + }) + ); + }); + + test('Change order of column `host.name` from asc to desc without changing index position', () => { + const wrapper = mount( + <TestProviders> + <ColumnHeadersComponent + actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH} + browserFields={mockBrowserFields} + columnHeaders={mockDefaultHeaders} + isSelectAllChecked={false} + onSelectAll={jest.fn()} + showEventsSelect={false} + showSelectAllCheckbox={false} + sort={mockSort} + tabType={TimelineTabs.query} + timelineId={timelineId} + leadingControlColumns={[]} + trailingControlColumns={[]} + /> + </TestProviders> + ); + + wrapper + .find('[data-test-subj="header-host.name"] [data-test-subj="header-sort-button"]') + .first() + .simulate('click'); + expect(mockDispatch).toHaveBeenCalledWith( + tGridActions.updateSort({ + id: timelineId, + sort: [ + { + columnId: '@timestamp', + columnType: 'number', + sortDirection: Direction.desc, + }, + { columnId: 'host.name', columnType: 'text', sortDirection: Direction.desc }, + ], + }) + ); + }); + test('Does not render the default leading action column header and renders a custom trailing header', () => { + const wrapper = mount( + <TestProviders> + <ColumnHeadersComponent + actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH} + browserFields={mockBrowserFields} + columnHeaders={mockDefaultHeaders} + isSelectAllChecked={false} + onSelectAll={jest.fn()} + showEventsSelect={false} + showSelectAllCheckbox={false} + sort={mockSort} + tabType={TimelineTabs.query} + timelineId={timelineId} + leadingControlColumns={[]} + trailingControlColumns={testTrailingControlColumns} + /> + </TestProviders> + ); + + expect(wrapper.exists('[data-test-subj="field-browser"]')).toBeFalsy(); + expect(wrapper.exists('[data-test-subj="test-header-action-cell"]')).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/index.tsx new file mode 100644 index 0000000000000..1d4141cd1ff5d --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/index.tsx @@ -0,0 +1,295 @@ +/* + * 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 { DRAG_TYPE_FIELD, droppableTimelineColumnsPrefix } from '@kbn/securitysolution-t-grid'; +import deepEqual from 'fast-deep-equal'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import { Droppable, DraggableChildrenFn } from 'react-beautiful-dnd'; + +import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; +// eslint-disable-next-line no-duplicate-imports +import type { + ControlColumnProps, + ColumnHeaderOptions, + HeaderActionProps, +} from '../../../../../common/types/timeline'; + +import type { BrowserFields } from '../../../../../common/search_strategy/index_fields'; + +import type { OnSelectAll } from '../../types'; +import { + EventsTh, + EventsThead, + EventsThGroupData, + EventsTrHeader, + EventsThGroupActions, +} from '../../styles'; +import { Sort } from '../sort'; +import { ColumnHeader } from './column_header'; +import { DraggableFieldBadge } from '../../../draggables'; + +interface Props { + actionsColumnWidth: number; + browserFields: BrowserFields; + columnHeaders: ColumnHeaderOptions[]; + isEventViewer?: boolean; + isSelectAllChecked: boolean; + onSelectAll: OnSelectAll; + showEventsSelect: boolean; + showSelectAllCheckbox: boolean; + sort: Sort[]; + tabType: TimelineTabs; + timelineId: string; + leadingControlColumns: ControlColumnProps[]; + trailingControlColumns: ControlColumnProps[]; +} + +interface DraggableContainerProps { + children: React.ReactNode; + onMount: () => void; + onUnmount: () => void; +} + +export const DraggableContainer = React.memo<DraggableContainerProps>( + ({ children, onMount, onUnmount }) => { + useEffect(() => { + onMount(); + + return () => onUnmount(); + }, [onMount, onUnmount]); + + return <>{children}</>; + } +); + +DraggableContainer.displayName = 'DraggableContainer'; + +export const isFullScreen = ({ + globalFullScreen, + timelineId, + timelineFullScreen, +}: { + globalFullScreen: boolean; + timelineId: string; + timelineFullScreen: boolean; +}) => + (timelineId === TimelineId.active && timelineFullScreen) || + (timelineId !== TimelineId.active && globalFullScreen); + +/** Renders the timeline header columns */ +export const ColumnHeadersComponent = ({ + actionsColumnWidth, + browserFields, + columnHeaders, + isEventViewer = false, + isSelectAllChecked, + onSelectAll, + showEventsSelect, + showSelectAllCheckbox, + sort, + tabType, + timelineId, + leadingControlColumns, + trailingControlColumns, +}: Props) => { + const [draggingIndex, setDraggingIndex] = useState<number | null>(null); + + const renderClone: DraggableChildrenFn = useCallback( + (dragProvided, _dragSnapshot, rubric) => { + const index = rubric.source.index; + const header = columnHeaders[index]; + + const onMount = () => setDraggingIndex(index); + const onUnmount = () => setDraggingIndex(null); + + return ( + <EventsTh + data-test-subj="draggable-header" + {...dragProvided.draggableProps} + {...dragProvided.dragHandleProps} + ref={dragProvided.innerRef} + > + <DraggableContainer onMount={onMount} onUnmount={onUnmount}> + <DraggableFieldBadge fieldId={header.id} fieldWidth={header.initialWidth} /> + </DraggableContainer> + </EventsTh> + ); + }, + [columnHeaders, setDraggingIndex] + ); + + const ColumnHeaderList = useMemo( + () => + columnHeaders.map((header, draggableIndex) => ( + <ColumnHeader + key={header.id} + draggableIndex={draggableIndex} + timelineId={timelineId} + header={header} + isDragging={draggingIndex === draggableIndex} + sort={sort} + tabType={tabType} + /> + )), + [columnHeaders, timelineId, draggingIndex, sort, tabType] + ); + + const DroppableContent = useCallback( + (dropProvided, snapshot) => ( + <> + <EventsThGroupData + data-test-subj="headers-group" + ref={dropProvided.innerRef} + isDragging={snapshot.isDraggingOver} + {...dropProvided.droppableProps} + > + {ColumnHeaderList} + </EventsThGroupData> + </> + ), + [ColumnHeaderList] + ); + + const leadingHeaderCells = useMemo( + () => + leadingControlColumns ? leadingControlColumns.map((column) => column.headerCellRender) : [], + [leadingControlColumns] + ); + + const trailingHeaderCells = useMemo( + () => + trailingControlColumns ? trailingControlColumns.map((column) => column.headerCellRender) : [], + [trailingControlColumns] + ); + + const LeadingHeaderActions = useMemo(() => { + return leadingHeaderCells.map( + (Header: React.ComponentType<HeaderActionProps> | React.ComponentType | undefined, index) => { + const passedWidth = leadingControlColumns[index] && leadingControlColumns[index].width; + const width = passedWidth ? passedWidth : actionsColumnWidth; + return ( + <EventsThGroupActions + actionsColumnWidth={width} + data-test-subj="actions-container" + isEventViewer={isEventViewer} + key={index} + > + {Header && ( + <Header + width={width} + browserFields={browserFields} + columnHeaders={columnHeaders} + isEventViewer={isEventViewer} + isSelectAllChecked={isSelectAllChecked} + onSelectAll={onSelectAll} + showEventsSelect={showEventsSelect} + showSelectAllCheckbox={showSelectAllCheckbox} + sort={sort} + tabType={tabType} + timelineId={timelineId} + /> + )} + </EventsThGroupActions> + ); + } + ); + }, [ + leadingHeaderCells, + leadingControlColumns, + actionsColumnWidth, + browserFields, + columnHeaders, + isEventViewer, + isSelectAllChecked, + onSelectAll, + showEventsSelect, + showSelectAllCheckbox, + sort, + tabType, + timelineId, + ]); + + const TrailingHeaderActions = useMemo(() => { + return trailingHeaderCells.map( + (Header: React.ComponentType<HeaderActionProps> | React.ComponentType | undefined, index) => { + const passedWidth = trailingControlColumns[index] && trailingControlColumns[index].width; + const width = passedWidth ? passedWidth : actionsColumnWidth; + return ( + <EventsThGroupActions + actionsColumnWidth={width} + data-test-subj="actions-container" + isEventViewer={isEventViewer} + key={index} + > + {Header && ( + <Header + width={width} + browserFields={browserFields} + columnHeaders={columnHeaders} + isEventViewer={isEventViewer} + isSelectAllChecked={isSelectAllChecked} + onSelectAll={onSelectAll} + showEventsSelect={showEventsSelect} + showSelectAllCheckbox={showSelectAllCheckbox} + sort={sort} + tabType={tabType} + timelineId={timelineId} + /> + )} + </EventsThGroupActions> + ); + } + ); + }, [ + trailingHeaderCells, + trailingControlColumns, + actionsColumnWidth, + browserFields, + columnHeaders, + isEventViewer, + isSelectAllChecked, + onSelectAll, + showEventsSelect, + showSelectAllCheckbox, + sort, + tabType, + timelineId, + ]); + return ( + <EventsThead data-test-subj="column-headers"> + <EventsTrHeader> + {LeadingHeaderActions} + <Droppable + direction={'horizontal'} + droppableId={`${droppableTimelineColumnsPrefix}-${tabType}.${timelineId}`} + isDropDisabled={false} + type={DRAG_TYPE_FIELD} + renderClone={renderClone} + > + {DroppableContent} + </Droppable> + {TrailingHeaderActions} + </EventsTrHeader> + </EventsThead> + ); +}; + +export const ColumnHeaders = React.memo( + ColumnHeadersComponent, + (prevProps, nextProps) => + prevProps.actionsColumnWidth === nextProps.actionsColumnWidth && + prevProps.isEventViewer === nextProps.isEventViewer && + prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && + prevProps.onSelectAll === nextProps.onSelectAll && + prevProps.showEventsSelect === nextProps.showEventsSelect && + prevProps.showSelectAllCheckbox === nextProps.showSelectAllCheckbox && + deepEqual(prevProps.sort, nextProps.sort) && + prevProps.timelineId === nextProps.timelineId && + deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && + prevProps.tabType === nextProps.tabType && + deepEqual(prevProps.browserFields, nextProps.browserFields) +); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/translations.ts new file mode 100644 index 0000000000000..2d4fbcbd54cfa --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/translations.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const CATEGORY = i18n.translate('xpack.timelines.timeline.categoryTooltip', { + defaultMessage: 'Category', +}); + +export const DESCRIPTION = i18n.translate('xpack.timelines.timeline.descriptionTooltip', { + defaultMessage: 'Description', +}); + +export const FIELD = i18n.translate('xpack.timelines.timeline.fieldTooltip', { + defaultMessage: 'Field', +}); + +export const FULL_SCREEN = i18n.translate('xpack.timelines.timeline.fullScreenButton', { + defaultMessage: 'Full screen', +}); + +export const HIDE_COLUMN = i18n.translate('xpack.timelines.timeline.hideColumnLabel', { + defaultMessage: 'Hide column', +}); + +export const SORT_AZ = i18n.translate('xpack.timelines.timeline.sortAZLabel', { + defaultMessage: 'Sort A-Z', +}); + +export const SORT_FIELDS = i18n.translate('xpack.timelines.timeline.sortFieldsButton', { + defaultMessage: 'Sort fields', +}); + +export const SORT_ZA = i18n.translate('xpack.timelines.timeline.sortZALabel', { + defaultMessage: 'Sort Z-A', +}); + +export const TYPE = i18n.translate('xpack.timelines.timeline.typeTooltip', { + defaultMessage: 'Type', +}); + +export const REMOVE_COLUMN = i18n.translate( + 'xpack.timelines.timeline.flyout.pane.removeColumnButtonLabel', + { + defaultMessage: 'Remove column', + } +); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/constants.ts b/x-pack/plugins/timelines/public/components/t_grid/body/constants.ts new file mode 100644 index 0000000000000..445211229574b --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/constants.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. + */ + +/** The minimum (fixed) width of the Actions column */ +export const MINIMUM_ACTIONS_COLUMN_WIDTH = 50; // px; + +/** Additional column width to include when checkboxes are shown **/ +export const SHOW_CHECK_BOXES_COLUMN_WIDTH = 24; // px; + +/** The (fixed) width of the Actions column */ +export const DEFAULT_ACTIONS_COLUMN_WIDTH = SHOW_CHECK_BOXES_COLUMN_WIDTH * 5; // px; +/** + * The (fixed) width of the Actions column when the timeline body is used as + * an events viewer, which has fewer actions than a regular events viewer + */ +export const EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH = SHOW_CHECK_BOXES_COLUMN_WIDTH * 4; // px; + +/** The default minimum width of a column (when a width for the column type is not specified) */ +export const DEFAULT_COLUMN_MIN_WIDTH = 180; // px + +/** The minimum width of a resized column */ +export const RESIZED_COLUMN_MIN_WITH = 70; // px + +/** The default minimum width of a column of type `date` */ +export const DEFAULT_DATE_COLUMN_MIN_WIDTH = 190; // px diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/__snapshots__/index.test.tsx.snap b/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..cbec3a3baa695 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/__snapshots__/index.test.tsx.snap @@ -0,0 +1,967 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Columns it renders the expected columns 1`] = ` +<styled.div + data-test-subj="data-driven-columns" +> + <TgridTdCell + _id="1" + ariaRowindex={2} + data={ + Array [ + Object { + "field": "@timestamp", + "value": Array [ + "2018-11-05T19:03:25.937Z", + ], + }, + Object { + "field": "event.severity", + "value": Array [ + "3", + ], + }, + Object { + "field": "event.category", + "value": Array [ + "Access", + ], + }, + Object { + "field": "event.action", + "value": Array [ + "Action", + ], + }, + Object { + "field": "host.name", + "value": Array [ + "apache", + ], + }, + Object { + "field": "source.ip", + "value": Array [ + "192.168.0.1", + ], + }, + Object { + "field": "destination.ip", + "value": Array [ + "192.168.0.3", + ], + }, + Object { + "field": "destination.bytes", + "value": Array [ + "123456", + ], + }, + Object { + "field": "user.name", + "value": Array [ + "john.dee", + ], + }, + ] + } + ecsData={ + Object { + "_id": "1", + "destination": Object { + "ip": Array [ + "192.168.0.3", + ], + "port": Array [ + 6343, + ], + }, + "event": Object { + "action": Array [ + "Action", + ], + "category": Array [ + "Access", + ], + "id": Array [ + "1", + ], + "module": Array [ + "nginx", + ], + "severity": Array [ + 3, + ], + }, + "geo": Object { + "country_iso_code": Array [ + "xx", + ], + "region_name": Array [ + "xx", + ], + }, + "host": Object { + "ip": Array [ + "192.168.0.1", + ], + "name": Array [ + "apache", + ], + }, + "source": Object { + "ip": Array [ + "192.168.0.1", + ], + "port": Array [ + 80, + ], + }, + "timestamp": "2018-11-05T19:03:25.937Z", + "user": Object { + "id": Array [ + "1", + ], + "name": Array [ + "john.dee", + ], + }, + } + } + hasRowRenderers={false} + header={ + Object { + "columnHeaderType": "not-filtered", + "id": "message", + "initialWidth": 180, + } + } + index={0} + key="message" + renderCellValue={[Function]} + timelineId="test" + /> + <TgridTdCell + _id="1" + ariaRowindex={2} + data={ + Array [ + Object { + "field": "@timestamp", + "value": Array [ + "2018-11-05T19:03:25.937Z", + ], + }, + Object { + "field": "event.severity", + "value": Array [ + "3", + ], + }, + Object { + "field": "event.category", + "value": Array [ + "Access", + ], + }, + Object { + "field": "event.action", + "value": Array [ + "Action", + ], + }, + Object { + "field": "host.name", + "value": Array [ + "apache", + ], + }, + Object { + "field": "source.ip", + "value": Array [ + "192.168.0.1", + ], + }, + Object { + "field": "destination.ip", + "value": Array [ + "192.168.0.3", + ], + }, + Object { + "field": "destination.bytes", + "value": Array [ + "123456", + ], + }, + Object { + "field": "user.name", + "value": Array [ + "john.dee", + ], + }, + ] + } + ecsData={ + Object { + "_id": "1", + "destination": Object { + "ip": Array [ + "192.168.0.3", + ], + "port": Array [ + 6343, + ], + }, + "event": Object { + "action": Array [ + "Action", + ], + "category": Array [ + "Access", + ], + "id": Array [ + "1", + ], + "module": Array [ + "nginx", + ], + "severity": Array [ + 3, + ], + }, + "geo": Object { + "country_iso_code": Array [ + "xx", + ], + "region_name": Array [ + "xx", + ], + }, + "host": Object { + "ip": Array [ + "192.168.0.1", + ], + "name": Array [ + "apache", + ], + }, + "source": Object { + "ip": Array [ + "192.168.0.1", + ], + "port": Array [ + 80, + ], + }, + "timestamp": "2018-11-05T19:03:25.937Z", + "user": Object { + "id": Array [ + "1", + ], + "name": Array [ + "john.dee", + ], + }, + } + } + hasRowRenderers={false} + header={ + Object { + "columnHeaderType": "not-filtered", + "id": "event.category", + "initialWidth": 180, + } + } + index={1} + key="event.category" + renderCellValue={[Function]} + timelineId="test" + /> + <TgridTdCell + _id="1" + ariaRowindex={2} + data={ + Array [ + Object { + "field": "@timestamp", + "value": Array [ + "2018-11-05T19:03:25.937Z", + ], + }, + Object { + "field": "event.severity", + "value": Array [ + "3", + ], + }, + Object { + "field": "event.category", + "value": Array [ + "Access", + ], + }, + Object { + "field": "event.action", + "value": Array [ + "Action", + ], + }, + Object { + "field": "host.name", + "value": Array [ + "apache", + ], + }, + Object { + "field": "source.ip", + "value": Array [ + "192.168.0.1", + ], + }, + Object { + "field": "destination.ip", + "value": Array [ + "192.168.0.3", + ], + }, + Object { + "field": "destination.bytes", + "value": Array [ + "123456", + ], + }, + Object { + "field": "user.name", + "value": Array [ + "john.dee", + ], + }, + ] + } + ecsData={ + Object { + "_id": "1", + "destination": Object { + "ip": Array [ + "192.168.0.3", + ], + "port": Array [ + 6343, + ], + }, + "event": Object { + "action": Array [ + "Action", + ], + "category": Array [ + "Access", + ], + "id": Array [ + "1", + ], + "module": Array [ + "nginx", + ], + "severity": Array [ + 3, + ], + }, + "geo": Object { + "country_iso_code": Array [ + "xx", + ], + "region_name": Array [ + "xx", + ], + }, + "host": Object { + "ip": Array [ + "192.168.0.1", + ], + "name": Array [ + "apache", + ], + }, + "source": Object { + "ip": Array [ + "192.168.0.1", + ], + "port": Array [ + 80, + ], + }, + "timestamp": "2018-11-05T19:03:25.937Z", + "user": Object { + "id": Array [ + "1", + ], + "name": Array [ + "john.dee", + ], + }, + } + } + hasRowRenderers={false} + header={ + Object { + "columnHeaderType": "not-filtered", + "id": "event.action", + "initialWidth": 180, + } + } + index={2} + key="event.action" + renderCellValue={[Function]} + timelineId="test" + /> + <TgridTdCell + _id="1" + ariaRowindex={2} + data={ + Array [ + Object { + "field": "@timestamp", + "value": Array [ + "2018-11-05T19:03:25.937Z", + ], + }, + Object { + "field": "event.severity", + "value": Array [ + "3", + ], + }, + Object { + "field": "event.category", + "value": Array [ + "Access", + ], + }, + Object { + "field": "event.action", + "value": Array [ + "Action", + ], + }, + Object { + "field": "host.name", + "value": Array [ + "apache", + ], + }, + Object { + "field": "source.ip", + "value": Array [ + "192.168.0.1", + ], + }, + Object { + "field": "destination.ip", + "value": Array [ + "192.168.0.3", + ], + }, + Object { + "field": "destination.bytes", + "value": Array [ + "123456", + ], + }, + Object { + "field": "user.name", + "value": Array [ + "john.dee", + ], + }, + ] + } + ecsData={ + Object { + "_id": "1", + "destination": Object { + "ip": Array [ + "192.168.0.3", + ], + "port": Array [ + 6343, + ], + }, + "event": Object { + "action": Array [ + "Action", + ], + "category": Array [ + "Access", + ], + "id": Array [ + "1", + ], + "module": Array [ + "nginx", + ], + "severity": Array [ + 3, + ], + }, + "geo": Object { + "country_iso_code": Array [ + "xx", + ], + "region_name": Array [ + "xx", + ], + }, + "host": Object { + "ip": Array [ + "192.168.0.1", + ], + "name": Array [ + "apache", + ], + }, + "source": Object { + "ip": Array [ + "192.168.0.1", + ], + "port": Array [ + 80, + ], + }, + "timestamp": "2018-11-05T19:03:25.937Z", + "user": Object { + "id": Array [ + "1", + ], + "name": Array [ + "john.dee", + ], + }, + } + } + hasRowRenderers={false} + header={ + Object { + "columnHeaderType": "not-filtered", + "id": "host.name", + "initialWidth": 180, + } + } + index={3} + key="host.name" + renderCellValue={[Function]} + timelineId="test" + /> + <TgridTdCell + _id="1" + ariaRowindex={2} + data={ + Array [ + Object { + "field": "@timestamp", + "value": Array [ + "2018-11-05T19:03:25.937Z", + ], + }, + Object { + "field": "event.severity", + "value": Array [ + "3", + ], + }, + Object { + "field": "event.category", + "value": Array [ + "Access", + ], + }, + Object { + "field": "event.action", + "value": Array [ + "Action", + ], + }, + Object { + "field": "host.name", + "value": Array [ + "apache", + ], + }, + Object { + "field": "source.ip", + "value": Array [ + "192.168.0.1", + ], + }, + Object { + "field": "destination.ip", + "value": Array [ + "192.168.0.3", + ], + }, + Object { + "field": "destination.bytes", + "value": Array [ + "123456", + ], + }, + Object { + "field": "user.name", + "value": Array [ + "john.dee", + ], + }, + ] + } + ecsData={ + Object { + "_id": "1", + "destination": Object { + "ip": Array [ + "192.168.0.3", + ], + "port": Array [ + 6343, + ], + }, + "event": Object { + "action": Array [ + "Action", + ], + "category": Array [ + "Access", + ], + "id": Array [ + "1", + ], + "module": Array [ + "nginx", + ], + "severity": Array [ + 3, + ], + }, + "geo": Object { + "country_iso_code": Array [ + "xx", + ], + "region_name": Array [ + "xx", + ], + }, + "host": Object { + "ip": Array [ + "192.168.0.1", + ], + "name": Array [ + "apache", + ], + }, + "source": Object { + "ip": Array [ + "192.168.0.1", + ], + "port": Array [ + 80, + ], + }, + "timestamp": "2018-11-05T19:03:25.937Z", + "user": Object { + "id": Array [ + "1", + ], + "name": Array [ + "john.dee", + ], + }, + } + } + hasRowRenderers={false} + header={ + Object { + "columnHeaderType": "not-filtered", + "id": "source.ip", + "initialWidth": 180, + } + } + index={4} + key="source.ip" + renderCellValue={[Function]} + timelineId="test" + /> + <TgridTdCell + _id="1" + ariaRowindex={2} + data={ + Array [ + Object { + "field": "@timestamp", + "value": Array [ + "2018-11-05T19:03:25.937Z", + ], + }, + Object { + "field": "event.severity", + "value": Array [ + "3", + ], + }, + Object { + "field": "event.category", + "value": Array [ + "Access", + ], + }, + Object { + "field": "event.action", + "value": Array [ + "Action", + ], + }, + Object { + "field": "host.name", + "value": Array [ + "apache", + ], + }, + Object { + "field": "source.ip", + "value": Array [ + "192.168.0.1", + ], + }, + Object { + "field": "destination.ip", + "value": Array [ + "192.168.0.3", + ], + }, + Object { + "field": "destination.bytes", + "value": Array [ + "123456", + ], + }, + Object { + "field": "user.name", + "value": Array [ + "john.dee", + ], + }, + ] + } + ecsData={ + Object { + "_id": "1", + "destination": Object { + "ip": Array [ + "192.168.0.3", + ], + "port": Array [ + 6343, + ], + }, + "event": Object { + "action": Array [ + "Action", + ], + "category": Array [ + "Access", + ], + "id": Array [ + "1", + ], + "module": Array [ + "nginx", + ], + "severity": Array [ + 3, + ], + }, + "geo": Object { + "country_iso_code": Array [ + "xx", + ], + "region_name": Array [ + "xx", + ], + }, + "host": Object { + "ip": Array [ + "192.168.0.1", + ], + "name": Array [ + "apache", + ], + }, + "source": Object { + "ip": Array [ + "192.168.0.1", + ], + "port": Array [ + 80, + ], + }, + "timestamp": "2018-11-05T19:03:25.937Z", + "user": Object { + "id": Array [ + "1", + ], + "name": Array [ + "john.dee", + ], + }, + } + } + hasRowRenderers={false} + header={ + Object { + "columnHeaderType": "not-filtered", + "id": "destination.ip", + "initialWidth": 180, + } + } + index={5} + key="destination.ip" + renderCellValue={[Function]} + timelineId="test" + /> + <TgridTdCell + _id="1" + ariaRowindex={2} + data={ + Array [ + Object { + "field": "@timestamp", + "value": Array [ + "2018-11-05T19:03:25.937Z", + ], + }, + Object { + "field": "event.severity", + "value": Array [ + "3", + ], + }, + Object { + "field": "event.category", + "value": Array [ + "Access", + ], + }, + Object { + "field": "event.action", + "value": Array [ + "Action", + ], + }, + Object { + "field": "host.name", + "value": Array [ + "apache", + ], + }, + Object { + "field": "source.ip", + "value": Array [ + "192.168.0.1", + ], + }, + Object { + "field": "destination.ip", + "value": Array [ + "192.168.0.3", + ], + }, + Object { + "field": "destination.bytes", + "value": Array [ + "123456", + ], + }, + Object { + "field": "user.name", + "value": Array [ + "john.dee", + ], + }, + ] + } + ecsData={ + Object { + "_id": "1", + "destination": Object { + "ip": Array [ + "192.168.0.3", + ], + "port": Array [ + 6343, + ], + }, + "event": Object { + "action": Array [ + "Action", + ], + "category": Array [ + "Access", + ], + "id": Array [ + "1", + ], + "module": Array [ + "nginx", + ], + "severity": Array [ + 3, + ], + }, + "geo": Object { + "country_iso_code": Array [ + "xx", + ], + "region_name": Array [ + "xx", + ], + }, + "host": Object { + "ip": Array [ + "192.168.0.1", + ], + "name": Array [ + "apache", + ], + }, + "source": Object { + "ip": Array [ + "192.168.0.1", + ], + "port": Array [ + 80, + ], + }, + "timestamp": "2018-11-05T19:03:25.937Z", + "user": Object { + "id": Array [ + "1", + ], + "name": Array [ + "john.dee", + ], + }, + } + } + hasRowRenderers={false} + header={ + Object { + "columnHeaderType": "not-filtered", + "id": "user.name", + "initialWidth": 180, + } + } + index={6} + key="user.name" + renderCellValue={[Function]} + timelineId="test" + /> +</styled.div> +`; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.test.tsx new file mode 100644 index 0000000000000..e8459fa99d8c8 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.test.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { shallow } from 'enzyme'; + +import React from 'react'; + +import { defaultHeaders } from '../column_headers/default_headers'; + +import { DataDrivenColumns } from '.'; +import { mockTimelineData } from '../../../../mock/mock_timeline_data'; +import { TestCellRenderer } from '../../../../mock/cell_renderer'; + +window.matchMedia = jest.fn().mockImplementation((query) => { + return { + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + }; +}); + +describe('Columns', () => { + const headersSansTimestamp = defaultHeaders.filter((h) => h.id !== '@timestamp'); + + test('it renders the expected columns', () => { + const wrapper = shallow( + <DataDrivenColumns + ariaRowindex={2} + id={mockTimelineData[0]._id} + actionsColumnWidth={50} + checked={false} + columnHeaders={headersSansTimestamp} + data={mockTimelineData[0].data} + ecsData={mockTimelineData[0].ecs} + hasRowRenderers={false} + renderCellValue={TestCellRenderer} + timelineId="test" + columnValues={'abc def'} + showCheckboxes={false} + selectedEventIds={{}} + loadingEventIds={[]} + onEventDetailsPanelOpened={jest.fn()} + onRowSelected={jest.fn()} + leadingControlColumns={[]} + trailingControlColumns={[]} + /> + ); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.tsx new file mode 100644 index 0000000000000..23e94b92eaf3d --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/index.tsx @@ -0,0 +1,394 @@ +/* + * 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 { EuiScreenReaderOnly } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { getOr } from 'lodash/fp'; + +import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid'; +import { OnRowSelected } from '../../types'; + +import { + EventsTd, + EVENTS_TD_CLASS_NAME, + EventsTdContent, + EventsTdGroupData, + EventsTdGroupActions, +} from '../../styles'; + +import { StatefulCell } from './stateful_cell'; +import * as i18n from './translations'; +import { TimelineTabs } from '../../../../../common/types/timeline'; +// eslint-disable-next-line no-duplicate-imports +import type { + ActionProps, + CellValueElementProps, + ColumnHeaderOptions, + ControlColumnProps, + RowCellRender, +} from '../../../../../common/types/timeline'; +import type { TimelineNonEcsData } from '../../../../../common/search_strategy'; +import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers'; +import type { Ecs } from '../../../../../common/ecs'; + +interface CellProps { + _id: string; + ariaRowindex: number; + index: number; + header: ColumnHeaderOptions; + data: TimelineNonEcsData[]; + ecsData: Ecs; + hasRowRenderers: boolean; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + tabType?: TimelineTabs; + timelineId: string; +} + +interface DataDrivenColumnProps { + id: string; + actionsColumnWidth: number; + ariaRowindex: number; + checked: boolean; + columnHeaders: ColumnHeaderOptions[]; + columnValues: string; + data: TimelineNonEcsData[]; + ecsData: Ecs; + isEventViewer?: boolean; + loadingEventIds: Readonly<string[]>; + onEventDetailsPanelOpened: () => void; + onRowSelected: OnRowSelected; + onRuleChange?: () => void; + hasRowRenderers: boolean; + selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>; + showCheckboxes: boolean; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + tabType?: TimelineTabs; + timelineId: string; + trailingControlColumns: ControlColumnProps[]; + leadingControlColumns: ControlColumnProps[]; +} + +const SPACE = ' '; + +export const shouldForwardKeyDownEvent = (key: string): boolean => { + switch (key) { + case SPACE: // fall through + case 'Enter': + return true; + default: + return false; + } +}; + +export const onKeyDown = (keyboardEvent: React.KeyboardEvent) => { + const { altKey, ctrlKey, key, metaKey, shiftKey, target, type } = keyboardEvent; + + const targetElement = target as Element; + + // we *only* forward the event to the (child) draggable keyboard wrapper + // if the keyboard event originated from the container (TD) element + if (shouldForwardKeyDownEvent(key) && targetElement.className?.includes(EVENTS_TD_CLASS_NAME)) { + const draggableKeyboardWrapper = targetElement.querySelector<HTMLDivElement>( + `.${DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME}` + ); + + const newEvent = new KeyboardEvent(type, { + altKey, + bubbles: true, + cancelable: true, + ctrlKey, + key, + metaKey, + shiftKey, + }); + + if (key === ' ') { + // prevent the default behavior of scrolling the table when space is pressed + keyboardEvent.preventDefault(); + } + + draggableKeyboardWrapper?.dispatchEvent(newEvent); + } +}; + +const TgridActionTdCell = ({ + action: Action, + width, + actionsColumnWidth, + ariaRowindex, + columnId, + columnValues, + data, + ecsData, + eventIdToNoteIds, + index, + isEventPinned, + isEventViewer, + eventId, + loadingEventIds, + onEventDetailsPanelOpened, + onRowSelected, + rowIndex, + hasRowRenderers, + onRuleChange, + selectedEventIds, + showCheckboxes, + showNotes = false, + tabType, + timelineId, + toggleShowNotes, +}: ActionProps & { + columnId: string; + hasRowRenderers: boolean; + actionsColumnWidth: number; + selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>; +}) => { + const displayWidth = width ? width : actionsColumnWidth; + return ( + <EventsTdGroupActions + width={displayWidth} + data-test-subj="event-actions-container" + tabIndex={0} + > + <EventsTd + $ariaColumnIndex={index + ARIA_COLUMN_INDEX_OFFSET} + key={tabType != null ? `${eventId}_${tabType}` : `${eventId}`} + onKeyDown={onKeyDown} + role="button" + tabIndex={0} + width={width} + > + <EventsTdContent data-test-subj="cell-container"> + <> + <EuiScreenReaderOnly data-test-subj="screenReaderOnly"> + <p>{i18n.YOU_ARE_IN_A_TABLE_CELL({ row: ariaRowindex, column: index + 2 })}</p> + </EuiScreenReaderOnly> + {Action && ( + <Action + ariaRowindex={ariaRowindex} + width={width} + checked={Object.keys(selectedEventIds).includes(eventId)} + columnId={columnId} + columnValues={columnValues} + eventId={eventId} + data={data} + ecsData={ecsData} + eventIdToNoteIds={eventIdToNoteIds} + index={index} + isEventPinned={isEventPinned} + isEventViewer={isEventViewer} + loadingEventIds={loadingEventIds} + onEventDetailsPanelOpened={onEventDetailsPanelOpened} + onRowSelected={onRowSelected} + rowIndex={rowIndex} + onRuleChange={onRuleChange} + showCheckboxes={showCheckboxes} + showNotes={showNotes} + timelineId={timelineId} + toggleShowNotes={toggleShowNotes} + /> + )} + </> + </EventsTdContent> + {hasRowRenderers ? ( + <EuiScreenReaderOnly data-test-subj="hasRowRendererScreenReaderOnly"> + <p>{i18n.EVENT_HAS_AN_EVENT_RENDERER(ariaRowindex)}</p> + </EuiScreenReaderOnly> + ) : null} + </EventsTd> + </EventsTdGroupActions> + ); +}; + +const TgridTdCell = ({ + _id, + ariaRowindex, + index, + header, + data, + ecsData, + hasRowRenderers, + renderCellValue, + tabType, + timelineId, +}: CellProps) => { + return ( + <EventsTd + $ariaColumnIndex={index + ARIA_COLUMN_INDEX_OFFSET} + key={tabType != null ? `${header.id}_${tabType}` : `${header.id}`} + onKeyDown={onKeyDown} + role="button" + tabIndex={0} + width={header.initialWidth} + > + <EventsTdContent data-test-subj="cell-container"> + <> + <EuiScreenReaderOnly data-test-subj="screenReaderOnly"> + <p>{i18n.YOU_ARE_IN_A_TABLE_CELL({ row: ariaRowindex, column: index + 2 })}</p> + </EuiScreenReaderOnly> + <StatefulCell + ariaRowindex={ariaRowindex} + data={data} + header={header} + eventId={_id} + linkValues={getOr([], header.linkField ?? '', ecsData)} + renderCellValue={renderCellValue} + tabType={tabType} + timelineId={timelineId} + /> + </> + </EventsTdContent> + {hasRowRenderers ? ( + <EuiScreenReaderOnly data-test-subj="hasRowRendererScreenReaderOnly"> + <p>{i18n.EVENT_HAS_AN_EVENT_RENDERER(ariaRowindex)}</p> + </EuiScreenReaderOnly> + ) : null} + </EventsTd> + ); +}; + +export const DataDrivenColumns = React.memo<DataDrivenColumnProps>( + ({ + ariaRowindex, + actionsColumnWidth, + columnHeaders, + columnValues, + data, + ecsData, + isEventViewer, + id: _id, + loadingEventIds, + onEventDetailsPanelOpened, + onRowSelected, + hasRowRenderers, + onRuleChange, + renderCellValue, + selectedEventIds, + showCheckboxes, + tabType, + timelineId, + trailingControlColumns, + leadingControlColumns, + }) => { + const trailingActionCells = useMemo( + () => + trailingControlColumns ? trailingControlColumns.map((column) => column.rowCellRender) : [], + [trailingControlColumns] + ); + const leadingAndDataColumnCount = useMemo( + () => leadingControlColumns.length + columnHeaders.length, + [leadingControlColumns, columnHeaders] + ); + const TrailingActions = useMemo( + () => + trailingActionCells.map((Action: RowCellRender | undefined, index) => { + return ( + Action && ( + <TgridActionTdCell + action={Action} + width={trailingControlColumns[index].width} + actionsColumnWidth={actionsColumnWidth} + ariaRowindex={ariaRowindex} + checked={Object.keys(selectedEventIds).includes(_id)} + columnId={trailingControlColumns[index].id || ''} + columnValues={columnValues} + onRowSelected={onRowSelected} + data-test-subj="actions" + eventId={_id} + data={data} + key={index} + index={leadingAndDataColumnCount + index} + rowIndex={ariaRowindex} + ecsData={ecsData} + loadingEventIds={loadingEventIds} + onEventDetailsPanelOpened={onEventDetailsPanelOpened} + showCheckboxes={showCheckboxes} + isEventViewer={isEventViewer} + hasRowRenderers={hasRowRenderers} + onRuleChange={onRuleChange} + selectedEventIds={selectedEventIds} + tabType={tabType} + timelineId={timelineId} + /> + ) + ); + }), + [ + trailingControlColumns, + _id, + data, + ecsData, + onRowSelected, + isEventViewer, + actionsColumnWidth, + ariaRowindex, + columnValues, + hasRowRenderers, + leadingAndDataColumnCount, + loadingEventIds, + onEventDetailsPanelOpened, + onRuleChange, + selectedEventIds, + showCheckboxes, + tabType, + timelineId, + trailingActionCells, + ] + ); + const ColumnHeaders = useMemo( + () => + columnHeaders.map((header, index) => ( + <TgridTdCell + _id={_id} + index={index} + header={header} + key={tabType != null ? `${header.id}_${tabType}` : `${header.id}`} + ariaRowindex={ariaRowindex} + data={data} + ecsData={ecsData} + hasRowRenderers={hasRowRenderers} + renderCellValue={renderCellValue} + tabType={tabType} + timelineId={timelineId} + /> + )), + [ + _id, + ariaRowindex, + columnHeaders, + data, + ecsData, + hasRowRenderers, + renderCellValue, + tabType, + timelineId, + ] + ); + return ( + <EventsTdGroupData data-test-subj="data-driven-columns"> + {ColumnHeaders} + {TrailingActions} + </EventsTdGroupData> + ); + } +); + +DataDrivenColumns.displayName = 'DataDrivenColumns'; + +export const getMappedNonEcsValue = ({ + data, + fieldName, +}: { + data: TimelineNonEcsData[]; + fieldName: string; +}): string[] | undefined => { + const item = data.find((d) => d.field === fieldName); + if (item != null && item.value != null) { + return item.value; + } + return undefined; +}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/stateful_cell.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/stateful_cell.test.tsx new file mode 100644 index 0000000000000..752e3018fc404 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/stateful_cell.test.tsx @@ -0,0 +1,173 @@ +/* + * 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 { mount } from 'enzyme'; +import { cloneDeep } from 'lodash/fp'; +import React, { useEffect } from 'react'; + +import { StatefulCell } from './stateful_cell'; +import { getMappedNonEcsValue } from '.'; +import { defaultHeaders } from '../../../../mock/header'; +import { + CellValueElementProps, + ColumnHeaderOptions, + TimelineTabs, +} from '../../../../../common/types/timeline'; +import { TimelineNonEcsData } from '../../../../../common/search_strategy'; +import { mockTimelineData } from '../../../../mock/mock_timeline_data'; + +/** + * This (test) component implement's `EuiDataGrid`'s `renderCellValue` interface, + * as documented here: https://elastic.github.io/eui/#/tabular-content/data-grid + * + * Its `CellValueElementProps` props are a superset of `EuiDataGridCellValueElementProps`. + * The `setCellProps` function, defined by the `EuiDataGridCellValueElementProps` interface, + * is typically called in a `useEffect`, as illustrated by `EuiDataGrid`'s code sandbox example: + * https://codesandbox.io/s/zhxmo + */ +const RenderCellValue: React.FC<CellValueElementProps> = ({ columnId, data, setCellProps }) => { + useEffect(() => { + // branching logic that conditionally renders a specific cell green: + if (columnId === defaultHeaders[0].id) { + const value = getMappedNonEcsValue({ + data, + fieldName: columnId, + }); + + if (value?.length) { + setCellProps({ + style: { + backgroundColor: 'green', + }, + }); + } + } + }, [columnId, data, setCellProps]); + + return ( + <div data-test-subj="renderCellValue"> + {getMappedNonEcsValue({ + data, + fieldName: columnId, + })} + </div> + ); +}; + +describe('StatefulCell', () => { + const ariaRowindex = 123; + const eventId = '_id-123'; + const linkValues = ['foo', 'bar', '@baz']; + const tabType = TimelineTabs.query; + const timelineId = 'test'; + + let header: ColumnHeaderOptions; + let data: TimelineNonEcsData[]; + beforeEach(() => { + data = cloneDeep(mockTimelineData[0].data); + header = cloneDeep(defaultHeaders[0]); + }); + + test('it invokes renderCellValue with the expected arguments when tabType is specified', () => { + const renderCellValue = jest.fn(); + + mount( + <StatefulCell + ariaRowindex={ariaRowindex} + data={data} + header={header} + eventId={eventId} + linkValues={linkValues} + renderCellValue={renderCellValue} + tabType={TimelineTabs.query} + timelineId={timelineId} + /> + ); + + expect(renderCellValue).toBeCalledWith( + expect.objectContaining({ + columnId: header.id, + eventId, + data, + header, + isExpandable: true, + isExpanded: false, + isDetails: false, + linkValues, + rowIndex: ariaRowindex - 1, + timelineId: `${timelineId}-${tabType}`, + }) + ); + }); + + test('it invokes renderCellValue with the expected arguments when tabType is NOT specified', () => { + const renderCellValue = jest.fn(); + + mount( + <StatefulCell + ariaRowindex={ariaRowindex} + data={data} + header={header} + eventId={eventId} + linkValues={linkValues} + renderCellValue={renderCellValue} + timelineId={timelineId} + /> + ); + + expect(renderCellValue).toBeCalledWith( + expect.objectContaining({ + columnId: header.id, + eventId, + data, + header, + isExpandable: true, + isExpanded: false, + isDetails: false, + linkValues, + rowIndex: ariaRowindex - 1, + timelineId, + }) + ); + }); + + test('it renders the React.Node returned by renderCellValue', () => { + const renderCellValue = () => <div data-test-subj="renderCellValue" />; + + const wrapper = mount( + <StatefulCell + ariaRowindex={ariaRowindex} + data={data} + header={header} + eventId={eventId} + linkValues={linkValues} + renderCellValue={renderCellValue} + timelineId={timelineId} + /> + ); + + expect(wrapper.find('[data-test-subj="renderCellValue"]').exists()).toBe(true); + }); + + test("it renders a div with the styles set by `renderCellValue`'s `setCellProps` argument", () => { + const wrapper = mount( + <StatefulCell + ariaRowindex={ariaRowindex} + data={data} + header={header} + eventId={eventId} + linkValues={linkValues} + renderCellValue={RenderCellValue} + timelineId={timelineId} + /> + ); + + expect( + wrapper.find('[data-test-subj="statefulCell"]').getDOMNode().getAttribute('style') + ).toEqual('background-color: green;'); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/stateful_cell.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/stateful_cell.tsx new file mode 100644 index 0000000000000..82d872d30c273 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/stateful_cell.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { HTMLAttributes, useState } from 'react'; +import type { TimelineNonEcsData } from '../../../../../common/search_strategy'; + +import { TimelineTabs } from '../../../../../common/types/timeline'; +// eslint-disable-next-line no-duplicate-imports +import type { + CellValueElementProps, + ColumnHeaderOptions, +} from '../../../../../common/types/timeline'; + +export interface CommonProps { + className?: string; + 'aria-label'?: string; + 'data-test-subj'?: string; +} + +const StatefulCellComponent = ({ + ariaRowindex, + data, + header, + eventId, + linkValues, + renderCellValue, + tabType, + timelineId, +}: { + ariaRowindex: number; + data: TimelineNonEcsData[]; + header: ColumnHeaderOptions; + eventId: string; + linkValues: string[] | undefined; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + tabType?: TimelineTabs; + timelineId: string; +}) => { + const [cellProps, setCellProps] = useState<CommonProps & HTMLAttributes<HTMLDivElement>>({}); + return ( + <div data-test-subj="statefulCell" {...cellProps}> + {renderCellValue({ + columnId: header.id, + eventId, + data, + header, + isExpandable: true, + isExpanded: false, + isDetails: false, + linkValues, + rowIndex: ariaRowindex - 1, + setCellProps, + timelineId: tabType != null ? `${timelineId}-${tabType}` : timelineId, + })} + </div> + ); +}; + +StatefulCellComponent.displayName = 'StatefulCellComponent'; + +export const StatefulCell = React.memo(StatefulCellComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/translations.ts new file mode 100644 index 0000000000000..1e5b10bb7cbc2 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/data_driven_columns/translations.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const YOU_ARE_IN_A_TABLE_CELL = ({ column, row }: { column: number; row: number }) => + i18n.translate('xpack.timelines.timeline.youAreInATableCellScreenReaderOnly', { + values: { column, row }, + defaultMessage: 'You are in a table cell. row: {row}, column: {column}', + }); + +export const EVENT_HAS_AN_EVENT_RENDERER = (row: number) => + i18n.translate('xpack.timelines.timeline.eventHasEventRendererScreenReaderOnly', { + values: { row }, + defaultMessage: + 'The event in row {row} has an event renderer. Press shift + down arrow to focus it.', + }); + +export const EVENT_HAS_NOTES = ({ notesCount, row }: { notesCount: number; row: number }) => + i18n.translate('xpack.timelines.timeline.eventHasNotesScreenReaderOnly', { + values: { notesCount, row }, + defaultMessage: + 'The event in row {row} has {notesCount, plural, =1 {a note} other {{notesCount} notes}}. Press shift + right arrow to focus notes.', + }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/events/event_column_view.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/events/event_column_view.test.tsx new file mode 100644 index 0000000000000..23a66c9e18f7d --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/events/event_column_view.test.tsx @@ -0,0 +1,115 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; + +import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants'; + +import { EventColumnView } from './event_column_view'; +import { TestCellRenderer } from '../../../../mock/cell_renderer'; +import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; +import { TestProviders } from '../../../../mock/test_providers'; +import { testLeadingControlColumn } from '../../../../mock/mock_timeline_control_columns'; +import { mockGlobalState } from '../../../../mock/global_state'; + +jest.mock('../../../../hooks/use_selector', () => ({ + useShallowEqualSelector: () => mockGlobalState.timelineById.test, + useDeepEqualSelector: () => mockGlobalState.timelineById.test, +})); + +describe('EventColumnView', () => { + const props = { + ariaRowindex: 2, + id: 'event-id', + actionsColumnWidth: DEFAULT_ACTIONS_COLUMN_WIDTH, + associateNote: jest.fn(), + columnHeaders: [], + columnRenderers: [], + data: [ + { + field: 'host.name', + }, + ], + ecsData: { + _id: 'id', + }, + eventIdToNoteIds: {}, + expanded: false, + hasRowRenderers: false, + loading: false, + loadingEventIds: [], + notesCount: 0, + onEventDetailsPanelOpened: jest.fn(), + onPinEvent: jest.fn(), + onRowSelected: jest.fn(), + onUnPinEvent: jest.fn(), + refetch: jest.fn(), + renderCellValue: TestCellRenderer, + selectedEventIds: {}, + showCheckboxes: false, + showNotes: false, + tabType: TimelineTabs.query, + timelineId: TimelineId.active, + toggleShowNotes: jest.fn(), + updateNote: jest.fn(), + isEventPinned: false, + leadingControlColumns: [], + trailingControlColumns: [], + }; + + // TODO: next 3 tests will be re-enabled in the future. + test.skip('it render AddToCaseAction if timelineId === TimelineId.detectionsPage', () => { + const wrapper = mount(<EventColumnView {...props} timelineId={TimelineId.detectionsPage} />, { + wrappingComponent: TestProviders, + }); + + expect(wrapper.find('[data-test-subj="add-to-case-action"]').exists()).toBeTruthy(); + }); + + test.skip('it render AddToCaseAction if timelineId === TimelineId.detectionsRulesDetailsPage', () => { + const wrapper = mount( + <EventColumnView {...props} timelineId={TimelineId.detectionsRulesDetailsPage} />, + { + wrappingComponent: TestProviders, + } + ); + + expect(wrapper.find('[data-test-subj="add-to-case-action"]').exists()).toBeTruthy(); + }); + + test.skip('it render AddToCaseAction if timelineId === TimelineId.active', () => { + const wrapper = mount(<EventColumnView {...props} timelineId={TimelineId.active} />, { + wrappingComponent: TestProviders, + }); + + expect(wrapper.find('[data-test-subj="add-to-case-action"]').exists()).toBeTruthy(); + }); + + test.skip('it does NOT render AddToCaseAction when timelineId is not in the allowed list', () => { + const wrapper = mount(<EventColumnView {...props} timelineId="timeline-test" />, { + wrappingComponent: TestProviders, + }); + + expect(wrapper.find('[data-test-subj="add-to-case-action"]').exists()).toBeFalsy(); + }); + + test('it renders a custom control column in addition to the default control column', () => { + const wrapper = mount( + <EventColumnView + {...props} + timelineId="timeline-test" + leadingControlColumns={[testLeadingControlColumn]} + />, + { + wrappingComponent: TestProviders, + } + ); + + expect(wrapper.find('[data-test-subj="test-body-control-column-cell"]').exists()).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/events/event_column_view.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/events/event_column_view.tsx new file mode 100644 index 0000000000000..dca3b84eb84b7 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/events/event_column_view.tsx @@ -0,0 +1,182 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; + +import type { OnRowSelected } from '../../types'; +import { EventsTrData, EventsTdGroupActions } from '../../styles'; +import { DataDrivenColumns, getMappedNonEcsValue } from '../data_driven_columns'; +import { TimelineTabs } from '../../../../../common/types/timeline'; +// eslint-disable-next-line no-duplicate-imports +import type { + CellValueElementProps, + ColumnHeaderOptions, + ControlColumnProps, + RowCellRender, +} from '../../../../../common/types/timeline'; +import type { TimelineNonEcsData } from '../../../../../common/search_strategy'; +import type { Ecs } from '../../../../../common/ecs'; + +interface Props { + id: string; + actionsColumnWidth: number; + ariaRowindex: number; + columnHeaders: ColumnHeaderOptions[]; + data: TimelineNonEcsData[]; + ecsData: Ecs; + isEventViewer?: boolean; + loadingEventIds: Readonly<string[]>; + onEventDetailsPanelOpened: () => void; + onRowSelected: OnRowSelected; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + onRuleChange?: () => void; + hasRowRenderers: boolean; + selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>; + showCheckboxes: boolean; + tabType?: TimelineTabs; + timelineId: string; + leadingControlColumns: ControlColumnProps[]; + trailingControlColumns: ControlColumnProps[]; +} + +export const EventColumnView = React.memo<Props>( + ({ + id, + actionsColumnWidth, + ariaRowindex, + columnHeaders, + data, + ecsData, + isEventViewer = false, + loadingEventIds, + onEventDetailsPanelOpened, + onRowSelected, + hasRowRenderers, + onRuleChange, + renderCellValue, + selectedEventIds, + showCheckboxes, + tabType, + timelineId, + leadingControlColumns, + trailingControlColumns, + }) => { + // Each action button shall announce itself to screen readers via an `aria-label` + // in the following format: + // "button description, for the event in row {ariaRowindex}, with columns {columnValues}", + // so we combine the column values here: + const columnValues = useMemo( + () => + columnHeaders + .map( + (header) => + getMappedNonEcsValue({ + data, + fieldName: header.id, + }) ?? [] + ) + .join(' '), + [columnHeaders, data] + ); + + const leadingActionCells = useMemo( + () => + leadingControlColumns ? leadingControlColumns.map((column) => column.rowCellRender) : [], + [leadingControlColumns] + ); + const LeadingActions = useMemo( + () => + leadingActionCells.map((Action: RowCellRender | undefined, index) => { + const width = leadingControlColumns[index].width + ? leadingControlColumns[index].width + : actionsColumnWidth; + return ( + <EventsTdGroupActions + width={width} + data-test-subj="event-actions-container" + tabIndex={0} + key={index} + > + {Action && ( + <Action + width={width} + rowIndex={ariaRowindex} + ariaRowindex={ariaRowindex} + checked={Object.keys(selectedEventIds).includes(id)} + columnId={leadingControlColumns[index].id || ''} + columnValues={columnValues} + onRowSelected={onRowSelected} + data-test-subj="actions" + eventId={id} + data={data} + index={index} + ecsData={ecsData} + loadingEventIds={loadingEventIds} + onEventDetailsPanelOpened={onEventDetailsPanelOpened} + showCheckboxes={showCheckboxes} + isEventViewer={isEventViewer} + onRuleChange={onRuleChange} + tabType={tabType} + timelineId={timelineId} + /> + )} + </EventsTdGroupActions> + ); + }), + [ + actionsColumnWidth, + ariaRowindex, + columnValues, + data, + ecsData, + id, + isEventViewer, + leadingActionCells, + leadingControlColumns, + loadingEventIds, + onEventDetailsPanelOpened, + onRowSelected, + onRuleChange, + selectedEventIds, + showCheckboxes, + tabType, + timelineId, + ] + ); + return ( + <EventsTrData data-test-subj="event-column-view"> + {LeadingActions} + <DataDrivenColumns + id={id} + actionsColumnWidth={actionsColumnWidth} + ariaRowindex={ariaRowindex} + columnHeaders={columnHeaders} + data={data} + ecsData={ecsData} + hasRowRenderers={hasRowRenderers} + renderCellValue={renderCellValue} + tabType={tabType} + timelineId={timelineId} + trailingControlColumns={trailingControlColumns} + leadingControlColumns={leadingControlColumns} + checked={Object.keys(selectedEventIds).includes(id)} + columnValues={columnValues} + onRowSelected={onRowSelected} + data-test-subj="actions" + loadingEventIds={loadingEventIds} + onEventDetailsPanelOpened={onEventDetailsPanelOpened} + showCheckboxes={showCheckboxes} + isEventViewer={isEventViewer} + onRuleChange={onRuleChange} + selectedEventIds={selectedEventIds} + /> + </EventsTrData> + ); + } +); + +EventColumnView.displayName = 'EventColumnView'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/events/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/events/index.tsx new file mode 100644 index 0000000000000..8036fdd8f858f --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/events/index.tsx @@ -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 React from 'react'; +import { isEmpty } from 'lodash'; + +import { EventsTbody } from '../../styles'; +import { StatefulEvent } from './stateful_event'; +import type { BrowserFields } from '../../../../../common/search_strategy/index_fields'; +import { TimelineTabs } from '../../../../../common/types/timeline'; +// eslint-disable-next-line no-duplicate-imports +import type { + CellValueElementProps, + ColumnHeaderOptions, + ControlColumnProps, + OnRowSelected, + RowRenderer, +} from '../../../../../common/types/timeline'; + +import { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy'; + +/** This offset begins at two, because the header row counts as "row 1", and aria-rowindex starts at "1" */ +const ARIA_ROW_INDEX_OFFSET = 2; + +interface Props { + actionsColumnWidth: number; + browserFields: BrowserFields; + columnHeaders: ColumnHeaderOptions[]; + containerRef: React.MutableRefObject<HTMLDivElement | null>; + data: TimelineItem[]; + id: string; + isEventViewer?: boolean; + lastFocusedAriaColindex: number; + loadingEventIds: Readonly<string[]>; + onRowSelected: OnRowSelected; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + onRuleChange?: () => void; + rowRenderers: RowRenderer[]; + selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>; + showCheckboxes: boolean; + tabType?: TimelineTabs; + leadingControlColumns: ControlColumnProps[]; + trailingControlColumns: ControlColumnProps[]; +} + +const EventsComponent: React.FC<Props> = ({ + actionsColumnWidth, + browserFields, + columnHeaders, + containerRef, + data, + id, + isEventViewer = false, + lastFocusedAriaColindex, + loadingEventIds, + onRowSelected, + onRuleChange, + renderCellValue, + rowRenderers, + selectedEventIds, + showCheckboxes, + tabType, + leadingControlColumns, + trailingControlColumns, +}) => ( + <EventsTbody data-test-subj="events"> + {data.map((event, i) => ( + <StatefulEvent + actionsColumnWidth={actionsColumnWidth} + ariaRowindex={i + ARIA_ROW_INDEX_OFFSET} + browserFields={browserFields} + columnHeaders={columnHeaders} + containerRef={containerRef} + event={event} + isEventViewer={isEventViewer} + key={`${id}_${tabType}_${event._id}_${event._index}_${ + !isEmpty(event.ecs.eql?.sequenceNumber) ? event.ecs.eql?.sequenceNumber : '' + }`} + lastFocusedAriaColindex={lastFocusedAriaColindex} + loadingEventIds={loadingEventIds} + onRowSelected={onRowSelected} + renderCellValue={renderCellValue} + rowRenderers={rowRenderers} + onRuleChange={onRuleChange} + selectedEventIds={selectedEventIds} + showCheckboxes={showCheckboxes} + tabType={tabType} + timelineId={id} + leadingControlColumns={leadingControlColumns} + trailingControlColumns={trailingControlColumns} + /> + ))} + </EventsTbody> +); + +export const Events = React.memo(EventsComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_event.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_event.tsx new file mode 100644 index 0000000000000..4eaa22ce5e2a9 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_event.tsx @@ -0,0 +1,207 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import { useDispatch } from 'react-redux'; + +import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../../helpers'; +import { EventsTrGroup, EventsTrSupplement } from '../../styles'; +import type { OnRowSelected } from '../../types'; +import { isEventBuildingBlockType, getEventType, isEvenEqlSequence } from '../helpers'; +import { EventColumnView } from './event_column_view'; +import { getRowRenderer } from '../renderers/get_row_renderer'; +import { StatefulRowRenderer } from './stateful_row_renderer'; +import { getMappedNonEcsValue } from '../data_driven_columns'; +import { StatefulEventContext } from './stateful_event_context'; +import type { BrowserFields } from '../../../../../common/search_strategy/index_fields'; +import { TimelineTabs } from '../../../../../common/types/timeline'; +// eslint-disable-next-line no-duplicate-imports +import type { + CellValueElementProps, + ColumnHeaderOptions, + ControlColumnProps, + RowRenderer, + TimelineExpandedDetailType, +} from '../../../../../common/types/timeline'; + +import type { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy'; +import { tGridActions, tGridSelectors } from '../../../../store/t_grid'; +import { useDeepEqualSelector } from '../../../../hooks/use_selector'; + +interface Props { + actionsColumnWidth: number; + containerRef: React.MutableRefObject<HTMLDivElement | null>; + browserFields: BrowserFields; + columnHeaders: ColumnHeaderOptions[]; + event: TimelineItem; + isEventViewer?: boolean; + lastFocusedAriaColindex: number; + loadingEventIds: Readonly<string[]>; + onRowSelected: OnRowSelected; + ariaRowindex: number; + onRuleChange?: () => void; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; + selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>; + showCheckboxes: boolean; + tabType?: TimelineTabs; + timelineId: string; + leadingControlColumns: ControlColumnProps[]; + trailingControlColumns: ControlColumnProps[]; +} + +const StatefulEventComponent: React.FC<Props> = ({ + actionsColumnWidth, + browserFields, + containerRef, + columnHeaders, + event, + isEventViewer = false, + lastFocusedAriaColindex, + loadingEventIds, + onRowSelected, + renderCellValue, + rowRenderers, + onRuleChange, + ariaRowindex, + selectedEventIds, + showCheckboxes, + tabType, + timelineId, + leadingControlColumns, + trailingControlColumns, +}) => { + const trGroupRef = useRef<HTMLDivElement | null>(null); + const dispatch = useDispatch(); + // Store context in state rather than creating object in provider value={} to prevent re-renders caused by a new object being created + const [activeStatefulEventContext] = useState({ timelineID: timelineId, tabType }); + const getTGrid = useMemo(() => tGridSelectors.getTGridByIdSelector(), []); + const expandedDetail = useDeepEqualSelector( + (state) => getTGrid(state, timelineId).expandedDetail ?? {} + ); + const hostName = useMemo(() => { + const hostNameArr = getMappedNonEcsValue({ data: event?.data, fieldName: 'host.name' }); + return hostNameArr && hostNameArr.length > 0 ? hostNameArr[0] : null; + }, [event?.data]); + + const hostIPAddresses = useMemo(() => { + const hostIpList = getMappedNonEcsValue({ data: event?.data, fieldName: 'host.ip' }) ?? []; + const sourceIpList = getMappedNonEcsValue({ data: event?.data, fieldName: 'source.ip' }) ?? []; + const destinationIpList = + getMappedNonEcsValue({ + data: event?.data, + fieldName: 'destination.ip', + }) ?? []; + return new Set([...hostIpList, ...sourceIpList, ...destinationIpList]); + }, [event?.data]); + + const activeTab = tabType ?? TimelineTabs.query; + const activeExpandedDetail = expandedDetail[activeTab]; + + const isDetailPanelExpanded: boolean = + (activeExpandedDetail?.panelView === 'eventDetail' && + activeExpandedDetail?.params?.eventId === event._id) || + (activeExpandedDetail?.panelView === 'hostDetail' && + activeExpandedDetail?.params?.hostName === hostName) || + (activeExpandedDetail?.panelView === 'networkDetail' && + activeExpandedDetail?.params?.ip && + hostIPAddresses?.has(activeExpandedDetail?.params?.ip)) || + false; + + const hasRowRenderers: boolean = useMemo(() => getRowRenderer(event.ecs, rowRenderers) != null, [ + event.ecs, + rowRenderers, + ]); + + const handleOnEventDetailPanelOpened = useCallback(() => { + const eventId = event._id; + const indexName = event._index!; + + const updatedExpandedDetail: TimelineExpandedDetailType = { + panelView: 'eventDetail', + params: { + eventId, + indexName, + }, + }; + + dispatch( + tGridActions.toggleDetailPanel({ + ...updatedExpandedDetail, + tabType, + timelineId, + }) + ); + }, [dispatch, event._id, event._index, tabType, timelineId]); + + const RowRendererContent = useMemo( + () => ( + <EventsTrSupplement> + <StatefulRowRenderer + ariaRowindex={ariaRowindex} + browserFields={browserFields} + containerRef={containerRef} + event={event} + lastFocusedAriaColindex={lastFocusedAriaColindex} + rowRenderers={rowRenderers} + timelineId={timelineId} + /> + </EventsTrSupplement> + ), + [ + ariaRowindex, + browserFields, + containerRef, + event, + lastFocusedAriaColindex, + rowRenderers, + timelineId, + ] + ); + + return ( + <StatefulEventContext.Provider value={activeStatefulEventContext}> + <EventsTrGroup + $ariaRowindex={ariaRowindex} + className={STATEFUL_EVENT_CSS_CLASS_NAME} + data-test-subj="event" + eventType={getEventType(event.ecs)} + isBuildingBlockType={isEventBuildingBlockType(event.ecs)} + isEvenEqlSequence={isEvenEqlSequence(event.ecs)} + isExpanded={isDetailPanelExpanded} + ref={trGroupRef} + showLeftBorder={!isEventViewer} + > + <EventColumnView + id={event._id} + actionsColumnWidth={actionsColumnWidth} + ariaRowindex={ariaRowindex} + columnHeaders={columnHeaders} + data={event.data} + ecsData={event.ecs} + hasRowRenderers={hasRowRenderers} + isEventViewer={isEventViewer} + loadingEventIds={loadingEventIds} + onEventDetailsPanelOpened={handleOnEventDetailPanelOpened} + onRowSelected={onRowSelected} + renderCellValue={renderCellValue} + onRuleChange={onRuleChange} + selectedEventIds={selectedEventIds} + showCheckboxes={showCheckboxes} + tabType={tabType} + timelineId={timelineId} + leadingControlColumns={leadingControlColumns} + trailingControlColumns={trailingControlColumns} + /> + + <div>{RowRendererContent}</div> + </EventsTrGroup> + </StatefulEventContext.Provider> + ); +}; + +export const StatefulEvent = React.memo(StatefulEventComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_event_context.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_event_context.tsx new file mode 100644 index 0000000000000..a2ad0b55f5cbc --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_event_context.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { TimelineTabs } from '../../../../../common/types/timeline'; + +interface StatefulEventContext { + tabType: TimelineTabs | undefined; + timelineID: string; +} + +// This context is available to all children of the stateful_event component where the provider is currently set +export const StatefulEventContext = React.createContext<StatefulEventContext | null>(null); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_row_renderer/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_row_renderer/index.tsx new file mode 100644 index 0000000000000..65762b93cd43f --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_row_renderer/index.tsx @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { noop } from 'lodash/fp'; +import { EuiFocusTrap, EuiOutsideClickDetector, EuiScreenReaderOnly } from '@elastic/eui'; +import React, { useMemo } from 'react'; + +import { + ARIA_COLINDEX_ATTRIBUTE, + ARIA_ROWINDEX_ATTRIBUTE, + getRowRendererClassName, +} from '../../../../../../common'; +import { useStatefulEventFocus } from '../use_stateful_event_focus'; + +import * as i18n from '../translations'; +import type { BrowserFields } from '../../../../../../common/search_strategy/index_fields'; +import type { TimelineItem } from '../../../../../../common/search_strategy'; +import type { RowRenderer } from '../../../../../../common/types/timeline'; +import { getRowRenderer } from '../../renderers/get_row_renderer'; + +/** + * This component addresses the accessibility of row renderers. + * + * accessibility details: + * - This component has a 'dialog' `role` because it's rendered as a dialog + * "outside" the current row for screen readers, similar to a popover + * - It has tabIndex="0" to allow for keyboard focus + * - It traps keyboard focus when a user clicks inside a row renderer, to + * allow for tabbing through the contents of row renderers + * - The "dialog" can be dismissed via the up arrow key, down arrow key, + * which focuses the current or next row, respectively. + * - A screen-reader-only message provides additional context and instruction + */ +export const StatefulRowRenderer = ({ + ariaRowindex, + browserFields, + containerRef, + event, + lastFocusedAriaColindex, + rowRenderers, + timelineId, +}: { + ariaRowindex: number; + browserFields: BrowserFields; + containerRef: React.MutableRefObject<HTMLDivElement | null>; + event: TimelineItem; + lastFocusedAriaColindex: number; + rowRenderers: RowRenderer[]; + timelineId: string; +}) => { + const { focusOwnership, onFocus, onKeyDown, onOutsideClick } = useStatefulEventFocus({ + ariaRowindex, + colindexAttribute: ARIA_COLINDEX_ATTRIBUTE, + containerRef, + lastFocusedAriaColindex, + onColumnFocused: noop, + rowindexAttribute: ARIA_ROWINDEX_ATTRIBUTE, + }); + + const rowRenderer = useMemo(() => getRowRenderer(event.ecs, rowRenderers), [ + event.ecs, + rowRenderers, + ]); + + const content = useMemo( + () => + rowRenderer && ( + // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions + <div className={getRowRendererClassName(ariaRowindex)} role="dialog" onFocus={onFocus}> + <EuiOutsideClickDetector onOutsideClick={onOutsideClick}> + <EuiFocusTrap clickOutsideDisables={true} disabled={focusOwnership !== 'owned'}> + <EuiScreenReaderOnly data-test-subj="eventRendererScreenReaderOnly"> + <p>{i18n.YOU_ARE_IN_AN_EVENT_RENDERER(ariaRowindex)}</p> + </EuiScreenReaderOnly> + <div onKeyDown={onKeyDown}> + {rowRenderer.renderRow({ + browserFields, + data: event.ecs, + timelineId, + })} + </div> + </EuiFocusTrap> + </EuiOutsideClickDetector> + </div> + ), + [ + ariaRowindex, + browserFields, + event.ecs, + focusOwnership, + onFocus, + onKeyDown, + onOutsideClick, + rowRenderer, + timelineId, + ] + ); + + return content; +}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/events/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/body/events/translations.ts new file mode 100644 index 0000000000000..9d1071a80071b --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/events/translations.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. + */ + +import { i18n } from '@kbn/i18n'; + +export const YOU_ARE_IN_AN_EVENT_RENDERER = (row: number) => + i18n.translate('xpack.timelines.timeline.youAreInAnEventRendererScreenReaderOnly', { + values: { row }, + defaultMessage: + 'You are in an event renderer for row: {row}. Press the up arrow key to exit and return to the current row, or the down arrow key to exit and advance to the next row.', + }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/events/use_stateful_event_focus/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/events/use_stateful_event_focus/index.tsx new file mode 100644 index 0000000000000..27d6ba846eb98 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/events/use_stateful_event_focus/index.tsx @@ -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 React, { useCallback, useState, useMemo } from 'react'; +import { focusColumn, isArrowDownOrArrowUp, isArrowUp, isEscape } from '../../../../../../common'; +// eslint-disable-next-line no-duplicate-imports +import type { OnColumnFocused } from '../../../../../../common'; + +type FocusOwnership = 'not-owned' | 'owned'; + +export const getSameOrNextAriaRowindex = ({ + ariaRowindex, + event, +}: { + ariaRowindex: number; + event: React.KeyboardEvent<HTMLDivElement>; +}): number => (isArrowUp(event) ? ariaRowindex : ariaRowindex + 1); + +export const useStatefulEventFocus = ({ + ariaRowindex, + colindexAttribute, + containerRef, + lastFocusedAriaColindex, + onColumnFocused, + rowindexAttribute, +}: { + ariaRowindex: number; + colindexAttribute: string; + containerRef: React.MutableRefObject<HTMLDivElement | null>; + lastFocusedAriaColindex: number; + onColumnFocused: OnColumnFocused; + rowindexAttribute: string; +}) => { + const [focusOwnership, setFocusOwnership] = useState<FocusOwnership>('not-owned'); + + const onFocus = useCallback(() => { + setFocusOwnership((prevFocusOwnership) => { + if (prevFocusOwnership !== 'owned') { + return 'owned'; + } + return prevFocusOwnership; + }); + }, []); + + const onOutsideClick = useCallback(() => { + setFocusOwnership('not-owned'); + }, []); + + const onKeyDown = useCallback( + (e: React.KeyboardEvent<HTMLDivElement>) => { + if (isArrowDownOrArrowUp(e) || isEscape(e)) { + e.preventDefault(); + e.stopPropagation(); + + setFocusOwnership('not-owned'); + + const newAriaRowindex = isEscape(e) + ? ariaRowindex // return focus to the same row + : getSameOrNextAriaRowindex({ ariaRowindex, event: e }); + + setTimeout(() => { + onColumnFocused( + focusColumn({ + ariaColindex: lastFocusedAriaColindex, + ariaRowindex: newAriaRowindex, + colindexAttribute, + containerElement: containerRef.current, + rowindexAttribute, + }) + ); + }, 0); + } + }, + [ + ariaRowindex, + colindexAttribute, + containerRef, + lastFocusedAriaColindex, + onColumnFocused, + rowindexAttribute, + ] + ); + + const memoizedReturn = useMemo(() => ({ focusOwnership, onFocus, onOutsideClick, onKeyDown }), [ + focusOwnership, + onFocus, + onKeyDown, + onOutsideClick, + ]); + + return memoizedReturn; +}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.ts b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.ts new file mode 100644 index 0000000000000..ffdf91425c4f7 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.ts @@ -0,0 +1,178 @@ +/* + * 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 { Ecs } from '../../../../common/ecs'; +import { stringifyEvent } from './helpers'; + +describe('helpers', () => { + describe('stringifyEvent', () => { + test('it omits __typename when it appears at arbitrary levels', () => { + const toStringify: Ecs = { + __typename: 'level 0', + _id: '4', + timestamp: '2018-11-08T19:03:25.937Z', + host: { + __typename: 'level 1', + name: ['suricata'], + ip: ['192.168.0.1'], + }, + event: { + id: ['4'], + category: ['Attempted Administrator Privilege Gain'], + type: ['Alert'], + module: ['suricata'], + severity: [1], + }, + source: { + ip: ['192.168.0.3'], + port: [53], + }, + destination: { + ip: ['192.168.0.3'], + port: [6343], + }, + suricata: { + eve: { + flow_id: [4], + proto: [''], + alert: { + signature: ['ET PHONE HOME Stack Overflow (CVE-2019-90210)'], + signature_id: [4], + __typename: 'level 2', + }, + }, + }, + user: { + id: ['4'], + name: ['jack.black'], + }, + geo: { + region_name: ['neither'], + country_iso_code: ['sasquatch'], + }, + } as Ecs; // as cast so that `__typename` can be added for the tests even though it is not part of ECS + const expected: Ecs = { + _id: '4', + timestamp: '2018-11-08T19:03:25.937Z', + host: { + name: ['suricata'], + ip: ['192.168.0.1'], + }, + event: { + id: ['4'], + category: ['Attempted Administrator Privilege Gain'], + type: ['Alert'], + module: ['suricata'], + severity: [1], + }, + source: { + ip: ['192.168.0.3'], + port: [53], + }, + destination: { + ip: ['192.168.0.3'], + port: [6343], + }, + suricata: { + eve: { + flow_id: [4], + proto: [''], + alert: { + signature: ['ET PHONE HOME Stack Overflow (CVE-2019-90210)'], + signature_id: [4], + }, + }, + }, + user: { + id: ['4'], + name: ['jack.black'], + }, + geo: { + region_name: ['neither'], + country_iso_code: ['sasquatch'], + }, + }; + expect(JSON.parse(stringifyEvent(toStringify))).toEqual(expected); + }); + + test('it omits null and undefined values at arbitrary levels, for arbitrary data types', () => { + const expected: Ecs = { + _id: '4', + host: {}, + event: { + id: ['4'], + category: ['theory'], + type: ['Alert'], + module: ['me'], + severity: [1], + }, + source: { + port: [53], + }, + destination: { + ip: ['192.168.0.3'], + port: [6343], + }, + suricata: { + eve: { + flow_id: [4], + proto: [''], + alert: { + signature: ['dance moves'], + }, + }, + }, + user: { + id: ['4'], + name: ['no use for a'], + }, + geo: { + region_name: ['bizzaro'], + country_iso_code: ['world'], + }, + }; + const toStringify: Ecs = { + _id: '4', + host: {}, + event: { + id: ['4'], + category: ['theory'], + type: ['Alert'], + module: ['me'], + severity: [1], + }, + source: { + ip: undefined, + port: [53], + }, + destination: { + ip: ['192.168.0.3'], + port: [6343], + }, + suricata: { + eve: { + flow_id: [4], + proto: [''], + alert: { + signature: ['dance moves'], + signature_id: undefined, + }, + }, + }, + user: { + id: ['4'], + name: ['no use for a'], + }, + geo: { + region_name: ['bizzaro'], + country_iso_code: ['world'], + }, + }; + expect(JSON.parse(stringifyEvent(toStringify))).toEqual(expected); + }); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx new file mode 100644 index 0000000000000..85edefc0c0fa6 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEmpty } from 'lodash/fp'; + +import type { Ecs } from '../../../../common/ecs'; +import type { TimelineItem, TimelineNonEcsData } from '../../../../common/search_strategy'; +import type { TimelineEventsType } from '../../../../common/types/timeline'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const omitTypenameAndEmpty = (k: string, v: any): any | undefined => + k !== '__typename' && v != null ? v : undefined; + +export const stringifyEvent = (ecs: Ecs): string => JSON.stringify(ecs, omitTypenameAndEmpty, 2); + +/** + * Creates mapping of eventID -> fieldData for given fieldsToKeep. Used to store additional field + * data necessary for custom timeline actions in conjunction with selection state + * @param timelineData + * @param eventIds + * @param fieldsToKeep + */ +export const getEventIdToDataMapping = ( + timelineData: TimelineItem[], + eventIds: string[], + fieldsToKeep: string[] +): Record<string, TimelineNonEcsData[]> => + timelineData.reduce((acc, v) => { + const fvm = eventIds.includes(v._id) + ? { [v._id]: v.data.filter((ti) => fieldsToKeep.includes(ti.field)) } + : {}; + return { + ...acc, + ...fvm, + }; + }, {}); + +export const isEventBuildingBlockType = (event: Ecs): boolean => + !isEmpty(event.signal?.rule?.building_block_type); + +export const isEvenEqlSequence = (event: Ecs): boolean => { + if (!isEmpty(event.eql?.sequenceNumber)) { + try { + const sequenceNumber = (event.eql?.sequenceNumber ?? '').split('-')[0]; + return parseInt(sequenceNumber, 10) % 2 === 0; + } catch { + return false; + } + } + return false; +}; +/** Return eventType raw or signal or eql */ +export const getEventType = (event: Ecs): Omit<TimelineEventsType, 'all'> => { + if (!isEmpty(event.signal?.rule?.id)) { + return 'signal'; + } else if (!isEmpty(event.eql?.parentId)) { + return 'eql'; + } + return 'raw'; +}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx new file mode 100644 index 0000000000000..b8533f33a82e9 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx @@ -0,0 +1,132 @@ +/* + * 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 { BodyComponent, StatefulBodyProps } from '.'; +import { Sort } from './sort'; +import { Direction } from '../../../../common/search_strategy'; +import { useMountAppended } from '../../utils/use_mount_appended'; +import { defaultHeaders, mockBrowserFields, mockTimelineData, TestProviders } from '../../../mock'; +import { TimelineTabs } from '../../../../common/types/timeline'; +import { TestCellRenderer } from '../../../mock/cell_renderer'; +import { mockGlobalState } from '../../../mock/global_state'; + +const mockSort: Sort[] = [ + { + columnId: '@timestamp', + columnType: 'number', + sortDirection: Direction.desc, + }, +]; + +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); + +jest.mock('../../../hooks/use_selector', () => ({ + useShallowEqualSelector: () => mockGlobalState.timelineById.test, + useDeepEqualSelector: () => mockGlobalState.timelineById.test, +})); + +jest.mock( + 'react-visibility-sensor', + () => ({ children }: { children: (args: { isVisible: boolean }) => React.ReactNode }) => + children({ isVisible: true }) +); + +window.matchMedia = jest.fn().mockImplementation((query) => { + return { + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + }; +}); + +describe('Body', () => { + const mount = useMountAppended(); + const props: StatefulBodyProps = { + activePage: 0, + browserFields: mockBrowserFields, + clearSelected: (jest.fn() as unknown) as StatefulBodyProps['clearSelected'], + columnHeaders: defaultHeaders, + data: mockTimelineData, + excludedRowRendererIds: [], + id: 'timeline-test', + isSelectAllChecked: false, + loadingEventIds: [], + renderCellValue: TestCellRenderer, + rowRenderers: [], + selectedEventIds: {}, + setSelected: (jest.fn() as unknown) as StatefulBodyProps['setSelected'], + sort: mockSort, + showCheckboxes: false, + tabType: TimelineTabs.query, + totalPages: 1, + leadingControlColumns: [], + trailingControlColumns: [], + }; + + describe('rendering', () => { + test('it renders the column headers', () => { + const wrapper = mount( + <TestProviders> + <BodyComponent {...props} /> + </TestProviders> + ); + + expect(wrapper.find('[data-test-subj="column-headers"]').first().exists()).toEqual(true); + }); + + test('it renders the scroll container', () => { + const wrapper = mount( + <TestProviders> + <BodyComponent {...props} /> + </TestProviders> + ); + + expect(wrapper.find('[data-test-subj="timeline-body"]').first().exists()).toEqual(true); + }); + + test('it renders events', () => { + const wrapper = mount( + <TestProviders> + <BodyComponent {...props} /> + </TestProviders> + ); + + expect(wrapper.find('[data-test-subj="events"]').first().exists()).toEqual(true); + }); + + test('it renders a tooltip for timestamp', () => { + const headersJustTimestamp = defaultHeaders.filter((h) => h.id === '@timestamp'); + const testProps = { ...props, columnHeaders: headersJustTimestamp }; + const wrapper = mount( + <TestProviders> + <BodyComponent {...testProps} /> + </TestProviders> + ); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="data-driven-columns"]') + .first() + .find('[data-test-subj="statefulCell"]') + .last() + .text() + ).toEqual(mockTimelineData[0].ecs.timestamp); + }); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx new file mode 100644 index 0000000000000..51227c0e811f2 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx @@ -0,0 +1,334 @@ +/* + * 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 { noop } from 'lodash/fp'; +import memoizeOne from 'memoize-one'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; + +import { + ARIA_COLINDEX_ATTRIBUTE, + ARIA_ROWINDEX_ATTRIBUTE, + FIRST_ARIA_INDEX, + onKeyDownFocusHandler, +} from '../../../../common'; +import { DEFAULT_COLUMN_MIN_WIDTH } from './constants'; +import { RowRendererId, TimelineId, TimelineTabs } from '../../../../common/types/timeline'; +// eslint-disable-next-line no-duplicate-imports +import type { + CellValueElementProps, + ColumnHeaderOptions, + ControlColumnProps, + RowRenderer, +} from '../../../../common/types/timeline'; +import type { TimelineItem } from '../../../../common/search_strategy/timeline'; + +import { getActionsColumnWidth, getColumnHeaders } from './column_headers/helpers'; +import { getEventIdToDataMapping } from './helpers'; +import { Sort } from './sort'; + +import { EventsTable, TimelineBody, TimelineBodyGlobalStyle } from '../styles'; +import { ColumnHeaders } from './column_headers'; +import { Events } from './events'; +import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers'; +import { BrowserFields } from '../../../../common/search_strategy/index_fields'; +import { OnRowSelected, OnSelectAll } from '../types'; +import { tGridActions } from '../../../'; +import { TGridModel, tGridSelectors, TimelineState } from '../../../store/t_grid'; +import { useDeepEqualSelector } from '../../../hooks/use_selector'; +import { plainRowRenderer } from './renderers/plain_row_renderer'; + +interface OwnProps { + activePage: number; + browserFields: BrowserFields; + data: TimelineItem[]; + id: string; + isEventViewer?: boolean; + sort: Sort[]; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; + leadingControlColumns: ControlColumnProps[]; + trailingControlColumns: ControlColumnProps[]; + tabType: TimelineTabs; + totalPages: number; + onRuleChange?: () => void; +} + +const NUM_OF_ICON_IN_TIMELINE_ROW = 2; + +export const hasAdditionalActions = (id: TimelineId): boolean => + [TimelineId.detectionsPage, TimelineId.detectionsRulesDetailsPage, TimelineId.active].includes( + id + ); + +const EXTRA_WIDTH = 4; // px + +export type StatefulBodyProps = OwnProps & PropsFromRedux; + +/** + * The Body component is used everywhere timeline is used within the security application. It is the highest level component + * that is shared across all implementations of the timeline. + */ +export const BodyComponent = React.memo<StatefulBodyProps>( + ({ + activePage, + browserFields, + columnHeaders, + data, + excludedRowRendererIds, + id, + isEventViewer = false, + isSelectAllChecked, + loadingEventIds, + selectedEventIds, + setSelected, + clearSelected, + onRuleChange, + showCheckboxes, + renderCellValue, + rowRenderers, + sort, + tabType, + totalPages, + leadingControlColumns = [], + trailingControlColumns = [], + }) => { + const containerRef = useRef<HTMLDivElement | null>(null); + const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []); + const { queryFields, selectAll } = useDeepEqualSelector((state) => + getManageTimeline(state, id) + ); + + const onRowSelected: OnRowSelected = useCallback( + ({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => { + setSelected!({ + id, + eventIds: getEventIdToDataMapping(data, eventIds, queryFields), + isSelected, + isSelectAllChecked: + isSelected && Object.keys(selectedEventIds).length + 1 === data.length, + }); + }, + [setSelected, id, data, selectedEventIds, queryFields] + ); + + const onSelectAll: OnSelectAll = useCallback( + ({ isSelected }: { isSelected: boolean }) => + isSelected + ? setSelected!({ + id, + eventIds: getEventIdToDataMapping( + data, + data.map((event) => event._id), + queryFields + ), + isSelected, + isSelectAllChecked: isSelected, + }) + : clearSelected!({ id }), + [setSelected, clearSelected, id, data, queryFields] + ); + + // Sync to selectAll so parent components can select all events + useEffect(() => { + if (selectAll && !isSelectAllChecked) { + onSelectAll({ isSelected: true }); + } + }, [isSelectAllChecked, onSelectAll, selectAll]); + + const enabledRowRenderers = useMemo(() => { + if ( + excludedRowRendererIds && + excludedRowRendererIds.length === Object.keys(RowRendererId).length + ) + return [plainRowRenderer]; + + if (!excludedRowRendererIds) return rowRenderers; + + return rowRenderers.filter((rowRenderer) => !excludedRowRendererIds.includes(rowRenderer.id)); + }, [excludedRowRendererIds, rowRenderers]); + + const actionsColumnWidth = useMemo( + () => + getActionsColumnWidth( + isEventViewer, + showCheckboxes, + hasAdditionalActions(id as TimelineId) + ? DEFAULT_ICON_BUTTON_WIDTH * NUM_OF_ICON_IN_TIMELINE_ROW + EXTRA_WIDTH + : 0 + ), + [isEventViewer, showCheckboxes, id] + ); + + const columnWidths = useMemo( + () => + columnHeaders.reduce( + (totalWidth, header) => totalWidth + (header.initialWidth ?? DEFAULT_COLUMN_MIN_WIDTH), + 0 + ), + [columnHeaders] + ); + + const leadingActionColumnsWidth = useMemo(() => { + return leadingControlColumns + ? leadingControlColumns.reduce( + (totalWidth, header) => + header.width ? totalWidth + header.width : totalWidth + actionsColumnWidth, + 0 + ) + : 0; + }, [actionsColumnWidth, leadingControlColumns]); + + const trailingActionColumnsWidth = useMemo(() => { + return trailingControlColumns + ? trailingControlColumns.reduce( + (totalWidth, header) => + header.width ? totalWidth + header.width : totalWidth + actionsColumnWidth, + 0 + ) + : 0; + }, [actionsColumnWidth, trailingControlColumns]); + + const totalWidth = useMemo(() => { + return columnWidths + leadingActionColumnsWidth + trailingActionColumnsWidth; + }, [columnWidths, leadingActionColumnsWidth, trailingActionColumnsWidth]); + + const [lastFocusedAriaColindex] = useState(FIRST_ARIA_INDEX); + + const columnCount = useMemo(() => { + return columnHeaders.length + trailingControlColumns.length + leadingControlColumns.length; + }, [columnHeaders, trailingControlColumns, leadingControlColumns]); + + const onKeyDown = useCallback( + (e: React.KeyboardEvent) => { + onKeyDownFocusHandler({ + colindexAttribute: ARIA_COLINDEX_ATTRIBUTE, + containerElement: containerRef.current, + event: e, + maxAriaColindex: columnHeaders.length + 1, + maxAriaRowindex: data.length + 1, + onColumnFocused: noop, + rowindexAttribute: ARIA_ROWINDEX_ATTRIBUTE, + }); + }, + [columnHeaders.length, containerRef, data.length] + ); + return ( + <> + <TimelineBody data-test-subj="timeline-body" ref={containerRef}> + <EventsTable + $activePage={activePage} + $columnCount={columnCount} + data-test-subj={`${tabType}-events-table`} + columnWidths={totalWidth} + onKeyDown={onKeyDown} + $rowCount={data.length} + $totalPages={totalPages} + > + <ColumnHeaders + actionsColumnWidth={actionsColumnWidth} + browserFields={browserFields} + columnHeaders={columnHeaders} + isEventViewer={isEventViewer} + isSelectAllChecked={isSelectAllChecked} + onSelectAll={onSelectAll} + showEventsSelect={false} + showSelectAllCheckbox={showCheckboxes} + sort={sort} + tabType={tabType} + timelineId={id} + leadingControlColumns={leadingControlColumns} + trailingControlColumns={trailingControlColumns} + /> + + <Events + containerRef={containerRef} + actionsColumnWidth={actionsColumnWidth} + browserFields={browserFields} + columnHeaders={columnHeaders} + data={data} + id={id} + isEventViewer={isEventViewer} + lastFocusedAriaColindex={lastFocusedAriaColindex} + loadingEventIds={loadingEventIds} + onRowSelected={onRowSelected} + renderCellValue={renderCellValue} + rowRenderers={enabledRowRenderers} + onRuleChange={onRuleChange} + selectedEventIds={selectedEventIds} + showCheckboxes={showCheckboxes} + leadingControlColumns={leadingControlColumns} + trailingControlColumns={trailingControlColumns} + tabType={tabType} + /> + </EventsTable> + </TimelineBody> + <TimelineBodyGlobalStyle /> + </> + ); + }, + (prevProps, nextProps) => + deepEqual(prevProps.browserFields, nextProps.browserFields) && + deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && + deepEqual(prevProps.data, nextProps.data) && + deepEqual(prevProps.excludedRowRendererIds, nextProps.excludedRowRendererIds) && + deepEqual(prevProps.sort, nextProps.sort) && + deepEqual(prevProps.selectedEventIds, nextProps.selectedEventIds) && + deepEqual(prevProps.loadingEventIds, nextProps.loadingEventIds) && + prevProps.id === nextProps.id && + prevProps.isEventViewer === nextProps.isEventViewer && + prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && + prevProps.renderCellValue === nextProps.renderCellValue && + prevProps.rowRenderers === nextProps.rowRenderers && + prevProps.showCheckboxes === nextProps.showCheckboxes && + prevProps.tabType === nextProps.tabType +); + +BodyComponent.displayName = 'BodyComponent'; + +const makeMapStateToProps = () => { + const memoizedColumnHeaders: ( + headers: ColumnHeaderOptions[], + browserFields: BrowserFields + ) => ColumnHeaderOptions[] = memoizeOne(getColumnHeaders); + + const getTGrid = tGridSelectors.getTGridByIdSelector(); + const mapStateToProps = (state: TimelineState, { browserFields, id }: OwnProps) => { + const timeline: TGridModel = getTGrid(state, id); + const { + columns, + excludedRowRendererIds, + isSelectAllChecked, + loadingEventIds, + selectedEventIds, + showCheckboxes, + } = timeline; + + return { + columnHeaders: memoizedColumnHeaders(columns, browserFields), + excludedRowRendererIds, + isSelectAllChecked, + loadingEventIds, + id, + selectedEventIds, + showCheckboxes, + }; + }; + return mapStateToProps; +}; + +const mapDispatchToProps = { + clearSelected: tGridActions.clearSelected, + setSelected: tGridActions.setSelected, +}; + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const StatefulBody = connector(BodyComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap b/x-pack/plugins/timelines/public/components/t_grid/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap new file mode 100644 index 0000000000000..66a1b293cf8b9 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`plain_row_renderer renders correctly against snapshot 1`] = `<span />`; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/renderers/get_column_renderer.ts b/x-pack/plugins/timelines/public/components/t_grid/body/renderers/get_column_renderer.ts new file mode 100644 index 0000000000000..78f7119124e0a --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/renderers/get_column_renderer.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 type { TimelineNonEcsData } from '../../../../../common/search_strategy'; +import type { ColumnRenderer } from '../../../../../common/types/timeline'; + +const unhandledColumnRenderer = (): never => { + throw new Error('Unhandled Column Renderer'); +}; + +export const getColumnRenderer = ( + columnName: string, + columnRenderers: ColumnRenderer[], + data: TimelineNonEcsData[] +): ColumnRenderer => { + const renderer = columnRenderers.find((columnRenderer) => + columnRenderer.isInstance(columnName, data) + ); + return renderer != null ? renderer : unhandledColumnRenderer(); +}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/renderers/get_row_renderer.ts b/x-pack/plugins/timelines/public/components/t_grid/body/renderers/get_row_renderer.ts new file mode 100644 index 0000000000000..eba694c935e85 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/renderers/get_row_renderer.ts @@ -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. + */ + +import type { Ecs } from '../../../../../common/ecs'; +import type { RowRenderer } from '../../../../../common/types/timeline'; + +export const getRowRenderer = (ecs: Ecs, rowRenderers: RowRenderer[]): RowRenderer | null => + rowRenderers.find((rowRenderer) => rowRenderer.isInstance(ecs)) ?? null; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/renderers/plain_row_renderer.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/renderers/plain_row_renderer.test.tsx new file mode 100644 index 0000000000000..5cd709d2de3c7 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/renderers/plain_row_renderer.test.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 { mount, shallow } from 'enzyme'; +import { cloneDeep } from 'lodash'; +import React from 'react'; +import { Ecs } from '../../../../../common/ecs'; +import { mockBrowserFields, mockTimelineData } from '../../../../mock'; + +import { plainRowRenderer } from './plain_row_renderer'; + +describe('plain_row_renderer', () => { + let mockDatum: Ecs; + beforeEach(() => { + mockDatum = cloneDeep(mockTimelineData[0].ecs); + }); + + test('renders correctly against snapshot', () => { + const children = plainRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: mockDatum, + timelineId: 'test', + }); + const wrapper = shallow(<span>{children}</span>); + expect(wrapper).toMatchSnapshot(); + }); + + test('should always return isInstance true', () => { + expect(plainRowRenderer.isInstance(mockDatum)).toBe(true); + }); + + test('should render a plain row', () => { + const children = plainRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: mockDatum, + timelineId: 'test', + }); + const wrapper = mount(<span>{children}</span>); + expect(wrapper.text()).toEqual(''); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/renderers/plain_row_renderer.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/renderers/plain_row_renderer.tsx new file mode 100644 index 0000000000000..8462da3c02fb5 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/renderers/plain_row_renderer.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { RowRendererId } from '../../../../../common/types/timeline'; +// eslint-disable-next-line no-duplicate-imports +import type { RowRenderer } from '../../../../../common/types/timeline'; + +const PlainRowRenderer = () => <></>; + +PlainRowRenderer.displayName = 'PlainRowRenderer'; + +export const plainRowRenderer: RowRenderer = { + id: RowRendererId.plain, + isInstance: (_) => true, + renderRow: PlainRowRenderer, +}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/renderers/row_renderer.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/renderers/row_renderer.tsx new file mode 100644 index 0000000000000..64f1338b11a58 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/renderers/row_renderer.tsx @@ -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 React from 'react'; + +import { EventsTrSupplement } from '../../styles'; + +interface RowRendererContainerProps { + children: React.ReactNode; +} + +export const RowRendererContainer = React.memo<RowRendererContainerProps>(({ children }) => ( + <EventsTrSupplement className="siemEventsTable__trSupplement--summary"> + {children} + </EventsTrSupplement> +)); +RowRendererContainer.displayName = 'RowRendererContainer'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/sort/__snapshots__/sort_indicator.test.tsx.snap b/x-pack/plugins/timelines/public/components/t_grid/body/sort/__snapshots__/sort_indicator.test.tsx.snap new file mode 100644 index 0000000000000..596a05c4c8ab4 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/sort/__snapshots__/sort_indicator.test.tsx.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SortIndicator rendering renders correctly against snapshot 1`] = ` +<EuiToolTip + content="Sorted descending" + data-test-subj="sort-indicator-tooltip" + delay="regular" + position="top" +> + <EuiIcon + data-test-subj="sortIndicator" + type="sortDown" + /> + <SortNumber + sortNumber={-1} + /> +</EuiToolTip> +`; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/sort/index.ts b/x-pack/plugins/timelines/public/components/t_grid/body/sort/index.ts new file mode 100644 index 0000000000000..e4e02cd188600 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/sort/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SortDirection } from '../../../../../common/types/timeline'; +// eslint-disable-next-line no-duplicate-imports +import type { SortColumnTimeline } from '../../../../../common/types/timeline'; + +// TODO: Cleanup this type to match SortColumnTimeline +export { SortDirection }; + +/** Specifies which column the timeline is sorted on */ +export type Sort = SortColumnTimeline; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/sort/sort_indicator.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/sort/sort_indicator.test.tsx new file mode 100644 index 0000000000000..3812f44d95ccd --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/sort/sort_indicator.test.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mount, shallow } from 'enzyme'; +import React from 'react'; +import { Direction } from '../../../../../common/search_strategy'; + +import * as i18n from '../translations'; + +import { getDirection, SortIndicator } from './sort_indicator'; + +describe('SortIndicator', () => { + describe('rendering', () => { + test('renders correctly against snapshot', () => { + const wrapper = shallow(<SortIndicator sortDirection={Direction.desc} sortNumber={-1} />); + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders the expected sort indicator when direction is ascending', () => { + const wrapper = mount(<SortIndicator sortDirection={Direction.asc} sortNumber={-1} />); + + expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual( + 'sortUp' + ); + }); + + test('it renders the expected sort indicator when direction is descending', () => { + const wrapper = mount(<SortIndicator sortDirection={Direction.desc} sortNumber={-1} />); + + expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual( + 'sortDown' + ); + }); + + test('it renders the expected sort indicator when direction is `none`', () => { + const wrapper = mount(<SortIndicator sortDirection="none" sortNumber={-1} />); + + expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual( + 'empty' + ); + }); + }); + + describe('getDirection', () => { + test('it returns the expected symbol when the direction is ascending', () => { + expect(getDirection(Direction.asc)).toEqual('sortUp'); + }); + + test('it returns the expected symbol when the direction is descending', () => { + expect(getDirection(Direction.desc)).toEqual('sortDown'); + }); + + test('it returns the expected symbol (undefined) when the direction is neither ascending, nor descending', () => { + expect(getDirection('none')).toEqual(undefined); + }); + }); + + describe('sort indicator tooltip', () => { + test('it returns the expected tooltip when the direction is ascending', () => { + const wrapper = mount(<SortIndicator sortDirection={Direction.asc} sortNumber={-1} />); + + expect( + wrapper.find('[data-test-subj="sort-indicator-tooltip"]').first().props().content + ).toEqual(i18n.SORTED_ASCENDING); + }); + + test('it returns the expected tooltip when the direction is descending', () => { + const wrapper = mount(<SortIndicator sortDirection={Direction.desc} sortNumber={-1} />); + + expect( + wrapper.find('[data-test-subj="sort-indicator-tooltip"]').first().props().content + ).toEqual(i18n.SORTED_DESCENDING); + }); + + test('it does NOT render a tooltip when sort direction is `none`', () => { + const wrapper = mount(<SortIndicator sortDirection="none" sortNumber={-1} />); + + expect(wrapper.find('[data-test-subj="sort-indicator-tooltip"]').exists()).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/sort/sort_indicator.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/sort/sort_indicator.tsx new file mode 100644 index 0000000000000..3c7d8a35b9021 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/sort/sort_indicator.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiIcon, EuiToolTip } from '@elastic/eui'; +import React from 'react'; + +import * as i18n from '../translations'; +import { SortNumber } from './sort_number'; + +import type { SortDirection } from '.'; +import { Direction } from '../../../../../common/search_strategy'; + +enum SortDirectionIndicatorEnum { + SORT_UP = 'sortUp', + SORT_DOWN = 'sortDown', +} + +export type SortDirectionIndicator = undefined | SortDirectionIndicatorEnum; + +/** Returns the symbol that corresponds to the specified `SortDirection` */ +export const getDirection = (sortDirection: SortDirection): SortDirectionIndicator => { + switch (sortDirection) { + case Direction.asc: + return SortDirectionIndicatorEnum.SORT_UP; + case Direction.desc: + return SortDirectionIndicatorEnum.SORT_DOWN; + case 'none': + return undefined; + default: + throw new Error('Unhandled sort direction'); + } +}; + +interface Props { + sortDirection: SortDirection; + sortNumber: number; +} + +/** Renders a sort indicator */ +export const SortIndicator = React.memo<Props>(({ sortDirection, sortNumber }) => { + const direction = getDirection(sortDirection); + + if (direction != null) { + return ( + <EuiToolTip + content={ + direction === SortDirectionIndicatorEnum.SORT_UP + ? i18n.SORTED_ASCENDING + : i18n.SORTED_DESCENDING + } + data-test-subj="sort-indicator-tooltip" + > + <> + <EuiIcon data-test-subj="sortIndicator" type={direction} /> + <SortNumber sortNumber={sortNumber} /> + </> + </EuiToolTip> + ); + } else { + return <EuiIcon data-test-subj="sortIndicator" type={'empty'} />; + } +}); + +SortIndicator.displayName = 'SortIndicator'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/sort/sort_number.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/sort/sort_number.tsx new file mode 100644 index 0000000000000..3fdd31eae5c47 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/sort/sort_number.tsx @@ -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 { EuiIcon, EuiNotificationBadge } from '@elastic/eui'; +import React from 'react'; + +interface Props { + sortNumber: number; +} + +export const SortNumber = React.memo<Props>(({ sortNumber }) => { + if (sortNumber >= 0) { + return ( + <EuiNotificationBadge color="subdued" data-test-subj="sortNumber"> + {sortNumber + 1} + </EuiNotificationBadge> + ); + } else { + return <EuiIcon data-test-subj="sortEmptyNumber" type={'empty'} />; + } +}); + +SortNumber.displayName = 'SortNumber'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/body/translations.ts new file mode 100644 index 0000000000000..1a00a4eaf6bc6 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/body/translations.ts @@ -0,0 +1,229 @@ +/* + * 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 NOTES_TOOLTIP = i18n.translate( + 'xpack.timelines.timeline.body.notes.addOrViewNotesForThisEventTooltip', + { + defaultMessage: 'Add notes for this event', + } +); + +export const NOTES_DISABLE_TOOLTIP = i18n.translate( + 'xpack.timelines.timeline.body.notes.disableEventTooltip', + { + defaultMessage: 'Notes may not be added here while editing a template timeline', + } +); + +export const COPY_TO_CLIPBOARD = i18n.translate( + 'xpack.timelines.timeline.body.copyToClipboardButtonLabel', + { + defaultMessage: 'Copy to Clipboard', + } +); + +export const INVESTIGATE = i18n.translate( + 'xpack.timelines.timeline.body.actions.investigateLabel', + { + defaultMessage: 'Investigate', + } +); + +export const UNPINNED = i18n.translate('xpack.timelines.timeline.body.pinning.unpinnedTooltip', { + defaultMessage: 'Unpinned event', +}); + +export const PINNED = i18n.translate('xpack.timelines.timeline.body.pinning.pinnedTooltip', { + defaultMessage: 'Pinned event', +}); + +export const PINNED_WITH_NOTES = i18n.translate( + 'xpack.timelines.timeline.body.pinning.pinnnedWithNotesTooltip', + { + defaultMessage: 'This event cannot be unpinned because it has notes', + } +); + +export const SORTED_ASCENDING = i18n.translate( + 'xpack.timelines.timeline.body.sort.sortedAscendingTooltip', + { + defaultMessage: 'Sorted ascending', + } +); + +export const SORTED_DESCENDING = i18n.translate( + 'xpack.timelines.timeline.body.sort.sortedDescendingTooltip', + { + defaultMessage: 'Sorted descending', + } +); + +export const DISABLE_PIN = i18n.translate( + 'xpack.timelines.timeline.body.pinning.disablePinnnedTooltip', + { + defaultMessage: 'This event may not be pinned while editing a template timeline', + } +); + +export const VIEW_DETAILS = i18n.translate( + 'xpack.timelines.timeline.body.actions.viewDetailsAriaLabel', + { + defaultMessage: 'View details', + } +); + +export const VIEW_SUMMARY = i18n.translate( + 'xpack.timelines.timeline.body.actions.viewSummaryLabel', + { + defaultMessage: 'View summary', + } +); + +export const VIEW_DETAILS_FOR_ROW = ({ + ariaRowindex, + columnValues, +}: { + ariaRowindex: number; + columnValues: string; +}) => + i18n.translate('xpack.timelines.timeline.body.actions.viewDetailsForRowAriaLabel', { + values: { ariaRowindex, columnValues }, + defaultMessage: + 'View details for the alert or event in row {ariaRowindex}, with columns {columnValues}', + }); + +export const EXPAND_EVENT = i18n.translate( + 'xpack.timelines.timeline.body.actions.expandEventTooltip', + { + defaultMessage: 'View details', + } +); + +export const COLLAPSE = i18n.translate('xpack.timelines.timeline.body.actions.collapseAriaLabel', { + defaultMessage: 'Collapse', +}); + +export const ACTION_INVESTIGATE_IN_RESOLVER = i18n.translate( + 'xpack.timelines.timeline.body.actions.investigateInResolverTooltip', + { + defaultMessage: 'Analyze event', + } +); + +export const CHECKBOX_FOR_ROW = ({ + ariaRowindex, + columnValues, + checked, +}: { + ariaRowindex: number; + columnValues: string; + checked: boolean; +}) => + i18n.translate('xpack.timelines.timeline.body.actions.checkboxForRowAriaLabel', { + values: { ariaRowindex, checked, columnValues }, + defaultMessage: + '{checked, select, false {unchecked} true {checked}} checkbox for the alert or event in row {ariaRowindex}, with columns {columnValues}', + }); + +export const ACTION_INVESTIGATE_IN_RESOLVER_FOR_ROW = ({ + ariaRowindex, + columnValues, +}: { + ariaRowindex: number; + columnValues: string; +}) => + i18n.translate('xpack.timelines.timeline.body.actions.investigateInResolverForRowAriaLabel', { + values: { ariaRowindex, columnValues }, + defaultMessage: 'Analyze the alert or event in row {ariaRowindex}, with columns {columnValues}', + }); + +export const SEND_ALERT_TO_TIMELINE_FOR_ROW = ({ + ariaRowindex, + columnValues, +}: { + ariaRowindex: number; + columnValues: string; +}) => + i18n.translate('xpack.timelines.timeline.body.actions.sendAlertToTimelineForRowAriaLabel', { + values: { ariaRowindex, columnValues }, + defaultMessage: 'Send the alert in row {ariaRowindex} to timeline, with columns {columnValues}', + }); + +export const ADD_NOTES_FOR_ROW = ({ + ariaRowindex, + columnValues, +}: { + ariaRowindex: number; + columnValues: string; +}) => + i18n.translate('xpack.timelines.timeline.body.actions.addNotesForRowAriaLabel', { + values: { ariaRowindex, columnValues }, + defaultMessage: + 'Add notes for the event in row {ariaRowindex} to timeline, with columns {columnValues}', + }); + +export const PIN_EVENT_FOR_ROW = ({ + ariaRowindex, + columnValues, + isEventPinned, +}: { + ariaRowindex: number; + columnValues: string; + isEventPinned: boolean; +}) => + i18n.translate('xpack.timelines.timeline.body.actions.pinEventForRowAriaLabel', { + values: { ariaRowindex, columnValues, isEventPinned }, + defaultMessage: + '{isEventPinned, select, false {Pin} true {Unpin}} the event in row {ariaRowindex} to timeline, with columns {columnValues}', + }); + +export const TIMELINE_TOGGLE_BUTTON_ARIA_LABEL = ({ + isOpen, + title, +}: { + isOpen: boolean; + title: string; +}) => + i18n.translate('xpack.timelines.timeline.properties.timelineToggleButtonAriaLabel', { + values: { isOpen, title }, + defaultMessage: '{isOpen, select, false {Open} true {Close} other {Toggle}} timeline {title}', + }); + +export const ATTACH_ALERT_TO_CASE_FOR_ROW = ({ + ariaRowindex, + columnValues, +}: { + ariaRowindex: number; + columnValues: string; +}) => + i18n.translate('xpack.timelines.timeline.body.actions.attachAlertToCaseForRowAriaLabel', { + values: { ariaRowindex, columnValues }, + defaultMessage: + 'Attach the alert or event in row {ariaRowindex} to a case, with columns {columnValues}', + }); + +export const MORE_ACTIONS_FOR_ROW = ({ + ariaRowindex, + columnValues, +}: { + ariaRowindex: number; + columnValues: string; +}) => + i18n.translate('xpack.timelines.timeline.body.actions.moreActionsForRowAriaLabel', { + values: { ariaRowindex, columnValues }, + defaultMessage: + 'Select more actions for the alert or event in row {ariaRowindex}, with columns {columnValues}', + }); + +export const INVESTIGATE_IN_RESOLVER_DISABLED = i18n.translate( + 'xpack.timelines.timeline.body.actions.investigateInResolverDisabledTooltip', + { + defaultMessage: 'This event cannot be analyzed since it has incompatible field mappings', + } +); diff --git a/x-pack/plugins/timelines/public/components/t_grid/footer/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/footer/index.test.tsx new file mode 100644 index 0000000000000..fe57ab8d2d0f3 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/footer/index.test.tsx @@ -0,0 +1,259 @@ +/* + * 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 { mount, shallow } from 'enzyme'; +import React from 'react'; + +import { TestProviders } from '../../../mock/test_providers'; + +import { FooterComponent, PagingControlComponent } from './index'; + +describe('Footer Timeline Component', () => { + const loadMore = jest.fn(); + const updatedAt = 1546878704036; + const serverSideEventCount = 15546; + const itemsCount = 2; + + describe('rendering', () => { + test('it renders the default timeline footer', () => { + const wrapper = mount( + <TestProviders> + <FooterComponent + activePage={0} + updatedAt={updatedAt} + height={100} + id={'timeline-id'} + isLive={false} + isLoading={false} + itemsCount={itemsCount} + itemsPerPage={2} + itemsPerPageOptions={[1, 5, 10, 20]} + onChangePage={loadMore} + totalCount={serverSideEventCount} + /> + </TestProviders> + ); + + expect(wrapper.find('FooterContainer').exists()).toBeTruthy(); + }); + + test('it renders the loading panel at the beginning ', () => { + const wrapper = mount( + <TestProviders> + <FooterComponent + activePage={0} + updatedAt={updatedAt} + height={100} + id={'timeline-id'} + isLive={false} + isLoading={true} + itemsCount={itemsCount} + itemsPerPage={2} + itemsPerPageOptions={[1, 5, 10, 20]} + onChangePage={loadMore} + totalCount={serverSideEventCount} + /> + </TestProviders> + ); + + expect(wrapper.find('[data-test-subj="LoadingPanelTimeline"]').exists()).toBeTruthy(); + }); + + test('it renders the loadMore button if need to fetch more', () => { + const wrapper = mount( + <TestProviders> + <FooterComponent + activePage={0} + updatedAt={updatedAt} + height={100} + id={'timeline-id'} + isLive={false} + isLoading={false} + itemsCount={itemsCount} + itemsPerPage={2} + itemsPerPageOptions={[1, 5, 10, 20]} + onChangePage={loadMore} + totalCount={serverSideEventCount} + /> + </TestProviders> + ); + + expect(wrapper.find('[data-test-subj="timeline-pagination"]').exists()).toBeTruthy(); + }); + + test('it renders the Loading... in the more load button when fetching new data', () => { + const wrapper = shallow( + <PagingControlComponent + activePage={0} + totalCount={30} + totalPages={3} + onPageClick={loadMore} + isLoading={true} + /> + ); + + const loadButton = wrapper.text(); + expect(wrapper.find('[data-test-subj="LoadingPanelTimeline"]').exists()).toBeFalsy(); + expect(loadButton).toContain('Loading...'); + }); + + test('it renders the Pagination in the more load button when fetching new data', () => { + const wrapper = shallow( + <PagingControlComponent + activePage={0} + totalCount={30} + totalPages={3} + onPageClick={loadMore} + isLoading={false} + /> + ); + + expect(wrapper.find('[data-test-subj="timeline-pagination"]').exists()).toBeTruthy(); + }); + + test('it does NOT render the loadMore button because there is nothing else to fetch', () => { + const wrapper = mount( + <TestProviders> + <FooterComponent + activePage={0} + updatedAt={updatedAt} + height={100} + id={'timeline-id'} + isLive={false} + isLoading={true} + itemsCount={itemsCount} + itemsPerPage={2} + itemsPerPageOptions={[1, 5, 10, 20]} + onChangePage={loadMore} + totalCount={serverSideEventCount} + /> + </TestProviders> + ); + + expect(wrapper.find('[data-test-subj="timeline-pagination"]').exists()).toBeFalsy(); + }); + + test('it render popover to select new itemsPerPage in timeline', () => { + const wrapper = mount( + <TestProviders> + <FooterComponent + activePage={0} + updatedAt={updatedAt} + height={100} + id={'timeline-id'} + isLive={false} + isLoading={false} + itemsCount={itemsCount} + itemsPerPage={1} + itemsPerPageOptions={[1, 5, 10, 20]} + onChangePage={loadMore} + totalCount={serverSideEventCount} + /> + </TestProviders> + ); + + wrapper.find('[data-test-subj="timelineSizeRowPopover"] button').first().simulate('click'); + expect(wrapper.find('[data-test-subj="timelinePickSizeRow"]').exists()).toBeTruthy(); + }); + }); + + describe('Events', () => { + test('should call loadmore when clicking on the button load more', () => { + const wrapper = mount( + <TestProviders> + <FooterComponent + activePage={0} + updatedAt={updatedAt} + height={100} + id={'timeline-id'} + isLive={false} + isLoading={false} + itemsCount={itemsCount} + itemsPerPage={2} + itemsPerPageOptions={[1, 5, 10, 20]} + onChangePage={loadMore} + totalCount={serverSideEventCount} + /> + </TestProviders> + ); + + wrapper.find('[data-test-subj="pagination-button-next"]').first().simulate('click'); + expect(loadMore).toBeCalled(); + }); + + // test('Should call onChangeItemsPerPage when you pick a new limit', () => { + // const wrapper = mount( + // <TestProviders> + // <FooterComponent + // activePage={0} + // updatedAt={updatedAt} + // height={100} + // id={'timeline-id'} + // isLive={false} + // isLoading={false} + // itemsCount={itemsCount} + // itemsPerPage={1} + // itemsPerPageOptions={[1, 5, 10, 20]} + // onChangePage={loadMore} + // totalCount={serverSideEventCount} + // /> + // </TestProviders> + // ); + + // wrapper.find('[data-test-subj="timelineSizeRowPopover"] button').first().simulate('click'); + // wrapper.update(); + // wrapper.find('[data-test-subj="timelinePickSizeRow"] button').first().simulate('click'); + // expect(onChangeItemsPerPage).toBeCalled(); + // }); + + test('it does render the auto-refresh message instead of load more button when stream live is on', () => { + const wrapper = mount( + <TestProviders> + <FooterComponent + activePage={0} + updatedAt={updatedAt} + height={100} + id={'timeline-id'} + isLive={true} + isLoading={false} + itemsCount={itemsCount} + itemsPerPage={2} + itemsPerPageOptions={[1, 5, 10, 20]} + onChangePage={loadMore} + totalCount={serverSideEventCount} + /> + </TestProviders> + ); + + expect(wrapper.find('[data-test-subj="paging-control"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="is-live-on-message"]').exists()).toBeTruthy(); + }); + + test('it does render the load more button when stream live is off', () => { + const wrapper = mount( + <TestProviders> + <FooterComponent + activePage={0} + updatedAt={updatedAt} + height={100} + id={'timeline-id'} + isLive={false} + isLoading={false} + itemsCount={itemsCount} + itemsPerPage={2} + itemsPerPageOptions={[1, 5, 10, 20]} + onChangePage={loadMore} + totalCount={serverSideEventCount} + /> + </TestProviders> + ); + + expect(wrapper.find('[data-test-subj="paging-control"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="is-live-on-message"]').exists()).toBeFalsy(); + }); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/footer/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/footer/index.tsx new file mode 100644 index 0000000000000..2978759b6d148 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/footer/index.tsx @@ -0,0 +1,394 @@ +/* + * 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 { + EuiBadge, + EuiButtonEmpty, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiIconTip, + EuiPopover, + EuiText, + EuiToolTip, + EuiPopoverProps, + EuiPagination, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { FC, useCallback, useEffect, useState, useMemo } from 'react'; +import styled from 'styled-components'; +import { useDispatch } from 'react-redux'; + +import { EVENTS_COUNT_BUTTON_CLASS_NAME } from '../helpers'; + +import * as i18n from './translations'; +import { OnChangePage } from '../types'; +import { tGridActions, tGridSelectors } from '../../../store/t_grid'; +import { useDeepEqualSelector } from '../../../hooks/use_selector'; +import { LoadingPanel } from '../../loading'; +import { LastUpdatedAt } from '../../last_updated'; + +export const isCompactFooter = (width: number): boolean => width < 600; + +const FixedWidthLastUpdated = styled.div<{ compact?: boolean }>` + width: ${({ compact }) => (!compact ? 200 : 25)}px; + overflow: hidden; + text-align: end; +`; + +FixedWidthLastUpdated.displayName = 'FixedWidthLastUpdated'; + +interface HeightProp { + height: number; +} + +const FooterContainer = styled(EuiFlexGroup).attrs<HeightProp>(({ height }) => ({ + style: { + height: `${height}px`, + }, +}))<HeightProp>` + flex: 0 0 auto; +`; + +FooterContainer.displayName = 'FooterContainer'; + +const FooterFlexGroup = styled(EuiFlexGroup)` + height: 35px; + width: 100%; +`; + +FooterFlexGroup.displayName = 'FooterFlexGroup'; + +const LoadingPanelContainer = styled.div` + padding-top: 3px; +`; + +LoadingPanelContainer.displayName = 'LoadingPanelContainer'; + +const PopoverRowItems = styled((EuiPopover as unknown) as FC)< + EuiPopoverProps & { + className?: string; + id?: string; + } +>` + .euiButtonEmpty__content { + padding: 0px 0px; + } +`; + +PopoverRowItems.displayName = 'PopoverRowItems'; + +export const ServerSideEventCount = styled.div` + margin: 0 5px 0 5px; +`; + +ServerSideEventCount.displayName = 'ServerSideEventCount'; + +/** The height of the footer, exported for use in height calculations */ +export const footerHeight = 40; // px + +/** Displays the server-side count of events */ +export const EventsCountComponent = ({ + closePopover, + documentType, + footerText, + isOpen, + items, + itemsCount, + onClick, + serverSideEventCount, +}: { + closePopover: () => void; + documentType: string; + isOpen: boolean; + items: React.ReactElement[]; + itemsCount: number; + onClick: () => void; + serverSideEventCount: number; + footerText: string; +}) => { + const totalCount = useMemo(() => (serverSideEventCount > 0 ? serverSideEventCount : 0), [ + serverSideEventCount, + ]); + return ( + <h5> + <PopoverRowItems + className="footer-popover" + id="customizablePagination" + data-test-subj="timelineSizeRowPopover" + button={ + <> + <EuiBadge data-test-subj="local-events-count" color="hollow"> + {itemsCount} + <EuiButtonEmpty + className={EVENTS_COUNT_BUTTON_CLASS_NAME} + size="s" + color="text" + iconType="arrowDown" + iconSide="right" + onClick={onClick} + data-test-subj="local-events-count-button" + /> + </EuiBadge> + {` ${i18n.OF} `} + </> + } + isOpen={isOpen} + closePopover={closePopover} + panelPaddingSize="none" + > + <EuiContextMenuPanel items={items} data-test-subj="timelinePickSizeRow" /> + </PopoverRowItems> + <EuiToolTip content={`${totalCount} ${footerText}`}> + <ServerSideEventCount> + <EuiBadge color="hollow" data-test-subj="server-side-event-count"> + {totalCount} + </EuiBadge>{' '} + {documentType} + </ServerSideEventCount> + </EuiToolTip> + </h5> + ); +}; + +EventsCountComponent.displayName = 'EventsCountComponent'; + +export const EventsCount = React.memo(EventsCountComponent); + +EventsCount.displayName = 'EventsCount'; + +interface PagingControlProps { + activePage: number; + isLoading: boolean; + onPageClick: OnChangePage; + totalCount: number; + totalPages: number; +} + +const TimelinePaginationContainer = styled.div<{ hideLastPage: boolean }>` + ul.euiPagination__list { + li.euiPagination__item:last-child { + ${({ hideLastPage }) => `${hideLastPage ? 'display:none' : ''}`}; + } + } +`; + +export const PagingControlComponent: React.FC<PagingControlProps> = ({ + activePage, + isLoading, + onPageClick, + totalCount, + totalPages, +}) => { + if (isLoading) { + return <>{`${i18n.LOADING}...`}</>; + } + + if (!totalPages) { + return null; + } + + return ( + <TimelinePaginationContainer hideLastPage={totalCount > 9999}> + <EuiPagination + data-test-subj="timeline-pagination" + pageCount={totalPages} + activePage={activePage} + onPageClick={onPageClick} + /> + </TimelinePaginationContainer> + ); +}; + +PagingControlComponent.displayName = 'PagingControlComponent'; + +export const PagingControl = React.memo(PagingControlComponent); + +PagingControl.displayName = 'PagingControl'; +interface FooterProps { + updatedAt: number; + activePage: number; + height: number; + id: string; + isLive: boolean; + isLoading: boolean; + itemsCount: number; + itemsPerPage: number; + itemsPerPageOptions: number[]; + onChangePage: OnChangePage; + totalCount: number; +} + +/** Renders a loading indicator and paging controls */ +export const FooterComponent = ({ + activePage, + updatedAt, + height, + id, + isLive, + isLoading, + itemsCount, + itemsPerPage, + itemsPerPageOptions, + onChangePage, + totalCount, +}: FooterProps) => { + const dispatch = useDispatch(); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [paginationLoading, setPaginationLoading] = useState(false); + + const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []); + const { documentType, loadingText, footerText } = useDeepEqualSelector((state) => + getManageTimeline(state, id) + ); + + const handleChangePageClick = useCallback( + (nextPage: number) => { + setPaginationLoading(true); + onChangePage(nextPage); + }, + [onChangePage] + ); + + const onButtonClick = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [ + isPopoverOpen, + setIsPopoverOpen, + ]); + + const closePopover = useCallback(() => setIsPopoverOpen(false), [setIsPopoverOpen]); + + const onChangeItemsPerPage = useCallback( + (itemsChangedPerPage) => + dispatch(tGridActions.updateItemsPerPage({ id, itemsPerPage: itemsChangedPerPage })), + [dispatch, id] + ); + + const rowItems = useMemo( + () => + itemsPerPageOptions && + itemsPerPageOptions.map((item) => ( + <EuiContextMenuItem + key={item} + icon={itemsPerPage === item ? 'check' : 'empty'} + data-test-subj={`items-per-page-option-${item}`} + onClick={() => { + closePopover(); + onChangeItemsPerPage(item); + }} + > + {`${item} ${i18n.ROWS}`} + </EuiContextMenuItem> + )), + [closePopover, itemsPerPage, itemsPerPageOptions, onChangeItemsPerPage] + ); + + const totalPages = useMemo(() => Math.ceil(totalCount / itemsPerPage), [ + itemsPerPage, + totalCount, + ]); + + useEffect(() => { + if (paginationLoading && !isLoading) { + setPaginationLoading(false); + } + }, [isLoading, paginationLoading]); + + if (isLoading && !paginationLoading) { + return ( + <LoadingPanelContainer> + <LoadingPanel + data-test-subj="LoadingPanelTimeline" + height="35px" + showBorder={false} + text={`${loadingText}...`} + width="100%" + /> + </LoadingPanelContainer> + ); + } + + return ( + <FooterContainer + data-test-subj="timeline-footer" + direction="column" + gutterSize="none" + height={height} + justifyContent="spaceAround" + > + <FooterFlexGroup + alignItems="center" + data-test-subj="footer-flex-group" + direction="row" + gutterSize="none" + justifyContent="spaceBetween" + > + <EuiFlexItem data-test-subj="event-count-container" grow={false}> + <EuiFlexGroup + alignItems="center" + data-test-subj="events-count" + direction="row" + gutterSize="none" + > + <EventsCount + closePopover={closePopover} + documentType={documentType} + footerText={footerText} + isOpen={isPopoverOpen} + items={rowItems} + itemsCount={itemsCount} + onClick={onButtonClick} + serverSideEventCount={totalCount} + /> + </EuiFlexGroup> + </EuiFlexItem> + + <EuiFlexItem data-test-subj="paging-control-container" grow={false}> + {isLive ? ( + <EuiText size="s" data-test-subj="is-live-on-message"> + <b> + {i18n.AUTO_REFRESH_ACTIVE}{' '} + <EuiIconTip + color="subdued" + content={ + <FormattedMessage + id="xpack.timelines.footer.autoRefreshActiveTooltip" + defaultMessage="While auto-refresh is enabled, timeline will show you the latest {numberOfItems} events that match your query." + values={{ + numberOfItems: itemsCount, + }} + /> + } + type="iInCircle" + /> + </b> + </EuiText> + ) : ( + <PagingControl + data-test-subj="paging-control" + totalCount={totalCount} + totalPages={totalPages} + activePage={activePage} + onPageClick={handleChangePageClick} + isLoading={isLoading} + /> + )} + </EuiFlexItem> + + <EuiFlexItem data-test-subj="last-updated-container" grow={false}> + <LastUpdatedAt updatedAt={updatedAt} compact={false} /> + </EuiFlexItem> + </FooterFlexGroup> + </FooterContainer> + ); +}; + +FooterComponent.displayName = 'FooterComponent'; + +export const Footer = React.memo(FooterComponent); + +Footer.displayName = 'Footer'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/footer/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/footer/translations.ts new file mode 100644 index 0000000000000..e237ca39e10ab --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/footer/translations.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 { i18n } from '@kbn/i18n'; + +export const LOADING_TIMELINE_DATA = i18n.translate('xpack.timelines.footer.loadingTimelineData', { + defaultMessage: 'Loading Timeline data', +}); + +export const EVENTS = i18n.translate('xpack.timelines.footer.events', { + defaultMessage: 'Events', +}); + +export const OF = i18n.translate('xpack.timelines.footer.of', { + defaultMessage: 'of', +}); + +export const ROWS = i18n.translate('xpack.timelines.footer.rows', { + defaultMessage: 'rows', +}); + +export const LOADING = i18n.translate('xpack.timelines.footer.loadingLabel', { + defaultMessage: 'Loading', +}); + +export const TOTAL_COUNT_OF_EVENTS = i18n.translate('xpack.timelines.footer.totalCountOfEvents', { + defaultMessage: 'events', +}); + +export const AUTO_REFRESH_ACTIVE = i18n.translate( + 'xpack.timelines.footer.autoRefreshActiveDescription', + { + defaultMessage: 'Auto-Refresh Active', + } +); diff --git a/x-pack/plugins/timelines/public/components/t_grid/header_section/__snapshots__/index.test.tsx.snap b/x-pack/plugins/timelines/public/components/t_grid/header_section/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..d3d20c7183570 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/header_section/__snapshots__/index.test.tsx.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`HeaderSection it renders 1`] = ` +<Header + data-test-subj="header-section" +> + <EuiFlexGroup + alignItems="center" + > + <EuiFlexItem + grow={true} + > + <EuiFlexGroup + alignItems="center" + responsive={false} + > + <EuiFlexItem> + <EuiTitle + size="m" + > + <h2 + data-test-subj="header-section-title" + > + Test title + </h2> + </EuiTitle> + <Subtitle + data-test-subj="header-section-subtitle" + /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> +</Header> +`; diff --git a/x-pack/plugins/timelines/public/components/t_grid/header_section/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/header_section/index.test.tsx new file mode 100644 index 0000000000000..c5b4e679fe9f8 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/header_section/index.test.tsx @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { mount, shallow } from 'enzyme'; +import React from 'react'; +import { TestProviders } from '../../../mock'; + +import { HeaderSection } from './index'; + +describe('HeaderSection', () => { + test('it renders', () => { + const wrapper = shallow(<HeaderSection title="Test title" inspect={null} loading={false} />); + + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders the title', () => { + const wrapper = mount( + <TestProviders> + <HeaderSection title="Test title" inspect={null} loading={false} /> + </TestProviders> + ); + + expect(wrapper.find('[data-test-subj="header-section-title"]').first().exists()).toBe(true); + }); + + test('it renders the subtitle when provided', () => { + const wrapper = mount( + <TestProviders> + <HeaderSection subtitle="Test subtitle" title="Test title" inspect={null} loading={false} /> + </TestProviders> + ); + + expect(wrapper.find('[data-test-subj="header-section-subtitle"]').first().exists()).toBe(true); + }); + + test('renders the subtitle when not provided (to prevent layout thrash)', () => { + const wrapper = mount( + <TestProviders> + <HeaderSection title="Test title" inspect={null} loading={false} /> + </TestProviders> + ); + + expect(wrapper.find('[data-test-subj="header-section-subtitle"]').first().exists()).toBe(true); + }); + + test('it renders supplements when children provided', () => { + const wrapper = mount( + <TestProviders> + <HeaderSection title="Test title" inspect={null} loading={false}> + <p>{'Test children'}</p> + </HeaderSection> + </TestProviders> + ); + + expect(wrapper.find('[data-test-subj="header-section-supplements"]').first().exists()).toBe( + true + ); + }); + + test('it DOES NOT render supplements when children not provided', () => { + const wrapper = mount( + <TestProviders> + <HeaderSection title="Test title" inspect={null} loading={false} /> + </TestProviders> + ); + + expect(wrapper.find('[data-test-subj="header-section-supplements"]').first().exists()).toBe( + false + ); + }); + + test('it applies border styles when border is true', () => { + const wrapper = mount( + <TestProviders> + <HeaderSection border title="Test title" inspect={null} loading={false} /> + </TestProviders> + ); + const siemHeaderSection = wrapper.find('.siemHeaderSection').first(); + + expect(siemHeaderSection).toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin); + expect(siemHeaderSection).toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.l); + }); + + test('it DOES NOT apply border styles when border is false', () => { + const wrapper = mount( + <TestProviders> + <HeaderSection title="Test title" inspect={null} loading={false} /> + </TestProviders> + ); + const siemHeaderSection = wrapper.find('.siemHeaderSection').first(); + + expect(siemHeaderSection).not.toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin); + expect(siemHeaderSection).not.toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.l); + }); + + test('it splits the title and supplement areas evenly when split is true', () => { + const wrapper = mount( + <TestProviders> + <HeaderSection split title="Test title" inspect={null} loading={false}> + <p>{'Test children'}</p> + </HeaderSection> + </TestProviders> + ); + + expect( + wrapper + .find('.euiFlexItem--flexGrowZero[data-test-subj="header-section-supplements"]') + .first() + .exists() + ).toBe(false); + }); + + test('it DOES NOT split the title and supplement areas evenly when split is false', () => { + const wrapper = mount( + <TestProviders> + <HeaderSection title="Test title" inspect={null} loading={false}> + <p>{'Test children'}</p> + </HeaderSection> + </TestProviders> + ); + + expect( + wrapper + .find('.euiFlexItem--flexGrowZero[data-test-subj="header-section-supplements"]') + .first() + .exists() + ).toBe(true); + }); + + test('it renders an inspect button when an `id` is provided', () => { + const wrapper = mount( + <TestProviders> + <HeaderSection id="an id" title="Test title" inspect={null} loading={false}> + <p>{'Test children'}</p> + </HeaderSection> + </TestProviders> + ); + + expect(wrapper.find('[data-test-subj="inspect-icon-button"]').first().exists()).toBe(true); + }); + + test('it does NOT an inspect button when an `id` is NOT provided', () => { + const wrapper = mount( + <TestProviders> + <HeaderSection title="Test title" inspect={null} loading={false}> + <p>{'Test children'}</p> + </HeaderSection> + </TestProviders> + ); + + expect(wrapper.find('[data-test-subj="inspect-icon-button"]').first().exists()).toBe(false); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/header_section/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/header_section/index.tsx new file mode 100644 index 0000000000000..3a6838f4d8640 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/header_section/index.tsx @@ -0,0 +1,106 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiTitle, EuiTitleSize } from '@elastic/eui'; +import React from 'react'; +import styled, { css } from 'styled-components'; +import { InspectQuery } from '../../../store/t_grid/inputs'; +import { InspectButton } from '../../inspect'; + +import { Subtitle } from '../subtitle'; + +interface HeaderProps { + border?: boolean; + height?: number; +} + +const Header = styled.header.attrs(() => ({ + className: 'siemHeaderSection', +}))<HeaderProps>` + ${({ height }) => + height && + css` + height: ${height}px; + `} + margin-bottom: ${({ height, theme }) => (height ? 0 : theme.eui.euiSizeL)}; + user-select: text; + + ${({ border }) => + border && + css` + border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; + padding-bottom: ${({ theme }) => theme.eui.paddingSizes.l}; + `} +`; +Header.displayName = 'Header'; + +export interface HeaderSectionProps extends HeaderProps { + children?: React.ReactNode; + height?: number; + id?: string; + inspect: InspectQuery | null; + loading: boolean; + split?: boolean; + subtitle?: string | React.ReactNode; + title: string | React.ReactNode; + titleSize?: EuiTitleSize; + tooltip?: string; + growLeftSplit?: boolean; +} + +const HeaderSectionComponent: React.FC<HeaderSectionProps> = ({ + border, + children, + height, + id, + inspect, + loading, + split, + subtitle, + title, + titleSize = 'm', + tooltip, + growLeftSplit = true, +}) => ( + <Header data-test-subj="header-section" border={border} height={height}> + <EuiFlexGroup alignItems="center"> + <EuiFlexItem grow={growLeftSplit}> + <EuiFlexGroup alignItems="center" responsive={false}> + <EuiFlexItem> + <EuiTitle size={titleSize}> + <h2 data-test-subj="header-section-title"> + {title} + {tooltip && ( + <> + {' '} + <EuiIconTip color="subdued" content={tooltip} size="l" type="iInCircle" /> + </> + )} + </h2> + </EuiTitle> + + <Subtitle data-test-subj="header-section-subtitle" items={subtitle} /> + </EuiFlexItem> + + {id && ( + <EuiFlexItem grow={false}> + <InspectButton title={title} inspect={inspect} loading={loading} /> + </EuiFlexItem> + )} + </EuiFlexGroup> + </EuiFlexItem> + + {children && ( + <EuiFlexItem data-test-subj="header-section-supplements" grow={split ? true : false}> + {children} + </EuiFlexItem> + )} + </EuiFlexGroup> + </Header> +); + +export const HeaderSection = React.memo(HeaderSectionComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/helpers.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/helpers.test.tsx new file mode 100644 index 0000000000000..0fa47b22e5505 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/helpers.test.tsx @@ -0,0 +1,578 @@ +/* + * 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 { cloneDeep } from 'lodash/fp'; +import { esFilters, EsQueryConfig, Filter } from '../../../../../../src/plugins/data/public'; +import { DataProviderType } from '../../../common/types/timeline'; +import { mockBrowserFields, mockDataProviders, mockIndexPattern } from '../../mock'; + +import { buildGlobalQuery, combineQueries, resolverIsShowing, showGlobalFilters } from './helpers'; + +const cleanUpKqlQuery = (str: string) => str.replace(/\n/g, '').replace(/\s\s+/g, ' '); + +describe('Build KQL Query', () => { + test('Build KQL query with one data provider', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1"'); + }); + + test('Build KQL query with one template data provider', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].type = DataProviderType.template; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name :*'); + }); + + test('Build KQL query with one disabled data provider', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].enabled = false; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual(''); + }); + + test('Build KQL query with one data provider as timestamp (string input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = '@timestamp'; + dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('@timestamp: 1521848183232'); + }); + + test('Buld KQL query with one data provider as timestamp (numeric input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = '@timestamp'; + dataProviders[0].queryMatch.value = 1521848183232; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('@timestamp: 1521848183232'); + }); + + test('Buld KQL query with one data provider as timestamp (numeric input as string)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = '@timestamp'; + dataProviders[0].queryMatch.value = '1521848183232'; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('@timestamp: 1521848183232'); + }); + + test('Build KQL query with one data provider as date type (string input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = 'event.end'; + dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('event.end: 1521848183232'); + }); + + test('Buld KQL query with one data provider as date type (numeric input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = 'event.end'; + dataProviders[0].queryMatch.value = 1521848183232; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('event.end: 1521848183232'); + }); + + test('Buld KQL query with one data provider as date type (numeric input as string)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = 'event.end'; + dataProviders[0].queryMatch.value = '1521848183232'; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('event.end: 1521848183232'); + }); + + test('Build KQL query with two data provider', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('(name : "Provider 1") or (name : "Provider 2")'); + }); + + test('Build KQL query with two data provider and first is disabled', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); + dataProviders[0].enabled = false; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 2"'); + }); + + test('Build KQL query with two data provider and second is disabled', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); + dataProviders[1].enabled = false; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1"'); + }); + + test('Build KQL query with two data provider (first is template)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); + dataProviders[0].type = DataProviderType.template; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('(name :*) or (name : "Provider 2")'); + }); + + test('Build KQL query with two data provider (second is template)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); + dataProviders[1].type = DataProviderType.template; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('(name : "Provider 1") or (name :*)'); + }); + + test('Build KQL query with one data provider and one and', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and name : "Provider 2"'); + }); + + test('Build KQL query with one disabled data provider and one and', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].enabled = false; + dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 2"'); + }); + + test('Build KQL query with one data provider and one and as timestamp (string input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); + dataProviders[0].and[0].queryMatch.field = '@timestamp'; + dataProviders[0].and[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and @timestamp: 1521848183232'); + }); + + test('Build KQL query with one data provider and one and as timestamp (numeric input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); + dataProviders[0].and[0].queryMatch.field = '@timestamp'; + dataProviders[0].and[0].queryMatch.value = 1521848183232; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and @timestamp: 1521848183232'); + }); + + test('Build KQL query with one data provider and one and as date type (string input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); + dataProviders[0].and[0].queryMatch.field = 'event.end'; + dataProviders[0].and[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and event.end: 1521848183232'); + }); + + test('Build KQL query with one data provider and one and as date type (numeric input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); + dataProviders[0].and[0].queryMatch.field = 'event.end'; + dataProviders[0].and[0].queryMatch.value = 1521848183232; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and event.end: 1521848183232'); + }); + + test('Build KQL query with two data provider and multiple and', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); + dataProviders[0].and = cloneDeep(mockDataProviders.slice(2, 4)); + dataProviders[1].and = cloneDeep(mockDataProviders.slice(4, 5)); + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual( + '(name : "Provider 1" and name : "Provider 3" and name : "Provider 4") or (name : "Provider 2" and name : "Provider 5")' + ); + }); + + test('Build KQL query with two data provider and multiple and and first data provider is disabled', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); + dataProviders[0].enabled = false; + dataProviders[0].and = cloneDeep(mockDataProviders.slice(2, 4)); + dataProviders[1].and = cloneDeep(mockDataProviders.slice(4, 5)); + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual( + '(name : "Provider 3" and name : "Provider 4") or (name : "Provider 2" and name : "Provider 5")' + ); + }); + + test('Build KQL query with two data provider and multiple and and first and provider is disabled', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); + dataProviders[0].and = cloneDeep(mockDataProviders.slice(2, 4)); + dataProviders[0].and[0].enabled = false; + dataProviders[1].and = cloneDeep(mockDataProviders.slice(4, 5)); + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual( + '(name : "Provider 1" and name : "Provider 4") or (name : "Provider 2" and name : "Provider 5")' + ); + }); + + test('Build KQL query with all data provider', () => { + const kqlQuery = buildGlobalQuery(mockDataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual( + '(name : "Provider 1") or (name : "Provider 2") or (name : "Provider 3") or (name : "Provider 4") or (name : "Provider 5") or (name : "Provider 6") or (name : "Provider 7") or (name : "Provider 8") or (name : "Provider 9") or (name : "Provider 10")' + ); + }); + + test('Build complex KQL query with and and or', () => { + const dataProviders = cloneDeep(mockDataProviders); + dataProviders[0].and = cloneDeep(mockDataProviders.slice(2, 4)); + dataProviders[1].and = cloneDeep(mockDataProviders.slice(4, 5)); + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual( + '(name : "Provider 1" and name : "Provider 3" and name : "Provider 4") or (name : "Provider 2" and name : "Provider 5") or (name : "Provider 3") or (name : "Provider 4") or (name : "Provider 5") or (name : "Provider 6") or (name : "Provider 7") or (name : "Provider 8") or (name : "Provider 9") or (name : "Provider 10")' + ); + }); +}); + +describe('Combined Queries', () => { + const config: EsQueryConfig = { + allowLeadingWildcards: true, + queryStringOptions: {}, + ignoreFilterIfFieldNotInIndex: true, + dateFormatTZ: 'America/New_York', + }; + test('No Data Provider & No kqlQuery & and isEventViewer is false', () => { + expect( + combineQueries({ + config, + dataProviders: [], + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + }) + ).toBeNull(); + }); + + test('No Data Provider & No kqlQuery & isEventViewer is true', () => { + const isEventViewer = true; + expect( + combineQueries({ + config, + dataProviders: [], + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + isEventViewer, + }) + ).toEqual({ + filterQuery: '{"bool":{"must":[],"filter":[],"should":[],"must_not":[]}}', + }); + }); + + test('No Data Provider & No kqlQuery & with Filters', () => { + const isEventViewer = true; + expect( + combineQueries({ + config, + dataProviders: [], + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [ + { + $state: { store: esFilters.FilterStateStore.APP_STATE }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { query: 'file' }, + type: 'phrase', + }, + query: { match_phrase: { 'event.category': 'file' } }, + }, + { + $state: { store: esFilters.FilterStateStore.APP_STATE }, + meta: { + alias: null, + disabled: false, + key: 'host.name', + negate: false, + type: 'exists', + value: 'exists', + }, + exists: { field: 'host.name' }, + } as Filter, + ], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + isEventViewer, + }) + ).toEqual({ + filterQuery: + '{"bool":{"must":[],"filter":[{"exists":{"field":"host.name"}}],"should":[],"must_not":[]}}', + }); + }); + + test('Only Data Provider', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}' + ); + }); + + test('Only Data Provider with timestamp (string input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = '@timestamp'; + dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + })!; + expect(filterQuery).toMatchInlineSnapshot( + `"{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"range\\":{\\"@timestamp\\":{\\"gte\\":\\"1521848183232\\",\\"lte\\":\\"1521848183232\\"}}}],\\"minimum_should_match\\":1}}],\\"should\\":[],\\"must_not\\":[]}}"` + ); + }); + + test('Only Data Provider with timestamp (numeric input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = '@timestamp'; + dataProviders[0].queryMatch.value = 1521848183232; + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + })!; + expect(filterQuery).toMatchInlineSnapshot( + `"{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"range\\":{\\"@timestamp\\":{\\"gte\\":\\"1521848183232\\",\\"lte\\":\\"1521848183232\\"}}}],\\"minimum_should_match\\":1}}],\\"should\\":[],\\"must_not\\":[]}}"` + ); + }); + + test('Only Data Provider with a date type (string input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = 'event.end'; + dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + })!; + expect(filterQuery).toMatchInlineSnapshot( + `"{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"event.end\\":\\"1521848183232\\"}}],\\"minimum_should_match\\":1}}],\\"should\\":[],\\"must_not\\":[]}}"` + ); + }); + + test('Only Data Provider with date type (numeric input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = 'event.end'; + dataProviders[0].queryMatch.value = 1521848183232; + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + })!; + expect(filterQuery).toMatchInlineSnapshot( + `"{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"event.end\\":\\"1521848183232\\"}}],\\"minimum_should_match\\":1}}],\\"should\\":[],\\"must_not\\":[]}}"` + ); + }); + + test('Only KQL search/filter query', () => { + const { filterQuery } = combineQueries({ + config, + dataProviders: [], + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, + kqlMode: 'search', + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}' + ); + }); + + test('Data Provider & KQL search query', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, + kqlMode: 'search', + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"should":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}' + ); + }); + + test('Data Provider & KQL filter query', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, + kqlMode: 'filter', + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Data Provider & KQL search query multiple', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); + dataProviders[0].and = cloneDeep(mockDataProviders.slice(2, 4)); + dataProviders[1].and = cloneDeep(mockDataProviders.slice(4, 5)); + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, + kqlMode: 'search', + })!; + expect(filterQuery).toMatchInlineSnapshot( + `"{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 1\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 3\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 4\\"}}],\\"minimum_should_match\\":1}}]}},{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 2\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 5\\"}}],\\"minimum_should_match\\":1}}]}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"host.name\\":\\"host-1\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"should\\":[],\\"must_not\\":[]}}"` + ); + }); + + test('Data Provider & KQL filter query multiple', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); + dataProviders[0].and = cloneDeep(mockDataProviders.slice(2, 4)); + dataProviders[1].and = cloneDeep(mockDataProviders.slice(4, 5)); + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, + kqlMode: 'filter', + })!; + expect(filterQuery).toMatchInlineSnapshot( + `"{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 1\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 3\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 4\\"}}],\\"minimum_should_match\\":1}}]}},{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 2\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 5\\"}}],\\"minimum_should_match\\":1}}]}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"host.name\\":\\"host-1\\"}}],\\"minimum_should_match\\":1}}]}}],\\"should\\":[],\\"must_not\\":[]}}"` + ); + }); + + test('Data Provider & kql filter query with nested field that exists', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + const query = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'exists', + key: 'nestedField.firstAttributes', + value: 'exists', + }, + exists: { + field: 'nestedField.firstAttributes', + }, + $state: { + store: esFilters.FilterStateStore.APP_STATE, + }, + } as Filter, + ], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'filter', + }); + const filterQuery = query && query.filterQuery; + expect(filterQuery).toMatchInlineSnapshot( + `"{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 1\\"}}],\\"minimum_should_match\\":1}},{\\"exists\\":{\\"field\\":\\"nestedField.firstAttributes\\"}}],\\"should\\":[],\\"must_not\\":[]}}"` + ); + }); + + test('Data Provider & kql filter query with nested field of a particular value', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + const query = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [ + { + $state: { store: esFilters.FilterStateStore.APP_STATE }, + meta: { + alias: null, + disabled: false, + key: 'nestedField.secondAttributes', + negate: false, + params: { query: 'test' }, + type: 'phrase', + }, + query: { match_phrase: { 'nestedField.secondAttributes': 'test' } }, + }, + ], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'filter', + }); + const filterQuery = query && query.filterQuery; + expect(filterQuery).toMatchInlineSnapshot( + `"{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 1\\"}}],\\"minimum_should_match\\":1}},{\\"match_phrase\\":{\\"nestedField.secondAttributes\\":\\"test\\"}}],\\"should\\":[],\\"must_not\\":[]}}"` + ); + }); + + describe('resolverIsShowing', () => { + test('it returns true when graphEventId is NOT an empty string', () => { + expect(resolverIsShowing('a valid id')).toBe(true); + }); + + test('it returns false when graphEventId is undefined', () => { + expect(resolverIsShowing(undefined)).toBe(false); + }); + + test('it returns false when graphEventId is an empty string', () => { + expect(resolverIsShowing('')).toBe(false); + }); + }); + + describe('showGlobalFilters', () => { + test('it returns false when `globalFullScreen` is true and `graphEventId` is NOT an empty string, because Resolver IS showing', () => { + expect(showGlobalFilters({ globalFullScreen: true, graphEventId: 'a valid id' })).toBe(false); + }); + + test('it returns true when `globalFullScreen` is true and `graphEventId` is undefined, because Resolver is NOT showing', () => { + expect(showGlobalFilters({ globalFullScreen: true, graphEventId: undefined })).toBe(true); + }); + + test('it returns true when `globalFullScreen` is true and `graphEventId` is an empty string, because Resolver is NOT showing', () => { + expect(showGlobalFilters({ globalFullScreen: true, graphEventId: '' })).toBe(true); + }); + + test('it returns true when `globalFullScreen` is false and `graphEventId` is NOT an empty string, because Resolver IS showing', () => { + expect(showGlobalFilters({ globalFullScreen: false, graphEventId: 'a valid id' })).toBe(true); + }); + + test('it returns true when `globalFullScreen` is false and `graphEventId` is undefined, because Resolver is NOT showing', () => { + expect(showGlobalFilters({ globalFullScreen: false, graphEventId: undefined })).toBe(true); + }); + + test('it returns true when `globalFullScreen` is false and `graphEventId` is an empty string, because Resolver is NOT showing', () => { + expect(showGlobalFilters({ globalFullScreen: false, graphEventId: '' })).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx new file mode 100644 index 0000000000000..fc040522f3e15 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx @@ -0,0 +1,314 @@ +/* + * 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 { isEmpty, get } from 'lodash/fp'; +import memoizeOne from 'memoize-one'; +import { + elementOrChildrenHasFocus, + getFocusedAriaColindexCell, + getTableSkipFocus, + handleSkipFocus, + stopPropagationAndPreventDefault, +} from '../../../common'; +import type { + EsQueryConfig, + Filter, + IIndexPattern, + Query, +} from '../../../../../../src/plugins/data/public'; +import type { BrowserFields } from '../../../common/search_strategy/index_fields'; +import { DataProviderType, EXISTS_OPERATOR } from '../../../common/types/timeline'; +// eslint-disable-next-line no-duplicate-imports +import type { DataProvider, DataProvidersAnd } from '../../../common/types/timeline'; +import { convertToBuildEsQuery, escapeQueryValue } from '../utils/keury'; + +import { EVENTS_TABLE_CLASS_NAME } from './styles'; + +const isNumber = (value: string | number) => !isNaN(Number(value)); + +const convertDateFieldToQuery = (field: string, value: string | number) => + `${field}: ${isNumber(value) ? value : new Date(value).valueOf()}`; + +const getBaseFields = memoizeOne((browserFields: BrowserFields): string[] => { + const baseFields = get('base', browserFields); + if (baseFields != null && baseFields.fields != null) { + return Object.keys(baseFields.fields); + } + return []; +}); + +const getBrowserFieldPath = (field: string, browserFields: BrowserFields) => { + const splitFields = field.split('.'); + const baseFields = getBaseFields(browserFields); + if (baseFields.includes(field)) { + return ['base', 'fields', field]; + } + return [splitFields[0], 'fields', field]; +}; + +const checkIfFieldTypeIsDate = (field: string, browserFields: BrowserFields) => { + const pathBrowserField = getBrowserFieldPath(field, browserFields); + const browserField = get(pathBrowserField, browserFields); + if (browserField != null && browserField.type === 'date') { + return true; + } + return false; +}; + +const convertNestedFieldToQuery = ( + field: string, + value: string | number, + browserFields: BrowserFields +) => { + const pathBrowserField = getBrowserFieldPath(field, browserFields); + const browserField = get(pathBrowserField, browserFields); + const nestedPath = browserField.subType.nested.path; + const key = field.replace(`${nestedPath}.`, ''); + return `${nestedPath}: { ${key}: ${browserField.type === 'date' ? `"${value}"` : value} }`; +}; + +const convertNestedFieldToExistQuery = (field: string, browserFields: BrowserFields) => { + const pathBrowserField = getBrowserFieldPath(field, browserFields); + const browserField = get(pathBrowserField, browserFields); + const nestedPath = browserField.subType.nested.path; + const key = field.replace(`${nestedPath}.`, ''); + return `${nestedPath}: { ${key}: * }`; +}; + +const checkIfFieldTypeIsNested = (field: string, browserFields: BrowserFields) => { + const pathBrowserField = getBrowserFieldPath(field, browserFields); + const browserField = get(pathBrowserField, browserFields); + if (browserField != null && browserField.subType && browserField.subType.nested) { + return true; + } + return false; +}; + +const buildQueryMatch = ( + dataProvider: DataProvider | DataProvidersAnd, + browserFields: BrowserFields +) => + `${dataProvider.excluded ? 'NOT ' : ''}${ + dataProvider.queryMatch.operator !== EXISTS_OPERATOR && + dataProvider.type !== DataProviderType.template + ? checkIfFieldTypeIsNested(dataProvider.queryMatch.field, browserFields) + ? convertNestedFieldToQuery( + dataProvider.queryMatch.field, + dataProvider.queryMatch.value, + browserFields + ) + : checkIfFieldTypeIsDate(dataProvider.queryMatch.field, browserFields) + ? convertDateFieldToQuery(dataProvider.queryMatch.field, dataProvider.queryMatch.value) + : `${dataProvider.queryMatch.field} : ${ + isNumber(dataProvider.queryMatch.value) + ? dataProvider.queryMatch.value + : escapeQueryValue(dataProvider.queryMatch.value) + }` + : checkIfFieldTypeIsNested(dataProvider.queryMatch.field, browserFields) + ? convertNestedFieldToExistQuery(dataProvider.queryMatch.field, browserFields) + : `${dataProvider.queryMatch.field} ${EXISTS_OPERATOR}` + }`.trim(); + +export const buildGlobalQuery = (dataProviders: DataProvider[], browserFields: BrowserFields) => + dataProviders + .reduce((queries: string[], dataProvider: DataProvider) => { + const flatDataProviders = [dataProvider, ...dataProvider.and]; + const activeDataProviders = flatDataProviders.filter( + (flatDataProvider) => flatDataProvider.enabled + ); + + if (!activeDataProviders.length) return queries; + + const activeDataProvidersQueries = activeDataProviders.map((activeDataProvider) => + buildQueryMatch(activeDataProvider, browserFields) + ); + + const activeDataProvidersQueryMatch = activeDataProvidersQueries.join(' and '); + + return [...queries, activeDataProvidersQueryMatch]; + }, []) + .filter((queriesItem) => !isEmpty(queriesItem)) + .reduce((globalQuery: string, queryMatch: string, index: number, queries: string[]) => { + if (queries.length <= 1) return queryMatch; + + return !index ? `(${queryMatch})` : `${globalQuery} or (${queryMatch})`; + }, ''); + +export const combineQueries = ({ + config, + dataProviders, + indexPattern, + browserFields, + filters = [], + kqlQuery, + kqlMode, + isEventViewer, +}: { + config: EsQueryConfig; + dataProviders: DataProvider[]; + indexPattern: IIndexPattern; + browserFields: BrowserFields; + filters: Filter[]; + kqlQuery: Query; + kqlMode: string; + isEventViewer?: boolean; +}): { filterQuery: string } | null => { + const kuery: Query = { query: '', language: kqlQuery.language }; + if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEmpty(filters) && !isEventViewer) { + return null; + } else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEventViewer) { + return { + filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + }; + } else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && !isEmpty(filters)) { + return { + filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + }; + } else if (isEmpty(dataProviders) && !isEmpty(kqlQuery.query)) { + kuery.query = `(${kqlQuery.query})`; + return { + filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + }; + } else if (!isEmpty(dataProviders) && isEmpty(kqlQuery)) { + kuery.query = `(${buildGlobalQuery(dataProviders, browserFields)})`; + return { + filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + }; + } + const operatorKqlQuery = kqlMode === 'filter' ? 'and' : 'or'; + const postpend = (q: string) => `${!isEmpty(q) ? ` ${operatorKqlQuery} (${q})` : ''}`; + kuery.query = `((${buildGlobalQuery(dataProviders, browserFields)})${postpend( + kqlQuery.query as string + )})`; + return { + filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + }; +}; + +/** + * The CSS class name of a "stateful event", which appears in both + * the `Timeline` and the `Events Viewer` widget + */ +export const STATEFUL_EVENT_CSS_CLASS_NAME = 'event-column-view'; + +export const DEFAULT_ICON_BUTTON_WIDTH = 24; + +export const resolverIsShowing = (graphEventId: string | undefined): boolean => + graphEventId != null && graphEventId !== ''; + +export const showGlobalFilters = ({ + globalFullScreen, + graphEventId, +}: { + globalFullScreen: boolean; + graphEventId: string | undefined; +}): boolean => (globalFullScreen && resolverIsShowing(graphEventId) ? false : true); + +/** + * The `aria-colindex` of the Timeline actions column + */ +export const ACTIONS_COLUMN_ARIA_COL_INDEX = '1'; + +/** + * Every column index offset by `2`, because, per https://www.w3.org/TR/wai-aria-practices-1.1/examples/grid/dataGrids.html + * the `aria-colindex` attribute starts at `1`, and the "actions column" is always the first column + */ +export const ARIA_COLUMN_INDEX_OFFSET = 2; + +export const EVENTS_COUNT_BUTTON_CLASS_NAME = 'local-events-count-button'; + +/** Calculates the total number of pages in a (timeline) events view */ +export const calculateTotalPages = ({ + itemsCount, + itemsPerPage, +}: { + itemsCount: number; + itemsPerPage: number; +}): number => (itemsCount === 0 || itemsPerPage === 0 ? 0 : Math.ceil(itemsCount / itemsPerPage)); + +/** Returns true if the events table has focus */ +export const tableHasFocus = (containerElement: HTMLElement | null): boolean => + elementOrChildrenHasFocus( + containerElement?.querySelector<HTMLDivElement>(`.${EVENTS_TABLE_CLASS_NAME}`) + ); + +/** + * This function has a side effect. It will skip focus "after" or "before" + * Timeline's events table, with exceptions as noted below. + * + * If the currently-focused table cell has additional focusable children, + * i.e. action buttons, draggables, or always-open popover content, the + * browser's "natural" focus management will determine which element is + * focused next. + */ +export const onTimelineTabKeyPressed = ({ + containerElement, + keyboardEvent, + onSkipFocusBeforeEventsTable, + onSkipFocusAfterEventsTable, +}: { + containerElement: HTMLElement | null; + keyboardEvent: React.KeyboardEvent; + onSkipFocusBeforeEventsTable: () => void; + onSkipFocusAfterEventsTable: () => void; +}) => { + const { shiftKey } = keyboardEvent; + + const eventsTableSkipFocus = getTableSkipFocus({ + containerElement, + getFocusedCell: getFocusedAriaColindexCell, + shiftKey, + tableHasFocus, + tableClassName: EVENTS_TABLE_CLASS_NAME, + }); + + if (eventsTableSkipFocus !== 'SKIP_FOCUS_NOOP') { + stopPropagationAndPreventDefault(keyboardEvent); + handleSkipFocus({ + onSkipFocusBackwards: onSkipFocusBeforeEventsTable, + onSkipFocusForward: onSkipFocusAfterEventsTable, + skipFocus: eventsTableSkipFocus, + }); + } +}; + +export const ACTIVE_TIMELINE_BUTTON_CLASS_NAME = 'active-timeline-button'; +export const FLYOUT_BUTTON_BAR_CLASS_NAME = 'timeline-flyout-button-bar'; +export const FLYOUT_BUTTON_CLASS_NAME = 'timeline-flyout-button'; + +/** + * This function focuses the active timeline button on the next tick. Focus + * is updated on the next tick because this function is typically + * invoked in `onClick` handlers that also dispatch Redux actions (that + * in-turn update focus states). + */ +export const focusActiveTimelineButton = () => { + setTimeout(() => { + document + .querySelector<HTMLButtonElement>( + `div.${FLYOUT_BUTTON_BAR_CLASS_NAME} .${ACTIVE_TIMELINE_BUTTON_CLASS_NAME}` + ) + ?.focus(); + }, 0); +}; + +/** + * Focuses the utility bar action contained by the provided `containerElement` + * when a valid container is provided + */ +export const focusUtilityBarAction = (containerElement: HTMLElement | null) => { + containerElement + ?.querySelector<HTMLButtonElement>('div.siemUtilityBar__action:last-of-type button') + ?.focus(); +}; + +/** + * Resets keyboard focus on the page + */ +export const resetKeyboardFocus = () => { + document.querySelector<HTMLAnchorElement>('header.headerGlobalNav a.euiHeaderLogo')?.focus(); +}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx new file mode 100644 index 0000000000000..d52174b02f88e --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx @@ -0,0 +1,355 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import React, { useEffect, useMemo, useState } from 'react'; +import styled from 'styled-components'; +import { useDispatch } from 'react-redux'; + +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { Direction } from '../../../../common/search_strategy'; +// eslint-disable-next-line no-duplicate-imports +import type { DocValueFields } from '../../../../common/search_strategy'; +import type { CoreStart } from '../../../../../../../src/core/public'; +import type { BrowserFields } from '../../../../common/search_strategy/index_fields'; +import { TimelineId, TimelineTabs } from '../../../../common/types/timeline'; +// eslint-disable-next-line no-duplicate-imports +import type { + CellValueElementProps, + ColumnHeaderOptions, + ControlColumnProps, + DataProvider, + RowRenderer, +} from '../../../../common/types/timeline'; +import { + esQuery, + Filter, + IIndexPattern, + Query, + DataPublicPluginStart, +} from '../../../../../../../src/plugins/data/public'; +import { useDeepEqualSelector } from '../../../hooks/use_selector'; +import { Refetch } from '../../../store/t_grid/inputs'; +import { defaultHeaders } from '../body/column_headers/default_headers'; +import { calculateTotalPages, combineQueries, resolverIsShowing } from '../helpers'; +import { tGridActions, tGridSelectors } from '../../../store/t_grid'; +import { useTimelineEvents } from '../../../container'; +import { HeaderSection } from '../header_section'; +import { StatefulBody } from '../body'; +import { Footer, footerHeight } from '../footer'; +import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../styles'; +import * as i18n from './translations'; +import { ExitFullScreen } from '../../exit_full_screen'; +import { Sort } from '../body/sort'; +import { InspectButtonContainer } from '../../inspect'; + +export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px +const UTILITY_BAR_HEIGHT = 19; // px +const COMPACT_HEADER_HEIGHT = EVENTS_VIEWER_HEADER_HEIGHT - UTILITY_BAR_HEIGHT; // px + +const UtilityBar = styled.div` + height: ${UTILITY_BAR_HEIGHT}px; +`; + +const TitleText = styled.span` + margin-right: 12px; +`; + +const StyledEuiPanel = styled(EuiPanel)<{ $isFullScreen: boolean }>` + display: flex; + flex-direction: column; + + ${({ $isFullScreen }) => + $isFullScreen && + ` + border: 0; + box-shadow: none; + padding-top: 0; + padding-bottom: 0; + `} +`; + +const TitleFlexGroup = styled(EuiFlexGroup)` + margin-top: 8px; +`; + +const EventsContainerLoading = styled.div.attrs(({ className = '' }) => ({ + className: `${SELECTOR_TIMELINE_GLOBAL_CONTAINER} ${className}`, +}))` + width: 100%; + overflow: hidden; + flex: 1; + display: flex; + flex-direction: column; +`; + +const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible: boolean }>` + overflow: hidden; + margin: 0; + display: ${({ $visible }) => ($visible ? 'flex' : 'none')}; +`; + +const ScrollableFlexItem = styled(EuiFlexItem)` + overflow: auto; +`; + +/** + * Hides stateful headerFilterGroup implementations, but prevents the component + * from being unmounted, to preserve the state of the component + */ +const HeaderFilterGroupWrapper = styled.header<{ show: boolean }>` + ${({ show }) => (show ? '' : 'visibility: hidden;')} +`; + +export interface TGridIntegratedProps { + browserFields: BrowserFields; + columns: ColumnHeaderOptions[]; + dataProviders: DataProvider[]; + deletedEventIds: Readonly<string[]>; + docValueFields: DocValueFields[]; + end: string; + filters: Filter[]; + globalFullScreen: boolean; + headerFilterGroup?: React.ReactNode; + height?: number; + id: TimelineId; + indexNames: string[]; + indexPattern: IIndexPattern; + isLive: boolean; + isLoadingIndexPattern: boolean; + itemsPerPage: number; + itemsPerPageOptions: number[]; + kqlMode: 'filter' | 'search'; + query: Query; + onRuleChange?: () => void; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; + setGlobalFullScreen: (fullscreen: boolean) => void; + start: string; + sort: Sort[]; + utilityBar?: (refetch: Refetch, totalCount: number) => React.ReactNode; + // If truthy, the graph viewer (Resolver) is showing + graphEventId: string | undefined; + leadingControlColumns: ControlColumnProps[]; + trailingControlColumns: ControlColumnProps[]; + data?: DataPublicPluginStart; +} + +const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({ + browserFields, + columns, + dataProviders, + deletedEventIds, + docValueFields, + end, + filters, + globalFullScreen, + headerFilterGroup, + id, + indexNames, + indexPattern, + isLive, + isLoadingIndexPattern, + itemsPerPage, + itemsPerPageOptions, + kqlMode, + onRuleChange, + query, + renderCellValue, + rowRenderers, + setGlobalFullScreen, + start, + sort, + utilityBar, + graphEventId, + leadingControlColumns, + trailingControlColumns, + data, +}) => { + const dispatch = useDispatch(); + const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; + const { uiSettings } = useKibana<CoreStart>().services; + const [isQueryLoading, setIsQueryLoading] = useState(false); + + const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []); + const unit = useMemo(() => (n: number) => i18n.UNIT(n), []); + const { queryFields, title } = useDeepEqualSelector((state) => + getManageTimeline(state, id ?? '') + ); + + useEffect(() => { + dispatch(tGridActions.updateIsLoading({ id, isLoading: isQueryLoading })); + }, [dispatch, id, isQueryLoading]); + + const justTitle = useMemo(() => <TitleText data-test-subj="title">{title}</TitleText>, [title]); + const titleWithExitFullScreen = useMemo( + () => ( + <TitleFlexGroup alignItems="center" data-test-subj="title-flex-group" gutterSize="none"> + <EuiFlexItem grow={false}>{justTitle}</EuiFlexItem> + <EuiFlexItem grow={false}> + <ExitFullScreen fullScreen={globalFullScreen} setFullScreen={setGlobalFullScreen} /> + </EuiFlexItem> + </TitleFlexGroup> + ), + [globalFullScreen, justTitle, setGlobalFullScreen] + ); + + const combinedQueries = combineQueries({ + config: esQuery.getEsQueryConfig(uiSettings), + dataProviders, + indexPattern, + browserFields, + filters, + kqlQuery: query, + kqlMode, + isEventViewer: true, + }); + + const canQueryTimeline = useMemo( + () => + combinedQueries != null && + isLoadingIndexPattern != null && + !isLoadingIndexPattern && + !isEmpty(start) && + !isEmpty(end), + [isLoadingIndexPattern, combinedQueries, start, end] + ); + + const fields = useMemo(() => [...columnsHeader.map((c) => c.id), ...(queryFields ?? [])], [ + columnsHeader, + queryFields, + ]); + + const sortField = useMemo( + () => + sort.map(({ columnId, columnType, sortDirection }) => ({ + field: columnId, + type: columnType, + direction: sortDirection as Direction, + })), + [sort] + ); + + const [ + loading, + { events, updatedAt, loadPage, pageInfo, refetch, totalCount = 0, inspect }, + ] = useTimelineEvents({ + docValueFields, + fields, + filterQuery: combinedQueries!.filterQuery, + id, + indexNames, + limit: itemsPerPage, + sort: sortField, + startDate: start, + endDate: end, + skip: !canQueryTimeline, + data, + }); + + const totalCountMinusDeleted = useMemo( + () => (totalCount > 0 ? totalCount - deletedEventIds.length : 0), + [deletedEventIds.length, totalCount] + ); + + const subtitle = useMemo( + () => + `${i18n.SHOWING}: ${totalCountMinusDeleted.toLocaleString()} ${ + unit && unit(totalCountMinusDeleted) + }`, + [totalCountMinusDeleted, unit] + ); + + const nonDeletedEvents = useMemo(() => events.filter((e) => !deletedEventIds.includes(e._id)), [ + deletedEventIds, + events, + ]); + + const HeaderSectionContent = useMemo( + () => + headerFilterGroup && ( + <HeaderFilterGroupWrapper + data-test-subj="header-filter-group-wrapper" + show={!resolverIsShowing(graphEventId)} + > + {headerFilterGroup} + </HeaderFilterGroupWrapper> + ), + [headerFilterGroup, graphEventId] + ); + + useEffect(() => { + setIsQueryLoading(loading); + }, [loading]); + + return ( + <InspectButtonContainer> + <StyledEuiPanel data-test-subj="events-viewer-panel" $isFullScreen={globalFullScreen}> + {canQueryTimeline ? ( + <> + <HeaderSection + id={!resolverIsShowing(graphEventId) ? id : undefined} + inspect={inspect} + loading={loading} + height={headerFilterGroup ? COMPACT_HEADER_HEIGHT : EVENTS_VIEWER_HEADER_HEIGHT} + subtitle={utilityBar ? undefined : subtitle} + title={globalFullScreen ? titleWithExitFullScreen : justTitle} + > + {HeaderSectionContent} + </HeaderSection> + {utilityBar && !resolverIsShowing(graphEventId) && ( + <UtilityBar>{utilityBar?.(refetch, totalCountMinusDeleted)}</UtilityBar> + )} + <EventsContainerLoading + data-timeline-id={id} + data-test-subj={`events-container-loading-${loading}`} + > + <FullWidthFlexGroup $visible={!graphEventId} gutterSize="none"> + <ScrollableFlexItem grow={1}> + <StatefulBody + activePage={pageInfo.activePage} + browserFields={browserFields} + data={nonDeletedEvents} + id={id} + isEventViewer={true} + onRuleChange={onRuleChange} + renderCellValue={renderCellValue} + rowRenderers={rowRenderers} + sort={sort} + tabType={TimelineTabs.query} + totalPages={calculateTotalPages({ + itemsCount: totalCountMinusDeleted, + itemsPerPage, + })} + leadingControlColumns={leadingControlColumns} + trailingControlColumns={trailingControlColumns} + /> + <Footer + activePage={pageInfo.activePage} + data-test-subj="events-viewer-footer" + updatedAt={updatedAt} + height={footerHeight} + id={id} + isLive={isLive} + isLoading={loading} + itemsCount={nonDeletedEvents.length} + itemsPerPage={itemsPerPage} + itemsPerPageOptions={itemsPerPageOptions} + onChangePage={loadPage} + totalCount={totalCountMinusDeleted} + /> + </ScrollableFlexItem> + </FullWidthFlexGroup> + </EventsContainerLoading> + </> + ) : null} + </StyledEuiPanel> + </InspectButtonContainer> + ); +}; + +export const TGridIntegrated = React.memo(TGridIntegratedComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/integrated/translations.ts new file mode 100644 index 0000000000000..75ce592b0a564 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/translations.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 { i18n } from '@kbn/i18n'; + +export const SHOWING = i18n.translate('xpack.timelines.eventsViewer.showingLabel', { + defaultMessage: 'Showing', +}); + +export const ERROR_FETCHING_EVENTS_DATA = i18n.translate( + 'xpack.timelines.eventsViewer.errorFetchingEventsData', + { + defaultMessage: 'Failed to query events data', + } +); + +export const EVENTS = i18n.translate('xpack.timelines.eventsViewer.eventsLabel', { + defaultMessage: 'Events', +}); + +export const LOADING_EVENTS = i18n.translate( + 'xpack.timelines.eventsViewer.footer.loadingEventsDataLabel', + { + defaultMessage: 'Loading Events', + } +); + +export const UNIT = (totalCount: number) => + i18n.translate('xpack.timelines.eventsViewer.unit', { + values: { totalCount }, + defaultMessage: `{totalCount, plural, =1 {event} other {events}}`, + }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx new file mode 100644 index 0000000000000..75aae2ed55c4b --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx @@ -0,0 +1,339 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import React, { useEffect, useMemo, useState } from 'react'; +import styled from 'styled-components'; +import { useDispatch } from 'react-redux'; + +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { Direction } from '../../../../common/search_strategy'; +import type { CoreStart } from '../../../../../../../src/core/public'; +import { TimelineTabs } from '../../../../common/types/timeline'; +// eslint-disable-next-line no-duplicate-imports +import type { + CellValueElementProps, + ColumnHeaderOptions, + ControlColumnProps, + DataProvider, + RowRenderer, + SortColumnTimeline, +} from '../../../../common/types/timeline'; +import { + esQuery, + Filter, + Query, + DataPublicPluginStart, +} from '../../../../../../../src/plugins/data/public'; +import { useDeepEqualSelector } from '../../../hooks/use_selector'; +import { Refetch } from '../../../store/t_grid/inputs'; +import { defaultHeaders } from '../body/column_headers/default_headers'; +import { calculateTotalPages, combineQueries, resolverIsShowing } from '../helpers'; +import { tGridActions, tGridSelectors } from '../../../store/t_grid'; +import { useTimelineEvents } from '../../../container'; +import { HeaderSection } from '../header_section'; +import { StatefulBody } from '../body'; +import { Footer, footerHeight } from '../footer'; +import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../styles'; +import * as i18n from './translations'; + +export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px +const UTILITY_BAR_HEIGHT = 19; // px +const COMPACT_HEADER_HEIGHT = EVENTS_VIEWER_HEADER_HEIGHT - UTILITY_BAR_HEIGHT; // px +const STANDALONE_ID = 'standalone-t-grid'; +const EMPTY_BROWSER_FIELDS = {}; +const EMPTY_INDEX_PATTERN = { title: '', fields: [] }; +const EMPTY_DATA_PROVIDERS: DataProvider[] = []; + +const UtilityBar = styled.div` + height: ${UTILITY_BAR_HEIGHT}px; +`; + +const TitleText = styled.span` + margin-right: 12px; +`; + +const StyledEuiPanel = styled(EuiPanel)<{ $isFullScreen: boolean }>` + display: flex; + flex-direction: column; + + ${({ $isFullScreen }) => + $isFullScreen && + ` + border: 0; + box-shadow: none; + padding-top: 0; + padding-bottom: 0; + `} +`; + +const EventsContainerLoading = styled.div.attrs(({ className = '' }) => ({ + className: `${SELECTOR_TIMELINE_GLOBAL_CONTAINER} ${className}`, +}))` + width: 100%; + overflow: hidden; + flex: 1; + display: flex; + flex-direction: column; +`; + +const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible: boolean }>` + overflow: hidden; + margin: 0; + display: ${({ $visible }) => ($visible ? 'flex' : 'none')}; +`; + +const ScrollableFlexItem = styled(EuiFlexItem)` + overflow: auto; +`; + +/** + * Hides stateful headerFilterGroup implementations, but prevents the component + * from being unmounted, to preserve the state of the component + */ +const HeaderFilterGroupWrapper = styled.header<{ show: boolean }>` + ${({ show }) => (show ? '' : 'visibility: hidden;')} +`; + +export interface TGridStandaloneProps { + columns: ColumnHeaderOptions[]; + deletedEventIds: Readonly<string[]>; + end: string; + filters: Filter[]; + headerFilterGroup?: React.ReactNode; + height?: number; + indexNames: string[]; + itemsPerPage: number; + itemsPerPageOptions: number[]; + query: Query; + onRuleChange?: () => void; + renderCellValue: (props: CellValueElementProps) => React.ReactNode; + rowRenderers: RowRenderer[]; + start: string; + sort: SortColumnTimeline[]; + utilityBar?: (refetch: Refetch, totalCount: number) => React.ReactNode; + graphEventId?: string; + leadingControlColumns: ControlColumnProps[]; + trailingControlColumns: ControlColumnProps[]; + data?: DataPublicPluginStart; +} + +const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({ + columns, + deletedEventIds, + end, + filters, + headerFilterGroup, + indexNames, + itemsPerPage, + itemsPerPageOptions, + onRuleChange, + query, + renderCellValue, + rowRenderers, + start, + sort, + utilityBar, + graphEventId, + leadingControlColumns, + trailingControlColumns, + data, +}) => { + const dispatch = useDispatch(); + const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; + const { uiSettings } = useKibana<CoreStart>().services; + const [isQueryLoading, setIsQueryLoading] = useState(false); + + const getTGrid = useMemo(() => tGridSelectors.getTGridByIdSelector(), []); + const { + itemsPerPage: itemsPerPageStore, + itemsPerPageOptions: itemsPerPageOptionsStore, + queryFields, + title, + } = useDeepEqualSelector((state) => getTGrid(state, STANDALONE_ID ?? '')); + const unit = useMemo(() => (n: number) => i18n.UNIT(n), []); + useEffect(() => { + dispatch(tGridActions.updateIsLoading({ id: STANDALONE_ID, isLoading: isQueryLoading })); + }, [dispatch, isQueryLoading]); + + const justTitle = useMemo(() => <TitleText data-test-subj="title">{title}</TitleText>, [title]); + + const combinedQueries = combineQueries({ + config: esQuery.getEsQueryConfig(uiSettings), + dataProviders: EMPTY_DATA_PROVIDERS, + indexPattern: EMPTY_INDEX_PATTERN, + browserFields: EMPTY_BROWSER_FIELDS, + filters, + kqlQuery: query, + kqlMode: 'search', + isEventViewer: true, + }); + + const canQueryTimeline = useMemo( + () => combinedQueries != null && !isEmpty(start) && !isEmpty(end), + [combinedQueries, start, end] + ); + + const fields = useMemo( + () => [ + ...columnsHeader.reduce<string[]>( + (acc, c) => (c.linkField != null ? [...acc, c.id, c.linkField] : [...acc, c.id]), + [] + ), + ...(queryFields ?? []), + ], + [columnsHeader, queryFields] + ); + + const sortField = useMemo( + () => + sort.map(({ columnId, columnType, sortDirection }) => ({ + field: columnId, + type: columnType, + direction: sortDirection as Direction, + })), + [sort] + ); + + const [ + loading, + { events, updatedAt, loadPage, pageInfo, refetch, totalCount = 0, inspect }, + ] = useTimelineEvents({ + docValueFields: [], + excludeEcsData: true, + fields, + filterQuery: combinedQueries!.filterQuery, + id: STANDALONE_ID, + indexNames, + limit: itemsPerPageStore, + sort: sortField, + startDate: start, + endDate: end, + skip: !canQueryTimeline, + data, + }); + + const totalCountMinusDeleted = useMemo( + () => (totalCount > 0 ? totalCount - deletedEventIds.length : 0), + [deletedEventIds.length, totalCount] + ); + + const subtitle = useMemo( + () => + `${i18n.SHOWING}: ${totalCountMinusDeleted.toLocaleString()} ${ + unit && unit(totalCountMinusDeleted) + }`, + [totalCountMinusDeleted, unit] + ); + + const nonDeletedEvents = useMemo(() => events.filter((e) => !deletedEventIds.includes(e._id)), [ + deletedEventIds, + events, + ]); + + const HeaderSectionContent = useMemo( + () => + headerFilterGroup && ( + <HeaderFilterGroupWrapper + data-test-subj="header-filter-group-wrapper" + show={!resolverIsShowing(graphEventId)} + > + {headerFilterGroup} + </HeaderFilterGroupWrapper> + ), + [headerFilterGroup, graphEventId] + ); + + useEffect(() => { + setIsQueryLoading(loading); + }, [loading]); + + useEffect(() => { + dispatch( + tGridActions.createTGrid({ + id: STANDALONE_ID, + columns, + dateRange: { + start, + end, + }, + indexNames, + sort, + itemsPerPage, + itemsPerPageOptions, + showCheckboxes: false, + }) + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + <StyledEuiPanel data-test-subj="events-viewer-panel" $isFullScreen={false}> + {canQueryTimeline ? ( + <> + <HeaderSection + id={!resolverIsShowing(graphEventId) ? STANDALONE_ID : undefined} + inspect={inspect} + loading={loading} + height={headerFilterGroup ? COMPACT_HEADER_HEIGHT : EVENTS_VIEWER_HEADER_HEIGHT} + subtitle={utilityBar ? undefined : subtitle} + title={justTitle} + // title={globalFullScreen ? titleWithExitFullScreen : justTitle} + > + {HeaderSectionContent} + </HeaderSection> + {utilityBar && !resolverIsShowing(graphEventId) && ( + <UtilityBar>{utilityBar?.(refetch, totalCountMinusDeleted)}</UtilityBar> + )} + <EventsContainerLoading + data-timeline-id={STANDALONE_ID} + data-test-subj={`events-container-loading-${loading}`} + > + <FullWidthFlexGroup $visible={!graphEventId} gutterSize="none"> + <ScrollableFlexItem grow={1}> + <StatefulBody + activePage={pageInfo.activePage} + browserFields={EMPTY_BROWSER_FIELDS} + data={nonDeletedEvents} + id={STANDALONE_ID} + isEventViewer={true} + onRuleChange={onRuleChange} + renderCellValue={renderCellValue} + rowRenderers={rowRenderers} + sort={sort} + tabType={TimelineTabs.query} + totalPages={calculateTotalPages({ + itemsCount: totalCountMinusDeleted, + itemsPerPage: itemsPerPageStore, + })} + leadingControlColumns={leadingControlColumns} + trailingControlColumns={trailingControlColumns} + /> + <Footer + activePage={pageInfo.activePage} + data-test-subj="events-viewer-footer" + updatedAt={updatedAt} + height={footerHeight} + id={STANDALONE_ID} + isLive={false} + isLoading={loading} + itemsCount={nonDeletedEvents.length} + itemsPerPage={itemsPerPageStore} + itemsPerPageOptions={itemsPerPageOptionsStore} + onChangePage={loadPage} + totalCount={totalCountMinusDeleted} + /> + </ScrollableFlexItem> + </FullWidthFlexGroup> + </EventsContainerLoading> + </> + ) : null} + </StyledEuiPanel> + ); +}; + +export const TGridStandalone = React.memo(TGridStandaloneComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/standalone/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/standalone/translations.ts new file mode 100644 index 0000000000000..75ce592b0a564 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/standalone/translations.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 { i18n } from '@kbn/i18n'; + +export const SHOWING = i18n.translate('xpack.timelines.eventsViewer.showingLabel', { + defaultMessage: 'Showing', +}); + +export const ERROR_FETCHING_EVENTS_DATA = i18n.translate( + 'xpack.timelines.eventsViewer.errorFetchingEventsData', + { + defaultMessage: 'Failed to query events data', + } +); + +export const EVENTS = i18n.translate('xpack.timelines.eventsViewer.eventsLabel', { + defaultMessage: 'Events', +}); + +export const LOADING_EVENTS = i18n.translate( + 'xpack.timelines.eventsViewer.footer.loadingEventsDataLabel', + { + defaultMessage: 'Loading Events', + } +); + +export const UNIT = (totalCount: number) => + i18n.translate('xpack.timelines.eventsViewer.unit', { + values: { totalCount }, + defaultMessage: `{totalCount, plural, =1 {event} other {events}}`, + }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/styles.tsx b/x-pack/plugins/timelines/public/components/t_grid/styles.tsx new file mode 100644 index 0000000000000..bc224bea1a50c --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/styles.tsx @@ -0,0 +1,460 @@ +/* + * 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 { EuiLoadingSpinner } from '@elastic/eui'; +import { IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid'; +import { rgba } from 'polished'; +import styled, { createGlobalStyle } from 'styled-components'; +import type { TimelineEventsType } from '../../../common/types/timeline'; + +import { ACTIONS_COLUMN_ARIA_COL_INDEX } from './helpers'; +import { EVENTS_TABLE_ARIA_LABEL } from './translations'; + +/** + * TIMELINE BODY + */ +export const SELECTOR_TIMELINE_GLOBAL_CONTAINER = 'securitySolutionTimeline__container'; +export const TimelineContainer = styled.div.attrs(({ className = '' }) => ({ + className: `${SELECTOR_TIMELINE_GLOBAL_CONTAINER} ${className}`, +}))` + height: 100%; + display: flex; + flex-direction: column; + position: relative; +`; + +/** + * TIMELINE BODY + */ +export const SELECTOR_TIMELINE_BODY_CLASS_NAME = 'securitySolutionTimeline__body'; + +// SIDE EFFECT: the following creates a global class selector +export const TimelineBodyGlobalStyle = createGlobalStyle` + body.${IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME} .${SELECTOR_TIMELINE_BODY_CLASS_NAME} { + overflow: hidden; + } +`; + +export const TimelineBody = styled.div.attrs(({ className = '' }) => ({ + className: `${SELECTOR_TIMELINE_BODY_CLASS_NAME} ${className}`, +}))` + height: auto; + overflow: auto; + scrollbar-width: thin; + flex: 1; + display: block; + + &::-webkit-scrollbar { + height: ${({ theme }) => theme.eui.euiScrollBar}; + width: ${({ theme }) => theme.eui.euiScrollBar}; + } + + &::-webkit-scrollbar-thumb { + background-clip: content-box; + background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)}; + border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent; + } + + &::-webkit-scrollbar-corner, + &::-webkit-scrollbar-track { + background-color: transparent; + } +`; +TimelineBody.displayName = 'TimelineBody'; + +/** + * EVENTS TABLE + */ + +export const EVENTS_TABLE_CLASS_NAME = 'siemEventsTable'; +export const EVENTS_TABLE_HEAD_CLASS_NAME = 'siemEventsTable__thead'; + +interface EventsTableProps { + $activePage: number; + $columnCount: number; + columnWidths: number; + $rowCount: number; + $totalPages: number; +} + +export const EventsTable = styled.div.attrs<EventsTableProps>( + ({ className = '', $columnCount, columnWidths, $activePage, $rowCount, $totalPages }) => ({ + 'aria-label': EVENTS_TABLE_ARIA_LABEL({ activePage: $activePage + 1, totalPages: $totalPages }), + 'aria-colcount': `${$columnCount}`, + 'aria-rowcount': `${$rowCount + 1}`, + className: `siemEventsTable ${className}`, + role: 'grid', + style: { + minWidth: `${columnWidths}px`, + }, + tabindex: '-1', + }) +)<EventsTableProps>` + padding: 3px; +`; + +/* EVENTS HEAD */ + +export const EventsThead = styled.div.attrs(({ className = '' }) => ({ + className: `siemEventsTable__thead ${className}`, + role: 'rowgroup', +}))` + background-color: ${({ theme }) => theme.eui.euiColorEmptyShade}; + border-bottom: ${({ theme }) => theme.eui.euiBorderWidthThick} solid + ${({ theme }) => theme.eui.euiColorLightShade}; + position: sticky; + top: 0; + z-index: ${({ theme }) => theme.eui.euiZLevel1}; +`; + +export const EventsTrHeader = styled.div.attrs(({ className }) => ({ + 'aria-rowindex': '1', + className: `siemEventsTable__trHeader ${className}`, + role: 'row', +}))` + display: flex; +`; + +export const EventsThGroupActions = styled.div.attrs(({ className = '' }) => ({ + 'aria-colindex': `${ACTIONS_COLUMN_ARIA_COL_INDEX}`, + className: `siemEventsTable__thGroupActions ${className}`, + role: 'columnheader', + tabIndex: '0', +}))<{ actionsColumnWidth: number; isEventViewer: boolean }>` + display: flex; + flex: 0 0 + ${({ actionsColumnWidth, isEventViewer }) => + `${!isEventViewer ? actionsColumnWidth + 4 : actionsColumnWidth}px`}; + min-width: 0; + padding-left: ${({ isEventViewer }) => + !isEventViewer ? '4px;' : '0;'}; // match timeline event border +`; + +export const EventsThGroupData = styled.div.attrs(({ className = '' }) => ({ + className: `siemEventsTable__thGroupData ${className}`, +}))<{ isDragging?: boolean }>` + display: flex; + + > div:hover .siemEventsHeading__handle { + display: ${({ isDragging }) => (isDragging ? 'none' : 'block')}; + opacity: 1; + visibility: visible; + } +`; + +export const EventsTh = styled.div.attrs<{ role: string }>( + ({ className = '', role = 'columnheader' }) => ({ + className: `siemEventsTable__th ${className}`, + role, + }) +)` + align-items: center; + display: flex; + flex-shrink: 0; + min-width: 0; + + .siemEventsTable__thGroupActions &:first-child:last-child { + flex: 1; + } + + .siemEventsTable__thGroupData &:hover { + background-color: ${({ theme }) => theme.eui.euiTableHoverColor}; + cursor: move; /* Fallback for IE11 */ + cursor: grab; + } + + > div:focus { + outline: 0; /* disable focus on Resizable element */ + } + + /* don't display Draggable placeholder */ + [data-rbd-placeholder-context-id] { + display: none !important; + } +`; + +export const EventsThContent = styled.div.attrs(({ className = '' }) => ({ + className: `siemEventsTable__thContent ${className}`, +}))<{ textAlign?: string; width?: number }>` + font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; + font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold}; + line-height: ${({ theme }) => theme.eui.euiLineHeight}; + min-width: 0; + padding: ${({ theme }) => theme.eui.paddingSizes.xs}; + text-align: ${({ textAlign }) => textAlign}; + width: ${({ width }) => + width != null + ? `${width}px` + : '100%'}; /* Using width: 100% instead of flex: 1 and max-width: 100% for IE11 */ + + > button.euiButtonIcon, + > .euiToolTipAnchor > button.euiButtonIcon { + margin-left: ${({ theme }) => `-${theme.eui.paddingSizes.xs}`}; + } +`; + +/* EVENTS BODY */ + +export const EventsTbody = styled.div.attrs(({ className = '' }) => ({ + className: `siemEventsTable__tbody ${className}`, + role: 'rowgroup', +}))` + overflow-x: hidden; +`; + +export const EventsTrGroup = styled.div.attrs( + ({ className = '', $ariaRowindex }: { className?: string; $ariaRowindex: number }) => ({ + 'aria-rowindex': `${$ariaRowindex}`, + className: `siemEventsTable__trGroup ${className}`, + role: 'row', + }) +)<{ + className?: string; + eventType: Omit<TimelineEventsType, 'all'>; + isEvenEqlSequence: boolean; + isBuildingBlockType: boolean; + isExpanded: boolean; + showLeftBorder: boolean; +}>` + border-bottom: ${({ theme }) => theme.eui.euiBorderWidthThin} solid + ${({ theme }) => theme.eui.euiColorLightShade}; + ${({ theme, eventType, isEvenEqlSequence, showLeftBorder }) => + showLeftBorder + ? `border-left: 4px solid + ${ + eventType === 'raw' + ? theme.eui.euiColorLightShade + : eventType === 'eql' && isEvenEqlSequence + ? theme.eui.euiColorPrimary + : eventType === 'eql' && !isEvenEqlSequence + ? theme.eui.euiColorAccent + : theme.eui.euiColorWarning + }` + : ''}; + ${({ isBuildingBlockType }) => + isBuildingBlockType + ? 'background: repeating-linear-gradient(127deg, rgba(245, 167, 0, 0.2), rgba(245, 167, 0, 0.2) 1px, rgba(245, 167, 0, 0.05) 2px, rgba(245, 167, 0, 0.05) 10px);' + : ''}; + ${({ eventType, isEvenEqlSequence }) => + eventType === 'eql' + ? isEvenEqlSequence + ? 'background: repeating-linear-gradient(127deg, rgba(0, 107, 180, 0.2), rgba(0, 107, 180, 0.2) 1px, rgba(0, 107, 180, 0.05) 2px, rgba(0, 107, 180, 0.05) 10px);' + : 'background: repeating-linear-gradient(127deg, rgba(221, 10, 115, 0.2), rgba(221, 10, 115, 0.2) 1px, rgba(221, 10, 115, 0.05) 2px, rgba(221, 10, 115, 0.05) 10px);' + : ''}; + + &:hover { + background-color: ${({ theme }) => theme.eui.euiTableHoverColor}; + } + + ${({ isExpanded, theme }) => + isExpanded && + ` + background: ${theme.eui.euiTableSelectedColor}; + + &:hover { + ${theme.eui.euiTableHoverSelectedColor} + } + `} +`; + +export const EventsTrData = styled.div.attrs(({ className = '' }) => ({ + className: `siemEventsTable__trData ${className}`, +}))` + display: flex; +`; + +const TIMELINE_EVENT_DETAILS_OFFSET = 40; + +interface WidthProp { + width?: number; +} + +export const EventsTrSupplementContainer = styled.div.attrs<WidthProp>(({ width }) => ({ + role: 'dialog', + style: { + width: `${width! - TIMELINE_EVENT_DETAILS_OFFSET}px`, + }, +}))<WidthProp>``; + +export const EventsTrSupplement = styled.div.attrs(({ className = '' }) => ({ + className: `siemEventsTable__trSupplement ${className}`, +}))<{ className: string }>` + font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; + line-height: ${({ theme }) => theme.eui.euiLineHeight}; + padding-left: ${({ theme }) => theme.eui.paddingSizes.m}; + .euiAccordion + div { + background-color: ${({ theme }) => theme.eui.euiColorEmptyShade}; + padding: 0 ${({ theme }) => theme.eui.paddingSizes.s}; + border: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; + border-radius: ${({ theme }) => theme.eui.paddingSizes.xs}; + } +`; + +export const EventsTdGroupActions = styled.div.attrs(({ className = '' }) => ({ + 'aria-colindex': `${ACTIONS_COLUMN_ARIA_COL_INDEX}`, + className: `siemEventsTable__tdGroupActions ${className}`, + role: 'gridcell', +}))<{ width: number }>` + align-items: center; + display: flex; + flex: 0 0 ${({ width }) => `${width}px`}; + min-width: 0; +`; + +export const EventsTdGroupData = styled.div.attrs(({ className = '' }) => ({ + className: `siemEventsTable__tdGroupData ${className}`, +}))` + display: flex; +`; +interface EventsTdProps { + $ariaColumnIndex?: number; + width?: number; +} + +export const EVENTS_TD_CLASS_NAME = 'siemEventsTable__td'; + +export const EventsTd = styled.div.attrs<EventsTdProps>( + ({ className = '', $ariaColumnIndex, width }) => { + const common = { + className: `siemEventsTable__td ${className}`, + role: 'gridcell', + style: { + flexBasis: width ? `${width}px` : 'auto', + }, + }; + + return $ariaColumnIndex != null + ? { + ...common, + 'aria-colindex': `${$ariaColumnIndex}`, + } + : common; + } +)<EventsTdProps>` + align-items: center; + display: flex; + flex-shrink: 0; + min-width: 0; + + .siemEventsTable__tdGroupActions &:first-child:last-child { + flex: 1; + } +`; + +export const EventsTdContent = styled.div.attrs(({ className }) => ({ + className: `siemEventsTable__tdContent ${className != null ? className : ''}`, +}))<{ textAlign?: string; width?: number }>` + font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; + line-height: ${({ theme }) => theme.eui.euiLineHeight}; + min-width: 0; + padding: ${({ theme }) => theme.eui.paddingSizes.xs}; + text-align: ${({ textAlign }) => textAlign}; + width: ${({ width }) => + width != null + ? `${width}px` + : '100%'}; /* Using width: 100% instead of flex: 1 and max-width: 100% for IE11 */ + + button.euiButtonIcon { + margin-left: ${({ theme }) => `-${theme.eui.paddingSizes.xs}`}; + } +`; + +/** + * EVENTS HEADING + */ + +export const EventsHeading = styled.div.attrs(({ className = '' }) => ({ + className: `siemEventsHeading ${className}`, +}))<{ isLoading: boolean }>` + align-items: center; + display: flex; + + &:hover { + cursor: ${({ isLoading }) => (isLoading ? 'wait' : 'grab')}; + } +`; + +export const EventsHeadingTitleButton = styled.button.attrs(({ className = '' }) => ({ + className: `siemEventsHeading__title siemEventsHeading__title--aggregatable ${className}`, + type: 'button', +}))` + align-items: center; + display: flex; + font-weight: inherit; + min-width: 0; + + &:hover, + &:focus { + color: ${({ theme }) => theme.eui.euiColorPrimary}; + text-decoration: underline; + } + + &:hover { + cursor: pointer; + } + + & > * + * { + margin-left: ${({ theme }) => theme.eui.euiSizeXS}; + } +`; + +export const EventsHeadingTitleSpan = styled.span.attrs(({ className }) => ({ + className: `siemEventsHeading__title siemEventsHeading__title--notAggregatable ${className}`, +}))` + min-width: 0; +`; + +export const EventsHeadingExtra = styled.div.attrs(({ className = '' }) => ({ + className: `siemEventsHeading__extra ${className}`, +}))` + margin-left: auto; + margin-right: 2px; + + &.siemEventsHeading__extra--close { + opacity: 0; + transition: all ${({ theme }) => theme.eui.euiAnimSpeedNormal} ease; + visibility: hidden; + + .siemEventsTable__th:hover & { + opacity: 1; + visibility: visible; + } + } +`; + +export const EventsHeadingHandle = styled.div.attrs(({ className = '' }) => ({ + className: `siemEventsHeading__handle ${className}`, +}))` + background-color: ${({ theme }) => theme.eui.euiBorderColor}; + height: 100%; + opacity: 0; + transition: all ${({ theme }) => theme.eui.euiAnimSpeedNormal} ease; + visibility: hidden; + width: ${({ theme }) => theme.eui.euiBorderWidthThick}; + + &:hover { + background-color: ${({ theme }) => theme.eui.euiColorPrimary}; + cursor: col-resize; + } +`; + +/** + * EVENTS LOADING + */ + +export const EventsLoading = styled(EuiLoadingSpinner)` + margin: 0 2px; + vertical-align: middle; +`; + +export const HideShowContainer = styled.div.attrs<{ $isVisible: boolean }>( + ({ $isVisible = false }) => ({ + style: { + display: $isVisible ? 'block' : 'none', + }, + }) +)<{ $isVisible: boolean }>``; diff --git a/x-pack/plugins/timelines/public/components/t_grid/subtitle/__snapshots__/index.test.tsx.snap b/x-pack/plugins/timelines/public/components/t_grid/subtitle/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..1c6ff628df1e6 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/subtitle/__snapshots__/index.test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Subtitle it renders 1`] = ` +<Wrapper + className="siemSubtitle" +> + <SubtitleItem> + Test subtitle + </SubtitleItem> +</Wrapper> +`; diff --git a/x-pack/plugins/timelines/public/components/t_grid/subtitle/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/subtitle/index.test.tsx new file mode 100644 index 0000000000000..37cb2b7fc92e5 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/subtitle/index.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mount, shallow } from 'enzyme'; +import React from 'react'; + +import { TestProviders } from '../../../mock'; + +import { Subtitle } from './index'; + +describe('Subtitle', () => { + test('it renders', () => { + const wrapper = shallow(<Subtitle items="Test subtitle" />); + + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders one subtitle string item', () => { + const wrapper = mount( + <TestProviders> + <Subtitle items="Test subtitle" /> + </TestProviders> + ); + + expect(wrapper.find('.siemSubtitle__item--text').length).toEqual(1); + }); + + test('it renders multiple subtitle string items', () => { + const wrapper = mount( + <TestProviders> + <Subtitle items={['Test subtitle 1', 'Test subtitle 2']} /> + </TestProviders> + ); + + expect(wrapper.find('.siemSubtitle__item--text').length).toEqual(2); + }); + + test('it renders one subtitle React.ReactNode item', () => { + const wrapper = mount( + <TestProviders> + <Subtitle items={<span>{'Test subtitle'}</span>} /> + </TestProviders> + ); + + expect(wrapper.find('.siemSubtitle__item--node').length).toEqual(1); + }); + + test('it renders multiple subtitle React.ReactNode items', () => { + const wrapper = mount( + <TestProviders> + <Subtitle items={[<span>{'Test subtitle 1'}</span>, <span>{'Test subtitle 2'}</span>]} /> + </TestProviders> + ); + + expect(wrapper.find('.siemSubtitle__item--node').length).toEqual(2); + }); + + test('it renders multiple subtitle items of mixed type', () => { + const wrapper = mount( + <TestProviders> + <Subtitle items={['Test subtitle 1', <span>{'Test subtitle 2'}</span>]} /> + </TestProviders> + ); + + expect(wrapper.find('.siemSubtitle__item').length).toEqual(2); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/subtitle/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/subtitle/index.tsx new file mode 100644 index 0000000000000..c2f3d7d096b5c --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/subtitle/index.tsx @@ -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 React from 'react'; +import styled, { css } from 'styled-components'; + +const Wrapper = styled.div` + ${({ theme }) => css` + margin-top: ${theme.eui.euiSizeS}; + + .siemSubtitle__item { + color: ${theme.eui.euiTextSubduedColor}; + font-size: ${theme.eui.euiFontSizeXS}; + line-height: ${theme.eui.euiLineHeight}; + + @media only screen and (min-width: ${theme.eui.euiBreakpoints.s}) { + display: inline-block; + margin-right: ${theme.eui.euiSize}; + + &:last-child { + margin-right: 0; + } + } + } + `} +`; +Wrapper.displayName = 'Wrapper'; + +interface SubtitleItemProps { + children: string | React.ReactNode; + dataTestSubj?: string; +} + +const SubtitleItem = React.memo<SubtitleItemProps>( + ({ children, dataTestSubj = 'header-panel-subtitle' }) => { + if (typeof children === 'string') { + return ( + <p className="siemSubtitle__item siemSubtitle__item--text" data-test-subj={dataTestSubj}> + {children} + </p> + ); + } else { + return ( + <div className="siemSubtitle__item siemSubtitle__item--node" data-test-subj={dataTestSubj}> + {children} + </div> + ); + } + } +); +SubtitleItem.displayName = 'SubtitleItem'; + +export interface SubtitleProps { + items: string | React.ReactNode | Array<string | React.ReactNode>; +} + +export const Subtitle = React.memo<SubtitleProps>(({ items }) => { + return ( + <Wrapper className="siemSubtitle"> + {Array.isArray(items) ? ( + items.map((item, i) => <SubtitleItem key={i}>{item}</SubtitleItem>) + ) : ( + <SubtitleItem>{items}</SubtitleItem> + )} + </Wrapper> + ); +}); +Subtitle.displayName = 'Subtitle'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/translations.ts new file mode 100644 index 0000000000000..05965fa5f5752 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/translations.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 { i18n } from '@kbn/i18n'; + +export const EVENTS_TABLE_ARIA_LABEL = ({ + activePage, + totalPages, +}: { + activePage: number; + totalPages: number; +}) => + i18n.translate('xpack.timelines.timeline.eventsTableAriaLabel', { + values: { activePage, totalPages }, + defaultMessage: 'events; Page {activePage} of {totalPages}', + }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/types.ts b/x-pack/plugins/timelines/public/components/t_grid/types.ts new file mode 100644 index 0000000000000..494e06c9f2e0c --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/types.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 type { + OnColumnSorted, + OnColumnsSorted, + OnColumnRemoved, + OnColumnResized, + OnChangePage, + OnRowSelected, + OnSelectAll, + OnUpdateColumns, +} from '../../../common/types/timeline'; diff --git a/x-pack/plugins/timelines/public/components/tgrid.tsx b/x-pack/plugins/timelines/public/components/tgrid.tsx new file mode 100644 index 0000000000000..9d74c9287236a --- /dev/null +++ b/x-pack/plugins/timelines/public/components/tgrid.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 type { TGridProps } from '../types'; +import { TGridIntegrated, TGridIntegratedProps } from './t_grid/integrated'; +import { TGridStandalone } from './t_grid/standalone'; + +export const TGrid = (props: TGridProps) => { + const { type, ...componentsProps } = props; + if (type === 'standalone') { + return <TGridStandalone {...componentsProps} />; + } else if (type === 'embedded') { + return <TGridIntegrated {...((componentsProps as unknown) as TGridIntegratedProps)} />; + } + return null; +}; + +// eslint-disable-next-line import/no-default-export +export { TGrid as default }; diff --git a/x-pack/plugins/timelines/public/components/truncatable_text/__snapshots__/index.test.tsx.snap b/x-pack/plugins/timelines/public/components/truncatable_text/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..23b930c7a114b --- /dev/null +++ b/x-pack/plugins/timelines/public/components/truncatable_text/__snapshots__/index.test.tsx.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TruncatableText renders correctly against snapshot 1`] = ` +.c0, +.c0 * { + display: inline-block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: top; + white-space: nowrap; +} + +<span + className="c0" +> + Hiding in plain sight +</span> +`; diff --git a/x-pack/plugins/timelines/public/components/truncatable_text/index.test.tsx b/x-pack/plugins/timelines/public/components/truncatable_text/index.test.tsx new file mode 100644 index 0000000000000..f54d9e4ed0b88 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/truncatable_text/index.test.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 { mount, shallow } from 'enzyme'; +import React from 'react'; + +import { TruncatableText } from '.'; + +describe('TruncatableText', () => { + test('renders correctly against snapshot', () => { + const wrapper = shallow(<TruncatableText>{'Hiding in plain sight'}</TruncatableText>); + expect(wrapper).toMatchSnapshot(); + }); + + test('it adds the hidden overflow style', () => { + const wrapper = mount(<TruncatableText>{'Hiding in plain sight'}</TruncatableText>); + + expect(wrapper).toHaveStyleRule('overflow', 'hidden'); + }); + + test('it adds the ellipsis text-overflow style', () => { + const wrapper = mount(<TruncatableText>{'Dramatic pause'}</TruncatableText>); + + expect(wrapper).toHaveStyleRule('text-overflow', 'ellipsis'); + }); + + test('it adds the nowrap white-space style', () => { + const wrapper = mount(<TruncatableText>{'Who stopped the beats?'}</TruncatableText>); + + expect(wrapper).toHaveStyleRule('white-space', 'nowrap'); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/truncatable_text/index.tsx b/x-pack/plugins/timelines/public/components/truncatable_text/index.tsx new file mode 100644 index 0000000000000..2dd3c35f731e9 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/truncatable_text/index.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import styled from 'styled-components'; + +/** + * Applies CSS styling to enable text to be truncated with an ellipsis. + * Example: "Don't leave me hanging..." + * + * Note: Requires a parent container with a defined width or max-width. + */ + +export const TruncatableText = styled.span` + &, + & * { + display: inline-block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: top; + white-space: nowrap; + } +`; +TruncatableText.displayName = 'TruncatableText'; diff --git a/x-pack/plugins/timelines/public/components/utils/helpers.ts b/x-pack/plugins/timelines/public/components/utils/helpers.ts new file mode 100644 index 0000000000000..29d83eb1bd7aa --- /dev/null +++ b/x-pack/plugins/timelines/public/components/utils/helpers.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const getIconFromType = (type: string | null) => { + switch (type) { + case 'string': // fall through + case 'keyword': + return 'string'; + case 'number': // fall through + case 'long': + return 'number'; + case 'date': + return 'clock'; + case 'ip': + case 'geo_point': + return 'globe'; + case 'object': + return 'questionInCircle'; + case 'float': + return 'number'; + default: + return 'questionInCircle'; + } +}; diff --git a/x-pack/plugins/timelines/public/components/utils/keury/index.test.ts b/x-pack/plugins/timelines/public/components/utils/keury/index.test.ts new file mode 100644 index 0000000000000..936053a18be5c --- /dev/null +++ b/x-pack/plugins/timelines/public/components/utils/keury/index.test.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { escapeKuery } from '.'; + +describe('Kuery escape', () => { + it('should not remove white spaces quotes', () => { + const value = ' netcat'; + const expected = ' netcat'; + expect(escapeKuery(value)).to.be(expected); + }); + + it('should escape quotes', () => { + const value = 'I said, "Hello."'; + const expected = 'I said, \\"Hello.\\"'; + expect(escapeKuery(value)).to.be(expected); + }); + + it('should escape special characters', () => { + const value = `This \\ has (a lot of) <special> characters, don't you *think*? "Yes."`; + const expected = `This \\ has (a lot of) <special> characters, don't you *think*? \\"Yes.\\"`; + expect(escapeKuery(value)).to.be(expected); + }); + + it('should NOT escape keywords', () => { + const value = 'foo and bar or baz not qux'; + const expected = 'foo and bar or baz not qux'; + expect(escapeKuery(value)).to.be(expected); + }); + + it('should NOT escape keywords next to each other', () => { + const value = 'foo and bar or not baz'; + const expected = 'foo and bar or not baz'; + expect(escapeKuery(value)).to.be(expected); + }); + + it('should not escape keywords without surrounding spaces', () => { + const value = 'And this has keywords, or does it not?'; + const expected = 'And this has keywords, or does it not?'; + expect(escapeKuery(value)).to.be(expected); + }); + + it('should NOT escape uppercase keywords', () => { + const value = 'foo AND bar'; + const expected = 'foo AND bar'; + expect(escapeKuery(value)).to.be(expected); + }); + + it('should escape special characters and NOT keywords', () => { + const value = 'Hello, "world", and <nice> to meet you!'; + const expected = 'Hello, \\"world\\", and <nice> to meet you!'; + expect(escapeKuery(value)).to.be(expected); + }); + + it('should escape newlines and tabs', () => { + const value = 'This\nhas\tnewlines\r\nwith\ttabs'; + const expected = 'This\\nhas\\tnewlines\\r\\nwith\\ttabs'; + expect(escapeKuery(value)).to.be(expected); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/utils/keury/index.ts b/x-pack/plugins/timelines/public/components/utils/keury/index.ts new file mode 100644 index 0000000000000..e31d682fd7021 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/utils/keury/index.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEmpty, isString, flow } from 'lodash/fp'; +import { JsonObject } from '@kbn/common-utils'; + +import { + EsQueryConfig, + Query, + Filter, + esQuery, + esKuery, + IIndexPattern, +} from '../../../../../../../src/plugins/data/public'; + +export const convertKueryToElasticSearchQuery = ( + kueryExpression: string, + indexPattern?: IIndexPattern +) => { + try { + return kueryExpression + ? JSON.stringify( + esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) + ) + : ''; + } catch (err) { + return ''; + } +}; + +export const convertKueryToDslFilter = ( + kueryExpression: string, + indexPattern: IIndexPattern +): JsonObject => { + try { + return kueryExpression + ? esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) + : {}; + } catch (err) { + return {}; + } +}; + +export const escapeQueryValue = (val: number | string = ''): string | number => { + if (isString(val)) { + if (isEmpty(val)) { + return '""'; + } + return `"${escapeKuery(val)}"`; + } + + return val; +}; + +const escapeWhitespace = (val: string) => + val.replace(/\t/g, '\\t').replace(/\r/g, '\\r').replace(/\n/g, '\\n'); + +// See the SpecialCharacter rule in kuery.peg +const escapeSpecialCharacters = (val: string) => val.replace(/["]/g, '\\$&'); // $& means the whole matched string + +// See the Keyword rule in kuery.peg +// I do not think that we need that anymore since we are doing a full match_phrase all the time now => return `"${escapeKuery(val)}"`; +// const escapeAndOr = (val: string) => val.replace(/(\s+)(and|or)(\s+)/gi, '$1\\$2$3'); + +// const escapeNot = (val: string) => val.replace(/not(\s+)/gi, '\\$&'); + +export const escapeKuery = flow(escapeSpecialCharacters, escapeWhitespace); + +export const convertToBuildEsQuery = ({ + config, + indexPattern, + queries, + filters, +}: { + config: EsQueryConfig; + indexPattern: IIndexPattern; + queries: Query[]; + filters: Filter[]; +}) => { + try { + return JSON.stringify( + esQuery.buildEsQuery( + indexPattern, + queries, + filters.filter((f) => f.meta.disabled === false), + { + ...config, + dateFormatTZ: undefined, + } + ) + ); + } catch (exp) { + return ''; + } +}; diff --git a/x-pack/plugins/timelines/public/components/utils/use_mount_appended.ts b/x-pack/plugins/timelines/public/components/utils/use_mount_appended.ts new file mode 100644 index 0000000000000..e63a2b20a5ad5 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/utils/use_mount_appended.ts @@ -0,0 +1,31 @@ +/* + * 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-next-line import/no-extraneous-dependencies +import { mount } from 'enzyme'; + +type WrapperOf<F extends (...args: any) => any> = (...args: Parameters<F>) => ReturnType<F>; // eslint-disable-line +export type MountAppended = WrapperOf<typeof mount>; + +export const useMountAppended = () => { + let root: HTMLElement; + + beforeEach(() => { + root = document.createElement('div'); + root.id = 'root'; + document.body.appendChild(root); + }); + + afterEach(() => { + document.body.removeChild(root); + }); + + const mountAppended: MountAppended = (node, options) => + mount(node, { ...options, attachTo: root }); + + return mountAppended; +}; diff --git a/x-pack/plugins/timelines/public/container/index.tsx b/x-pack/plugins/timelines/public/container/index.tsx new file mode 100644 index 0000000000000..d8797e2335475 --- /dev/null +++ b/x-pack/plugins/timelines/public/container/index.tsx @@ -0,0 +1,346 @@ +/* + * 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 deepEqual from 'fast-deep-equal'; +import { isEmpty, isString, noop } from 'lodash/fp'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { Subscription } from 'rxjs'; +import { tGridActions } from '..'; + +import { + DataPublicPluginStart, + isCompleteResponse, + isErrorResponse, +} from '../../../../../src/plugins/data/public'; +import { + Direction, + TimelineFactoryQueryTypes, + TimelineEventsQueries, +} from '../../common/search_strategy'; +// eslint-disable-next-line no-duplicate-imports +import type { + DocValueFields, + Inspect, + PaginationInputPaginated, + TimelineStrategyResponseType, + TimelineEdges, + TimelineEventsAllRequestOptions, + TimelineEventsAllStrategyResponse, + TimelineItem, + TimelineRequestSortField, +} from '../../common/search_strategy'; +import type { ESQuery } from '../../common/typed_json'; +import type { KueryFilterQueryKind } from '../../common/types/timeline'; +import { useAppToasts } from '../hooks/use_app_toasts'; +import { TimelineId } from '../store/t_grid/types'; +import * as i18n from './translations'; + +type InspectResponse = Inspect & { response: string[] }; + +export const detectionsTimelineIds = [ + TimelineId.detectionsPage, + TimelineId.detectionsRulesDetailsPage, +]; + +type Refetch = () => void; + +export interface TimelineArgs { + events: TimelineItem[]; + id: string; + inspect: InspectResponse; + loadPage: LoadPage; + pageInfo: Pick<PaginationInputPaginated, 'activePage' | 'querySize'>; + refetch: Refetch; + totalCount: number; + updatedAt: number; +} + +type LoadPage = (newActivePage: number) => void; + +type TimelineRequest<T extends KueryFilterQueryKind> = TimelineEventsAllRequestOptions; + +type TimelineResponse<T extends KueryFilterQueryKind> = TimelineEventsAllStrategyResponse; + +export interface UseTimelineEventsProps { + docValueFields?: DocValueFields[]; + filterQuery?: ESQuery | string; + skip?: boolean; + endDate: string; + excludeEcsData?: boolean; + id: string; + fields: string[]; + indexNames: string[]; + language?: KueryFilterQueryKind; + limit: number; + sort?: TimelineRequestSortField[]; + startDate: string; + timerangeKind?: 'absolute' | 'relative'; + data?: DataPublicPluginStart; +} + +const createFilter = (filterQuery: ESQuery | string | undefined) => + isString(filterQuery) ? filterQuery : JSON.stringify(filterQuery); + +const getTimelineEvents = (timelineEdges: TimelineEdges[]): TimelineItem[] => + timelineEdges.map((e: TimelineEdges) => e.node); + +const getInspectResponse = <T extends TimelineFactoryQueryTypes>( + response: TimelineStrategyResponseType<T>, + prevResponse: InspectResponse +): InspectResponse => ({ + dsl: response?.inspect?.dsl ?? prevResponse?.dsl ?? [], + response: + response != null ? [JSON.stringify(response.rawResponse, null, 2)] : prevResponse?.response, +}); + +const ID = 'timelineEventsQuery'; +export const initSortDefault = [ + { + field: '@timestamp', + direction: Direction.asc, + type: 'number', + }, +]; + +export const useTimelineEvents = ({ + docValueFields, + endDate, + excludeEcsData = false, + id = ID, + indexNames, + fields, + filterQuery, + startDate, + language = 'kuery', + limit, + sort = initSortDefault, + skip = false, + timerangeKind, + data, +}: UseTimelineEventsProps): [boolean, TimelineArgs] => { + const dispatch = useDispatch(); + const refetch = useRef<Refetch>(noop); + const abortCtrl = useRef(new AbortController()); + const searchSubscription$ = useRef(new Subscription()); + const [loading, setLoading] = useState(false); + const [activePage, setActivePage] = useState(0); + const [timelineRequest, setTimelineRequest] = useState<TimelineRequest<typeof language> | null>( + null + ); + const prevTimelineRequest = useRef<TimelineRequest<typeof language> | null>(null); + + const clearSignalsState = useCallback(() => { + if (id != null && detectionsTimelineIds.some((timelineId) => timelineId === id)) { + dispatch(tGridActions.clearEventsLoading({ id })); + dispatch(tGridActions.clearEventsDeleted({ id })); + } + }, [dispatch, id]); + + const wrappedLoadPage = useCallback( + (newActivePage: number) => { + clearSignalsState(); + setActivePage(newActivePage); + }, + [clearSignalsState] + ); + + const refetchGrid = useCallback(() => { + if (refetch.current != null) { + refetch.current(); + } + wrappedLoadPage(0); + }, [wrappedLoadPage]); + + const [timelineResponse, setTimelineResponse] = useState<TimelineArgs>({ + id, + inspect: { + dsl: [], + response: [], + }, + refetch: refetchGrid, + totalCount: -1, + pageInfo: { + activePage: 0, + querySize: 0, + }, + events: [], + loadPage: wrappedLoadPage, + updatedAt: 0, + }); + const { addError, addWarning } = useAppToasts(); + + const timelineSearch = useCallback( + (request: TimelineRequest<typeof language> | null) => { + if (request == null || skip) { + return; + } + + const asyncSearch = async () => { + prevTimelineRequest.current = request; + abortCtrl.current = new AbortController(); + setLoading(true); + if (data && data.search) { + searchSubscription$.current = data.search + .search<TimelineRequest<typeof language>, TimelineResponse<typeof language>>(request, { + strategy: + request.language === 'eql' ? 'timelineEqlSearchStrategy' : 'timelineSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (isCompleteResponse(response)) { + setLoading(false); + setTimelineResponse((prevResponse) => { + const newTimelineResponse = { + ...prevResponse, + events: getTimelineEvents(response.edges), + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + totalCount: response.totalCount, + updatedAt: Date.now(), + }; + return newTimelineResponse; + }); + searchSubscription$.current.unsubscribe(); + } else if (isErrorResponse(response)) { + setLoading(false); + addWarning(i18n.ERROR_TIMELINE_EVENTS); + searchSubscription$.current.unsubscribe(); + } + }, + error: (msg) => { + setLoading(false); + addError(msg, { + title: i18n.FAIL_TIMELINE_EVENTS, + }); + searchSubscription$.current.unsubscribe(); + }, + }); + } + }; + + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + }, + [data, addWarning, addError, skip] + ); + + useEffect(() => { + if (indexNames.length === 0) { + return; + } + + setTimelineRequest((prevRequest) => { + const prevSearchParameters = { + defaultIndex: prevRequest?.defaultIndex ?? [], + filterQuery: prevRequest?.filterQuery ?? '', + querySize: prevRequest?.pagination.querySize ?? 0, + sort: prevRequest?.sort ?? initSortDefault, + timerange: prevRequest?.timerange ?? {}, + }; + + const currentSearchParameters = { + defaultIndex: indexNames, + filterQuery: createFilter(filterQuery), + querySize: limit, + sort, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }; + + const newActivePage = deepEqual(prevSearchParameters, currentSearchParameters) + ? activePage + : 0; + + const currentRequest = { + defaultIndex: indexNames, + docValueFields: docValueFields ?? [], + excludeEcsData, + factoryQueryType: TimelineEventsQueries.all, + fieldRequested: fields, + fields: [], + filterQuery: createFilter(filterQuery), + pagination: { + activePage: newActivePage, + querySize: limit, + }, + language, + sort, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }; + + if (activePage !== newActivePage) { + setActivePage(newActivePage); + } + if (!deepEqual(prevRequest, currentRequest)) { + return currentRequest; + } + return prevRequest; + }); + }, [ + dispatch, + indexNames, + activePage, + docValueFields, + endDate, + excludeEcsData, + filterQuery, + id, + language, + limit, + startDate, + sort, + fields, + ]); + + useEffect(() => { + if (!deepEqual(prevTimelineRequest.current, timelineRequest)) { + timelineSearch(timelineRequest); + } + return () => { + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + }; + }, [id, timelineRequest, timelineSearch, timerangeKind]); + + /* + cleanup timeline events response when the filters were removed completely + to avoid displaying previous query results + */ + useEffect(() => { + if (isEmpty(filterQuery)) { + setTimelineResponse({ + id, + inspect: { + dsl: [], + response: [], + }, + refetch: refetchGrid, + totalCount: -1, + pageInfo: { + activePage: 0, + querySize: 0, + }, + events: [], + loadPage: wrappedLoadPage, + updatedAt: 0, + }); + } + }, [filterQuery, id, refetchGrid, wrappedLoadPage]); + + return [loading, timelineResponse]; +}; diff --git a/x-pack/plugins/timelines/public/container/translations.ts b/x-pack/plugins/timelines/public/container/translations.ts new file mode 100644 index 0000000000000..4e159f6a5976f --- /dev/null +++ b/x-pack/plugins/timelines/public/container/translations.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_TIMELINE_EVENTS = i18n.translate( + 'xpack.timelines.timelineEvents.errorSearchDescription', + { + defaultMessage: `An error has occurred on timeline events search`, + } +); + +export const FAIL_TIMELINE_EVENTS = i18n.translate( + 'xpack.timelines.timelineEvents.failSearchDescription', + { + defaultMessage: `Failed to run search on timeline events`, + } +); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_add_to_timeline.tsx b/x-pack/plugins/timelines/public/hooks/use_add_to_timeline.ts similarity index 90% rename from x-pack/plugins/security_solution/public/common/hooks/use_add_to_timeline.tsx rename to x-pack/plugins/timelines/public/hooks/use_add_to_timeline.ts index 35f79be17a9e4..10382853405ab 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_add_to_timeline.tsx +++ b/x-pack/plugins/timelines/public/hooks/use_add_to_timeline.ts @@ -4,14 +4,16 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import d3 from 'd3'; +import { range } from 'd3-array'; +import { interpolate } from 'd3-interpolate'; import { useCallback } from 'react'; -import { DraggableId, FluidDragActions, Position, SensorAPI } from 'react-beautiful-dnd'; +import type { DraggableId, FluidDragActions, Position, SensorAPI } from 'react-beautiful-dnd'; -import { IS_DRAGGING_CLASS_NAME } from '../components/drag_and_drop/helpers'; -import { HIGHLIGHTED_DROP_TARGET_CLASS_NAME } from '../../timelines/components/timeline/data_providers/empty'; -import { EMPTY_PROVIDERS_GROUP_CLASS_NAME } from '../../timelines/components/timeline/data_providers/providers'; +import { + EMPTY_PROVIDERS_GROUP_CLASS_NAME, + HIGHLIGHTED_DROP_TARGET_CLASS_NAME, + IS_DRAGGING_CLASS_NAME, +} from '@kbn/securitysolution-t-grid'; let _sensorApiSingleton: SensorAPI; @@ -120,16 +122,7 @@ export const animate = ({ }); }; -/** - * This hook animates a draggable data provider to the timeline - */ -export const useAddToTimeline = ({ - draggableId, - fieldName, -}: { - draggableId: DraggableId | undefined; - fieldName: string; -}): { +export interface UseAddToTimeline { beginDrag: () => FluidDragActions | null; cancelDrag: (dragActions: FluidDragActions | null) => void; dragToLocation: ({ @@ -142,7 +135,20 @@ export const useAddToTimeline = ({ endDrag: (dragActions: FluidDragActions | null) => void; hasDraggableLock: () => boolean; startDragToTimeline: () => void; -} => { +} + +export interface UseAddToTimelineProps { + draggableId: DraggableId | undefined; + fieldName: string; +} + +/** + * This hook animates a draggable data provider to the timeline + */ +export const useAddToTimeline = ({ + draggableId, + fieldName, +}: UseAddToTimelineProps): UseAddToTimeline => { const startDragToTimeline = useCallback(() => { if (_sensorApiSingleton == null) { throw new TypeError( @@ -167,9 +173,9 @@ export const useAddToTimeline = ({ if (draggableCoordinate != null && dropTargetCoordinate != null && preDrag != null) { const steps = 10; - const points = d3.range(steps + 1).map((i) => ({ - x: d3.interpolate(draggableCoordinate.x, dropTargetCoordinate.x)(i * 0.1), - y: d3.interpolate(draggableCoordinate.y, dropTargetCoordinate.y)(i * 0.1), + const points = range(steps + 1).map((i) => ({ + x: interpolate(draggableCoordinate.x, dropTargetCoordinate.x)(i * 0.1), + y: interpolate(draggableCoordinate.y, dropTargetCoordinate.y)(i * 0.1), })); const drag = preDrag.fluidLift(draggableCoordinate); @@ -182,6 +188,7 @@ export const useAddToTimeline = ({ document.body.classList.remove(IS_DRAGGING_CLASS_NAME); // it was not possible to perform a drag and drop } }, 0); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [_sensorApiSingleton, draggableId]); diff --git a/x-pack/plugins/timelines/public/hooks/use_app_toasts.ts b/x-pack/plugins/timelines/public/hooks/use_app_toasts.ts new file mode 100644 index 0000000000000..d08d8ea8e8a34 --- /dev/null +++ b/x-pack/plugins/timelines/public/hooks/use_app_toasts.ts @@ -0,0 +1,241 @@ +/* + * 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 { useCallback, useRef } from 'react'; +import { isString } from 'lodash/fp'; +import { isAppError, isKibanaError, isSecurityAppError } from '@kbn/securitysolution-t-grid'; +// eslint-disable-next-line no-duplicate-imports +import type { AppError } from '@kbn/securitysolution-t-grid'; + +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { + ErrorToastOptions, + ToastsStart, + Toast, + NotificationsStart, +} from '../../../../../src/core/public'; +import { IEsError, isEsError } from '../../../../../src/plugins/data/public'; + +export type UseAppToasts = Pick<ToastsStart, 'addSuccess' | 'addWarning'> & { + api: ToastsStart; + addError: (error: unknown, options: ErrorToastOptions) => Toast; +}; + +/** + * This gives a better presentation of error data sent from the API (both general platform errors and app-specific errors). + * This uses platform's new Toasts service to prevent modal/toast z-index collision issues. + * This fixes some issues you can see with re-rendering since using a class such as notifications.toasts. + * This also has an adapter and transform for detecting if a bsearch's EsError is present and then adapts that to the + * Kibana error toaster model so that the network error message will be shown rather than a stack trace. + */ +export const useAppToasts = (): UseAppToasts => { + const { toasts } = useKibana<{ + notifications: NotificationsStart; + }>().services.notifications; + const addError = useRef(toasts?.addError.bind(toasts)).current; + const addSuccess = useRef(toasts?.addSuccess.bind(toasts)).current; + const addWarning = useRef(toasts?.addWarning.bind(toasts)).current; + + const _addError = useCallback( + (error: unknown, options: ErrorToastOptions) => { + const adaptedError = errorToErrorStackAdapter(error); + return addError(adaptedError, options); + }, + [addError] + ); + return { api: toasts, addError: _addError, addSuccess, addWarning }; +}; + +/** + * Given an error of one type vs. another type this tries to adapt + * the best it can to the existing error toaster which parses the .stack + * as its error when you click the button to show the full error message. + * @param error The error to adapt to. + * @returns The adapted toaster error message. + */ +export const errorToErrorStackAdapter = (error: unknown): Error => { + if (error != null && isEsError(error)) { + return esErrorToErrorStack(error); + } else if (isAppError(error)) { + return appErrorToErrorStack(error); + } else if (error instanceof Error) { + return errorToErrorStack(error); + } else { + return unknownToErrorStack(error); + } +}; + +/** + * See this file, we are not allowed to import files such as es_error. + * So instead we say maybe err is on there so that we can unwrap it and get + * our status code from it if possible within the error in our function. + * src/plugins/data/public/search/errors/es_error.tsx + */ +export type MaybeESError = IEsError & { err?: Record<string, unknown> }; + +/** + * This attempts its best to map between an IEsError which comes from bsearch to a error_toaster + * See the file: src/core/public/notifications/toasts/error_toast.tsx + * + * NOTE: This is brittle at the moment from bsearch and the hope is that better support between + * the error message and formatting of bsearch and the error_toast.tsx from Kibana core will be + * supported in the future. However, for now, this is _hopefully_ temporary. + * + * Also see the file: + * x-pack/plugins/security_solution/public/app/home/setup.tsx + * + * Where this same technique of overriding and changing the stack is occurring. + */ +export const esErrorToErrorStack = (error: IEsError & MaybeESError): Error => { + const maybeUnWrapped = error.err != null ? error.err : error; + const statusCode = + error.err?.statusCode != null + ? `(${error.err.statusCode})` + : error.statusCode != null + ? `(${error.statusCode})` + : ''; + const stringifiedError = getStringifiedStack(maybeUnWrapped); + const adaptedError = new Error(`${error.attributes?.reason ?? error.message} ${statusCode}`); + adaptedError.name = error.attributes?.reason ?? error.message; + if (stringifiedError != null) { + adaptedError.stack = stringifiedError; + } + return adaptedError; +}; + +/** + * This attempts its best to map between a Kibana application error which can come from backend + * REST API's that are typically of a particular format and form. + * + * The existing error_toaster code tries to consolidate network and software stack traces but really + * here and our toasters we are using them for network response errors so we can troubleshoot things + * as quick as possible. + * + * We override and use error.stack to be able to give _full_ network responses regardless of if they + * are from Kibana or if they are from elasticSearch since sometimes Kibana errors might wrap the errors. + * + * Sometimes the errors are wrapped from io-ts, Kibana Schema or something else and we want to show + * as full error messages as we can. + */ +export const appErrorToErrorStack = (error: AppError): Error => { + const statusCode = isKibanaError(error) + ? `(${error.body.statusCode})` + : isSecurityAppError(error) + ? `(${error.body.status_code})` + : ''; + const stringifiedError = getStringifiedStack(error); + const adaptedError = new Error( + `${String(error.body.message).trim() !== '' ? error.body.message : error.message} ${statusCode}` + ); + // Note although all the Typescript typings say that error.name is a string and exists, we still can encounter an undefined so we + // do an extra guard here and default to empty string if it is undefined + adaptedError.name = error.name != null ? error.name : ''; + if (stringifiedError != null) { + adaptedError.stack = stringifiedError; + } + return adaptedError; +}; + +/** + * Takes an error and tries to stringify it and use that as the stack for the error toaster + * @param error The error to convert into a message + * @returns The exception error to return back + */ +export const errorToErrorStack = (error: Error): Error => { + const stringifiedError = getStringifiedStack(error); + const adaptedError = new Error(error.message); + adaptedError.name = error.name; + if (stringifiedError != null) { + adaptedError.stack = stringifiedError; + } + return adaptedError; +}; + +/** + * Last ditch effort to take something unknown which could be a string, number, + * anything. This usually should not be called but just in case we do try our + * best to stringify it and give a message, name, and replace the stack of it. + * @param error The unknown error to convert into a message + * @returns The exception error to return back + */ +export const unknownToErrorStack = (error: unknown): Error => { + const stringifiedError = getStringifiedStack(error); + const message = isString(error) + ? error + : error instanceof Object && stringifiedError != null + ? stringifiedError + : String(error); + const adaptedError = new Error(message); + adaptedError.name = message; + if (stringifiedError != null) { + adaptedError.stack = stringifiedError; + } + return adaptedError; +}; + +/** + * Stringifies the error. However, since Errors can JSON.stringify into empty objects this will + * use a replacer to push those as enumerable properties so we can stringify them. + * @param error The error to get a string representation of + * @returns The string representation of the error + */ +export const getStringifiedStack = (error: unknown): string | undefined => { + try { + return JSON.stringify( + error, + (_, value) => { + const enumerable = convertErrorToEnumerable(value); + if (isEmptyObjectWhenStringified(enumerable)) { + return undefined; + } else { + return enumerable; + } + }, + 2 + ); + } catch (err) { + return undefined; + } +}; + +/** + * Converts an error if this is an error to have enumerable so it can stringified + * @param error The error which might not have enumerable properties. + * @returns Enumerable error + */ +export const convertErrorToEnumerable = (error: unknown): unknown => { + if (error instanceof Error) { + return { + ...error, + name: error.name, + message: error.message, + stack: error.stack, + }; + } else { + return error; + } +}; + +/** + * If the object strings into an empty object we shouldn't show it as it doesn't + * add value and sometimes different people/frameworks attach req,res,request,response + * objects which don't stringify into anything or can have circular references. + * @param item The item to see if we are empty or have a circular reference error with. + * @returns True if this is a good object to stringify, otherwise false + */ +export const isEmptyObjectWhenStringified = (item: unknown): boolean => { + if (item instanceof Object) { + try { + return JSON.stringify(item) === '{}'; + } catch (_) { + // Do nothing, return false if we have a circular reference or other oddness. + return false; + } + } else { + return false; + } +}; diff --git a/x-pack/plugins/timelines/public/hooks/use_selector.tsx b/x-pack/plugins/timelines/public/hooks/use_selector.tsx new file mode 100644 index 0000000000000..07e1e7d5cc298 --- /dev/null +++ b/x-pack/plugins/timelines/public/hooks/use_selector.tsx @@ -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 { shallowEqual, useSelector } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; + +export type TypedUseSelectorHook = <TSelected, TState = unknown>( + selector: (state: TState) => TSelected, + equalityFn?: (left: TSelected, right: TSelected) => boolean +) => TSelected; + +export const useShallowEqualSelector: TypedUseSelectorHook = (selector) => + useSelector(selector, shallowEqual); + +export const useDeepEqualSelector: TypedUseSelectorHook = (selector) => + useSelector(selector, deepEqual); diff --git a/x-pack/plugins/timelines/public/index.scss b/x-pack/plugins/timelines/public/index.scss deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/x-pack/plugins/timelines/public/index.ts b/x-pack/plugins/timelines/public/index.ts index c3d24d49e2401..9fe022a670399 100644 --- a/x-pack/plugins/timelines/public/index.ts +++ b/x-pack/plugins/timelines/public/index.ts @@ -5,14 +5,55 @@ * 2.0. */ -import './index.scss'; +import { PluginInitializerContext } from '../../../../src/core/public'; -import { PluginInitializerContext } from 'src/core/public'; import { TimelinesPlugin } from './plugin'; +export * as tGridActions from './store/t_grid/actions'; +export * as tGridSelectors from './store/t_grid/selectors'; +export type { + Inspect, + SortField, + TimerangeInput, + PaginationInputPaginated, + DocValueFields, + CursorType, + TotalValue, +} from '../common/search_strategy/common'; +export { Direction } from '../common/search_strategy/common'; +export { tGridReducer } from './store/t_grid/reducer'; +export type { TGridModelForTimeline, TimelineState, TimelinesUIStart } from './types'; +export { + ARIA_COLINDEX_ATTRIBUTE, + ARIA_ROWINDEX_ATTRIBUTE, + DATA_COLINDEX_ATTRIBUTE, + DATA_ROWINDEX_ATTRIBUTE, + FIRST_ARIA_INDEX, + OnColumnFocused, + arrayIndexToAriaIndex, + elementOrChildrenHasFocus, + isArrowDownOrArrowUp, + isArrowUp, + isEscape, + isTab, + focusColumn, + getFocusedAriaColindexCell, + getFocusedDataColindexCell, + getNotesContainerClassName, + getRowRendererClassName, + getTableSkipFocus, + handleSkipFocus, + onFocusReFocusDraggable, + onKeyDownFocusHandler, + skipFocusInContainerTo, + stopPropagationAndPreventDefault, +} from '../common/utils/accessibility'; +export { + addFieldToTimelineColumns, + getTimelineIdFromColumnDroppableId, +} from './components/drag_and_drop/helpers'; // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. export function plugin(initializerContext: PluginInitializerContext) { return new TimelinesPlugin(initializerContext); } -export { TimelinesPluginSetup } from './types'; diff --git a/x-pack/plugins/timelines/public/methods/index.tsx b/x-pack/plugins/timelines/public/methods/index.tsx index f999e14ce910c..cd98021c500c5 100644 --- a/x-pack/plugins/timelines/public/methods/index.tsx +++ b/x-pack/plugins/timelines/public/methods/index.tsx @@ -5,15 +5,40 @@ * 2.0. */ +import { Store } from 'redux'; import React, { lazy, Suspense } from 'react'; import { EuiLoadingSpinner } from '@elastic/eui'; -import { TimelineProps } from '../types'; +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; +import type { TGridProps } from '../types'; +import { LastUpdatedAtProps, LoadingPanelProps } from '../components'; -export const getTimelineLazy = (props: TimelineProps) => { - const TimelineLazy = lazy(() => import('../components')); +const TimelineLazy = lazy(() => import('../components')); +export const getTGridLazy = ( + props: TGridProps, + { store, storage, data }: { store?: Store; storage: Storage; data: DataPublicPluginStart } +) => { return ( <Suspense fallback={<EuiLoadingSpinner />}> - <TimelineLazy {...props} /> + <TimelineLazy {...props} store={store} storage={storage} data={data} /> + </Suspense> + ); +}; + +const LastUpdatedLazy = lazy(() => import('../components/last_updated')); +export const getLastUpdatedLazy = (props: LastUpdatedAtProps) => { + return ( + <Suspense fallback={<EuiLoadingSpinner />}> + <LastUpdatedLazy {...props} /> + </Suspense> + ); +}; + +const LoadingPanelLazy = lazy(() => import('../components/loading')); +export const getLoadingPanelLazy = (props: LoadingPanelProps) => { + return ( + <Suspense fallback={<EuiLoadingSpinner />}> + <LoadingPanelLazy {...props} /> </Suspense> ); }; diff --git a/x-pack/plugins/timelines/public/mock/browser_fields.ts b/x-pack/plugins/timelines/public/mock/browser_fields.ts new file mode 100644 index 0000000000000..1581175e32904 --- /dev/null +++ b/x-pack/plugins/timelines/public/mock/browser_fields.ts @@ -0,0 +1,737 @@ +/* + * 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 { DocValueFields } from '../../common/search_strategy'; +import type { BrowserFields } from '../../common/search_strategy/index_fields'; + +const DEFAULT_INDEX_PATTERN = [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', +]; + +export const mocksSource = { + indexFields: [ + { + category: 'base', + description: + 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', + example: '2016-05-23T08:05:34.853Z', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: '@timestamp', + searchable: true, + type: 'date', + aggregatable: true, + }, + { + category: 'agent', + description: + 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', + example: '8a4f500f', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.ephemeral_id', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'agent', + description: null, + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.hostname', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'agent', + description: + 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', + example: '8a4f500d', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.id', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'agent', + description: + 'Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', + example: 'foo', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.name', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a0', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a1', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a2', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'client', + description: + 'Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.address', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'client', + description: 'Bytes sent from the client to the server.', + example: '184', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.bytes', + searchable: true, + type: 'number', + aggregatable: true, + }, + { + category: 'client', + description: 'Client domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.domain', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'client', + description: 'Country ISO code.', + example: 'CA', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.geo.country_iso_code', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'cloud', + description: + 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', + example: '666777888999', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.account.id', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'cloud', + description: 'Availability zone in which this host is running.', + example: 'us-east-1c', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.availability_zone', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'container', + description: 'Unique container id.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.id', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'container', + description: 'Name of the image the container was built on.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.image.name', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'container', + description: 'Container image tag.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.image.tag', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'destination', + description: + 'Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.address', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'destination', + description: 'Bytes sent from the destination to the source.', + example: '184', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.bytes', + searchable: true, + type: 'number', + aggregatable: true, + }, + { + category: 'destination', + description: 'Destination domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.domain', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + aggregatable: true, + category: 'destination', + description: 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.ip', + searchable: true, + type: 'ip', + }, + { + aggregatable: true, + category: 'destination', + description: 'Port of the destination.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.port', + searchable: true, + type: 'long', + }, + { + aggregatable: true, + category: 'source', + description: 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'source.ip', + searchable: true, + type: 'ip', + }, + { + aggregatable: true, + category: 'source', + description: 'Port of the source.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'source.port', + searchable: true, + type: 'long', + }, + { + aggregatable: true, + category: 'event', + description: + 'event.end contains the date when the event ended or when the activity was last observed.', + example: null, + format: '', + indexes: DEFAULT_INDEX_PATTERN, + name: 'event.end', + searchable: true, + type: 'date', + }, + { + aggregatable: false, + category: 'nestedField', + description: '', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'nestedField.firstAttributes', + searchable: true, + type: 'string', + subType: { + nested: { + path: 'nestedField', + }, + }, + }, + { + aggregatable: false, + category: 'nestedField', + description: '', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'nestedField.secondAttributes', + searchable: true, + type: 'string', + subType: { + nested: { + path: 'nestedField', + }, + }, + }, + ], +}; + +export const mockIndexFields = [ + { aggregatable: true, name: '@timestamp', searchable: true, type: 'date' }, + { aggregatable: true, name: 'agent.ephemeral_id', searchable: true, type: 'string' }, + { aggregatable: true, name: 'agent.hostname', searchable: true, type: 'string' }, + { aggregatable: true, name: 'agent.id', searchable: true, type: 'string' }, + { aggregatable: true, name: 'agent.name', searchable: true, type: 'string' }, + { aggregatable: true, name: 'auditd.data.a0', searchable: true, type: 'string' }, + { aggregatable: true, name: 'auditd.data.a1', searchable: true, type: 'string' }, + { aggregatable: true, name: 'auditd.data.a2', searchable: true, type: 'string' }, + { aggregatable: true, name: 'client.address', searchable: true, type: 'string' }, + { aggregatable: true, name: 'client.bytes', searchable: true, type: 'number' }, + { aggregatable: true, name: 'client.domain', searchable: true, type: 'string' }, + { aggregatable: true, name: 'client.geo.country_iso_code', searchable: true, type: 'string' }, + { aggregatable: true, name: 'cloud.account.id', searchable: true, type: 'string' }, + { aggregatable: true, name: 'cloud.availability_zone', searchable: true, type: 'string' }, + { aggregatable: true, name: 'container.id', searchable: true, type: 'string' }, + { aggregatable: true, name: 'container.image.name', searchable: true, type: 'string' }, + { aggregatable: true, name: 'container.image.tag', searchable: true, type: 'string' }, + { aggregatable: true, name: 'destination.address', searchable: true, type: 'string' }, + { aggregatable: true, name: 'destination.bytes', searchable: true, type: 'number' }, + { aggregatable: true, name: 'destination.domain', searchable: true, type: 'string' }, + { aggregatable: true, name: 'destination.ip', searchable: true, type: 'ip' }, + { aggregatable: true, name: 'destination.port', searchable: true, type: 'long' }, + { aggregatable: true, name: 'source.ip', searchable: true, type: 'ip' }, + { aggregatable: true, name: 'source.port', searchable: true, type: 'long' }, + { aggregatable: true, name: 'event.end', searchable: true, type: 'date' }, +]; + +export const mockBrowserFields: BrowserFields = { + agent: { + fields: { + 'agent.ephemeral_id': { + aggregatable: true, + category: 'agent', + description: + 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', + example: '8a4f500f', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.ephemeral_id', + searchable: true, + type: 'string', + }, + 'agent.hostname': { + aggregatable: true, + category: 'agent', + description: null, + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.hostname', + searchable: true, + type: 'string', + }, + 'agent.id': { + aggregatable: true, + category: 'agent', + description: + 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', + example: '8a4f500d', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.id', + searchable: true, + type: 'string', + }, + 'agent.name': { + aggregatable: true, + category: 'agent', + description: + 'Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', + example: 'foo', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.name', + searchable: true, + type: 'string', + }, + }, + }, + auditd: { + fields: { + 'auditd.data.a0': { + aggregatable: true, + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a0', + searchable: true, + type: 'string', + }, + 'auditd.data.a1': { + aggregatable: true, + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a1', + searchable: true, + type: 'string', + }, + 'auditd.data.a2': { + aggregatable: true, + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a2', + searchable: true, + type: 'string', + }, + }, + }, + base: { + fields: { + '@timestamp': { + aggregatable: true, + category: 'base', + description: + 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', + example: '2016-05-23T08:05:34.853Z', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: '@timestamp', + searchable: true, + type: 'date', + }, + }, + }, + client: { + fields: { + 'client.address': { + aggregatable: true, + category: 'client', + description: + 'Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.address', + searchable: true, + type: 'string', + }, + 'client.bytes': { + aggregatable: true, + category: 'client', + description: 'Bytes sent from the client to the server.', + example: '184', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.bytes', + searchable: true, + type: 'number', + }, + 'client.domain': { + aggregatable: true, + category: 'client', + description: 'Client domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.domain', + searchable: true, + type: 'string', + }, + 'client.geo.country_iso_code': { + aggregatable: true, + category: 'client', + description: 'Country ISO code.', + example: 'CA', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.geo.country_iso_code', + searchable: true, + type: 'string', + }, + }, + }, + cloud: { + fields: { + 'cloud.account.id': { + aggregatable: true, + category: 'cloud', + description: + 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', + example: '666777888999', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.account.id', + searchable: true, + type: 'string', + }, + 'cloud.availability_zone': { + aggregatable: true, + category: 'cloud', + description: 'Availability zone in which this host is running.', + example: 'us-east-1c', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.availability_zone', + searchable: true, + type: 'string', + }, + }, + }, + container: { + fields: { + 'container.id': { + aggregatable: true, + category: 'container', + description: 'Unique container id.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.id', + searchable: true, + type: 'string', + }, + 'container.image.name': { + aggregatable: true, + category: 'container', + description: 'Name of the image the container was built on.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.image.name', + searchable: true, + type: 'string', + }, + 'container.image.tag': { + aggregatable: true, + category: 'container', + description: 'Container image tag.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.image.tag', + searchable: true, + type: 'string', + }, + }, + }, + destination: { + fields: { + 'destination.address': { + aggregatable: true, + category: 'destination', + description: + 'Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.address', + searchable: true, + type: 'string', + }, + 'destination.bytes': { + aggregatable: true, + category: 'destination', + description: 'Bytes sent from the destination to the source.', + example: '184', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.bytes', + searchable: true, + type: 'number', + }, + 'destination.domain': { + aggregatable: true, + category: 'destination', + description: 'Destination domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.domain', + searchable: true, + type: 'string', + }, + 'destination.ip': { + aggregatable: true, + category: 'destination', + description: + 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.ip', + searchable: true, + type: 'ip', + }, + 'destination.port': { + aggregatable: true, + category: 'destination', + description: 'Port of the destination.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.port', + searchable: true, + type: 'long', + }, + }, + }, + event: { + fields: { + 'event.end': { + category: 'event', + description: + 'event.end contains the date when the event ended or when the activity was last observed.', + example: null, + format: '', + indexes: DEFAULT_INDEX_PATTERN, + name: 'event.end', + searchable: true, + type: 'date', + aggregatable: true, + }, + }, + }, + source: { + fields: { + 'source.ip': { + aggregatable: true, + category: 'source', + description: 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'source.ip', + searchable: true, + type: 'ip', + }, + 'source.port': { + aggregatable: true, + category: 'source', + description: 'Port of the source.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'source.port', + searchable: true, + type: 'long', + }, + }, + }, + nestedField: { + fields: { + 'nestedField.firstAttributes': { + aggregatable: false, + category: 'nestedField', + description: '', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'nestedField.firstAttributes', + searchable: true, + type: 'string', + subType: { + nested: { + path: 'nestedField', + }, + }, + }, + 'nestedField.secondAttributes': { + aggregatable: false, + category: 'nestedField', + description: '', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'nestedField.secondAttributes', + searchable: true, + type: 'string', + subType: { + nested: { + path: 'nestedField', + }, + }, + }, + }, + }, +}; + +export const mockDocValueFields: DocValueFields[] = [ + { + field: '@timestamp', + format: 'date_time', + }, + { + field: 'event.end', + format: 'date_time', + }, +]; diff --git a/x-pack/plugins/timelines/public/mock/cell_renderer.tsx b/x-pack/plugins/timelines/public/mock/cell_renderer.tsx new file mode 100644 index 0000000000000..74a20026cf2ab --- /dev/null +++ b/x-pack/plugins/timelines/public/mock/cell_renderer.tsx @@ -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 React from 'react'; + +import { getMappedNonEcsValue } from '../components/t_grid/body/data_driven_columns'; +import type { CellValueElementProps } from '../../common/types/timeline'; + +export const TestCellRenderer: React.FC<CellValueElementProps> = ({ columnId, data }) => ( + <> + {getMappedNonEcsValue({ + data, + fieldName: columnId, + })?.reduce((x) => x[0]) ?? ''} + </> +); diff --git a/x-pack/plugins/timelines/public/mock/global_state.ts b/x-pack/plugins/timelines/public/mock/global_state.ts new file mode 100644 index 0000000000000..bb7bee3d1552a --- /dev/null +++ b/x-pack/plugins/timelines/public/mock/global_state.ts @@ -0,0 +1,53 @@ +/* + * 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 { Direction } from '../../common'; +import { TimelineState } from '../types'; +import { defaultHeaders } from './header'; + +export const mockGlobalState: TimelineState = { + timelineById: { + test: { + columns: defaultHeaders, + dateRange: { + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', + }, + deletedEventIds: [], + excludedRowRendererIds: [], + expandedDetail: {}, + kqlQuery: { filterQuery: null }, + id: 'test', + indexNames: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + isLoading: false, + isSelectAllChecked: false, + itemsPerPage: 5, + itemsPerPageOptions: [5, 10, 20], + loadingEventIds: [], + showCheckboxes: false, + sort: [{ columnId: '@timestamp', columnType: 'number', sortDirection: Direction.desc }], + selectedEventIds: {}, + savedObjectId: null, + version: null, + documentType: '', + defaultColumns: defaultHeaders, + footerText: 'total of events', + loadingText: 'loading events', + queryFields: [], + selectAll: false, + title: 'Events', + }, + }, +}; diff --git a/x-pack/plugins/timelines/public/mock/header.ts b/x-pack/plugins/timelines/public/mock/header.ts new file mode 100644 index 0000000000000..a0a9f0fe15293 --- /dev/null +++ b/x-pack/plugins/timelines/public/mock/header.ts @@ -0,0 +1,133 @@ +/* + * 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 { ColumnHeaderOptions } from '../../common/types/timeline'; +import { defaultColumnHeaderType } from '../components/t_grid/body/column_headers/default_headers'; +import { + DEFAULT_COLUMN_MIN_WIDTH, + DEFAULT_DATE_COLUMN_MIN_WIDTH, +} from '../components/t_grid/body/constants'; + +export const defaultHeaders: ColumnHeaderOptions[] = [ + { + category: 'base', + columnHeaderType: defaultColumnHeaderType, + description: + 'Date/time when the event originated.\nFor log events this is the date/time when the event was generated, and not when it was read.\nRequired field for all events.', + example: '2016-05-23T08:05:34.853Z', + id: '@timestamp', + type: 'date', + aggregatable: true, + initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH, + }, + { + category: 'event', + columnHeaderType: defaultColumnHeaderType, + description: + "Severity describes the severity of the event. What the different severity values mean can very different between use cases. It's up to the implementer to make sure severities are consistent across events.", + example: '7', + id: 'event.severity', + type: 'long', + aggregatable: true, + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + category: 'event', + columnHeaderType: defaultColumnHeaderType, + description: + 'Event category.\nThis contains high-level information about the contents of the event. It is more generic than `event.action`, in the sense that typically a category contains multiple actions. Warning: In future versions of ECS, we plan to provide a list of acceptable values for this field, please use with caution.', + example: 'user-management', + id: 'event.category', + type: 'keyword', + aggregatable: true, + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + category: 'event', + columnHeaderType: defaultColumnHeaderType, + description: + 'The action captured by the event.\nThis describes the information in the event. It is more specific than `event.category`. Examples are `group-add`, `process-started`, `file-created`. The value is normally defined by the implementer.', + example: 'user-password-change', + id: 'event.action', + type: 'keyword', + aggregatable: true, + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + category: 'host', + columnHeaderType: defaultColumnHeaderType, + description: + 'Name of the host.\nIt can contain what `hostname` returns on Unix systems, the fully qualified domain name, or a name specified by the user. The sender decides which value to use.', + example: '', + id: 'host.name', + type: 'keyword', + aggregatable: true, + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + category: 'source', + columnHeaderType: defaultColumnHeaderType, + description: 'IP address of the source.\nCan be one or multiple IPv4 or IPv6 addresses.', + example: '', + id: 'source.ip', + type: 'ip', + aggregatable: true, + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + category: 'destination', + columnHeaderType: defaultColumnHeaderType, + description: 'IP address of the destination.\nCan be one or multiple IPv4 or IPv6 addresses.', + example: '', + id: 'destination.ip', + type: 'ip', + aggregatable: true, + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + aggregatable: true, + category: 'destination', + columnHeaderType: defaultColumnHeaderType, + description: 'Bytes sent from the source to the destination', + example: '123', + format: 'bytes', + id: 'destination.bytes', + type: 'number', + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + category: 'user', + columnHeaderType: defaultColumnHeaderType, + description: 'Short name or login of the user.', + example: 'albert', + id: 'user.name', + type: 'keyword', + aggregatable: true, + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + category: 'base', + columnHeaderType: defaultColumnHeaderType, + description: 'Each document has an _id that uniquely identifies it', + example: 'Y-6TfmcB0WOhS6qyMv3s', + id: '_id', + type: 'keyword', + aggregatable: true, + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + category: 'base', + columnHeaderType: defaultColumnHeaderType, + description: + 'For log events the message field contains the log message.\nIn other use cases the message field can be used to concatenate different values which are then freely searchable. If multiple messages exist, they can be combined into one message.', + example: 'Hello World', + id: 'message', + type: 'text', + aggregatable: false, + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, +]; diff --git a/x-pack/plugins/timelines/public/mock/index.ts b/x-pack/plugins/timelines/public/mock/index.ts new file mode 100644 index 0000000000000..e92097fbe6cc4 --- /dev/null +++ b/x-pack/plugins/timelines/public/mock/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './browser_fields'; +export * from './header'; +export * from './index_pattern'; +export * from './mock_and_providers'; +export * from './mock_data_providers'; +export * from './mock_timeline_control_columns'; +export * from './mock_timeline_data'; +export * from './test_providers'; +export * from './plugin_mock'; diff --git a/x-pack/plugins/timelines/public/mock/index_pattern.ts b/x-pack/plugins/timelines/public/mock/index_pattern.ts new file mode 100644 index 0000000000000..361dbf71bd6f4 --- /dev/null +++ b/x-pack/plugins/timelines/public/mock/index_pattern.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IIndexPattern } from '../../../../../src/plugins/data/public'; + +export const mockIndexPattern: IIndexPattern = { + fields: [ + { + name: '@timestamp', + searchable: true, + type: 'date', + aggregatable: true, + }, + { + name: '@version', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + name: 'agent.ephemeral_id', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + name: 'agent.hostname', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + name: 'agent.id', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + name: 'agent.test1', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + name: 'agent.test2', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + name: 'agent.test3', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + name: 'agent.test4', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + name: 'agent.test5', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + name: 'agent.test6', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + name: 'agent.test7', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + name: 'agent.test8', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + name: 'host.name', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + name: 'nestedField.firstAttributes', + searchable: true, + type: 'string', + aggregatable: false, + }, + { + name: 'nestedField.secondAttributes', + searchable: true, + type: 'string', + aggregatable: false, + }, + ], + title: 'filebeat-*,auditbeat-*,packetbeat-*', +}; + +export const mockIndexNames = ['filebeat-*', 'auditbeat-*', 'packetbeat-*']; diff --git a/x-pack/plugins/timelines/public/mock/kibana_react.mock.ts b/x-pack/plugins/timelines/public/mock/kibana_react.mock.ts new file mode 100644 index 0000000000000..b16be00a6c43f --- /dev/null +++ b/x-pack/plugins/timelines/public/mock/kibana_react.mock.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 React from 'react'; + +import { RecursivePartial } from '@elastic/eui/src/components/common'; +import { coreMock } from '../../../../../src/core/public/mocks'; +import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; +import { EuiTheme } from '../../../../../src/plugins/kibana_react/common'; +import { CoreStart } from '../../../../../src/core/public'; + +export const createStartServicesMock = (): CoreStart => + (coreMock.createStart() as unknown) as CoreStart; + +export const createWithKibanaMock = () => { + const services = createStartServicesMock(); + + return (Component: unknown) => (props: unknown) => { + return React.createElement(Component as string, { ...(props as object), kibana: { services } }); + }; +}; + +export const createKibanaContextProviderMock = () => { + const services = createStartServicesMock(); + + // eslint-disable-next-line react/display-name + return ({ children }: { children: React.ReactNode }) => + React.createElement(KibanaContextProvider, { services }, children); +}; + +export const getMockTheme = (partialTheme: RecursivePartial<EuiTheme>): EuiTheme => + partialTheme as EuiTheme; diff --git a/x-pack/plugins/timelines/public/mock/mock_and_providers.tsx b/x-pack/plugins/timelines/public/mock/mock_and_providers.tsx new file mode 100644 index 0000000000000..a4e49659fbaca --- /dev/null +++ b/x-pack/plugins/timelines/public/mock/mock_and_providers.tsx @@ -0,0 +1,93 @@ +/* + * 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 { IS_OPERATOR } from '../../common/types/timeline'; +// eslint-disable-next-line no-duplicate-imports +import type { DataProvider, DataProvidersAnd } from '../../common/types/timeline'; + +export const providerA: DataProvidersAnd = { + enabled: true, + excluded: false, + id: 'context-field.name-a', + kqlQuery: '', + name: 'a', + queryMatch: { + field: 'field.name', + value: 'a', + operator: IS_OPERATOR, + }, +}; + +export const providerB: DataProvidersAnd = { + enabled: true, + excluded: false, + id: 'context-field.name-b', + kqlQuery: '', + name: 'b', + queryMatch: { + field: 'field.name', + value: 'b', + operator: IS_OPERATOR, + }, +}; + +export const providerC: DataProvidersAnd = { + enabled: true, + excluded: false, + id: 'context-field.name-c', + kqlQuery: '', + name: 'c', + queryMatch: { + field: 'field.name', + value: 'c', + operator: IS_OPERATOR, + }, +}; + +export const providerD: DataProvidersAnd = { + enabled: true, + excluded: false, + id: 'context-field.name-d', + kqlQuery: '', + name: 'd', + queryMatch: { + field: 'field.name', + value: 'd', + operator: IS_OPERATOR, + }, +}; + +export const providerE: DataProvidersAnd = { + enabled: true, + excluded: false, + id: 'context-field.name-e', + kqlQuery: '', + name: 'e', + queryMatch: { + field: 'field.name', + value: 'e', + operator: IS_OPERATOR, + }, +}; + +export const providerF: DataProvidersAnd = { + enabled: true, + excluded: false, + id: 'context-field.name-f', + kqlQuery: '', + name: 'f', + queryMatch: { + field: 'field.name', + value: 'f', + operator: IS_OPERATOR, + }, +}; + +export const twoGroups: DataProvider[] = [ + { ...providerA, and: [providerB, providerC] }, + { ...providerD, and: [providerE, providerF] }, +]; diff --git a/x-pack/plugins/timelines/public/mock/mock_data_providers.tsx b/x-pack/plugins/timelines/public/mock/mock_data_providers.tsx new file mode 100644 index 0000000000000..3c1b166ff4506 --- /dev/null +++ b/x-pack/plugins/timelines/public/mock/mock_data_providers.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IS_OPERATOR } from '../../common/types/timeline'; +// eslint-disable-next-line no-duplicate-imports +import type { DataProvider } from '../../common/types/timeline'; + +interface NameToEventCount<TValue> { + [name: string]: TValue; +} + +/** + * A map of mock data provider name to a count of events for + * that mock data provider + */ +const mockSourceNameToEventCount: NameToEventCount<number> = { + 'Provider 1': 64, + 'Provider 2': 158, + 'Provider 3': 381, + 'Provider 4': 237, + 'Provider 5': 310, + 'Provider 6': 1052, + 'Provider 7': 533, + 'Provider 8': 429, + 'Provider 9': 706, + 'Provider 10': 863, +}; + +/** Returns a collection of mock data provider names */ +export const mockDataProviderNames = (): string[] => Object.keys(mockSourceNameToEventCount); + +/** Returns a count of the events for a mock data provider */ +export const getEventCount = (dataProviderName: string): number => + mockSourceNameToEventCount[dataProviderName] || 0; + +/** + * A collection of mock data providers, that can both be rendered + * in the browser, and also used as mocks in unit and functional tests. + */ +export const mockDataProviders: DataProvider[] = Object.keys(mockSourceNameToEventCount).map( + (name) => + ({ + id: `id-${name}`, + name, + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'name', + value: name, + operator: IS_OPERATOR, + }, + and: [], + } as DataProvider) +); diff --git a/x-pack/plugins/timelines/public/mock/mock_local_storage.ts b/x-pack/plugins/timelines/public/mock/mock_local_storage.ts new file mode 100644 index 0000000000000..89fb93a164a17 --- /dev/null +++ b/x-pack/plugins/timelines/public/mock/mock_local_storage.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IStorage, Storage } from '../../../../../src/plugins/kibana_utils/public'; + +export const localStorageMock = (): IStorage => { + let store: Record<string, unknown> = {}; + + return { + getItem: (key: string) => { + return store[key] || null; + }, + setItem: (key: string, value: unknown) => { + store[key] = value; + }, + clear() { + store = {}; + }, + removeItem(key: string) { + delete store[key]; + }, + }; +}; + +export const createSecuritySolutionStorageMock = () => { + const localStorage = localStorageMock(); + return { + localStorage, + storage: new Storage(localStorage), + }; +}; diff --git a/x-pack/plugins/timelines/public/mock/mock_timeline_control_columns.tsx b/x-pack/plugins/timelines/public/mock/mock_timeline_control_columns.tsx new file mode 100644 index 0000000000000..8a670a6cf90ab --- /dev/null +++ b/x-pack/plugins/timelines/public/mock/mock_timeline_control_columns.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { + EuiCheckbox, + EuiButtonIcon, + EuiPopover, + EuiFlexGroup, + EuiFlexItem, + EuiPopoverTitle, + EuiSpacer, +} from '@elastic/eui'; +import type { ControlColumnProps } from '../../common/types/timeline'; + +const SelectionHeaderCell = () => { + return ( + <div data-test-subj="test-header-control-column-cell"> + <EuiCheckbox id="selection-toggle" aria-label="Select all rows" onChange={() => null} /> + </div> + ); +}; + +const SimpleHeaderCell = () => { + return ( + <div + style={{ + fontSize: '12px', + fontWeight: 600, + lineHeight: 1.5, + minWidth: 0, + padding: '4px', + width: '100%', + display: 'flex', + alignItems: 'center', + }} + data-test-subj="test-header-action-cell" + > + {'Additional Actions'} + </div> + ); +}; + +const SelectionRowCell = ({ rowIndex }: { rowIndex: number }) => { + return ( + <div data-test-subj="test-body-control-column-cell"> + <EuiCheckbox + id={`${rowIndex}`} + aria-label={`Select row test`} + checked={false} + onChange={() => null} + /> + </div> + ); +}; + +const TestTrailingColumn = () => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + return ( + <EuiPopover + isOpen={isPopoverOpen} + anchorPosition="upCenter" + panelPaddingSize="s" + button={ + <EuiButtonIcon + aria-label="show actions" + iconType="boxesHorizontal" + color="text" + onClick={() => setIsPopoverOpen(!isPopoverOpen)} + /> + } + data-test-subj="test-trailing-column-popover-button" + closePopover={() => setIsPopoverOpen(false)} + > + <EuiPopoverTitle>{'Actions'}</EuiPopoverTitle> + <div style={{ width: 150 }}> + <button type="button" onClick={() => {}}> + <EuiFlexGroup alignItems="center" component="span" gutterSize="s"> + <EuiFlexItem grow={false}> + <EuiButtonIcon aria-label="Pin selected items" iconType="pin" color="text" /> + </EuiFlexItem> + <EuiFlexItem>{'Pin'}</EuiFlexItem> + </EuiFlexGroup> + </button> + <EuiSpacer size="s" /> + <button type="button" onClick={() => {}}> + <EuiFlexGroup alignItems="center" component="span" gutterSize="s"> + <EuiFlexItem grow={false}> + <EuiButtonIcon aria-label="Delete selected items" iconType="trash" color="text" /> + </EuiFlexItem> + <EuiFlexItem>{'Delete'}</EuiFlexItem> + </EuiFlexGroup> + </button> + </div> + </EuiPopover> + ); +}; + +export const testTrailingControlColumns = [ + { + id: 'actions', + width: 96, + headerCellRender: SimpleHeaderCell, + rowCellRender: TestTrailingColumn, + }, +]; + +export const testLeadingControlColumn: ControlColumnProps = { + id: 'test-leading-control', + headerCellRender: SelectionHeaderCell, + rowCellRender: SelectionRowCell, + width: 100, +}; diff --git a/x-pack/plugins/timelines/public/mock/mock_timeline_data.ts b/x-pack/plugins/timelines/public/mock/mock_timeline_data.ts new file mode 100644 index 0000000000000..63f807d6d19db --- /dev/null +++ b/x-pack/plugins/timelines/public/mock/mock_timeline_data.ts @@ -0,0 +1,1511 @@ +/* + * 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 { Ecs } from '../../common/ecs'; +import type { TimelineItem } from '../../common/search_strategy'; + +export const mockTimelineData: TimelineItem[] = [ + { + _id: '1', + data: [ + { field: '@timestamp', value: ['2018-11-05T19:03:25.937Z'] }, + { field: 'event.severity', value: ['3'] }, + { field: 'event.category', value: ['Access'] }, + { field: 'event.action', value: ['Action'] }, + { field: 'host.name', value: ['apache'] }, + { field: 'source.ip', value: ['192.168.0.1'] }, + { field: 'destination.ip', value: ['192.168.0.3'] }, + { field: 'destination.bytes', value: ['123456'] }, + { field: 'user.name', value: ['john.dee'] }, + ], + ecs: { + _id: '1', + timestamp: '2018-11-05T19:03:25.937Z', + host: { name: ['apache'], ip: ['192.168.0.1'] }, + event: { + id: ['1'], + action: ['Action'], + category: ['Access'], + module: ['nginx'], + severity: [3], + }, + source: { ip: ['192.168.0.1'], port: [80] }, + destination: { ip: ['192.168.0.3'], port: [6343] }, + user: { id: ['1'], name: ['john.dee'] }, + geo: { region_name: ['xx'], country_iso_code: ['xx'] }, + }, + }, + { + _id: '3', + data: [ + { field: '@timestamp', value: ['2018-11-07T19:03:25.937Z'] }, + { field: 'event.severity', value: ['1'] }, + { field: 'event.category', value: ['Access'] }, + { field: 'host.name', value: ['nginx'] }, + { field: 'source.ip', value: ['192.168.0.3'] }, + { field: 'destination.ip', value: ['192.168.0.3'] }, + { field: 'destination.bytes', value: ['123456'] }, + { field: 'user.name', value: ['evan.davis'] }, + ], + ecs: { + _id: '3', + timestamp: '2018-11-07T19:03:25.937Z', + host: { name: ['nginx'], ip: ['192.168.0.1'] }, + event: { + id: ['3'], + category: ['Access'], + type: ['HTTP Request'], + module: ['nginx'], + severity: [1], + }, + source: { ip: ['192.168.0.3'], port: [443] }, + destination: { ip: ['192.168.0.3'], port: [6343] }, + user: { id: ['3'], name: ['evan.davis'] }, + geo: { region_name: ['xx'], country_iso_code: ['xx'] }, + }, + }, + { + _id: '4', + data: [ + { field: '@timestamp', value: ['2018-11-08T19:03:25.937Z'] }, + { field: 'event.severity', value: ['1'] }, + { field: 'event.category', value: ['Attempted Administrator Privilege Gain'] }, + { field: 'host.name', value: ['suricata'] }, + { field: 'source.ip', value: ['192.168.0.3'] }, + { field: 'destination.ip', value: ['192.168.0.3'] }, + { field: 'destination.bytes', value: ['123456'] }, + { field: 'user.name', value: ['jenny.jones'] }, + ], + ecs: { + _id: '4', + timestamp: '2018-11-08T19:03:25.937Z', + host: { name: ['suricata'], ip: ['192.168.0.1'] }, + event: { + id: ['4'], + category: ['Attempted Administrator Privilege Gain'], + type: ['Alert'], + module: ['suricata'], + severity: [1], + }, + source: { ip: ['192.168.0.3'], port: [53] }, + destination: { ip: ['192.168.0.3'], port: [6343] }, + suricata: { + eve: { + flow_id: [4], + proto: [''], + alert: { + signature: [ + 'ET EXPLOIT NETGEAR WNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)', + ], + signature_id: [4], + }, + }, + }, + user: { id: ['4'], name: ['jenny.jones'] }, + geo: { region_name: ['xx'], country_iso_code: ['xx'] }, + }, + }, + { + _id: '5', + data: [ + { field: '@timestamp', value: ['2018-11-09T19:03:25.937Z'] }, + { field: 'event.severity', value: ['3'] }, + { field: 'event.category', value: ['Access'] }, + { field: 'host.name', value: ['joe.computer'] }, + { field: 'source.ip', value: ['192.168.0.3'] }, + { field: 'destination.ip', value: ['192.168.0.3'] }, + { field: 'destination.bytes', value: ['123456'] }, + { field: 'user.name', value: ['becky.davis'] }, + ], + ecs: { + _id: '5', + timestamp: '2018-11-09T19:03:25.937Z', + host: { name: ['joe.computer'], ip: ['192.168.0.1'] }, + event: { + id: ['5'], + category: ['Access'], + type: ['HTTP Request'], + module: ['nginx'], + severity: [3], + }, + source: { ip: ['192.168.0.3'], port: [80] }, + destination: { ip: ['192.168.0.3'], port: [6343] }, + user: { id: ['5'], name: ['becky.davis'] }, + geo: { region_name: ['xx'], country_iso_code: ['xx'] }, + }, + }, + { + _id: '6', + data: [ + { field: '@timestamp', value: ['2018-11-10T19:03:25.937Z'] }, + { field: 'event.severity', value: ['3'] }, + { field: 'event.category', value: ['Access'] }, + { field: 'host.name', value: ['braden.davis'] }, + { field: 'source.ip', value: ['192.168.0.6'] }, + { field: 'destination.ip', value: ['192.168.0.3'] }, + { field: 'destination.bytes', value: ['123456'] }, + ], + ecs: { + _id: '6', + timestamp: '2018-11-10T19:03:25.937Z', + host: { name: ['braden.davis'], ip: ['192.168.0.1'] }, + event: { + id: ['6'], + category: ['Access'], + type: ['HTTP Request'], + module: ['nginx'], + severity: [3], + }, + source: { ip: ['192.168.0.6'], port: [80] }, + destination: { ip: ['192.168.0.3'], port: [6343] }, + geo: { region_name: ['xx'], country_iso_code: ['xx'] }, + }, + }, + { + _id: '8', + data: [ + { field: '@timestamp', value: ['2018-11-12T19:03:25.937Z'] }, + { field: 'event.severity', value: ['2'] }, + { field: 'event.category', value: ['Web Application Attack'] }, + { field: 'host.name', value: ['joe.computer'] }, + { field: 'source.ip', value: ['192.168.0.8'] }, + { field: 'destination.ip', value: ['192.168.0.3'] }, + { field: 'destination.bytes', value: ['123456'] }, + { field: 'user.name', value: ['jone.doe'] }, + ], + ecs: { + _id: '8', + timestamp: '2018-11-12T19:03:25.937Z', + host: { name: ['joe.computer'], ip: ['192.168.0.1'] }, + event: { + id: ['8'], + category: ['Web Application Attack'], + type: ['Alert'], + module: ['suricata'], + severity: [2], + }, + suricata: { + eve: { + flow_id: [8], + proto: [''], + alert: { + signature: ['ET WEB_SERVER Possible CVE-2014-6271 Attempt in HTTP Cookie'], + signature_id: [8], + }, + }, + }, + source: { ip: ['192.168.0.8'], port: [80] }, + destination: { ip: ['192.168.0.3'], port: [6343] }, + user: { id: ['8'], name: ['jone.doe'] }, + geo: { region_name: ['xx'], country_iso_code: ['xx'] }, + }, + }, + { + _id: '7', + data: [ + { field: '@timestamp', value: ['2018-11-11T19:03:25.937Z'] }, + { field: 'event.severity', value: ['3'] }, + { field: 'event.category', value: ['Access'] }, + { field: 'host.name', value: ['joe.computer'] }, + { field: 'source.ip', value: ['192.168.0.7'] }, + { field: 'destination.ip', value: ['192.168.0.3'] }, + { field: 'destination.bytes', value: ['123456'] }, + { field: 'user.name', value: ['jone.doe'] }, + ], + ecs: { + _id: '7', + timestamp: '2018-11-11T19:03:25.937Z', + host: { name: ['joe.computer'], ip: ['192.168.0.1'] }, + event: { + id: ['7'], + category: ['Access'], + type: ['HTTP Request'], + module: ['apache'], + severity: [3], + }, + source: { ip: ['192.168.0.7'], port: [80] }, + destination: { ip: ['192.168.0.3'], port: [6343] }, + user: { id: ['7'], name: ['jone.doe'] }, + geo: { region_name: ['xx'], country_iso_code: ['xx'] }, + }, + }, + { + _id: '9', + data: [ + { field: '@timestamp', value: ['2018-11-13T19:03:25.937Z'] }, + { field: 'event.severity', value: ['3'] }, + { field: 'event.category', value: ['Access'] }, + { field: 'host.name', value: ['joe.computer'] }, + { field: 'source.ip', value: ['192.168.0.9'] }, + { field: 'destination.ip', value: ['192.168.0.3'] }, + { field: 'destination.bytes', value: ['123456'] }, + { field: 'user.name', value: ['jone.doe'] }, + ], + ecs: { + _id: '9', + timestamp: '2018-11-13T19:03:25.937Z', + host: { name: ['joe.computer'], ip: ['192.168.0.1'] }, + event: { + id: ['9'], + category: ['Access'], + type: ['HTTP Request'], + module: ['nginx'], + severity: [3], + }, + source: { ip: ['192.168.0.9'], port: [80] }, + destination: { ip: ['192.168.0.3'], port: [6343] }, + user: { id: ['9'], name: ['jone.doe'] }, + geo: { region_name: ['xx'], country_iso_code: ['xx'] }, + }, + }, + { + _id: '10', + data: [ + { field: '@timestamp', value: ['2018-11-14T19:03:25.937Z'] }, + { field: 'event.severity', value: ['3'] }, + { field: 'event.category', value: ['Access'] }, + { field: 'host.name', value: ['joe.computer'] }, + { field: 'source.ip', value: ['192.168.0.10'] }, + { field: 'destination.ip', value: ['192.168.0.3'] }, + { field: 'destination.bytes', value: ['123456'] }, + { field: 'user.name', value: ['jone.doe'] }, + ], + ecs: { + _id: '10', + timestamp: '2018-11-14T19:03:25.937Z', + host: { name: ['joe.computer'], ip: ['192.168.0.1'] }, + event: { + id: ['10'], + category: ['Access'], + type: ['HTTP Request'], + module: ['nginx'], + severity: [3], + }, + source: { ip: ['192.168.0.10'], port: [80] }, + destination: { ip: ['192.168.0.3'], port: [6343] }, + user: { id: ['10'], name: ['jone.doe'] }, + geo: { region_name: ['xx'], country_iso_code: ['xx'] }, + }, + }, + { + _id: '11', + data: [ + { field: '@timestamp', value: ['2018-11-15T19:03:25.937Z'] }, + { field: 'event.severity', value: ['3'] }, + { field: 'event.category', value: ['Access'] }, + { field: 'host.name', value: ['joe.computer'] }, + { field: 'source.ip', value: ['192.168.0.11'] }, + { field: 'destination.ip', value: ['192.168.0.3'] }, + { field: 'destination.bytes', value: ['123456'] }, + { field: 'user.name', value: ['jone.doe'] }, + ], + ecs: { + _id: '11', + timestamp: '2018-11-15T19:03:25.937Z', + host: { name: ['joe.computer'], ip: ['192.168.0.1'] }, + event: { + id: ['11'], + category: ['Access'], + type: ['HTTP Request'], + module: ['nginx'], + severity: [3], + }, + source: { ip: ['192.168.0.11'], port: [80] }, + destination: { ip: ['192.168.0.3'], port: [6343] }, + user: { id: ['11'], name: ['jone.doe'] }, + geo: { region_name: ['xx'], country_iso_code: ['xx'] }, + }, + }, + { + _id: '12', + data: [ + { field: '@timestamp', value: ['2018-11-16T19:03:25.937Z'] }, + { field: 'event.severity', value: ['3'] }, + { field: 'event.category', value: ['Access'] }, + { field: 'host.name', value: ['joe.computer'] }, + { field: 'source.ip', value: ['192.168.0.12'] }, + { field: 'destination.ip', value: ['192.168.0.3'] }, + { field: 'destination.bytes', value: ['123456'] }, + { field: 'user.name', value: ['jone.doe'] }, + ], + ecs: { + _id: '12', + timestamp: '2018-11-16T19:03:25.937Z', + host: { name: ['joe.computer'], ip: ['192.168.0.1'] }, + event: { + id: ['12'], + category: ['Access'], + type: ['HTTP Request'], + module: ['nginx'], + severity: [3], + }, + source: { ip: ['192.168.0.12'], port: [80] }, + destination: { ip: ['192.168.0.3'], port: [6343] }, + user: { id: ['12'], name: ['jone.doe'] }, + geo: { region_name: ['xx'], country_iso_code: ['xx'] }, + }, + }, + { + _id: '2', + data: [ + { field: '@timestamp', value: ['2018-11-06T19:03:25.937Z'] }, + { field: 'event.severity', value: ['3'] }, + { field: 'event.category', value: ['Authentication'] }, + { field: 'host.name', value: ['joe.computer'] }, + { field: 'source.ip', value: ['192.168.0.2'] }, + { field: 'destination.ip', value: ['192.168.0.3'] }, + { field: 'destination.bytes', value: ['123456'] }, + { field: 'user.name', value: ['joe.bob'] }, + ], + ecs: { + _id: '2', + timestamp: '2018-11-06T19:03:25.937Z', + host: { name: ['joe.computer'], ip: ['192.168.0.1'] }, + event: { + id: ['2'], + category: ['Authentication'], + type: ['Authentication Success'], + module: ['authlog'], + severity: [3], + }, + source: { ip: ['192.168.0.2'], port: [80] }, + destination: { ip: ['192.168.0.3'], port: [6343] }, + user: { id: ['1'], name: ['joe.bob'] }, + geo: { region_name: ['xx'], country_iso_code: ['xx'] }, + }, + }, + { + _id: '13', + data: [ + { field: '@timestamp', value: ['2018-13-12T19:03:25.937Z'] }, + { field: 'event.severity', value: ['1'] }, + { field: 'event.category', value: ['Web Application Attack'] }, + { field: 'host.name', value: ['joe.computer'] }, + { field: 'source.ip', value: ['192.168.0.8'] }, + { field: 'destination.ip', value: ['192.168.0.3'] }, + { field: 'destination.bytes', value: ['123456'] }, + ], + ecs: { + _id: '13', + timestamp: '2018-13-12T19:03:25.937Z', + host: { name: ['joe.computer'], ip: ['192.168.0.1'] }, + event: { + id: ['13'], + category: ['Web Application Attack'], + type: ['Alert'], + module: ['suricata'], + severity: [1], + }, + suricata: { + eve: { + flow_id: [13], + proto: [''], + alert: { + signature: ['ET WEB_SERVER Possible Attempt in HTTP Cookie'], + signature_id: [13], + }, + }, + }, + source: { ip: ['192.168.0.8'], port: [80] }, + destination: { ip: ['192.168.0.3'], port: [6343] }, + geo: { region_name: ['xx'], country_iso_code: ['xx'] }, + }, + }, + { + _id: '14', + data: [ + { field: '@timestamp', value: ['2019-03-07T05:06:51.000Z'] }, + { field: 'host.name', value: ['zeek-franfurt'] }, + { field: 'source.ip', value: ['192.168.26.101'] }, + { field: 'destination.ip', value: ['192.168.238.205'] }, + ], + ecs: { + _id: '14', + timestamp: '2019-03-07T05:06:51.000Z', + event: { + module: ['zeek'], + dataset: ['zeek.connection'], + }, + host: { + id: ['37c81253e0fc4c46839c19b981be5177'], + name: ['zeek-franfurt'], + ip: ['207.154.238.205', '10.19.0.5', 'fe80::d82b:9aff:fe0d:1e12'], + }, + source: { ip: ['185.176.26.101'], port: [44059] }, + destination: { ip: ['207.154.238.205'], port: [11568] }, + geo: { region_name: ['New York'], country_iso_code: ['US'] }, + network: { transport: ['tcp'] }, + zeek: { + session_id: ['C8DRTq362Fios6hw16'], + connection: { + local_resp: [false], + local_orig: [false], + missed_bytes: [0], + state: ['REJ'], + history: ['Sr'], + }, + }, + }, + }, + { + _id: '15', + data: [ + { field: '@timestamp', value: ['2019-03-07T00:51:28.000Z'] }, + { field: 'host.name', value: ['suricata-zeek-singapore'] }, + { field: 'source.ip', value: ['192.168.35.240'] }, + { field: 'destination.ip', value: ['192.168.67.3'] }, + ], + ecs: { + _id: '15', + timestamp: '2019-03-07T00:51:28.000Z', + event: { + module: ['zeek'], + dataset: ['zeek.dns'], + }, + host: { + id: ['af3fddf15f1d47979ce817ba0df10c6e'], + name: ['suricata-zeek-singapore'], + ip: ['206.189.35.240', '10.15.0.5', 'fe80::98c7:eff:fe29:4455'], + }, + source: { ip: ['206.189.35.240'], port: [57475] }, + destination: { ip: ['67.207.67.3'], port: [53] }, + geo: { region_name: ['New York'], country_iso_code: ['US'] }, + network: { transport: ['udp'] }, + zeek: { + session_id: ['CyIrMA1L1JtLqdIuol'], + dns: { + AA: [false], + RD: [false], + trans_id: [65252], + RA: [false], + TC: [false], + }, + }, + }, + }, + { + _id: '16', + data: [ + { field: '@timestamp', value: ['2019-03-05T07:00:20.000Z'] }, + { field: 'host.name', value: ['suricata-zeek-singapore'] }, + { field: 'source.ip', value: ['192.168.35.240'] }, + { field: 'destination.ip', value: ['192.168.164.26'] }, + ], + ecs: { + _id: '16', + timestamp: '2019-03-05T07:00:20.000Z', + event: { + module: ['zeek'], + dataset: ['zeek.http'], + }, + host: { + id: ['af3fddf15f1d47979ce817ba0df10c6e'], + name: ['suricata-zeek-singapore'], + ip: ['206.189.35.240', '10.15.0.5', 'fe80::98c7:eff:fe29:4455'], + }, + source: { ip: ['206.189.35.240'], port: [36220] }, + destination: { ip: ['192.241.164.26'], port: [80] }, + geo: { region_name: ['New York'], country_iso_code: ['US'] }, + http: { + version: ['1.1'], + request: { body: { bytes: [0] } }, + response: { status_code: [302], body: { bytes: [154] } }, + }, + zeek: { + session_id: ['CZLkpC22NquQJOpkwe'], + + http: { + resp_mime_types: ['text/html'], + trans_depth: ['3'], + status_msg: ['Moved Temporarily'], + resp_fuids: ['FzeujEPP7GTHmYPsc'], + tags: [], + }, + }, + }, + }, + { + _id: '17', + data: [ + { field: '@timestamp', value: ['2019-02-28T22:36:28.000Z'] }, + { field: 'host.name', value: ['zeek-franfurt'] }, + { field: 'source.ip', value: ['192.168.77.171'] }, + ], + ecs: { + _id: '17', + timestamp: '2019-02-28T22:36:28.000Z', + event: { + module: ['zeek'], + dataset: ['zeek.notice'], + }, + host: { + id: ['37c81253e0fc4c46839c19b981be5177'], + name: ['zeek-franfurt'], + ip: ['207.154.238.205', '10.19.0.5', 'fe80::d82b:9aff:fe0d:1e12'], + }, + source: { ip: ['8.42.77.171'] }, + zeek: { + notice: { + suppress_for: [3600], + msg: ['8.42.77.171 scanned at least 15 unique ports of host 207.154.238.205 in 0m0s'], + note: ['Scan::Port_Scan'], + sub: ['remote'], + dst: ['207.154.238.205'], + dropped: [false], + peer_descr: ['bro'], + }, + }, + }, + }, + { + _id: '18', + data: [ + { field: '@timestamp', value: ['2019-02-22T21:12:13.000Z'] }, + { field: 'host.name', value: ['zeek-sensor-amsterdam'] }, + { field: 'source.ip', value: ['192.168.66.184'] }, + { field: 'destination.ip', value: ['192.168.95.15'] }, + ], + ecs: { + _id: '18', + timestamp: '2019-02-22T21:12:13.000Z', + event: { + module: ['zeek'], + dataset: ['zeek.ssl'], + }, + host: { id: ['2ce8b1e7d69e4a1d9c6bcddc473da9d9'], name: ['zeek-sensor-amsterdam'] }, + source: { ip: ['188.166.66.184'], port: [34514] }, + destination: { ip: ['91.189.95.15'], port: [443] }, + geo: { region_name: ['England'], country_iso_code: ['GB'] }, + zeek: { + session_id: ['CmTxzt2OVXZLkGDaRe'], + ssl: { + cipher: ['TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256'], + established: [false], + resumed: [false], + version: ['TLSv12'], + }, + }, + }, + }, + { + _id: '19', + data: [ + { field: '@timestamp', value: ['2019-03-03T04:26:38.000Z'] }, + { field: 'host.name', value: ['suricata-zeek-singapore'] }, + ], + ecs: { + _id: '19', + timestamp: '2019-03-03T04:26:38.000Z', + event: { + module: ['zeek'], + dataset: ['zeek.files'], + }, + host: { + id: ['af3fddf15f1d47979ce817ba0df10c6e'], + name: ['suricata-zeek-singapore'], + ip: ['206.189.35.240', '10.15.0.5', 'fe80::98c7:eff:fe29:4455'], + }, + zeek: { + session_id: ['Cu0n232QMyvNtzb75j'], + files: { + session_ids: ['Cu0n232QMyvNtzb75j'], + timedout: [false], + local_orig: [false], + tx_host: ['5.101.111.50'], + source: ['HTTP'], + is_orig: [false], + overflow_bytes: [0], + sha1: ['fa5195a5dfacc9d1c68d43600f0e0262cad14dde'], + duration: [0], + depth: [0], + analyzers: ['MD5', 'SHA1'], + mime_type: ['text/plain'], + rx_host: ['206.189.35.240'], + total_bytes: [88722], + fuid: ['FePz1uVEVCZ3I0FQi'], + seen_bytes: [1198], + missing_bytes: [0], + md5: ['f7653f1951693021daa9e6be61226e32'], + }, + }, + }, + }, + { + _id: '20', + data: [ + { field: '@timestamp', value: ['2019-03-13T05:42:11.815Z'] }, + { field: 'event.category', value: ['audit-rule'] }, + { field: 'host.name', value: ['zeek-sanfran'] }, + ], + ecs: { + _id: '20', + timestamp: '2019-03-13T05:42:11.815Z', + event: { + action: ['executed'], + module: ['auditd'], + category: ['audit-rule'], + }, + host: { + id: ['f896741c3b3b44bdb8e351a4ab6d2d7c'], + name: ['zeek-sanfran'], + ip: ['134.209.63.134', '10.46.0.5', 'fe80::a0d9:16ff:fecf:e70b'], + }, + user: { name: ['alice'] }, + process: { + pid: [5402], + name: ['gpgconf'], + ppid: [5401], + args: ['gpgconf', '--list-dirs', 'agent-socket'], + executable: ['/usr/bin/gpgconf'], + title: ['gpgconf --list-dirs agent-socket'], + working_directory: ['/'], + }, + }, + }, + { + _id: '21', + data: [ + { field: '@timestamp', value: ['2019-03-14T22:30:25.527Z'] }, + { field: 'event.category', value: ['user-login'] }, + { field: 'host.name', value: ['zeek-london'] }, + { field: 'source.ip', value: ['192.168.77.171'] }, + { field: 'user.name', value: ['root'] }, + ], + ecs: { + _id: '21', + timestamp: '2019-03-14T22:30:25.527Z', + event: { + action: ['logged-in'], + module: ['auditd'], + category: ['user-login'], + }, + auditd: { + result: ['success'], + session: ['14'], + data: { terminal: ['/dev/pts/0'], op: ['login'] }, + summary: { + actor: { primary: ['alice'], secondary: ['alice'] }, + object: { primary: ['/dev/pts/0'], secondary: ['8.42.77.171'], type: ['user-session'] }, + how: ['/usr/sbin/sshd'], + }, + }, + host: { + id: ['7c21f5ed03b04d0299569d221fe18bbc'], + name: ['zeek-london'], + ip: ['46.101.3.136', '10.16.0.5', 'fe80::4066:42ff:fe19:b3b9'], + }, + source: { ip: ['8.42.77.171'] }, + user: { name: ['root'] }, + process: { + pid: [17471], + executable: ['/usr/sbin/sshd'], + }, + }, + }, + { + _id: '22', + data: [ + { field: '@timestamp', value: ['2019-03-13T03:35:21.614Z'] }, + { field: 'event.category', value: ['user-login'] }, + { field: 'host.name', value: ['suricata-bangalore'] }, + { field: 'user.name', value: ['root'] }, + ], + ecs: { + _id: '22', + timestamp: '2019-03-13T03:35:21.614Z', + event: { + action: ['disposed-credentials'], + module: ['auditd'], + category: ['user-login'], + }, + auditd: { + result: ['success'], + session: ['340'], + data: { acct: ['alice'], terminal: ['ssh'], op: ['PAM:setcred'] }, + summary: { + actor: { primary: ['alice'], secondary: ['alice'] }, + object: { primary: ['ssh'], secondary: ['8.42.77.171'], type: ['user-session'] }, + how: ['/usr/sbin/sshd'], + }, + }, + host: { + id: ['0a63559c1acf4c419d979c4b4d8b83ff'], + name: ['suricata-bangalore'], + ip: ['139.59.11.147', '10.47.0.5', 'fe80::ec0b:1bff:fe29:80bd'], + }, + user: { name: ['root'] }, + process: { + pid: [21202], + executable: ['/usr/sbin/sshd'], + }, + }, + }, + { + _id: '23', + data: [ + { field: '@timestamp', value: ['2019-03-13T03:35:21.614Z'] }, + { field: 'event.category', value: ['user-login'] }, + { field: 'host.name', value: ['suricata-bangalore'] }, + { field: 'user.name', value: ['root'] }, + ], + ecs: { + _id: '23', + timestamp: '2019-03-13T03:35:21.614Z', + event: { + action: ['ended-session'], + module: ['auditd'], + category: ['user-login'], + }, + auditd: { + result: ['success'], + session: ['340'], + data: { acct: ['alice'], terminal: ['ssh'], op: ['PAM:session_close'] }, + summary: { + actor: { primary: ['alice'], secondary: ['alice'] }, + object: { primary: ['ssh'], secondary: ['8.42.77.171'], type: ['user-session'] }, + how: ['/usr/sbin/sshd'], + }, + }, + host: { + id: ['0a63559c1acf4c419d979c4b4d8b83ff'], + name: ['suricata-bangalore'], + ip: ['139.59.11.147', '10.47.0.5', 'fe80::ec0b:1bff:fe29:80bd'], + }, + user: { name: ['root'] }, + process: { + pid: [21202], + executable: ['/usr/sbin/sshd'], + }, + }, + }, + { + _id: '24', + data: [ + { field: '@timestamp', value: ['2019-03-18T23:17:01.645Z'] }, + { field: 'event.category', value: ['user-login'] }, + { field: 'host.name', value: ['zeek-london'] }, + { field: 'user.name', value: ['root'] }, + ], + ecs: { + _id: '24', + timestamp: '2019-03-18T23:17:01.645Z', + event: { + action: ['acquired-credentials'], + module: ['auditd'], + category: ['user-login'], + }, + auditd: { + result: ['success'], + session: ['unset'], + data: { acct: ['root'], terminal: ['cron'], op: ['PAM:setcred'] }, + summary: { + actor: { primary: ['unset'], secondary: ['root'] }, + object: { primary: ['cron'], type: ['user-session'] }, + how: ['/usr/sbin/cron'], + }, + }, + host: { + id: ['7c21f5ed03b04d0299569d221fe18bbc'], + name: ['zeek-london'], + ip: ['46.101.3.136', '10.16.0.5', 'fe80::4066:42ff:fe19:b3b9'], + }, + user: { name: ['root'] }, + process: { + pid: [9592], + executable: ['/usr/sbin/cron'], + }, + }, + }, + { + _id: '25', + data: [ + { field: '@timestamp', value: ['2019-03-19T01:17:01.336Z'] }, + { field: 'event.category', value: ['user-login'] }, + { field: 'host.name', value: ['siem-kibana'] }, + { field: 'user.name', value: ['root'] }, + ], + ecs: { + _id: '25', + timestamp: '2019-03-19T01:17:01.336Z', + event: { + action: ['started-session'], + module: ['auditd'], + category: ['user-login'], + }, + auditd: { + result: ['success'], + session: ['2908'], + data: { acct: ['root'], terminal: ['cron'], op: ['PAM:session_open'] }, + summary: { + actor: { primary: ['root'], secondary: ['root'] }, + object: { primary: ['cron'], type: ['user-session'] }, + how: ['/usr/sbin/cron'], + }, + }, + host: { id: ['aa7ca589f1b8220002f2fc61c64cfbf1'], name: ['siem-kibana'] }, + user: { name: ['root'] }, + process: { + pid: [725], + executable: ['/usr/sbin/cron'], + }, + }, + }, + { + _id: '26', + data: [ + { field: '@timestamp', value: ['2019-03-13T03:34:08.890Z'] }, + { field: 'event.category', value: ['user-login'] }, + { field: 'host.name', value: ['suricata-bangalore'] }, + { field: 'user.name', value: ['alice'] }, + ], + ecs: { + _id: '26', + timestamp: '2019-03-13T03:34:08.890Z', + event: { + action: ['was-authorized'], + module: ['auditd'], + category: ['user-login'], + }, + auditd: { + result: ['success'], + session: ['338'], + data: { terminal: ['/dev/pts/0'] }, + summary: { + actor: { primary: ['root'], secondary: ['alice'] }, + object: { primary: ['/dev/pts/0'], type: ['user-session'] }, + how: ['/sbin/pam_tally2'], + }, + }, + host: { + id: ['0a63559c1acf4c419d979c4b4d8b83ff'], + name: ['suricata-bangalore'], + ip: ['139.59.11.147', '10.47.0.5', 'fe80::ec0b:1bff:fe29:80bd'], + }, + user: { name: ['alice'] }, + process: { + pid: [21170], + executable: ['/sbin/pam_tally2'], + }, + }, + }, + { + _id: '27', + data: [ + { field: '@timestamp', value: ['2019-03-22T19:13:11.026Z'] }, + { field: 'event.action', value: ['connected-to'] }, + { field: 'event.category', value: ['audit-rule'] }, + { field: 'host.name', value: ['zeek-london'] }, + { field: 'destination.ip', value: ['192.168.216.34'] }, + { field: 'user.name', value: ['alice'] }, + ], + ecs: { + _id: '27', + timestamp: '2019-03-22T19:13:11.026Z', + event: { + action: ['connected-to'], + module: ['auditd'], + category: ['audit-rule'], + }, + auditd: { + result: ['success'], + session: ['246'], + summary: { + actor: { primary: ['alice'], secondary: ['alice'] }, + object: { primary: ['192.168.216.34'], secondary: ['80'], type: ['socket'] }, + how: ['/usr/bin/wget'], + }, + }, + host: { + id: ['7c21f5ed03b04d0299569d221fe18bbc'], + name: ['zeek-london'], + ip: ['46.101.3.136', '10.16.0.5', 'fe80::4066:42ff:fe19:b3b9'], + }, + destination: { ip: ['192.168.216.34'], port: [80] }, + user: { name: ['alice'] }, + process: { + pid: [1490], + name: ['wget'], + ppid: [1476], + executable: ['/usr/bin/wget'], + title: ['wget www.example.com'], + }, + }, + }, + { + _id: '28', + data: [ + { field: '@timestamp', value: ['2019-03-26T22:12:18.609Z'] }, + { field: 'event.action', value: ['opened-file'] }, + { field: 'event.category', value: ['audit-rule'] }, + { field: 'host.name', value: ['zeek-london'] }, + { field: 'user.name', value: ['root'] }, + ], + ecs: { + _id: '28', + timestamp: '2019-03-26T22:12:18.609Z', + event: { + action: ['opened-file'], + module: ['auditd'], + category: ['audit-rule'], + }, + auditd: { + result: ['success'], + session: ['242'], + summary: { + actor: { primary: ['unset'], secondary: ['root'] }, + object: { primary: ['/proc/15990/attr/current'], type: ['file'] }, + how: ['/lib/systemd/systemd-journald'], + }, + }, + file: { + path: ['/proc/15990/attr/current'], + device: ['00:00'], + inode: ['27672309'], + uid: ['0'], + owner: ['root'], + gid: ['0'], + group: ['root'], + mode: ['0666'], + }, + host: { + id: ['7c21f5ed03b04d0299569d221fe18bbc'], + name: ['zeek-london'], + ip: ['46.101.3.136', '10.16.0.5', 'fe80::4066:42ff:fe19:b3b9'], + }, + + user: { name: ['root'] }, + process: { + pid: [27244], + name: ['systemd-journal'], + ppid: [1], + executable: ['/lib/systemd/systemd-journald'], + title: ['/lib/systemd/systemd-journald'], + working_directory: ['/'], + }, + }, + }, + { + _id: '29', + data: [ + { field: '@timestamp', value: ['2019-04-08T21:18:57.000Z'] }, + { field: 'event.action', value: ['user_login'] }, + { field: 'event.category', value: null }, + { field: 'host.name', value: ['zeek-london'] }, + { field: 'user.name', value: ['Braden'] }, + ], + ecs: { + _id: '29', + event: { + action: ['user_login'], + dataset: ['login'], + kind: ['event'], + module: ['system'], + outcome: ['failure'], + }, + host: { + id: ['7c21f5ed03b04d0299569d221fe18bbc'], + name: ['zeek-london'], + ip: ['46.101.3.136', '10.16.0.5', 'fe80::4066:42ff:fe19:b3b9'], + }, + source: { + ip: ['128.199.212.120'], + }, + user: { + name: ['Braden'], + }, + process: { + pid: [6278], + }, + }, + }, + { + _id: '30', + data: [ + { field: '@timestamp', value: ['2019-04-08T22:27:14.814Z'] }, + { field: 'event.action', value: ['process_started'] }, + { field: 'event.category', value: null }, + { field: 'host.name', value: ['zeek-london'] }, + { field: 'user.name', value: ['Evan'] }, + ], + ecs: { + _id: '30', + event: { + action: ['process_started'], + dataset: ['login'], + kind: ['event'], + module: ['system'], + outcome: ['failure'], + }, + host: { + id: ['7c21f5ed03b04d0299569d221fe18bbc'], + name: ['zeek-london'], + ip: ['46.101.3.136', '10.16.0.5', 'fe80::4066:42ff:fe19:b3b9'], + }, + source: { + ip: ['128.199.212.120'], + }, + user: { + name: ['Evan'], + }, + process: { + pid: [6278], + }, + }, + }, + { + _id: '31', + data: [ + { field: '@timestamp', value: ['2018-11-05T19:03:25.937Z'] }, + { field: 'message', value: ['I am a log file message'] }, + { field: 'event.severity', value: ['3'] }, + { field: 'event.category', value: ['Access'] }, + { field: 'event.action', value: ['Action'] }, + { field: 'host.name', value: ['apache'] }, + { field: 'source.ip', value: ['192.168.0.1'] }, + { field: 'destination.ip', value: ['192.168.0.3'] }, + { field: 'destination.bytes', value: ['123456'] }, + { field: 'user.name', value: ['john.dee'] }, + ], + ecs: { + _id: '1', + timestamp: '2018-11-05T19:03:25.937Z', + host: { name: ['apache'], ip: ['192.168.0.1'] }, + event: { + id: ['1'], + action: ['Action'], + category: ['Access'], + module: ['nginx'], + severity: [3], + }, + message: ['I am a log file message'], + source: { ip: ['192.168.0.1'], port: [80] }, + destination: { ip: ['192.168.0.3'], port: [6343] }, + user: { id: ['1'], name: ['john.dee'] }, + geo: { region_name: ['xx'], country_iso_code: ['xx'] }, + }, + }, + { + _id: '32', + data: [], + ecs: { + _id: 'BuBP4W0BOpWiDweSoYSg', + timestamp: '2019-10-18T23:59:15.091Z', + threat: { + indicator: [ + { + matched: { + atomic: ['192.168.1.1'], + field: ['source.ip'], + type: ['ip'], + }, + event: { + dataset: ['threatintel.example_dataset'], + reference: ['https://example.com'], + }, + provider: ['indicator_provider'], + }, + ], + }, + }, + }, +]; + +export const mockFimFileCreatedEvent: Ecs = { + _id: 'WuBP4W0BOpWiDweSoYSg', + timestamp: '2019-10-18T23:59:15.091Z', + host: { + architecture: ['x86_64'], + os: { + family: ['debian'], + name: ['Ubuntu'], + kernel: ['4.15.0-1046-gcp'], + platform: ['ubuntu'], + version: ['16.04.6 LTS (Xenial Xerus)'], + }, + id: ['host-id-123'], + name: ['foohost'], + }, + file: { + path: ['/etc/subgid'], + size: [4445], + owner: ['root'], + inode: ['90027'], + ctime: ['2019-10-18T23:59:14.872Z'], + gid: ['0'], + type: ['file'], + mode: ['0644'], + mtime: ['2019-10-18T23:59:14.872Z'], + uid: ['0'], + group: ['root'], + }, + event: { + module: ['file_integrity'], + dataset: ['file'], + action: ['created'], + }, +}; + +export const mockFimFileDeletedEvent: Ecs = { + _id: 'M-BP4W0BOpWiDweSo4cm', + timestamp: '2019-10-18T23:59:16.247Z', + host: { + name: ['foohost'], + os: { + platform: ['ubuntu'], + version: ['16.04.6 LTS (Xenial Xerus)'], + family: ['debian'], + name: ['Ubuntu'], + kernel: ['4.15.0-1046-gcp'], + }, + id: ['host-id-123'], + architecture: ['x86_64'], + }, + event: { + module: ['file_integrity'], + dataset: ['file'], + action: ['deleted'], + }, + file: { + path: ['/etc/gshadow.lock'], + }, +}; + +export const mockSocketOpenedEvent: Ecs = { + _id: 'Vusu4m0BOpWiDweSLkXY', + timestamp: '2019-10-19T04:02:19.473Z', + network: { + direction: ['outbound'], + transport: ['tcp'], + community_id: ['1:network-community_id'], + }, + host: { + name: ['foohost'], + architecture: ['x86_64'], + os: { + platform: ['centos'], + version: ['7 (Core)'], + family: ['redhat'], + name: ['CentOS Linux'], + kernel: ['3.10.0-1062.1.2.el7.x86_64'], + }, + id: ['host-id-123'], + }, + process: { + pid: [2166], + name: ['google_accounts'], + }, + destination: { + ip: ['10.1.2.3'], + port: [80], + }, + user: { + name: ['root'], + }, + source: { + port: [59554], + ip: ['10.4.20.1'], + }, + event: { + action: ['socket_opened'], + module: ['system'], + dataset: ['socket'], + kind: ['event'], + }, + message: [ + 'Outbound socket (10.4.20.1:59554 -> 10.1.2.3:80) OPENED by process google_accounts (PID: 2166) and user root (UID: 0)', + ], +}; + +export const mockSocketClosedEvent: Ecs = { + _id: 'V-su4m0BOpWiDweSLkXY', + timestamp: '2019-10-19T04:02:19.473Z', + process: { + pid: [2166], + name: ['google_accounts'], + }, + user: { + name: ['root'], + }, + source: { + port: [59508], + ip: ['10.4.20.1'], + }, + event: { + dataset: ['socket'], + kind: ['event'], + action: ['socket_closed'], + module: ['system'], + }, + message: [ + 'Outbound socket (10.4.20.1:59508 -> 10.1.2.3:80) CLOSED by process google_accounts (PID: 2166) and user root (UID: 0)', + ], + network: { + community_id: ['1:network-community_id'], + direction: ['outbound'], + transport: ['tcp'], + }, + destination: { + ip: ['10.1.2.3'], + port: [80], + }, + host: { + name: ['foohost'], + architecture: ['x86_64'], + os: { + version: ['7 (Core)'], + family: ['redhat'], + name: ['CentOS Linux'], + kernel: ['3.10.0-1062.1.2.el7.x86_64'], + platform: ['centos'], + }, + id: ['host-id-123'], + }, +}; + +export const mockDnsEvent: Ecs = { + _id: 'VUTUqm0BgJt5sZM7nd5g', + destination: { + domain: ['ten.one.one.one'], + port: [53], + bytes: [137], + ip: ['10.1.1.1'], + geo: { + continent_name: ['Oceania'], + location: { + lat: [-33.494], + lon: [143.2104], + }, + country_iso_code: ['AU'], + country_name: ['Australia'], + city_name: [''], + }, + }, + host: { + architecture: ['armv7l'], + id: ['host-id'], + os: { + family: ['debian'], + platform: ['raspbian'], + version: ['9 (stretch)'], + name: ['Raspbian GNU/Linux'], + kernel: ['4.19.57-v7+'], + }, + name: ['iot.example.com'], + }, + dns: { + question: { + name: ['lookup.example.com'], + type: ['A'], + }, + response_code: ['NOERROR'], + resolved_ip: ['10.1.2.3'], + }, + timestamp: '2019-10-08T10:05:23.241Z', + network: { + community_id: ['1:network-community_id'], + direction: ['outbound'], + bytes: [177], + transport: ['udp'], + protocol: ['dns'], + }, + event: { + duration: [6937500], + category: ['network_traffic'], + dataset: ['dns'], + kind: ['event'], + end: ['2019-10-08T10:05:23.248Z'], + start: ['2019-10-08T10:05:23.241Z'], + }, + source: { + port: [58732], + bytes: [40], + ip: ['10.9.9.9'], + }, +}; + +export const mockEndpointProcessExecutionMalwarePreventionAlert: Ecs = { + process: { + hash: { + md5: ['177afc1eb0be88eb9983fb74111260c4'], + sha256: ['3be13acde2f4dcded4fd8d518a513bfc9882407a6e384ffb17d12710db7d76fb'], + sha1: ['f573b85e9beb32121f1949217947b2adc6749e3d'], + }, + entity_id: [ + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTY5MjAtMTMyNDg5OTk2OTAuNDgzMzA3NzAw', + ], + executable: [ + 'C:\\Users\\sean\\Downloads\\3be13acde2f4dcded4fd8d518a513bfc9882407a6e384ffb17d12710db7d76fb.exe', + ], + name: [ + 'C:\\Users\\sean\\Downloads\\3be13acde2f4dcded4fd8d518a513bfc9882407a6e384ffb17d12710db7d76fb.exe', + ], + pid: [6920], + args: [ + 'C:\\Users\\sean\\Downloads\\3be13acde2f4dcded4fd8d518a513bfc9882407a6e384ffb17d12710db7d76fb.exe', + ], + }, + host: { + os: { + full: ['Windows Server 2019 Datacenter 1809 (10.0.17763.1518)'], + name: ['Windows'], + version: ['1809 (10.0.17763.1518)'], + platform: ['windows'], + family: ['windows'], + kernel: ['1809 (10.0.17763.1518)'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + architecture: ['x86_64'], + ip: ['10.1.2.3'], + id: ['d8ad572e-d224-4044-a57d-f5a84c0dfe5d'], + name: ['win2019-endpoint-1'], + }, + file: { + mtime: ['2020-11-04T21:40:51.494Z'], + path: [ + 'C:\\Users\\sean\\Downloads\\3be13acde2f4dcded4fd8d518a513bfc9882407a6e384ffb17d12710db7d76fb.exe', + ], + owner: ['sean'], + hash: { + md5: ['177afc1eb0be88eb9983fb74111260c4'], + sha256: ['3be13acde2f4dcded4fd8d518a513bfc9882407a6e384ffb17d12710db7d76fb'], + sha1: ['f573b85e9beb32121f1949217947b2adc6749e3d'], + }, + name: ['3be13acde2f4dcded4fd8d518a513bfc9882407a6e384ffb17d12710db7d76fb.exe'], + extension: ['exe'], + size: [1604112], + }, + event: { + category: ['malware', 'intrusion_detection', 'process'], + outcome: ['success'], + severity: [73], + code: ['malicious_file'], + action: ['execution'], + id: ['LsuMZVr+sdhvehVM++++Gp2Y'], + kind: ['alert'], + created: ['2020-11-04T21:41:30.533Z'], + module: ['endpoint'], + type: ['info', 'start', 'denied'], + dataset: ['endpoint.alerts'], + }, + agent: { + type: ['endpoint'], + }, + timestamp: '2020-11-04T21:41:30.533Z', + message: ['Malware Prevention Alert'], + _id: '0dA2lXUBn9bLIbfPkY7d', +}; + +export const mockEndpointLibraryLoadEvent: Ecs = { + file: { + path: ['C:\\Windows\\System32\\bcrypt.dll'], + hash: { + md5: ['00439016776de367bad087d739a03797'], + sha1: ['2c4ba5c1482987d50a182bad915f52cd6611ee63'], + sha256: ['e70f5d8f87aab14e3160227d38387889befbe37fa4f8f5adc59eff52804b35fd'], + }, + name: ['bcrypt.dll'], + }, + host: { + os: { + full: ['Windows Server 2019 Datacenter 1809 (10.0.17763.1697)'], + name: ['Windows'], + version: ['1809 (10.0.17763.1697)'], + family: ['windows'], + kernel: ['1809 (10.0.17763.1697)'], + platform: ['windows'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + name: ['win2019-endpoint-1'], + architecture: ['x86_64'], + ip: ['10.1.2.3'], + id: ['d8ad572e-d224-4044-a57d-f5a84c0dfe5d'], + }, + event: { + category: ['library'], + kind: ['event'], + created: ['2021-02-05T21:27:23.921Z'], + module: ['endpoint'], + action: ['load'], + type: ['start'], + id: ['LzzWB9jjGmCwGMvk++++Da5H'], + dataset: ['endpoint.events.library'], + }, + process: { + name: ['sshd.exe'], + pid: [9644], + entity_id: [ + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTk2NDQtMTMyNTcwMzQwNDEuNzgyMTczODAw', + ], + executable: ['C:\\Program Files\\OpenSSH-Win64\\sshd.exe'], + }, + agent: { + type: ['endpoint'], + }, + user: { + name: ['SYSTEM'], + domain: ['NT AUTHORITY'], + }, + message: ['Endpoint DLL load event'], + timestamp: '2021-02-05T21:27:23.921Z', + _id: 'IAUYdHcBGrBB52F2zo8Q', +}; + +export const mockEndpointRegistryModificationEvent: Ecs = { + host: { + os: { + full: ['Windows Server 2019 Datacenter 1809 (10.0.17763.1697)'], + name: ['Windows'], + version: ['1809 (10.0.17763.1697)'], + family: ['windows'], + kernel: ['1809 (10.0.17763.1697)'], + platform: ['windows'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + name: ['win2019-endpoint-1'], + architecture: ['x86_64'], + ip: ['10.1.2.3'], + id: ['d8ad572e-d224-4044-a57d-f5a84c0dfe5d'], + }, + event: { + category: ['registry'], + kind: ['event'], + created: ['2021-02-04T13:44:31.559Z'], + module: ['endpoint'], + action: ['modification'], + type: ['change'], + id: ['LzzWB9jjGmCwGMvk++++CbOn'], + dataset: ['endpoint.events.registry'], + }, + process: { + name: ['GoogleUpdate.exe'], + pid: [7408], + entity_id: [ + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTc0MDgtMTMyNTY5MTk4NDguODY4NTI0ODAw', + ], + executable: ['C:\\Program Files (x86)\\Google\\Update\\GoogleUpdate.exe'], + }, + registry: { + hive: ['HKLM'], + key: [ + 'SOFTWARE\\WOW6432Node\\Google\\Update\\ClientState\\{430FD4D0-B729-4F61-AA34-91526481799D}\\CurrentState', + ], + path: [ + 'HKLM\\SOFTWARE\\WOW6432Node\\Google\\Update\\ClientState\\{430FD4D0-B729-4F61-AA34-91526481799D}\\CurrentState\\StateValue', + ], + value: ['StateValue'], + }, + agent: { + type: ['endpoint'], + }, + user: { + name: ['SYSTEM'], + domain: ['NT AUTHORITY'], + }, + message: ['Endpoint registry event'], + timestamp: '2021-02-04T13:44:31.559Z', + _id: '4cxLbXcBGrBB52F2uOfF', +}; diff --git a/x-pack/plugins/timelines/public/mock/plugin_mock.tsx b/x-pack/plugins/timelines/public/mock/plugin_mock.tsx new file mode 100644 index 0000000000000..8d2141d62f253 --- /dev/null +++ b/x-pack/plugins/timelines/public/mock/plugin_mock.tsx @@ -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 React from 'react'; +import { + LastUpdatedAt, + LastUpdatedAtProps, + LoadingPanelProps, + LoadingPanel, + useDraggableKeyboardWrapper, +} from '../components'; +import { useAddToTimeline, useAddToTimelineSensor } from '../hooks/use_add_to_timeline'; + +export const createTGridMocks = () => ({ + // eslint-disable-next-line react/display-name + getTGrid: () => <>{'hello grid'}</>, + // eslint-disable-next-line react/display-name + getLastUpdated: (props: LastUpdatedAtProps) => <LastUpdatedAt {...props} />, + // eslint-disable-next-line react/display-name + getLoadingPanel: (props: LoadingPanelProps) => <LoadingPanel {...props} />, + getUseAddToTimeline: () => useAddToTimeline, + getUseAddToTimelineSensor: () => useAddToTimelineSensor, + getUseDraggableKeyboardWrapper: () => useDraggableKeyboardWrapper, +}); diff --git a/x-pack/plugins/timelines/public/mock/test_providers.tsx b/x-pack/plugins/timelines/public/mock/test_providers.tsx new file mode 100644 index 0000000000000..9fa6177cccee1 --- /dev/null +++ b/x-pack/plugins/timelines/public/mock/test_providers.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { I18nProvider } from '@kbn/i18n/react'; + +import React from 'react'; +import { DragDropContext, DropResult, ResponderProvided } from 'react-beautiful-dnd'; +import { Provider as ReduxStoreProvider } from 'react-redux'; +import { Store } from 'redux'; +import { BehaviorSubject } from 'rxjs'; +import { ThemeProvider } from 'styled-components'; +import { createStore, TimelineState } from '../types'; +import { mockGlobalState } from './global_state'; + +import { createKibanaContextProviderMock, createStartServicesMock } from './kibana_react.mock'; +import { createSecuritySolutionStorageMock, localStorageMock } from './mock_local_storage'; + +const state: TimelineState = mockGlobalState; + +interface Props { + children: React.ReactNode; + store?: Store; + onDragEnd?: (result: DropResult, provided: ResponderProvided) => void; +} + +export const kibanaObservable = new BehaviorSubject(createStartServicesMock()); + +Object.defineProperty(window, 'localStorage', { + value: localStorageMock(), +}); +window.scrollTo = jest.fn(); +const MockKibanaContextProvider = createKibanaContextProviderMock(); +const { storage } = createSecuritySolutionStorageMock(); + +/** A utility for wrapping children in the providers required to run most tests */ +const TestProvidersComponent: React.FC<Props> = ({ + children, + store = createStore(state, storage), + onDragEnd = jest.fn(), +}) => ( + <I18nProvider> + <MockKibanaContextProvider> + <ReduxStoreProvider store={store}> + <ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}> + <DragDropContext onDragEnd={onDragEnd}>{children}</DragDropContext> + </ThemeProvider> + </ReduxStoreProvider> + </MockKibanaContextProvider> + </I18nProvider> +); + +export const TestProviders = React.memo(TestProvidersComponent); diff --git a/x-pack/plugins/timelines/public/plugin.ts b/x-pack/plugins/timelines/public/plugin.ts index 76a692cf8ed10..a6076d91eea1d 100644 --- a/x-pack/plugins/timelines/public/plugin.ts +++ b/x-pack/plugins/timelines/public/plugin.ts @@ -5,27 +5,67 @@ * 2.0. */ -import { CoreSetup, Plugin, PluginInitializerContext } from '../../../../src/core/public'; -import { TimelinesPluginSetup, TimelineProps } from './types'; -import { getTimelineLazy } from './methods'; +import { Store } from 'redux'; -export class TimelinesPlugin implements Plugin<TimelinesPluginSetup> { +import { Storage } from '../../../../src/plugins/kibana_utils/public'; +import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import { + CoreSetup, + Plugin, + PluginInitializerContext, + CoreStart, +} from '../../../../src/core/public'; +import type { TimelinesUIStart, TGridProps } from './types'; +import { getLastUpdatedLazy, getLoadingPanelLazy, getTGridLazy } from './methods'; +import type { LastUpdatedAtProps, LoadingPanelProps } from './components'; +import { tGridReducer } from './store/t_grid/reducer'; +import { useDraggableKeyboardWrapper } from './components/drag_and_drop/draggable_keyboard_wrapper_hook'; +import { useAddToTimeline, useAddToTimelineSensor } from './hooks/use_add_to_timeline'; + +export class TimelinesPlugin implements Plugin<void, TimelinesUIStart> { constructor(private readonly initializerContext: PluginInitializerContext) {} + private _store: Store | undefined; + private _storage = new Storage(localStorage); + + public setup(core: CoreSetup) {} - public setup(core: CoreSetup): TimelinesPluginSetup { + public start(core: CoreStart, { data }: { data: DataPublicPluginStart }): TimelinesUIStart { const config = this.initializerContext.config.get<{ enabled: boolean }>(); if (!config.enabled) { - return {}; + return {} as TimelinesUIStart; } - return { - getTimeline: (props: TimelineProps) => { - return getTimelineLazy(props); + getTGrid: (props: TGridProps) => { + return getTGridLazy(props, { + store: this._store, + storage: this._storage, + data, + }); + }, + getTGridReducer: () => { + return tGridReducer; + }, + getLoadingPanel: (props: LoadingPanelProps) => { + return getLoadingPanelLazy(props); + }, + getLastUpdated: (props: LastUpdatedAtProps) => { + return getLastUpdatedLazy(props); + }, + getUseAddToTimeline: () => { + return useAddToTimeline; + }, + getUseAddToTimelineSensor: () => { + return useAddToTimelineSensor; + }, + getUseDraggableKeyboardWrapper: () => { + return useDraggableKeyboardWrapper; + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + setTGridEmbeddedStore: (store: any) => { + this._store = store; }, }; } - public start() {} - public stop() {} } diff --git a/x-pack/plugins/timelines/public/store/t_grid/actions.ts b/x-pack/plugins/timelines/public/store/t_grid/actions.ts new file mode 100644 index 0000000000000..74cccf4ac2401 --- /dev/null +++ b/x-pack/plugins/timelines/public/store/t_grid/actions.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import actionCreatorFactory from 'typescript-fsa'; +import type { TimelineNonEcsData } from '../../../common/search_strategy'; +import type { + ColumnHeaderOptions, + SortColumnTimeline, + TimelineExpandedDetailType, +} from '../../../common/types/timeline'; +// eslint-disable-next-line no-duplicate-imports +import { TimelineTabs } from '../../../common/types/timeline'; +import { InitialyzeTGridSettings, TGridPersistInput } from './types'; + +const actionCreator = actionCreatorFactory('x-pack/timelines/t-grid'); + +export const createTGrid = actionCreator<TGridPersistInput>('CREATE_TIMELINE'); + +export const upsertColumn = actionCreator<{ + column: ColumnHeaderOptions; + id: string; + index: number; +}>('UPSERT_COLUMN'); + +export const applyDeltaToColumnWidth = actionCreator<{ + id: string; + columnId: string; + delta: number; +}>('APPLY_DELTA_TO_COLUMN_WIDTH'); + +export type ToggleDetailPanel = TimelineExpandedDetailType & { + tabType?: TimelineTabs; + timelineId: string; +}; + +export const toggleDetailPanel = actionCreator<ToggleDetailPanel>('TOGGLE_DETAIL_PANEL'); + +export const removeColumn = actionCreator<{ + id: string; + columnId: string; +}>('REMOVE_COLUMN'); + +export const updateIsLoading = actionCreator<{ + id: string; + isLoading: boolean; +}>('UPDATE_LOADING'); + +export const updateColumns = actionCreator<{ + id: string; + columns: ColumnHeaderOptions[]; +}>('UPDATE_COLUMNS'); + +export const updateItemsPerPage = actionCreator<{ id: string; itemsPerPage: number }>( + 'UPDATE_ITEMS_PER_PAGE' +); + +export const updateItemsPerPageOptions = actionCreator<{ + id: string; + itemsPerPageOptions: number[]; +}>('UPDATE_ITEMS_PER_PAGE_OPTIONS'); + +export const updateSort = actionCreator<{ id: string; sort: SortColumnTimeline[] }>('UPDATE_SORT'); + +export const setSelected = actionCreator<{ + id: string; + eventIds: Readonly<Record<string, TimelineNonEcsData[]>>; + isSelected: boolean; + isSelectAllChecked: boolean; +}>('SET_TIMELINE_SELECTED'); + +export const clearSelected = actionCreator<{ + id: string; +}>('CLEAR_TIMELINE_SELECTED'); + +export const setEventsLoading = actionCreator<{ + id: string; + eventIds: string[]; + isLoading: boolean; +}>('SET_TIMELINE_EVENTS_LOADING'); + +export const clearEventsLoading = actionCreator<{ + id: string; +}>('CLEAR_TIMELINE_EVENTS_LOADING'); + +export const setEventsDeleted = actionCreator<{ + id: string; + eventIds: string[]; + isDeleted: boolean; +}>('SET_TIMELINE_EVENTS_DELETED'); + +export const clearEventsDeleted = actionCreator<{ + id: string; +}>('CLEAR_TIMELINE_EVENTS_DELETED'); + +export const initializeTGridSettings = actionCreator<InitialyzeTGridSettings>('INITIALIZE_TGRID'); + +export const setTGridSelectAll = actionCreator<{ id: string; selectAll: boolean }>( + 'SET_TGRID_SELECT_ALL' +); diff --git a/x-pack/plugins/timelines/public/store/t_grid/defaults.ts b/x-pack/plugins/timelines/public/store/t_grid/defaults.ts new file mode 100644 index 0000000000000..8caae1aabbe01 --- /dev/null +++ b/x-pack/plugins/timelines/public/store/t_grid/defaults.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Direction } from '../../../common/search_strategy'; +import type { ColumnHeaderOptions, ColumnHeaderType } from '../../../common/types/timeline'; +import { + DEFAULT_COLUMN_MIN_WIDTH, + DEFAULT_DATE_COLUMN_MIN_WIDTH, +} from '../../components/t_grid/body/constants'; +import type { SubsetTGridModel } from './model'; +import * as i18n from './translations'; + +export const defaultColumnHeaderType: ColumnHeaderType = 'not-filtered'; + +export const defaultHeaders: ColumnHeaderOptions[] = [ + { + columnHeaderType: defaultColumnHeaderType, + id: '@timestamp', + type: 'number', + initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'message', + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'event.category', + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'event.action', + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'host.name', + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'source.ip', + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'destination.ip', + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'user.name', + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, +]; + +export const tGridDefaults: SubsetTGridModel = { + columns: defaultHeaders, + dateRange: { start: '', end: '' }, + deletedEventIds: [], + excludedRowRendererIds: [], + expandedDetail: {}, + filters: [], + kqlQuery: { + filterQuery: null, + }, + indexNames: [], + isLoading: false, + isSelectAllChecked: false, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50, 100], + loadingEventIds: [], + selectedEventIds: {}, + showCheckboxes: false, + sort: [ + { + columnId: '@timestamp', + columnType: 'date', + sortDirection: Direction.desc, + }, + ], + savedObjectId: null, + version: null, +}; + +export const getTGridManageDefaults = (id: string) => ({ + defaultColumns: defaultHeaders, + loadingText: i18n.LOADING_EVENTS, + footerText: i18n.TOTAL_COUNT_OF_EVENTS, + documentType: '', + selectAll: false, + id, + isLoading: false, + queryFields: [], + title: '', + unit: (n: number) => i18n.UNIT(n), +}); diff --git a/x-pack/plugins/timelines/public/store/t_grid/helpers.ts b/x-pack/plugins/timelines/public/store/t_grid/helpers.ts new file mode 100644 index 0000000000000..e114f4516c79e --- /dev/null +++ b/x-pack/plugins/timelines/public/store/t_grid/helpers.ts @@ -0,0 +1,424 @@ +/* + * 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 { omit, union } from 'lodash/fp'; + +import { isEmpty } from 'lodash'; +import type { ToggleDetailPanel } from './actions'; +import { TGridPersistInput, TimelineById, TimelineId } from './types'; +import type { TGridModel, TGridModelSettings } from './model'; + +import type { ColumnHeaderOptions, SortColumnTimeline } from '../../../common/types/timeline'; +import { getTGridManageDefaults, tGridDefaults } from './defaults'; + +export const isNotNull = <T>(value: T | null): value is T => value !== null; +export type Maybe<T> = T | null; + +enum TimelineTabs { + query = 'query', + graph = 'graph', + notes = 'notes', + pinned = 'pinned', + eql = 'eql', +} + +/** The default minimum width of a column (when a width for the column type is not specified) */ +export const DEFAULT_COLUMN_MIN_WIDTH = 180; // px + +/** The minimum width of a resized column */ +export const RESIZED_COLUMN_MIN_WITH = 70; // px + +export const shouldResetActiveTimelineContext = ( + id: string, + oldTimeline: TGridModel, + newTimeline: TGridModel +) => { + if (id === TimelineId.active && oldTimeline.savedObjectId !== newTimeline.savedObjectId) { + return true; + } + return false; +}; + +interface AddTimelineColumnParams { + column: ColumnHeaderOptions; + id: string; + index: number; + timelineById: TimelineById; +} + +interface TimelineNonEcsData { + field: string; + value?: Maybe<string[]>; +} + +interface CreateTGridParams extends TGridPersistInput { + timelineById: TimelineById; +} + +/** Adds a new `Timeline` to the provided collection of `TimelineById` */ +export const createInitTGrid = ({ + id, + timelineById, + ...tGridProps +}: CreateTGridParams): TimelineById => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + ...tGridDefaults, + ...tGridProps, + isLoading: false, + savedObjectId: null, + version: null, + }, + }; +}; + +/** + * Adds or updates a column. When updating a column, it will be moved to the + * new index + */ +export const upsertTimelineColumn = ({ + column, + id, + index, + timelineById, +}: AddTimelineColumnParams): TimelineById => { + const timeline = timelineById[id]; + const alreadyExistsAtIndex = timeline.columns.findIndex((c) => c.id === column.id); + + if (alreadyExistsAtIndex !== -1) { + // remove the existing entry and add the new one at the specified index + const reordered = timeline.columns.filter((c) => c.id !== column.id); + reordered.splice(index, 0, column); // ⚠️ mutation + + return { + ...timelineById, + [id]: { + ...timeline, + columns: reordered, + }, + }; + } + + // add the new entry at the specified index + const columns = [...timeline.columns]; + columns.splice(index, 0, column); // ⚠️ mutation + + return { + ...timelineById, + [id]: { + ...timeline, + columns, + }, + }; +}; + +interface RemoveTimelineColumnParams { + id: string; + columnId: string; + timelineById: TimelineById; +} + +export const removeTimelineColumn = ({ + id, + columnId, + timelineById, +}: RemoveTimelineColumnParams): TimelineById => { + const timeline = timelineById[id]; + + const columns = timeline.columns.filter((c) => c.id !== columnId); + + return { + ...timelineById, + [id]: { + ...timeline, + columns, + }, + }; +}; + +interface InitializeTgridParams { + id: string; + timelineById: TimelineById; + tGridSettingsProps: Partial<TGridModelSettings>; +} + +export const setInitializeTgridSettings = ({ + id, + timelineById, + tGridSettingsProps, +}: InitializeTgridParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...tGridDefaults, + ...timeline, + ...getTGridManageDefaults(id), + ...tGridSettingsProps, + ...(!timeline || (isEmpty(timeline.columns) && !isEmpty(tGridSettingsProps.defaultColumns)) + ? { columns: tGridSettingsProps.defaultColumns } + : {}), + sort: tGridDefaults.sort, + loadingEventIds: tGridDefaults.loadingEventIds, + }, + }; +}; + +interface ApplyDeltaToTimelineColumnWidth { + id: string; + columnId: string; + delta: number; + timelineById: TimelineById; +} + +export const applyDeltaToTimelineColumnWidth = ({ + id, + columnId, + delta, + timelineById, +}: ApplyDeltaToTimelineColumnWidth): TimelineById => { + const timeline = timelineById[id]; + + const columnIndex = timeline.columns.findIndex((c) => c.id === columnId); + if (columnIndex === -1) { + // the column was not found + return { + ...timelineById, + [id]: { + ...timeline, + }, + }; + } + + const requestedWidth = + (timeline.columns[columnIndex].initialWidth ?? DEFAULT_COLUMN_MIN_WIDTH) + delta; // raw change in width + const initialWidth = Math.max(RESIZED_COLUMN_MIN_WITH, requestedWidth); // if the requested width is smaller than the min, use the min + + const columnWithNewWidth = { + ...timeline.columns[columnIndex], + initialWidth, + }; + + const columns = [ + ...timeline.columns.slice(0, columnIndex), + columnWithNewWidth, + ...timeline.columns.slice(columnIndex + 1), + ]; + + return { + ...timelineById, + [id]: { + ...timeline, + columns, + }, + }; +}; + +interface UpdateTimelineColumnsParams { + id: string; + columns: ColumnHeaderOptions[]; + timelineById: TimelineById; +} + +export const updateTimelineColumns = ({ + id, + columns, + timelineById, +}: UpdateTimelineColumnsParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + columns, + }, + }; +}; + +interface UpdateTimelineSortParams { + id: string; + sort: SortColumnTimeline[]; + timelineById: TimelineById; +} + +export const updateTimelineSort = ({ + id, + sort, + timelineById, +}: UpdateTimelineSortParams): TimelineById => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + sort, + }, + }; +}; + +interface UpdateTimelineItemsPerPageParams { + id: string; + itemsPerPage: number; + timelineById: TimelineById; +} + +export const updateTimelineItemsPerPage = ({ + id, + itemsPerPage, + timelineById, +}: UpdateTimelineItemsPerPageParams) => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + itemsPerPage, + }, + }; +}; + +interface UpdateTimelinePerPageOptionsParams { + id: string; + itemsPerPageOptions: number[]; + timelineById: TimelineById; +} + +export const updateTimelinePerPageOptions = ({ + id, + itemsPerPageOptions, + timelineById, +}: UpdateTimelinePerPageOptionsParams) => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + itemsPerPageOptions, + }, + }; +}; + +interface SetDeletedTimelineEventsParams { + id: string; + eventIds: string[]; + isDeleted: boolean; + timelineById: TimelineById; +} + +export const setDeletedTimelineEvents = ({ + id, + eventIds, + isDeleted, + timelineById, +}: SetDeletedTimelineEventsParams): TimelineById => { + const timeline = timelineById[id]; + + const deletedEventIds = isDeleted + ? union(timeline.deletedEventIds, eventIds) + : timeline.deletedEventIds.filter((currentEventId) => !eventIds.includes(currentEventId)); + + const selectedEventIds = Object.fromEntries( + Object.entries(timeline.selectedEventIds).filter( + ([selectedEventId]) => !deletedEventIds.includes(selectedEventId) + ) + ); + + const isSelectAllChecked = + Object.keys(selectedEventIds).length > 0 ? timeline.isSelectAllChecked : false; + + return { + ...timelineById, + [id]: { + ...timeline, + deletedEventIds, + selectedEventIds, + isSelectAllChecked, + }, + }; +}; + +interface SetLoadingTimelineEventsParams { + id: string; + eventIds: string[]; + isLoading: boolean; + timelineById: TimelineById; +} + +export const setLoadingTimelineEvents = ({ + id, + eventIds, + isLoading, + timelineById, +}: SetLoadingTimelineEventsParams): TimelineById => { + const timeline = timelineById[id]; + + const loadingEventIds = isLoading + ? union(timeline.loadingEventIds, eventIds) + : timeline.loadingEventIds.filter((currentEventId) => !eventIds.includes(currentEventId)); + + return { + ...timelineById, + [id]: { + ...timeline, + loadingEventIds, + }, + }; +}; + +interface SetSelectedTimelineEventsParams { + id: string; + eventIds: Record<string, TimelineNonEcsData[]>; + isSelectAllChecked: boolean; + isSelected: boolean; + timelineById: TimelineById; +} + +export const setSelectedTimelineEvents = ({ + id, + eventIds, + isSelectAllChecked = false, + isSelected, + timelineById, +}: SetSelectedTimelineEventsParams): TimelineById => { + const timeline = timelineById[id]; + + const selectedEventIds = isSelected + ? { ...timeline.selectedEventIds, ...eventIds } + : omit(Object.keys(eventIds), timeline.selectedEventIds); + + return { + ...timelineById, + [id]: { + ...timeline, + selectedEventIds, + isSelectAllChecked, + }, + }; +}; + +export const updateTimelineDetailsPanel = (action: ToggleDetailPanel) => { + const { tabType } = action; + + const panelViewOptions = new Set(['eventDetail', 'hostDetail', 'networkDetail']); + const expandedTabType = tabType ?? TimelineTabs.query; + + return action.panelView && panelViewOptions.has(action.panelView) + ? { + [expandedTabType]: { + params: action.params ? { ...action.params } : {}, + panelView: action.panelView, + }, + } + : { + [expandedTabType]: {}, + }; +}; diff --git a/x-pack/plugins/timelines/public/store/t_grid/index.ts b/x-pack/plugins/timelines/public/store/t_grid/index.ts new file mode 100644 index 0000000000000..d37c62bc8c265 --- /dev/null +++ b/x-pack/plugins/timelines/public/store/t_grid/index.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + Action, + applyMiddleware, + CombinedState, + compose, + createStore as createReduxStore, + PreloadedState, + Store, +} from 'redux'; + +import { createEpicMiddleware } from 'redux-observable'; +import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; +import { TimelineState, TGridEpicDependencies } from '../../types'; +import { tGridReducer } from './reducer'; +import { getTGridByIdSelector } from './selectors'; + +export * from './model'; +export * as tGridActions from './actions'; +export * as tGridSelectors from './selectors'; +export * from './types'; +export { tGridReducer }; + +export type State = CombinedState<TimelineState>; +type ComposeType = typeof compose; +declare global { + interface Window { + __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: ComposeType; + } +} + +/** + * Factory for Security App's redux store. + */ +export const createStore = ( + state: PreloadedState<TimelineState>, + storage: Storage +): Store<State, Action> => { + const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; + + const middlewareDependencies: TGridEpicDependencies<State> = { + tGridByIdSelector: getTGridByIdSelector, + storage, + }; + + const epicMiddleware = createEpicMiddleware<Action, Action, State, typeof middlewareDependencies>( + { + dependencies: middlewareDependencies, + } + ); + + const store: Store<State, Action> = createReduxStore( + tGridReducer, + state, + composeEnhancers(applyMiddleware(epicMiddleware)) + ); + + return store; +}; diff --git a/x-pack/plugins/timelines/public/store/t_grid/inputs.ts b/x-pack/plugins/timelines/public/store/t_grid/inputs.ts new file mode 100644 index 0000000000000..6c2beca3826aa --- /dev/null +++ b/x-pack/plugins/timelines/public/store/t_grid/inputs.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type Refetch = () => void; + +export interface InspectQuery { + dsl: string[]; + response: string[]; +} diff --git a/x-pack/plugins/timelines/public/store/t_grid/model.ts b/x-pack/plugins/timelines/public/store/t_grid/model.ts new file mode 100644 index 0000000000000..67b56540c8a42 --- /dev/null +++ b/x-pack/plugins/timelines/public/store/t_grid/model.ts @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { EuiDataGridColumn } from '@elastic/eui'; +import type { Filter, FilterManager } from '../../../../../../src/plugins/data/public'; +import type { TimelineNonEcsData } from '../../../common/search_strategy'; +import type { + ColumnHeaderOptions, + TimelineExpandedDetail, + SortColumnTimeline, + SerializedFilterQuery, +} from '../../../common/types/timeline'; +// eslint-disable-next-line no-duplicate-imports +import { RowRendererId } from '../../../common/types/timeline'; + +export interface TGridModelSettings { + documentType: string; + defaultColumns: Array< + Pick<EuiDataGridColumn, 'display' | 'displayAsText' | 'id' | 'initialWidth'> & + ColumnHeaderOptions + >; + /** A list of Ids of excluded Row Renderers */ + excludedRowRendererIds: RowRendererId[]; + filterManager?: FilterManager; + footerText: string; + loadingText: string; + queryFields: string[]; + selectAll: boolean; + showCheckboxes?: boolean; + title: string; +} +export interface TGridModel extends TGridModelSettings { + /** The columns displayed in the timeline */ + columns: Array< + Pick<EuiDataGridColumn, 'display' | 'displayAsText' | 'id' | 'initialWidth'> & + ColumnHeaderOptions + >; + /** Specifies the granularity of the date range (e.g. 1 Day / Week / Month) applicable to the mini-map */ + dateRange: { + start: string; + end: string; + }; + /** Events to not be rendered **/ + deletedEventIds: string[]; + /** This holds the view information for the flyout when viewing timeline in a consuming view (i.e. hosts page) or the side panel in the primary timeline view */ + expandedDetail: TimelineExpandedDetail; + filters?: Filter[]; + /** When non-empty, display a graph view for this event */ + graphEventId?: string; + /** the KQL query in the KQL bar */ + kqlQuery: { + // TODO convert to nodebuilder + filterQuery: SerializedFilterQuery | null; + }; + /** Uniquely identifies the timeline */ + id: string; + indexNames: string[]; + isLoading: boolean; + /** If selectAll checkbox in header is checked **/ + isSelectAllChecked: boolean; + /** The number of items to show in a single page of results */ + itemsPerPage: number; + /** Displays a series of choices that when selected, become the value of `itemsPerPage` */ + itemsPerPageOptions: number[]; + /** Events to be rendered as loading **/ + loadingEventIds: string[]; + /** When true, shows checkboxes enabling selection. Selected events store in selectedEventIds **/ + showCheckboxes: boolean; + /** Specifies which column the timeline is sorted on, and the direction (ascending / descending) */ + sort: SortColumnTimeline[]; + /** Events selected on this timeline -- eventId to TimelineNonEcsData[] mapping of data required for batch actions **/ + selectedEventIds: Record<string, TimelineNonEcsData[]>; + savedObjectId: string | null; + version: string | null; +} + +export type TGridModelForTimeline = Pick< + TGridModel, + | 'columns' + | 'dateRange' + | 'deletedEventIds' + | 'excludedRowRendererIds' + | 'expandedDetail' + | 'filters' + | 'graphEventId' + | 'kqlQuery' + | 'id' + | 'indexNames' + | 'isLoading' + | 'isSelectAllChecked' + | 'itemsPerPage' + | 'itemsPerPageOptions' + | 'loadingEventIds' + | 'showCheckboxes' + | 'sort' + | 'selectedEventIds' + | 'savedObjectId' + | 'title' + | 'version' +>; + +export type SubsetTGridModel = Readonly< + Pick< + TGridModel, + | 'columns' + | 'dateRange' + | 'deletedEventIds' + | 'excludedRowRendererIds' + | 'expandedDetail' + | 'filters' + | 'kqlQuery' + | 'indexNames' + | 'isLoading' + | 'isSelectAllChecked' + | 'itemsPerPage' + | 'itemsPerPageOptions' + | 'loadingEventIds' + | 'showCheckboxes' + | 'sort' + | 'selectedEventIds' + | 'savedObjectId' + | 'version' + > +>; diff --git a/x-pack/plugins/timelines/public/store/t_grid/reducer.ts b/x-pack/plugins/timelines/public/store/t_grid/reducer.ts new file mode 100644 index 0000000000000..57c45f857554d --- /dev/null +++ b/x-pack/plugins/timelines/public/store/t_grid/reducer.ts @@ -0,0 +1,212 @@ +/* + * 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 { reducerWithInitialState } from 'typescript-fsa-reducers'; + +import { + applyDeltaToColumnWidth, + clearEventsDeleted, + clearEventsLoading, + clearSelected, + createTGrid, + initializeTGridSettings, + removeColumn, + setEventsDeleted, + setEventsLoading, + setTGridSelectAll, + setSelected, + toggleDetailPanel, + updateColumns, + updateIsLoading, + updateItemsPerPage, + updateItemsPerPageOptions, + updateSort, + upsertColumn, +} from './actions'; + +import { + applyDeltaToTimelineColumnWidth, + createInitTGrid, + setInitializeTgridSettings, + removeTimelineColumn, + setDeletedTimelineEvents, + setLoadingTimelineEvents, + setSelectedTimelineEvents, + updateTimelineColumns, + updateTimelineItemsPerPage, + updateTimelinePerPageOptions, + updateTimelineSort, + upsertTimelineColumn, + updateTimelineDetailsPanel, +} from './helpers'; + +import { TimelineState, EMPTY_TIMELINE_BY_ID } from './types'; + +export const initialTGridState: TimelineState = { + timelineById: EMPTY_TIMELINE_BY_ID, +}; + +/** The reducer for all timeline actions */ +export const tGridReducer = reducerWithInitialState(initialTGridState) + .case(upsertColumn, (state, { column, id, index }) => ({ + ...state, + timelineById: upsertTimelineColumn({ column, id, index, timelineById: state.timelineById }), + })) + .case(createTGrid, (state, timelineProps) => { + return { + ...state, + timelineById: createInitTGrid({ + ...timelineProps, + timelineById: state.timelineById, + }), + }; + }) + .case(toggleDetailPanel, (state, action) => ({ + ...state, + timelineById: { + ...state.timelineById, + [action.timelineId]: { + ...state.timelineById[action.timelineId], + expandedDetail: { + ...state.timelineById[action.timelineId].expandedDetail, + ...updateTimelineDetailsPanel(action), + }, + }, + }, + })) + .case(applyDeltaToColumnWidth, (state, { id, columnId, delta }) => ({ + ...state, + timelineById: applyDeltaToTimelineColumnWidth({ + id, + columnId, + delta, + timelineById: state.timelineById, + }), + })) + .case(removeColumn, (state, { id, columnId }) => ({ + ...state, + timelineById: removeTimelineColumn({ + id, + columnId, + timelineById: state.timelineById, + }), + })) + .case(setEventsDeleted, (state, { id, eventIds, isDeleted }) => ({ + ...state, + timelineById: setDeletedTimelineEvents({ + id, + eventIds, + timelineById: state.timelineById, + isDeleted, + }), + })) + .case(clearEventsDeleted, (state, { id }) => ({ + ...state, + timelineById: { + ...state.timelineById, + [id]: { + ...state.timelineById[id], + deletedEventIds: [], + }, + }, + })) + .case(setEventsLoading, (state, { id, eventIds, isLoading }) => ({ + ...state, + timelineById: setLoadingTimelineEvents({ + id, + eventIds, + timelineById: state.timelineById, + isLoading, + }), + })) + .case(clearEventsLoading, (state, { id }) => ({ + ...state, + timelineById: { + ...state.timelineById, + [id]: { + ...state.timelineById[id], + loadingEventIds: [], + }, + }, + })) + .case(setSelected, (state, { id, eventIds, isSelected, isSelectAllChecked }) => ({ + ...state, + timelineById: setSelectedTimelineEvents({ + id, + eventIds, + timelineById: state.timelineById, + isSelected, + isSelectAllChecked, + }), + })) + .case(clearSelected, (state, { id }) => ({ + ...state, + timelineById: { + ...state.timelineById, + [id]: { + ...state.timelineById[id], + selectedEventIds: {}, + isSelectAllChecked: false, + }, + }, + })) + .case(updateIsLoading, (state, { id, isLoading }) => ({ + ...state, + timelineById: { + ...state.timelineById, + [id]: { + ...state.timelineById[id], + isLoading, + }, + }, + })) + .case(updateColumns, (state, { id, columns }) => ({ + ...state, + timelineById: updateTimelineColumns({ + id, + columns, + timelineById: state.timelineById, + }), + })) + .case(updateSort, (state, { id, sort }) => ({ + ...state, + timelineById: updateTimelineSort({ id, sort, timelineById: state.timelineById }), + })) + .case(updateItemsPerPage, (state, { id, itemsPerPage }) => ({ + ...state, + timelineById: updateTimelineItemsPerPage({ + id, + itemsPerPage, + timelineById: state.timelineById, + }), + })) + .case(updateItemsPerPageOptions, (state, { id, itemsPerPageOptions }) => ({ + ...state, + timelineById: updateTimelinePerPageOptions({ + id, + itemsPerPageOptions, + timelineById: state.timelineById, + }), + })) + .case(initializeTGridSettings, (state, { id, ...tGridSettingsProps }) => ({ + ...state, + timelineById: setInitializeTgridSettings({ + id, + timelineById: state.timelineById, + tGridSettingsProps, + }), + })) + .case(setTGridSelectAll, (state, { id, selectAll }) => ({ + ...state, + timelineById: { + ...state.timelineById, + [id]: { + ...state.timelineById[id], + selectAll, + }, + }, + })) + .build(); diff --git a/x-pack/plugins/timelines/public/store/t_grid/selectors.ts b/x-pack/plugins/timelines/public/store/t_grid/selectors.ts new file mode 100644 index 0000000000000..710a842d4563a --- /dev/null +++ b/x-pack/plugins/timelines/public/store/t_grid/selectors.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { getOr } from 'lodash/fp'; +import { createSelector } from 'reselect'; +import { TGridModel } from '.'; +import { tGridDefaults, getTGridManageDefaults } from './defaults'; + +const getDefaultTgrid = (id: string) => ({ ...tGridDefaults, ...getTGridManageDefaults(id) }); + +export const selectTGridById = (state: unknown, timelineId: string): TGridModel => { + return getOr( + getOr(getDefaultTgrid(timelineId), ['timelineById', timelineId], state), + ['timeline', 'timelineById', timelineId], + state + ); +}; + +export const getTGridByIdSelector = () => createSelector(selectTGridById, (tGrid) => tGrid); + +export const getManageTimelineById = () => + createSelector( + selectTGridById, + ({ + documentType, + defaultColumns, + isLoading, + filterManager, + footerText, + loadingText, + queryFields, + selectAll, + title, + }) => ({ + documentType, + defaultColumns, + isLoading, + filterManager, + footerText, + loadingText, + queryFields, + selectAll, + title, + }) + ); diff --git a/x-pack/plugins/timelines/public/store/t_grid/translations.ts b/x-pack/plugins/timelines/public/store/t_grid/translations.ts new file mode 100644 index 0000000000000..fa2b6f1c038fe --- /dev/null +++ b/x-pack/plugins/timelines/public/store/t_grid/translations.ts @@ -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 { i18n } from '@kbn/i18n'; + +export const EVENTS = i18n.translate('xpack.timelines.tGrid.eventsLabel', { + defaultMessage: 'Events', +}); + +export const LOADING_EVENTS = i18n.translate( + 'xpack.timelines.tGrid.footer.loadingEventsDataLabel', + { + defaultMessage: 'Loading Events', + } +); + +export const UNIT = (totalCount: number) => + i18n.translate('xpack.timelines.tGrid.unit', { + values: { totalCount }, + defaultMessage: `{totalCount, plural, =1 {event} other {events}}`, + }); + +export const TOTAL_COUNT_OF_EVENTS = i18n.translate( + 'xpack.timelines.tGrid.footer.totalCountOfEvents', + { + defaultMessage: 'events', + } +); diff --git a/x-pack/plugins/timelines/public/store/t_grid/types.ts b/x-pack/plugins/timelines/public/store/t_grid/types.ts new file mode 100644 index 0000000000000..c8c72e0310958 --- /dev/null +++ b/x-pack/plugins/timelines/public/store/t_grid/types.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; +import type { ColumnHeaderOptions } from '../../../common'; +import type { TGridModel, TGridModelSettings } from './model'; + +export interface AutoSavedWarningMsg { + timelineId: string | null; + newTimelineModel: TGridModel | null; +} + +/** A map of id to timeline */ +export interface TimelineById { + [id: string]: TGridModel; +} + +export interface InsertTimeline { + graphEventId?: string; + timelineId: string; + timelineSavedObjectId: string | null; + timelineTitle: string; +} + +export const EMPTY_TIMELINE_BY_ID: TimelineById = {}; // stable reference + +export interface TGridEpicDependencies<State> { + // kibana$: Observable<CoreStart>; + storage: Storage; + tGridByIdSelector: () => (state: State, timelineId: string) => TGridModel; +} + +/** The state of all timelines is stored here */ +export interface TimelineState { + timelineById: TimelineById; +} + +export enum TimelineId { + hostsPageEvents = 'hosts-page-events', + hostsPageExternalAlerts = 'hosts-page-external-alerts', + detectionsRulesDetailsPage = 'detections-rules-details-page', + detectionsPage = 'detections-page', + networkPageExternalAlerts = 'network-page-external-alerts', + active = 'timeline-1', + casePage = 'timeline-case', + test = 'test', // Reserved for testing purposes + alternateTest = 'alternateTest', +} + +export interface InitialyzeTGridSettings extends Partial<TGridModelSettings> { + id: string; +} + +export interface TGridPersistInput extends Partial<Omit<TGridModel, keyof TGridModelSettings>> { + id: string; + dateRange: { + start: string; + end: string; + }; + columns: ColumnHeaderOptions[]; + indexNames: string[]; + showCheckboxes?: boolean; +} diff --git a/x-pack/plugins/timelines/public/types.ts b/x-pack/plugins/timelines/public/types.ts index 1fa6d33a6af60..ffef1ee35c830 100644 --- a/x-pack/plugins/timelines/public/types.ts +++ b/x-pack/plugins/timelines/public/types.ts @@ -4,13 +4,44 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { ReactElement } from 'react'; - -export interface TimelinesPluginSetup { - getTimeline?: (props: TimelineProps) => ReactElement<TimelineProps>; +import type { SensorAPI } from 'react-beautiful-dnd'; +import { Store } from 'redux'; +import type { + LastUpdatedAtProps, + LoadingPanelProps, + UseDraggableKeyboardWrapper, + UseDraggableKeyboardWrapperProps, +} from './components'; +import type { TGridIntegratedProps } from './components/t_grid/integrated'; +import type { TGridStandaloneProps } from './components/t_grid/standalone'; +import type { UseAddToTimelineProps, UseAddToTimeline } from './hooks/use_add_to_timeline'; +export * from './store/t_grid'; +export interface TimelinesUIStart { + getTGrid: <T extends TGridType = 'embedded'>( + props: GetTGridProps<T> + ) => ReactElement<GetTGridProps<T>>; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getTGridReducer: () => any; + getLoadingPanel: (props: LoadingPanelProps) => ReactElement<LoadingPanelProps>; + getLastUpdated: (props: LastUpdatedAtProps) => ReactElement<LastUpdatedAtProps>; + getUseAddToTimeline: () => (props: UseAddToTimelineProps) => UseAddToTimeline; + getUseAddToTimelineSensor: () => (api: SensorAPI) => void; + getUseDraggableKeyboardWrapper: () => ( + props: UseDraggableKeyboardWrapperProps + ) => UseDraggableKeyboardWrapper; + setTGridEmbeddedStore: (store: Store) => void; } - -export interface TimelineProps { - timelineId: string; +interface TGridStandaloneCompProps extends TGridStandaloneProps { + type: 'standalone'; } +interface TGridIntegratedCompProps extends TGridIntegratedProps { + type: 'embedded'; +} +export type TGridType = 'standalone' | 'embedded'; +export type GetTGridProps<T extends TGridType> = T extends 'standalone' + ? TGridStandaloneCompProps + : T extends 'embedded' + ? TGridIntegratedCompProps + : TGridIntegratedCompProps; +export type TGridProps = TGridStandaloneCompProps | TGridIntegratedCompProps; diff --git a/x-pack/plugins/timelines/server/config.ts b/x-pack/plugins/timelines/server/config.ts index 31be256611803..958c673333873 100644 --- a/x-pack/plugins/timelines/server/config.ts +++ b/x-pack/plugins/timelines/server/config.ts @@ -8,7 +8,7 @@ import { TypeOf, schema } from '@kbn/config-schema'; export const ConfigSchema = schema.object({ - enabled: schema.boolean({ defaultValue: false }), + enabled: schema.boolean({ defaultValue: true }), }); export type ConfigType = TypeOf<typeof ConfigSchema>; diff --git a/x-pack/plugins/timelines/server/index.ts b/x-pack/plugins/timelines/server/index.ts index 65e2b6494c6f4..8ad2bafdcc13a 100644 --- a/x-pack/plugins/timelines/server/index.ts +++ b/x-pack/plugins/timelines/server/index.ts @@ -19,4 +19,4 @@ export function plugin(initializerContext: PluginInitializerContext) { return new TimelinesPlugin(initializerContext); } -export { TimelinesPluginSetup, TimelinesPluginStart } from './types'; +export { TimelinesPluginUI, TimelinesPluginStart } from './types'; diff --git a/x-pack/plugins/timelines/server/plugin.ts b/x-pack/plugins/timelines/server/plugin.ts index 825d42994e096..78e91f965e751 100644 --- a/x-pack/plugins/timelines/server/plugin.ts +++ b/x-pack/plugins/timelines/server/plugin.ts @@ -13,23 +13,41 @@ import { Logger, } from '../../../../src/core/server'; -import { TimelinesPluginSetup, TimelinesPluginStart } from './types'; +import { SetupPlugins, StartPlugins, TimelinesPluginUI, TimelinesPluginStart } from './types'; import { defineRoutes } from './routes'; +import { timelineSearchStrategyProvider } from './search_strategy/timeline'; +import { timelineEqlSearchStrategyProvider } from './search_strategy/timeline/eql'; +import { indexFieldsProvider } from './search_strategy/index_fields'; -export class TimelinesPlugin implements Plugin<TimelinesPluginSetup, TimelinesPluginStart> { +export class TimelinesPlugin + implements Plugin<TimelinesPluginUI, TimelinesPluginStart, SetupPlugins, StartPlugins> { private readonly logger: Logger; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); } - public setup(core: CoreSetup) { + public setup(core: CoreSetup<StartPlugins, TimelinesPluginStart>, plugins: SetupPlugins) { this.logger.debug('timelines: Setup'); const router = core.http.createRouter(); // Register server side APIs defineRoutes(router); + // Register search strategy + core.getStartServices().then(([_, depsStart]) => { + const TimelineSearchStrategy = timelineSearchStrategyProvider(depsStart.data); + const TimelineEqlSearchStrategy = timelineEqlSearchStrategyProvider(depsStart.data); + const IndexFields = indexFieldsProvider(); + + plugins.data.search.registerSearchStrategy('indexFields', IndexFields); + plugins.data.search.registerSearchStrategy('timelineSearchStrategy', TimelineSearchStrategy); + plugins.data.search.registerSearchStrategy( + 'timelineEqlSearchStrategy', + TimelineEqlSearchStrategy + ); + }); + return {}; } diff --git a/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.test.ts b/x-pack/plugins/timelines/server/search_strategy/index_fields/index.test.ts similarity index 99% rename from x-pack/plugins/security_solution/server/search_strategy/index_fields/index.test.ts rename to x-pack/plugins/timelines/server/search_strategy/index_fields/index.test.ts index 51892a1a05d55..f6d78f2f1259f 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.test.ts +++ b/x-pack/plugins/timelines/server/search_strategy/index_fields/index.test.ts @@ -126,7 +126,7 @@ describe('Index Fields', () => { }, { description: - 'Type of the agent. The agent type always stays the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', + 'Type of the agent. The agent type stays always the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', example: 'filebeat', name: 'agent.type', type: 'string', @@ -252,7 +252,7 @@ describe('Index Fields', () => { { category: 'agent', description: - 'Type of the agent. The agent type always stays the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', + 'Type of the agent. The agent type stays always the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', example: 'filebeat', name: 'agent.type', type: 'string', @@ -426,7 +426,7 @@ describe('Index Fields', () => { { category: 'agent', description: - 'Type of the agent. The agent type always stays the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', + 'Type of the agent. The agent type stays always the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', example: 'filebeat', name: 'agent.type', type: 'string', diff --git a/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.ts b/x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts similarity index 99% rename from x-pack/plugins/security_solution/server/search_strategy/index_fields/index.ts rename to x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts index 884621b13dea1..d100e8db21493 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts @@ -15,6 +15,8 @@ import { } from '../../../../../../src/plugins/data/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { FieldDescriptor } from '../../../../../../src/plugins/data/server/index_patterns'; + +// TODO cleanup path import { IndexFieldsStrategyResponse, IndexField, @@ -24,7 +26,7 @@ import { const apmIndexPattern = 'apm-*-transaction*'; -export const securitySolutionIndexFieldsProvider = (): ISearchStrategy< +export const indexFieldsProvider = (): ISearchStrategy< IndexFieldsStrategyRequest, IndexFieldsStrategyResponse > => { diff --git a/x-pack/plugins/security_solution/server/search_strategy/index_fields/mock.ts b/x-pack/plugins/timelines/server/search_strategy/index_fields/mock.ts similarity index 100% rename from x-pack/plugins/security_solution/server/search_strategy/index_fields/mock.ts rename to x-pack/plugins/timelines/server/search_strategy/index_fields/mock.ts diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/__mocks__/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/eql/__mocks__/index.ts similarity index 99% rename from x-pack/plugins/security_solution/server/search_strategy/timeline/eql/__mocks__/index.ts rename to x-pack/plugins/timelines/server/search_strategy/timeline/eql/__mocks__/index.ts index a3499b5855f50..7a2a754e8e6b9 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/__mocks__/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/eql/__mocks__/index.ts @@ -6,7 +6,7 @@ */ import { EqlSearchStrategyResponse } from '../../../../../../../../src/plugins/data/common'; -import { EqlSearchResponse } from '../../../../../common/detection_engine/types'; +import { EqlSearchResponse } from '../../../../../common'; export const sequenceResponse = ({ rawResponse: { diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/helpers.test.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.test.ts similarity index 100% rename from x-pack/plugins/security_solution/server/search_strategy/timeline/eql/helpers.test.ts rename to x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.test.ts diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/helpers.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.ts similarity index 96% rename from x-pack/plugins/security_solution/server/search_strategy/timeline/eql/helpers.ts rename to x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.ts index 65be9a773adb9..976185bb1b176 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/helpers.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.ts @@ -8,8 +8,8 @@ import { isEmpty } from 'lodash/fp'; import { EqlSearchStrategyResponse } from '../../../../../../../src/plugins/data/common'; import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../common/constants'; -import { EqlSearchResponse, EqlSequence } from '../../../../common/detection_engine/types'; -import { EventHit, TimelineEdges } from '../../../../common/search_strategy'; +import { EqlSearchResponse, EqlSequence, EventHit } from '../../../../common'; +import { TimelineEdges } from '../../../../common/search_strategy'; import { TimelineEqlRequestOptions, TimelineEqlResponse, diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/eql/index.ts similarity index 91% rename from x-pack/plugins/security_solution/server/search_strategy/timeline/eql/index.ts rename to x-pack/plugins/timelines/server/search_strategy/timeline/eql/index.ts index 56e5bd63d6b23..9c59a33a1c12a 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/eql/index.ts @@ -15,14 +15,14 @@ import { EqlSearchStrategyResponse, EQL_SEARCH_STRATEGY, } from '../../../../../../../src/plugins/data/common'; -import { EqlSearchResponse } from '../../../../common/detection_engine/types'; +import { EqlSearchResponse } from '../../../../common'; import { TimelineEqlRequestOptions, TimelineEqlResponse, } from '../../../../common/search_strategy/timeline/events/eql'; import { buildEqlDsl, parseEqlResponse } from './helpers'; -export const securitySolutionTimelineEqlSearchStrategyProvider = ( +export const timelineEqlSearchStrategyProvider = ( data: PluginStart ): ISearchStrategy<TimelineEqlRequestOptions, TimelineEqlResponse> => { const esEql = data.search.getSearchStrategy(EQL_SEARCH_STRATEGY); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/constants.ts similarity index 78% rename from x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts rename to x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/constants.ts index 38188a1616bfc..aae68dbcf86d1 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/constants.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/constants.ts @@ -5,7 +5,40 @@ * 2.0. */ -import { CTI_ROW_RENDERER_FIELDS } from '../../../../../../common/cti/constants'; +// import { CTI_ROW_RENDERER_FIELDS } from '../../../../../../common/cti/constants'; + +// TODO: share with security_solution/common/cti/constants.ts +export const INDICATOR_DESTINATION_PATH = 'threat.indicator'; + +export const MATCHED_ATOMIC = 'matched.atomic'; +export const MATCHED_FIELD = 'matched.field'; +export const MATCHED_TYPE = 'matched.type'; +export const INDICATOR_MATCH_SUBFIELDS = [MATCHED_ATOMIC, MATCHED_FIELD, MATCHED_TYPE]; + +export const INDICATOR_MATCHED_ATOMIC = `${INDICATOR_DESTINATION_PATH}.${MATCHED_ATOMIC}`; +export const INDICATOR_MATCHED_FIELD = `${INDICATOR_DESTINATION_PATH}.${MATCHED_FIELD}`; +export const INDICATOR_MATCHED_TYPE = `${INDICATOR_DESTINATION_PATH}.${MATCHED_TYPE}`; + +export const EVENT_DATASET = 'event.dataset'; +export const EVENT_REFERENCE = 'event.reference'; +export const PROVIDER = 'provider'; +export const FIRSTSEEN = 'first_seen'; + +export const INDICATOR_DATASET = `${INDICATOR_DESTINATION_PATH}.${EVENT_DATASET}`; +export const INDICATOR_EVENT_URL = `${INDICATOR_DESTINATION_PATH}.event.url`; +export const INDICATOR_FIRSTSEEN = `${INDICATOR_DESTINATION_PATH}.${FIRSTSEEN}`; +export const INDICATOR_LASTSEEN = `${INDICATOR_DESTINATION_PATH}.last_seen`; +export const INDICATOR_PROVIDER = `${INDICATOR_DESTINATION_PATH}.${PROVIDER}`; +export const INDICATOR_REFERENCE = `${INDICATOR_DESTINATION_PATH}.${EVENT_REFERENCE}`; + +export const CTI_ROW_RENDERER_FIELDS = [ + INDICATOR_MATCHED_ATOMIC, + INDICATOR_MATCHED_FIELD, + INDICATOR_MATCHED_TYPE, + INDICATOR_DATASET, + INDICATOR_REFERENCE, + INDICATOR_PROVIDER, +]; export const TIMELINE_EVENTS_FIELDS = [ '@timestamp', diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/helpers.test.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/helpers.test.ts new file mode 100644 index 0000000000000..9197917ad764f --- /dev/null +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/helpers.test.ts @@ -0,0 +1,570 @@ +/* + * 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 { eventHit } from '@kbn/securitysolution-t-grid'; +import { EventHit } from '../../../../../../common/search_strategy'; +import { TIMELINE_EVENTS_FIELDS } from './constants'; +import { buildObjectForFieldPath, formatTimelineData } from './helpers'; + +describe('#formatTimelineData', () => { + it('happy path', async () => { + const res = await formatTimelineData( + [ + '@timestamp', + 'host.name', + 'destination.ip', + 'source.ip', + 'source.geo.location', + 'threat.indicator.matched.field', + ], + TIMELINE_EVENTS_FIELDS, + eventHit + ); + expect(res).toEqual({ + cursor: { + tiebreaker: 'beats-ci-immutable-ubuntu-1804-1605624279743236239', + value: '1605624488922', + }, + node: { + _id: 'tkCt1nUBaEgqnrVSZ8R_', + _index: 'auditbeat-7.8.0-2020.11.05-000003', + data: [ + { + field: '@timestamp', + value: ['2020-11-17T14:48:08.922Z'], + }, + { + field: 'host.name', + value: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + }, + { + field: 'threat.indicator.matched.field', + value: ['matched_field', 'other_matched_field', 'matched_field_2'], + }, + { + field: 'source.geo.location', + value: [`{"lon":118.7778,"lat":32.0617}`], + }, + ], + ecs: { + '@timestamp': ['2020-11-17T14:48:08.922Z'], + _id: 'tkCt1nUBaEgqnrVSZ8R_', + _index: 'auditbeat-7.8.0-2020.11.05-000003', + agent: { + type: ['auditbeat'], + }, + event: { + action: ['process_started'], + category: ['process'], + dataset: ['process'], + kind: ['event'], + module: ['system'], + type: ['start'], + }, + host: { + id: ['e59991e835905c65ed3e455b33e13bd6'], + ip: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], + name: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + os: { + family: ['debian'], + }, + }, + message: ['Process go (PID: 4313) by user jenkins STARTED'], + process: { + args: ['go', 'vet', './...'], + entity_id: ['Z59cIkAAIw8ZoK0H'], + executable: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', + ], + hash: { + sha1: ['1eac22336a41e0660fb302add9d97daa2bcc7040'], + }, + name: ['go'], + pid: ['4313'], + ppid: ['3977'], + working_directory: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', + ], + }, + timestamp: '2020-11-17T14:48:08.922Z', + user: { + name: ['jenkins'], + }, + threat: { + indicator: [ + { + event: { + dataset: [], + reference: [], + }, + matched: { + atomic: ['matched_atomic'], + field: ['matched_field', 'other_matched_field'], + type: [], + }, + provider: ['yourself'], + }, + { + event: { + dataset: [], + reference: [], + }, + matched: { + atomic: ['matched_atomic_2'], + field: ['matched_field_2'], + type: [], + }, + provider: ['other_you'], + }, + ], + }, + }, + }, + }); + }); + + it('rule signal results', async () => { + const response: EventHit = { + _index: '.siem-signals-patrykkopycinski-default-000007', + _id: 'a77040f198355793c35bf22b900902371309be615381f0a2ec92c208b6132562', + _score: 0, + _source: { + signal: { + threshold_result: { + count: 10000, + value: '2a990c11-f61b-4c8e-b210-da2574e9f9db', + }, + parent: { + depth: 0, + index: + 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*', + id: '0268af90-d8da-576a-9747-2a191519416a', + type: 'event', + }, + depth: 1, + _meta: { + version: 14, + }, + rule: { + note: null, + throttle: null, + references: [], + severity_mapping: [], + description: 'asdasd', + created_at: '2021-01-09T11:25:45.046Z', + language: 'kuery', + threshold: { + field: '', + value: 200, + }, + building_block_type: null, + output_index: '.siem-signals-patrykkopycinski-default', + type: 'threshold', + rule_name_override: null, + enabled: true, + exceptions_list: [], + updated_at: '2021-01-09T13:36:39.204Z', + timestamp_override: null, + from: 'now-360s', + id: '696c24e0-526d-11eb-836c-e1620268b945', + timeline_id: null, + max_signals: 100, + severity: 'low', + risk_score: 21, + risk_score_mapping: [], + author: [], + query: '_id :*', + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + filters: [ + { + $state: { + store: 'appState', + }, + meta: { + negate: false, + alias: null, + disabled: false, + type: 'exists', + value: 'exists', + key: '_index', + }, + exists: { + field: '_index', + }, + }, + { + $state: { + store: 'appState', + }, + meta: { + negate: false, + alias: 'id_exists', + disabled: false, + type: 'exists', + value: 'exists', + key: '_id', + }, + exists: { + field: '_id', + }, + }, + ], + created_by: 'patryk_test_user', + version: 1, + saved_id: null, + tags: [], + rule_id: '2a990c11-f61b-4c8e-b210-da2574e9f9db', + license: '', + immutable: false, + timeline_title: null, + meta: { + from: '1m', + kibana_siem_app_url: 'http://localhost:5601/app/security', + }, + name: 'Threshold test', + updated_by: 'patryk_test_user', + interval: '5m', + false_positives: [], + to: 'now', + threat: [], + actions: [], + }, + original_time: '2021-01-09T13:39:32.595Z', + ancestors: [ + { + depth: 0, + index: + 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*', + id: '0268af90-d8da-576a-9747-2a191519416a', + type: 'event', + }, + ], + parents: [ + { + depth: 0, + index: + 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*', + id: '0268af90-d8da-576a-9747-2a191519416a', + type: 'event', + }, + ], + status: 'open', + }, + }, + fields: { + 'signal.rule.output_index': ['.siem-signals-patrykkopycinski-default'], + 'signal.rule.from': ['now-360s'], + 'signal.rule.language': ['kuery'], + '@timestamp': ['2021-01-09T13:41:40.517Z'], + 'signal.rule.query': ['_id :*'], + 'signal.rule.type': ['threshold'], + 'signal.rule.id': ['696c24e0-526d-11eb-836c-e1620268b945'], + 'signal.rule.risk_score': [21], + 'signal.status': ['open'], + 'event.kind': ['signal'], + 'signal.original_time': ['2021-01-09T13:39:32.595Z'], + 'signal.rule.severity': ['low'], + 'signal.rule.version': ['1'], + 'signal.rule.index': [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + 'signal.rule.name': ['Threshold test'], + 'signal.rule.to': ['now'], + }, + _type: '', + sort: ['1610199700517'], + }; + + expect( + await formatTimelineData( + ['@timestamp', 'host.name', 'destination.ip', 'source.ip'], + TIMELINE_EVENTS_FIELDS, + response + ) + ).toEqual({ + cursor: { + tiebreaker: null, + value: '', + }, + node: { + _id: 'a77040f198355793c35bf22b900902371309be615381f0a2ec92c208b6132562', + _index: '.siem-signals-patrykkopycinski-default-000007', + data: [ + { + field: '@timestamp', + value: ['2021-01-09T13:41:40.517Z'], + }, + ], + ecs: { + '@timestamp': ['2021-01-09T13:41:40.517Z'], + timestamp: '2021-01-09T13:41:40.517Z', + _id: 'a77040f198355793c35bf22b900902371309be615381f0a2ec92c208b6132562', + _index: '.siem-signals-patrykkopycinski-default-000007', + event: { + kind: ['signal'], + }, + signal: { + original_time: ['2021-01-09T13:39:32.595Z'], + status: ['open'], + threshold_result: ['{"count":10000,"value":"2a990c11-f61b-4c8e-b210-da2574e9f9db"}'], + rule: { + building_block_type: [], + exceptions_list: [], + from: ['now-360s'], + id: ['696c24e0-526d-11eb-836c-e1620268b945'], + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + language: ['kuery'], + name: ['Threshold test'], + output_index: ['.siem-signals-patrykkopycinski-default'], + risk_score: ['21'], + query: ['_id :*'], + severity: ['low'], + to: ['now'], + type: ['threshold'], + version: ['1'], + timeline_id: [], + timeline_title: [], + saved_id: [], + note: [], + threshold: [ + JSON.stringify({ + field: '', + value: 200, + }), + ], + filters: [ + JSON.stringify({ + $state: { + store: 'appState', + }, + meta: { + negate: false, + alias: null, + disabled: false, + type: 'exists', + value: 'exists', + key: '_index', + }, + exists: { + field: '_index', + }, + }), + JSON.stringify({ + $state: { + store: 'appState', + }, + meta: { + negate: false, + alias: 'id_exists', + disabled: false, + type: 'exists', + value: 'exists', + key: '_id', + }, + exists: { + field: '_id', + }, + }), + ], + }, + }, + }, + }, + }); + }); + + describe('buildObjectForFieldPath', () => { + it('builds an object from a single non-nested field', () => { + expect(buildObjectForFieldPath('@timestamp', eventHit)).toEqual({ + '@timestamp': ['2020-11-17T14:48:08.922Z'], + }); + }); + + it('builds an object with no fields response', () => { + const { fields, ...fieldLessHit } = eventHit; + // @ts-expect-error fieldLessHit is intentionally missing fields + expect(buildObjectForFieldPath('@timestamp', fieldLessHit)).toEqual({ + '@timestamp': [], + }); + }); + + it('does not misinterpret non-nested fields with a common prefix', () => { + // @ts-expect-error hit is minimal + const hit: EventHit = { + fields: { + 'foo.bar': ['baz'], + 'foo.barBaz': ['foo'], + }, + }; + + expect(buildObjectForFieldPath('foo.barBaz', hit)).toEqual({ + foo: { barBaz: ['foo'] }, + }); + }); + + it('builds an array of objects from a nested field', () => { + // @ts-expect-error hit is minimal + const hit: EventHit = { + fields: { + foo: [{ bar: ['baz'] }], + }, + }; + expect(buildObjectForFieldPath('foo.bar', hit)).toEqual({ + foo: [{ bar: ['baz'] }], + }); + }); + + it('builds intermediate objects for nested fields', () => { + // @ts-expect-error nestedHit is minimal + const nestedHit: EventHit = { + fields: { + 'foo.bar': [ + { + baz: ['host.name'], + }, + ], + }, + }; + expect(buildObjectForFieldPath('foo.bar.baz', nestedHit)).toEqual({ + foo: { + bar: [ + { + baz: ['host.name'], + }, + ], + }, + }); + }); + + it('builds intermediate objects at multiple levels', () => { + expect(buildObjectForFieldPath('threat.indicator.matched.atomic', eventHit)).toEqual({ + threat: { + indicator: [ + { + matched: { + atomic: ['matched_atomic'], + }, + }, + { + matched: { + atomic: ['matched_atomic_2'], + }, + }, + ], + }, + }); + }); + + it('preserves multiple values for a single leaf', () => { + expect(buildObjectForFieldPath('threat.indicator.matched.field', eventHit)).toEqual({ + threat: { + indicator: [ + { + matched: { + field: ['matched_field', 'other_matched_field'], + }, + }, + { + matched: { + field: ['matched_field_2'], + }, + }, + ], + }, + }); + }); + + describe('multiple levels of nested fields', () => { + let nestedHit: EventHit; + + beforeEach(() => { + // @ts-expect-error nestedHit is minimal + nestedHit = { + fields: { + 'nested_1.foo': [ + { + 'nested_2.bar': [ + { leaf: ['leaf_value'], leaf_2: ['leaf_2_value'] }, + { leaf_2: ['leaf_2_value_2', 'leaf_2_value_3'] }, + ], + }, + { + 'nested_2.bar': [ + { leaf: ['leaf_value_2'], leaf_2: ['leaf_2_value_4'] }, + { leaf: ['leaf_value_3'], leaf_2: ['leaf_2_value_5'] }, + ], + }, + ], + }, + }; + }); + + it('includes objects without the field', () => { + expect(buildObjectForFieldPath('nested_1.foo.nested_2.bar.leaf', nestedHit)).toEqual({ + nested_1: { + foo: [ + { + nested_2: { + bar: [{ leaf: ['leaf_value'] }, { leaf: [] }], + }, + }, + { + nested_2: { + bar: [{ leaf: ['leaf_value_2'] }, { leaf: ['leaf_value_3'] }], + }, + }, + ], + }, + }); + }); + + it('groups multiple leaf values', () => { + expect(buildObjectForFieldPath('nested_1.foo.nested_2.bar.leaf_2', nestedHit)).toEqual({ + nested_1: { + foo: [ + { + nested_2: { + bar: [ + { leaf_2: ['leaf_2_value'] }, + { leaf_2: ['leaf_2_value_2', 'leaf_2_value_3'] }, + ], + }, + }, + { + nested_2: { + bar: [{ leaf_2: ['leaf_2_value_4'] }, { leaf_2: ['leaf_2_value_5'] }], + }, + }, + ], + }, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/helpers.ts similarity index 96% rename from x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts rename to x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/helpers.ts index 8e0e5e9655193..4c07482ed53a8 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/helpers.ts @@ -30,8 +30,11 @@ const getTimestamp = (hit: EventHit): string => { return ''; }; -export const buildFieldsRequest = (fields: string[]) => - uniq([...fields.filter((f) => !f.startsWith('_')), ...TIMELINE_EVENTS_FIELDS]).map((field) => ({ +export const buildFieldsRequest = (fields: string[], excludeEcsData?: boolean) => + uniq([ + ...fields.filter((f) => !f.startsWith('_')), + ...(excludeEcsData ? [] : TIMELINE_EVENTS_FIELDS), + ]).map((field) => ({ field, include_unmapped: true, })); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/index.ts similarity index 70% rename from x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts rename to x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/index.ts index a26fbe05f7051..c1b567b99cfb1 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/index.ts @@ -6,7 +6,6 @@ */ import { cloneDeep } from 'lodash/fp'; - import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; import { @@ -16,33 +15,48 @@ import { TimelineEventsAllRequestOptions, TimelineEdges, } from '../../../../../../common/search_strategy'; -import { inspectStringifyObject } from '../../../../../utils/build_query'; -import { SecuritySolutionTimelineFactory } from '../../types'; +import { TimelineFactory } from '../../types'; import { buildTimelineEventsAllQuery } from './query.events_all.dsl'; import { TIMELINE_EVENTS_FIELDS } from './constants'; import { buildFieldsRequest, formatTimelineData } from './helpers'; +import { inspectStringifyObject } from '../../../../../utils/build_query'; -export const timelineEventsAll: SecuritySolutionTimelineFactory<TimelineEventsQueries.all> = { +export const timelineEventsAll: TimelineFactory<TimelineEventsQueries.all> = { buildDsl: (options: TimelineEventsAllRequestOptions) => { if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); } const { fieldRequested, ...queryOptions } = cloneDeep(options); - queryOptions.fields = buildFieldsRequest(fieldRequested); + queryOptions.fields = buildFieldsRequest(fieldRequested, queryOptions.excludeEcsData); return buildTimelineEventsAllQuery(queryOptions); }, parse: async ( options: TimelineEventsAllRequestOptions, response: IEsSearchResponse<unknown> ): Promise<TimelineEventsAllStrategyResponse> => { - const { fieldRequested, ...queryOptions } = cloneDeep(options); - queryOptions.fields = buildFieldsRequest(fieldRequested); + // eslint-disable-next-line prefer-const + let { fieldRequested, ...queryOptions } = cloneDeep(options); + queryOptions.fields = buildFieldsRequest(fieldRequested, queryOptions.excludeEcsData); const { activePage, querySize } = options.pagination; const totalCount = response.rawResponse.hits.total || 0; const hits = response.rawResponse.hits.hits; + + if (fieldRequested.includes('*') && hits.length > 0) { + fieldRequested = Object.keys(hits[0]?.fields ?? {}).reduce((acc, f) => { + if (!acc.includes(f)) { + return [...acc, f]; + } + return acc; + }, fieldRequested); + } + const edges: TimelineEdges[] = await Promise.all( hits.map((hit) => - formatTimelineData(options.fieldRequested, TIMELINE_EVENTS_FIELDS, hit as EventHit) + formatTimelineData( + fieldRequested, + options.excludeEcsData ? [] : TIMELINE_EVENTS_FIELDS, + hit as EventHit + ) ) ); const inspect = { diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts similarity index 96% rename from x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts rename to x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts index 8aa69b2d87dc9..40df5376cefc9 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts @@ -13,7 +13,7 @@ import { TimelineEventsAllRequestOptions, TimelineRequestSortField, } from '../../../../../../common/search_strategy'; -import { createQueryFilterClauses } from '../../../../../utils/build_query'; +import { createQueryFilterClauses } from '../../../../../../server/utils/build_query'; export const buildTimelineEventsAllQuery = ({ defaultIndex, diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/index.ts similarity index 89% rename from x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts rename to x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/index.ts index a4d6eebfb71b8..26e6267b36d77 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/index.ts @@ -16,8 +16,8 @@ import { TimelineEventsDetailsItem, EventSource, } from '../../../../../../common/search_strategy'; -import { inspectStringifyObject } from '../../../../../utils/build_query'; -import { SecuritySolutionTimelineFactory } from '../../types'; +import { inspectStringifyObject } from '../../../../../../server/utils/build_query'; +import { TimelineFactory } from '../../types'; import { buildTimelineDetailsQuery } from './query.events_details.dsl'; import { getDataFromFieldsHits, @@ -25,7 +25,7 @@ import { getDataSafety, } from '../../../../../../common/utils/field_formatters'; -export const timelineEventsDetails: SecuritySolutionTimelineFactory<TimelineEventsQueries.details> = { +export const timelineEventsDetails: TimelineFactory<TimelineEventsQueries.details> = { buildDsl: (options: TimelineEventsDetailsRequestOptions) => { const { indexName, eventId, docValueFields = [] } = options; return buildTimelineDetailsQuery(indexName, eventId, docValueFields); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.test.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.test.ts similarity index 100% rename from x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.test.ts rename to x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.test.ts diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts similarity index 100% rename from x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts rename to x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/index.ts similarity index 87% rename from x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/index.ts rename to x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/index.ts index e8de5ffc84c45..e140fa1038704 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/index.ts @@ -10,7 +10,7 @@ import { TimelineEventsQueries, } from '../../../../../common/search_strategy/timeline'; -import { SecuritySolutionTimelineFactory } from '../types'; +import { TimelineFactory } from '../types'; import { timelineEventsAll } from './all'; import { timelineEventsDetails } from './details'; import { timelineKpi } from './kpi'; @@ -18,7 +18,7 @@ import { timelineEventsLastEventTime } from './last_event_time'; export const timelineEventsFactory: Record< TimelineEventsQueries, - SecuritySolutionTimelineFactory<TimelineFactoryQueryTypes> + TimelineFactory<TimelineFactoryQueryTypes> > = { [TimelineEventsQueries.all]: timelineEventsAll, [TimelineEventsQueries.details]: timelineEventsDetails, diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/kpi/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/kpi/index.ts similarity index 90% rename from x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/kpi/index.ts rename to x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/kpi/index.ts index ad9ad538c1e49..86a7819e64156 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/kpi/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/kpi/index.ts @@ -14,10 +14,10 @@ import { TimelineKpiStrategyResponse, } from '../../../../../../common/search_strategy/timeline'; import { inspectStringifyObject } from '../../../../../utils/build_query'; -import { SecuritySolutionTimelineFactory } from '../../types'; +import { TimelineFactory } from '../../types'; import { buildTimelineKpiQuery } from './query.kpi.dsl'; -export const timelineKpi: SecuritySolutionTimelineFactory<TimelineEventsQueries.kpi> = { +export const timelineKpi: TimelineFactory<TimelineEventsQueries.kpi> = { buildDsl: (options: TimelineRequestBasicOptions) => buildTimelineKpiQuery(options), parse: async ( options: TimelineRequestBasicOptions, diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/kpi/query.kpi.dsl.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/kpi/query.kpi.dsl.ts similarity index 96% rename from x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/kpi/query.kpi.dsl.ts rename to x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/kpi/query.kpi.dsl.ts index 12b0a0baead0d..41eed7cbb4fa3 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/kpi/query.kpi.dsl.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/kpi/query.kpi.dsl.ts @@ -12,7 +12,7 @@ import { TimerangeInput, TimelineRequestBasicOptions, } from '../../../../../../common/search_strategy'; -import { createQueryFilterClauses } from '../../../../../utils/build_query'; +import { createQueryFilterClauses } from '../../../../../utils/filters'; export const buildTimelineKpiQuery = ({ defaultIndex, diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/last_event_time/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/last_event_time/index.ts similarity index 89% rename from x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/last_event_time/index.ts rename to x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/last_event_time/index.ts index 3b02e5621ed1a..9b96743ff8508 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/last_event_time/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/last_event_time/index.ts @@ -14,10 +14,10 @@ import { TimelineEventsLastEventTimeRequestOptions, } from '../../../../../../common/search_strategy/timeline'; import { inspectStringifyObject } from '../../../../../utils/build_query'; -import { SecuritySolutionTimelineFactory } from '../../types'; +import { TimelineFactory } from '../../types'; import { buildLastEventTimeQuery } from './query.events_last_event_time.dsl'; -export const timelineEventsLastEventTime: SecuritySolutionTimelineFactory<TimelineEventsQueries.lastEventTime> = { +export const timelineEventsLastEventTime: TimelineFactory<TimelineEventsQueries.lastEventTime> = { buildDsl: (options: TimelineEventsLastEventTimeRequestOptions) => buildLastEventTimeQuery(options), parse: async ( diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/last_event_time/query.events_last_event_time.dsl.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/last_event_time/query.events_last_event_time.dsl.ts similarity index 100% rename from x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/last_event_time/query.events_last_event_time.dsl.ts rename to x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/last_event_time/query.events_last_event_time.dsl.ts diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/index.ts similarity index 72% rename from x-pack/plugins/security_solution/server/search_strategy/timeline/factory/index.ts rename to x-pack/plugins/timelines/server/search_strategy/timeline/factory/index.ts index 264f95691b641..2ac9c343c843a 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/index.ts @@ -6,12 +6,12 @@ */ import { TimelineFactoryQueryTypes } from '../../../../common/search_strategy/timeline'; -import { SecuritySolutionTimelineFactory } from './types'; +import { TimelineFactory } from './types'; import { timelineEventsFactory } from './events'; -export const securitySolutionTimelineFactory: Record< +export const timelineFactory: Record< TimelineFactoryQueryTypes, - SecuritySolutionTimelineFactory<TimelineFactoryQueryTypes> + TimelineFactory<TimelineFactoryQueryTypes> > = { ...timelineEventsFactory, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/types.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/types.ts similarity index 88% rename from x-pack/plugins/security_solution/server/search_strategy/timeline/factory/types.ts rename to x-pack/plugins/timelines/server/search_strategy/timeline/factory/types.ts index d90b25c934b91..2f0f279c5baa0 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/types.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/types.ts @@ -12,7 +12,7 @@ import { TimelineStrategyResponseType, } from '../../../../common/search_strategy/timeline'; -export interface SecuritySolutionTimelineFactory<T extends TimelineFactoryQueryTypes> { +export interface TimelineFactory<T extends TimelineFactoryQueryTypes> { buildDsl: (options: TimelineStrategyRequestType<T>) => unknown; parse: ( options: TimelineStrategyRequestType<T>, diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts similarity index 81% rename from x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts rename to x-pack/plugins/timelines/server/search_strategy/timeline/index.ts index 4dfa9831f9e6e..dd46c0496df64 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts @@ -17,10 +17,10 @@ import { TimelineStrategyResponseType, TimelineStrategyRequestType, } from '../../../common/search_strategy/timeline'; -import { securitySolutionTimelineFactory } from './factory'; -import { SecuritySolutionTimelineFactory } from './factory/types'; +import { timelineFactory } from './factory'; +import { TimelineFactory } from './factory/types'; -export const securitySolutionTimelineSearchStrategyProvider = <T extends TimelineFactoryQueryTypes>( +export const timelineSearchStrategyProvider = <T extends TimelineFactoryQueryTypes>( data: PluginStart ): ISearchStrategy<TimelineStrategyRequestType<T>, TimelineStrategyResponseType<T>> => { const es = data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY); @@ -29,8 +29,7 @@ export const securitySolutionTimelineSearchStrategyProvider = <T extends Timelin if (request.factoryQueryType == null) { throw new Error('factoryQueryType is required'); } - const queryFactory: SecuritySolutionTimelineFactory<T> = - securitySolutionTimelineFactory[request.factoryQueryType]; + const queryFactory: TimelineFactory<T> = timelineFactory[request.factoryQueryType]; const dsl = queryFactory.buildDsl(request); return es.search({ ...request, params: dsl }, options, deps).pipe( map((response) => { diff --git a/x-pack/plugins/timelines/server/types.ts b/x-pack/plugins/timelines/server/types.ts index 5bcc90b48f0b9..9ea4ef430d8fd 100644 --- a/x-pack/plugins/timelines/server/types.ts +++ b/x-pack/plugins/timelines/server/types.ts @@ -5,7 +5,18 @@ * 2.0. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { DataPluginSetup, DataPluginStart } from '../../../../src/plugins/data/server/plugin'; + // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface TimelinesPluginSetup {} +export interface TimelinesPluginUI {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface TimelinesPluginStart {} + +export interface SetupPlugins { + data: DataPluginSetup; +} + +export interface StartPlugins { + data: DataPluginStart; +} diff --git a/x-pack/plugins/timelines/server/utils/beat_schema/fields.ts b/x-pack/plugins/timelines/server/utils/beat_schema/fields.ts new file mode 100644 index 0000000000000..4f1dc0079b236 --- /dev/null +++ b/x-pack/plugins/timelines/server/utils/beat_schema/fields.ts @@ -0,0 +1,36119 @@ +/* + * 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 { BeatFields } from '../../../common/search_strategy/index_fields'; + +/* eslint-disable @typescript-eslint/naming-convention */ +export const fieldsBeat: BeatFields = { + _id: { + category: 'base', + description: 'Each document has an _id that uniquely identifies it', + example: 'Y-6TfmcB0WOhS6qyMv3s', + name: '_id', + type: 'keyword', + }, + _index: { + category: 'base', + description: + 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', + example: 'auditbeat-8.0.0-2019.02.19-000001', + name: '_index', + type: 'keyword', + }, + '@timestamp': { + category: 'base', + description: + 'Date/time when the event originated. This is the date/time extracted from the event, typically representing when the event was generated by the source. If the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline. Required field for all events.', + example: '2016-05-23T08:05:34.853Z', + name: '@timestamp', + type: 'date', + }, + labels: { + category: 'base', + description: + 'Custom key/value pairs. Can be used to add meta information to events. Should not contain nested objects. All values are stored as keyword. Example: `docker` and `k8s` labels.', + example: '{"application": "foo-bar", "env": "production"}', + name: 'labels', + type: 'object', + }, + message: { + category: 'base', + description: + 'For log events the message field contains the log message, optimized for viewing in a log viewer. For structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event. If multiple messages exist, they can be combined into one message.', + example: 'Hello World', + name: 'message', + type: 'text', + }, + tags: { + category: 'base', + description: 'List of keywords used to tag each event.', + example: '["production", "env2"]', + name: 'tags', + type: 'keyword', + }, + 'agent.ephemeral_id': { + category: 'agent', + description: + 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', + example: '8a4f500f', + name: 'agent.ephemeral_id', + type: 'keyword', + }, + 'agent.id': { + category: 'agent', + description: + 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', + example: '8a4f500d', + name: 'agent.id', + type: 'keyword', + }, + 'agent.name': { + category: 'agent', + description: + 'Custom name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', + example: 'foo', + name: 'agent.name', + type: 'keyword', + }, + 'agent.type': { + category: 'agent', + description: + 'Type of the agent. The agent type stays always the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', + example: 'filebeat', + name: 'agent.type', + type: 'keyword', + }, + 'agent.version': { + category: 'agent', + description: 'Version of the agent.', + example: '6.0.0-rc2', + name: 'agent.version', + type: 'keyword', + }, + 'as.number': { + category: 'as', + description: + 'Unique number allocated to the autonomous system. The autonomous system number (ASN) uniquely identifies each network on the Internet.', + example: 15169, + name: 'as.number', + type: 'long', + }, + 'as.organization.name': { + category: 'as', + description: 'Organization name.', + example: 'Google LLC', + name: 'as.organization.name', + type: 'keyword', + }, + 'client.address': { + category: 'client', + description: + 'Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + name: 'client.address', + type: 'keyword', + }, + 'client.as.number': { + category: 'client', + description: + 'Unique number allocated to the autonomous system. The autonomous system number (ASN) uniquely identifies each network on the Internet.', + example: 15169, + name: 'client.as.number', + type: 'long', + }, + 'client.as.organization.name': { + category: 'client', + description: 'Organization name.', + example: 'Google LLC', + name: 'client.as.organization.name', + type: 'keyword', + }, + 'client.bytes': { + category: 'client', + description: 'Bytes sent from the client to the server.', + example: 184, + name: 'client.bytes', + type: 'long', + format: 'bytes', + }, + 'client.domain': { + category: 'client', + description: 'Client domain.', + name: 'client.domain', + type: 'keyword', + }, + 'client.geo.city_name': { + category: 'client', + description: 'City name.', + example: 'Montreal', + name: 'client.geo.city_name', + type: 'keyword', + }, + 'client.geo.continent_name': { + category: 'client', + description: 'Name of the continent.', + example: 'North America', + name: 'client.geo.continent_name', + type: 'keyword', + }, + 'client.geo.country_iso_code': { + category: 'client', + description: 'Country ISO code.', + example: 'CA', + name: 'client.geo.country_iso_code', + type: 'keyword', + }, + 'client.geo.country_name': { + category: 'client', + description: 'Country name.', + example: 'Canada', + name: 'client.geo.country_name', + type: 'keyword', + }, + 'client.geo.location': { + category: 'client', + description: 'Longitude and latitude.', + example: '{ "lon": -73.614830, "lat": 45.505918 }', + name: 'client.geo.location', + type: 'geo_point', + }, + 'client.geo.name': { + category: 'client', + description: + 'User-defined description of a location, at the level of granularity they care about. Could be the name of their data centers, the floor number, if this describes a local physical entity, city names. Not typically used in automated geolocation.', + example: 'boston-dc', + name: 'client.geo.name', + type: 'keyword', + }, + 'client.geo.region_iso_code': { + category: 'client', + description: 'Region ISO code.', + example: 'CA-QC', + name: 'client.geo.region_iso_code', + type: 'keyword', + }, + 'client.geo.region_name': { + category: 'client', + description: 'Region name.', + example: 'Quebec', + name: 'client.geo.region_name', + type: 'keyword', + }, + 'client.ip': { + category: 'client', + description: 'IP address of the client. Can be one or multiple IPv4 or IPv6 addresses.', + name: 'client.ip', + type: 'ip', + }, + 'client.mac': { + category: 'client', + description: 'MAC address of the client.', + name: 'client.mac', + type: 'keyword', + }, + 'client.nat.ip': { + category: 'client', + description: + 'Translated IP of source based NAT sessions (e.g. internal client to internet). Typically connections traversing load balancers, firewalls, or routers.', + name: 'client.nat.ip', + type: 'ip', + }, + 'client.nat.port': { + category: 'client', + description: + 'Translated port of source based NAT sessions (e.g. internal client to internet). Typically connections traversing load balancers, firewalls, or routers.', + name: 'client.nat.port', + type: 'long', + format: 'string', + }, + 'client.packets': { + category: 'client', + description: 'Packets sent from the client to the server.', + example: 12, + name: 'client.packets', + type: 'long', + }, + 'client.port': { + category: 'client', + description: 'Port of the client.', + name: 'client.port', + type: 'long', + format: 'string', + }, + 'client.registered_domain': { + category: 'client', + description: + 'The highest registered client domain, stripped of the subdomain. For example, the registered domain for "foo.google.com" is "google.com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last two labels will not work well for TLDs such as "co.uk".', + example: 'google.com', + name: 'client.registered_domain', + type: 'keyword', + }, + 'client.top_level_domain': { + category: 'client', + description: + 'The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for google.com is "com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last label will not work well for effective TLDs such as "co.uk".', + example: 'co.uk', + name: 'client.top_level_domain', + type: 'keyword', + }, + 'client.user.domain': { + category: 'client', + description: + 'Name of the directory the user is a member of. For example, an LDAP or Active Directory domain name.', + name: 'client.user.domain', + type: 'keyword', + }, + 'client.user.email': { + category: 'client', + description: 'User email address.', + name: 'client.user.email', + type: 'keyword', + }, + 'client.user.full_name': { + category: 'client', + description: "User's full name, if available.", + example: 'Albert Einstein', + name: 'client.user.full_name', + type: 'keyword', + }, + 'client.user.group.domain': { + category: 'client', + description: + 'Name of the directory the group is a member of. For example, an LDAP or Active Directory domain name.', + name: 'client.user.group.domain', + type: 'keyword', + }, + 'client.user.group.id': { + category: 'client', + description: 'Unique identifier for the group on the system/platform.', + name: 'client.user.group.id', + type: 'keyword', + }, + 'client.user.group.name': { + category: 'client', + description: 'Name of the group.', + name: 'client.user.group.name', + type: 'keyword', + }, + 'client.user.hash': { + category: 'client', + description: + 'Unique user hash to correlate information for a user in anonymized form. Useful if `user.id` or `user.name` contain confidential information and cannot be used.', + name: 'client.user.hash', + type: 'keyword', + }, + 'client.user.id': { + category: 'client', + description: 'Unique identifiers of the user.', + name: 'client.user.id', + type: 'keyword', + }, + 'client.user.name': { + category: 'client', + description: 'Short name or login of the user.', + example: 'albert', + name: 'client.user.name', + type: 'keyword', + }, + 'cloud.account.id': { + category: 'cloud', + description: + 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', + example: 666777888999, + name: 'cloud.account.id', + type: 'keyword', + }, + 'cloud.availability_zone': { + category: 'cloud', + description: 'Availability zone in which this host is running.', + example: 'us-east-1c', + name: 'cloud.availability_zone', + type: 'keyword', + }, + 'cloud.instance.id': { + category: 'cloud', + description: 'Instance ID of the host machine.', + example: 'i-1234567890abcdef0', + name: 'cloud.instance.id', + type: 'keyword', + }, + 'cloud.instance.name': { + category: 'cloud', + description: 'Instance name of the host machine.', + name: 'cloud.instance.name', + type: 'keyword', + }, + 'cloud.machine.type': { + category: 'cloud', + description: 'Machine type of the host machine.', + example: 't2.medium', + name: 'cloud.machine.type', + type: 'keyword', + }, + 'cloud.provider': { + category: 'cloud', + description: 'Name of the cloud provider. Example values are aws, azure, gcp, or digitalocean.', + example: 'aws', + name: 'cloud.provider', + type: 'keyword', + }, + 'cloud.region': { + category: 'cloud', + description: 'Region in which this host is running.', + example: 'us-east-1', + name: 'cloud.region', + type: 'keyword', + }, + 'code_signature.exists': { + category: 'code_signature', + description: 'Boolean to capture if a signature is present.', + example: 'true', + name: 'code_signature.exists', + type: 'boolean', + }, + 'code_signature.status': { + category: 'code_signature', + description: + 'Additional information about the certificate status. This is useful for logging cryptographic errors with the certificate validity or trust status. Leave unpopulated if the validity or trust of the certificate was unchecked.', + example: 'ERROR_UNTRUSTED_ROOT', + name: 'code_signature.status', + type: 'keyword', + }, + 'code_signature.subject_name': { + category: 'code_signature', + description: 'Subject name of the code signer', + example: 'Microsoft Corporation', + name: 'code_signature.subject_name', + type: 'keyword', + }, + 'code_signature.trusted': { + category: 'code_signature', + description: + 'Stores the trust status of the certificate chain. Validating the trust of the certificate chain may be complicated, and this field should only be populated by tools that actively check the status.', + example: 'true', + name: 'code_signature.trusted', + type: 'boolean', + }, + 'code_signature.valid': { + category: 'code_signature', + description: + 'Boolean to capture if the digital signature is verified against the binary content. Leave unpopulated if a certificate was unchecked.', + example: 'true', + name: 'code_signature.valid', + type: 'boolean', + }, + 'container.id': { + category: 'container', + description: 'Unique container id.', + name: 'container.id', + type: 'keyword', + }, + 'container.image.name': { + category: 'container', + description: 'Name of the image the container was built on.', + name: 'container.image.name', + type: 'keyword', + }, + 'container.image.tag': { + category: 'container', + description: 'Container image tags.', + name: 'container.image.tag', + type: 'keyword', + }, + 'container.labels': { + category: 'container', + description: 'Image labels.', + name: 'container.labels', + type: 'object', + }, + 'container.name': { + category: 'container', + description: 'Container name.', + name: 'container.name', + type: 'keyword', + }, + 'container.runtime': { + category: 'container', + description: 'Runtime managing this container.', + example: 'docker', + name: 'container.runtime', + type: 'keyword', + }, + 'destination.address': { + category: 'destination', + description: + 'Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + name: 'destination.address', + type: 'keyword', + }, + 'destination.as.number': { + category: 'destination', + description: + 'Unique number allocated to the autonomous system. The autonomous system number (ASN) uniquely identifies each network on the Internet.', + example: 15169, + name: 'destination.as.number', + type: 'long', + }, + 'destination.as.organization.name': { + category: 'destination', + description: 'Organization name.', + example: 'Google LLC', + name: 'destination.as.organization.name', + type: 'keyword', + }, + 'destination.bytes': { + category: 'destination', + description: 'Bytes sent from the destination to the source.', + example: 184, + name: 'destination.bytes', + type: 'long', + format: 'bytes', + }, + 'destination.domain': { + category: 'destination', + description: 'Destination domain.', + name: 'destination.domain', + type: 'keyword', + }, + 'destination.geo.city_name': { + category: 'destination', + description: 'City name.', + example: 'Montreal', + name: 'destination.geo.city_name', + type: 'keyword', + }, + 'destination.geo.continent_name': { + category: 'destination', + description: 'Name of the continent.', + example: 'North America', + name: 'destination.geo.continent_name', + type: 'keyword', + }, + 'destination.geo.country_iso_code': { + category: 'destination', + description: 'Country ISO code.', + example: 'CA', + name: 'destination.geo.country_iso_code', + type: 'keyword', + }, + 'destination.geo.country_name': { + category: 'destination', + description: 'Country name.', + example: 'Canada', + name: 'destination.geo.country_name', + type: 'keyword', + }, + 'destination.geo.location': { + category: 'destination', + description: 'Longitude and latitude.', + example: '{ "lon": -73.614830, "lat": 45.505918 }', + name: 'destination.geo.location', + type: 'geo_point', + }, + 'destination.geo.name': { + category: 'destination', + description: + 'User-defined description of a location, at the level of granularity they care about. Could be the name of their data centers, the floor number, if this describes a local physical entity, city names. Not typically used in automated geolocation.', + example: 'boston-dc', + name: 'destination.geo.name', + type: 'keyword', + }, + 'destination.geo.region_iso_code': { + category: 'destination', + description: 'Region ISO code.', + example: 'CA-QC', + name: 'destination.geo.region_iso_code', + type: 'keyword', + }, + 'destination.geo.region_name': { + category: 'destination', + description: 'Region name.', + example: 'Quebec', + name: 'destination.geo.region_name', + type: 'keyword', + }, + 'destination.ip': { + category: 'destination', + description: 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', + name: 'destination.ip', + type: 'ip', + }, + 'destination.mac': { + category: 'destination', + description: 'MAC address of the destination.', + name: 'destination.mac', + type: 'keyword', + }, + 'destination.nat.ip': { + category: 'destination', + description: + 'Translated ip of destination based NAT sessions (e.g. internet to private DMZ) Typically used with load balancers, firewalls, or routers.', + name: 'destination.nat.ip', + type: 'ip', + }, + 'destination.nat.port': { + category: 'destination', + description: + 'Port the source session is translated to by NAT Device. Typically used with load balancers, firewalls, or routers.', + name: 'destination.nat.port', + type: 'long', + format: 'string', + }, + 'destination.packets': { + category: 'destination', + description: 'Packets sent from the destination to the source.', + example: 12, + name: 'destination.packets', + type: 'long', + }, + 'destination.port': { + category: 'destination', + description: 'Port of the destination.', + name: 'destination.port', + type: 'long', + format: 'string', + }, + 'destination.registered_domain': { + category: 'destination', + description: + 'The highest registered destination domain, stripped of the subdomain. For example, the registered domain for "foo.google.com" is "google.com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last two labels will not work well for TLDs such as "co.uk".', + example: 'google.com', + name: 'destination.registered_domain', + type: 'keyword', + }, + 'destination.top_level_domain': { + category: 'destination', + description: + 'The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for google.com is "com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last label will not work well for effective TLDs such as "co.uk".', + example: 'co.uk', + name: 'destination.top_level_domain', + type: 'keyword', + }, + 'destination.user.domain': { + category: 'destination', + description: + 'Name of the directory the user is a member of. For example, an LDAP or Active Directory domain name.', + name: 'destination.user.domain', + type: 'keyword', + }, + 'destination.user.email': { + category: 'destination', + description: 'User email address.', + name: 'destination.user.email', + type: 'keyword', + }, + 'destination.user.full_name': { + category: 'destination', + description: "User's full name, if available.", + example: 'Albert Einstein', + name: 'destination.user.full_name', + type: 'keyword', + }, + 'destination.user.group.domain': { + category: 'destination', + description: + 'Name of the directory the group is a member of. For example, an LDAP or Active Directory domain name.', + name: 'destination.user.group.domain', + type: 'keyword', + }, + 'destination.user.group.id': { + category: 'destination', + description: 'Unique identifier for the group on the system/platform.', + name: 'destination.user.group.id', + type: 'keyword', + }, + 'destination.user.group.name': { + category: 'destination', + description: 'Name of the group.', + name: 'destination.user.group.name', + type: 'keyword', + }, + 'destination.user.hash': { + category: 'destination', + description: + 'Unique user hash to correlate information for a user in anonymized form. Useful if `user.id` or `user.name` contain confidential information and cannot be used.', + name: 'destination.user.hash', + type: 'keyword', + }, + 'destination.user.id': { + category: 'destination', + description: 'Unique identifiers of the user.', + name: 'destination.user.id', + type: 'keyword', + }, + 'destination.user.name': { + category: 'destination', + description: 'Short name or login of the user.', + example: 'albert', + name: 'destination.user.name', + type: 'keyword', + }, + 'dll.code_signature.exists': { + category: 'dll', + description: 'Boolean to capture if a signature is present.', + example: 'true', + name: 'dll.code_signature.exists', + type: 'boolean', + }, + 'dll.code_signature.status': { + category: 'dll', + description: + 'Additional information about the certificate status. This is useful for logging cryptographic errors with the certificate validity or trust status. Leave unpopulated if the validity or trust of the certificate was unchecked.', + example: 'ERROR_UNTRUSTED_ROOT', + name: 'dll.code_signature.status', + type: 'keyword', + }, + 'dll.code_signature.subject_name': { + category: 'dll', + description: 'Subject name of the code signer', + example: 'Microsoft Corporation', + name: 'dll.code_signature.subject_name', + type: 'keyword', + }, + 'dll.code_signature.trusted': { + category: 'dll', + description: + 'Stores the trust status of the certificate chain. Validating the trust of the certificate chain may be complicated, and this field should only be populated by tools that actively check the status.', + example: 'true', + name: 'dll.code_signature.trusted', + type: 'boolean', + }, + 'dll.code_signature.valid': { + category: 'dll', + description: + 'Boolean to capture if the digital signature is verified against the binary content. Leave unpopulated if a certificate was unchecked.', + example: 'true', + name: 'dll.code_signature.valid', + type: 'boolean', + }, + 'dll.hash.md5': { + category: 'dll', + description: 'MD5 hash.', + name: 'dll.hash.md5', + type: 'keyword', + }, + 'dll.hash.sha1': { + category: 'dll', + description: 'SHA1 hash.', + name: 'dll.hash.sha1', + type: 'keyword', + }, + 'dll.hash.sha256': { + category: 'dll', + description: 'SHA256 hash.', + name: 'dll.hash.sha256', + type: 'keyword', + }, + 'dll.hash.sha512': { + category: 'dll', + description: 'SHA512 hash.', + name: 'dll.hash.sha512', + type: 'keyword', + }, + 'dll.name': { + category: 'dll', + description: 'Name of the library. This generally maps to the name of the file on disk.', + example: 'kernel32.dll', + name: 'dll.name', + type: 'keyword', + }, + 'dll.path': { + category: 'dll', + description: 'Full file path of the library.', + example: 'C:\\Windows\\System32\\kernel32.dll', + name: 'dll.path', + type: 'keyword', + }, + 'dll.pe.company': { + category: 'dll', + description: 'Internal company name of the file, provided at compile-time.', + example: 'Microsoft Corporation', + name: 'dll.pe.company', + type: 'keyword', + }, + 'dll.pe.description': { + category: 'dll', + description: 'Internal description of the file, provided at compile-time.', + example: 'Paint', + name: 'dll.pe.description', + type: 'keyword', + }, + 'dll.pe.file_version': { + category: 'dll', + description: 'Internal version of the file, provided at compile-time.', + example: '6.3.9600.17415', + name: 'dll.pe.file_version', + type: 'keyword', + }, + 'dll.pe.original_file_name': { + category: 'dll', + description: 'Internal name of the file, provided at compile-time.', + example: 'MSPAINT.EXE', + name: 'dll.pe.original_file_name', + type: 'keyword', + }, + 'dll.pe.product': { + category: 'dll', + description: 'Internal product name of the file, provided at compile-time.', + example: 'Microsoft® Windows® Operating System', + name: 'dll.pe.product', + type: 'keyword', + }, + 'dns.answers': { + category: 'dns', + description: + 'An array containing an object for each answer section returned by the server. The main keys that should be present in these objects are defined by ECS. Records that have more information may contain more keys than what ECS defines. Not all DNS data sources give all details about DNS answers. At minimum, answer objects must contain the `data` key. If more information is available, map as much of it to ECS as possible, and add any additional fields to the answer objects as custom fields.', + name: 'dns.answers', + type: 'object', + }, + 'dns.answers.class': { + category: 'dns', + description: 'The class of DNS data contained in this resource record.', + example: 'IN', + name: 'dns.answers.class', + type: 'keyword', + }, + 'dns.answers.data': { + category: 'dns', + description: + 'The data describing the resource. The meaning of this data depends on the type and class of the resource record.', + example: '10.10.10.10', + name: 'dns.answers.data', + type: 'keyword', + }, + 'dns.answers.name': { + category: 'dns', + description: + "The domain name to which this resource record pertains. If a chain of CNAME is being resolved, each answer's `name` should be the one that corresponds with the answer's `data`. It should not simply be the original `question.name` repeated.", + example: 'www.google.com', + name: 'dns.answers.name', + type: 'keyword', + }, + 'dns.answers.ttl': { + category: 'dns', + description: + 'The time interval in seconds that this resource record may be cached before it should be discarded. Zero values mean that the data should not be cached.', + example: 180, + name: 'dns.answers.ttl', + type: 'long', + }, + 'dns.answers.type': { + category: 'dns', + description: 'The type of data contained in this resource record.', + example: 'CNAME', + name: 'dns.answers.type', + type: 'keyword', + }, + 'dns.header_flags': { + category: 'dns', + description: + 'Array of 2 letter DNS header flags. Expected values are: AA, TC, RD, RA, AD, CD, DO.', + example: '["RD","RA"]', + name: 'dns.header_flags', + type: 'keyword', + }, + 'dns.id': { + category: 'dns', + description: + 'The DNS packet identifier assigned by the program that generated the query. The identifier is copied to the response.', + example: 62111, + name: 'dns.id', + type: 'keyword', + }, + 'dns.op_code': { + category: 'dns', + description: + 'The DNS operation code that specifies the kind of query in the message. This value is set by the originator of a query and copied into the response.', + example: 'QUERY', + name: 'dns.op_code', + type: 'keyword', + }, + 'dns.question.class': { + category: 'dns', + description: 'The class of records being queried.', + example: 'IN', + name: 'dns.question.class', + type: 'keyword', + }, + 'dns.question.name': { + category: 'dns', + description: + 'The name being queried. If the name field contains non-printable characters (below 32 or above 126), those characters should be represented as escaped base 10 integers (\\DDD). Back slashes and quotes should be escaped. Tabs, carriage returns, and line feeds should be converted to \\t, \\r, and \\n respectively.', + example: 'www.google.com', + name: 'dns.question.name', + type: 'keyword', + }, + 'dns.question.registered_domain': { + category: 'dns', + description: + 'The highest registered domain, stripped of the subdomain. For example, the registered domain for "foo.google.com" is "google.com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last two labels will not work well for TLDs such as "co.uk".', + example: 'google.com', + name: 'dns.question.registered_domain', + type: 'keyword', + }, + 'dns.question.subdomain': { + category: 'dns', + description: + 'The subdomain is all of the labels under the registered_domain. If the domain has multiple levels of subdomain, such as "sub2.sub1.example.com", the subdomain field should contain "sub2.sub1", with no trailing period.', + example: 'www', + name: 'dns.question.subdomain', + type: 'keyword', + }, + 'dns.question.top_level_domain': { + category: 'dns', + description: + 'The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for google.com is "com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last label will not work well for effective TLDs such as "co.uk".', + example: 'co.uk', + name: 'dns.question.top_level_domain', + type: 'keyword', + }, + 'dns.question.type': { + category: 'dns', + description: 'The type of record being queried.', + example: 'AAAA', + name: 'dns.question.type', + type: 'keyword', + }, + 'dns.resolved_ip': { + category: 'dns', + description: + 'Array containing all IPs seen in `answers.data`. The `answers` array can be difficult to use, because of the variety of data formats it can contain. Extracting all IP addresses seen in there to `dns.resolved_ip` makes it possible to index them as IP addresses, and makes them easier to visualize and query for.', + example: '["10.10.10.10","10.10.10.11"]', + name: 'dns.resolved_ip', + type: 'ip', + }, + 'dns.response_code': { + category: 'dns', + description: 'The DNS response code.', + example: 'NOERROR', + name: 'dns.response_code', + type: 'keyword', + }, + 'dns.type': { + category: 'dns', + description: + 'The type of DNS event captured, query or answer. If your source of DNS events only gives you DNS queries, you should only create dns events of type `dns.type:query`. If your source of DNS events gives you answers as well, you should create one event per query (optionally as soon as the query is seen). And a second event containing all query details as well as an array of answers.', + example: 'answer', + name: 'dns.type', + type: 'keyword', + }, + 'ecs.version': { + category: 'ecs', + description: + 'ECS version this event conforms to. `ecs.version` is a required field and must exist in all events. When querying across multiple indices -- which may conform to slightly different ECS versions -- this field lets integrations adjust to the schema version of the events.', + example: '1.0.0', + name: 'ecs.version', + type: 'keyword', + }, + 'error.code': { + category: 'error', + description: 'Error code describing the error.', + name: 'error.code', + type: 'keyword', + }, + 'error.id': { + category: 'error', + description: 'Unique identifier for the error.', + name: 'error.id', + type: 'keyword', + }, + 'error.message': { + category: 'error', + description: 'Error message.', + name: 'error.message', + type: 'text', + }, + 'error.stack_trace': { + category: 'error', + description: 'The stack trace of this error in plain text.', + name: 'error.stack_trace', + type: 'keyword', + }, + 'error.type': { + category: 'error', + description: 'The type of the error, for example the class name of the exception.', + example: 'java.lang.NullPointerException', + name: 'error.type', + type: 'keyword', + }, + 'event.action': { + category: 'event', + description: + 'The action captured by the event. This describes the information in the event. It is more specific than `event.category`. Examples are `group-add`, `process-started`, `file-created`. The value is normally defined by the implementer.', + example: 'user-password-change', + name: 'event.action', + type: 'keyword', + }, + 'event.category': { + category: 'event', + description: + 'This is one of four ECS Categorization Fields, and indicates the second level in the ECS category hierarchy. `event.category` represents the "big buckets" of ECS categories. For example, filtering on `event.category:process` yields all events relating to process activity. This field is closely related to `event.type`, which is used as a subcategory. This field is an array. This will allow proper categorization of some events that fall in multiple categories.', + example: 'authentication', + name: 'event.category', + type: 'keyword', + }, + 'event.code': { + category: 'event', + description: + 'Identification code for this event, if one exists. Some event sources use event codes to identify messages unambiguously, regardless of message language or wording adjustments over time. An example of this is the Windows Event ID.', + example: 4648, + name: 'event.code', + type: 'keyword', + }, + 'event.created': { + category: 'event', + description: + "event.created contains the date/time when the event was first read by an agent, or by your pipeline. This field is distinct from @timestamp in that @timestamp typically contain the time extracted from the original event. In most situations, these two timestamps will be slightly different. The difference can be used to calculate the delay between your source generating an event, and the time when your agent first processed it. This can be used to monitor your agent's or pipeline's ability to keep up with your event source. In case the two timestamps are identical, @timestamp should be used.", + example: '2016-05-23T08:05:34.857Z', + name: 'event.created', + type: 'date', + }, + 'event.dataset': { + category: 'event', + description: + "Name of the dataset. If an event source publishes more than one type of log or events (e.g. access log, error log), the dataset is used to specify which one the event comes from. It's recommended but not required to start the dataset name with the module name, followed by a dot, then the dataset name.", + example: 'apache.access', + name: 'event.dataset', + type: 'keyword', + }, + 'event.duration': { + category: 'event', + description: + 'Duration of the event in nanoseconds. If event.start and event.end are known this value should be the difference between the end and start time.', + name: 'event.duration', + type: 'long', + format: 'duration', + }, + 'event.end': { + category: 'event', + description: + 'event.end contains the date when the event ended or when the activity was last observed.', + name: 'event.end', + type: 'date', + }, + 'event.hash': { + category: 'event', + description: + 'Hash (perhaps logstash fingerprint) of raw field to be able to demonstrate log integrity.', + example: '123456789012345678901234567890ABCD', + name: 'event.hash', + type: 'keyword', + }, + 'event.id': { + category: 'event', + description: 'Unique ID to describe the event.', + example: '8a4f500d', + name: 'event.id', + type: 'keyword', + }, + 'event.ingested': { + category: 'event', + description: + "Timestamp when an event arrived in the central data store. This is different from `@timestamp`, which is when the event originally occurred. It's also different from `event.created`, which is meant to capture the first time an agent saw the event. In normal conditions, assuming no tampering, the timestamps should chronologically look like this: `@timestamp` < `event.created` < `event.ingested`.", + example: '2016-05-23T08:05:35.101Z', + name: 'event.ingested', + type: 'date', + }, + 'event.kind': { + category: 'event', + description: + 'This is one of four ECS Categorization Fields, and indicates the highest level in the ECS category hierarchy. `event.kind` gives high-level information about what type of information the event contains, without being specific to the contents of the event. For example, values of this field distinguish alert events from metric events. The value of this field can be used to inform how these kinds of events should be handled. They may warrant different retention, different access control, it may also help understand whether the data coming in at a regular interval or not.', + example: 'alert', + name: 'event.kind', + type: 'keyword', + }, + 'event.module': { + category: 'event', + description: + 'Name of the module this data is coming from. If your monitoring agent supports the concept of modules or plugins to process events of a given source (e.g. Apache logs), `event.module` should contain the name of this module.', + example: 'apache', + name: 'event.module', + type: 'keyword', + }, + 'event.original': { + category: 'event', + description: + 'Raw text message of entire event. Used to demonstrate log integrity. This field is not indexed and doc_values are disabled. It cannot be searched, but it can be retrieved from `_source`.', + example: + 'Sep 19 08:26:10 host CEF:0|Security| threatmanager|1.0|100| worm successfully stopped|10|src=10.0.0.1 dst=2.1.2.2spt=1232', + name: 'event.original', + type: 'keyword', + }, + 'event.outcome': { + category: 'event', + description: + 'This is one of four ECS Categorization Fields, and indicates the lowest level in the ECS category hierarchy. `event.outcome` simply denotes whether the event represents a success or a failure from the perspective of the entity that produced the event. Note that when a single transaction is described in multiple events, each event may populate different values of `event.outcome`, according to their perspective. Also note that in the case of a compound event (a single event that contains multiple logical events), this field should be populated with the value that best captures the overall success or failure from the perspective of the event producer. Further note that not all events will have an associated outcome. For example, this field is generally not populated for metric events, events with `event.type:info`, or any events for which an outcome does not make logical sense.', + example: 'success', + name: 'event.outcome', + type: 'keyword', + }, + 'event.provider': { + category: 'event', + description: + 'Source of the event. Event transports such as Syslog or the Windows Event Log typically mention the source of an event. It can be the name of the software that generated the event (e.g. Sysmon, httpd), or of a subsystem of the operating system (kernel, Microsoft-Windows-Security-Auditing).', + example: 'kernel', + name: 'event.provider', + type: 'keyword', + }, + 'event.reference': { + category: 'event', + description: + 'Reference URL linking to additional information about this event. This URL links to a static definition of the this event. Alert events, indicated by `event.kind:alert`, are a common use case for this field.', + example: 'https://system.vendor.com/event/#0001234', + name: 'event.reference', + type: 'keyword', + }, + 'event.risk_score': { + category: 'event', + description: + "Risk score or priority of the event (e.g. security solutions). Use your system's original value here.", + name: 'event.risk_score', + type: 'float', + }, + 'event.risk_score_norm': { + category: 'event', + description: + 'Normalized risk score or priority of the event, on a scale of 0 to 100. This is mainly useful if you use more than one system that assigns risk scores, and you want to see a normalized value across all systems.', + name: 'event.risk_score_norm', + type: 'float', + }, + 'event.sequence': { + category: 'event', + description: + 'Sequence number of the event. The sequence number is a value published by some event sources, to make the exact ordering of events unambiguous, regardless of the timestamp precision.', + name: 'event.sequence', + type: 'long', + format: 'string', + }, + 'event.severity': { + category: 'event', + description: + "The numeric severity of the event according to your event source. What the different severity values mean can be different between sources and use cases. It's up to the implementer to make sure severities are consistent across events from the same source. The Syslog severity belongs in `log.syslog.severity.code`. `event.severity` is meant to represent the severity according to the event source (e.g. firewall, IDS). If the event source does not publish its own severity, you may optionally copy the `log.syslog.severity.code` to `event.severity`.", + example: 7, + name: 'event.severity', + type: 'long', + format: 'string', + }, + 'event.start': { + category: 'event', + description: + 'event.start contains the date when the event started or when the activity was first observed.', + name: 'event.start', + type: 'date', + }, + 'event.timezone': { + category: 'event', + description: + 'This field should be populated when the event\'s timestamp does not include timezone information already (e.g. default Syslog timestamps). It\'s optional otherwise. Acceptable timezone formats are: a canonical ID (e.g. "Europe/Amsterdam"), abbreviated (e.g. "EST") or an HH:mm differential (e.g. "-05:00").', + name: 'event.timezone', + type: 'keyword', + }, + 'event.type': { + category: 'event', + description: + 'This is one of four ECS Categorization Fields, and indicates the third level in the ECS category hierarchy. `event.type` represents a categorization "sub-bucket" that, when used along with the `event.category` field values, enables filtering events down to a level appropriate for single visualization. This field is an array. This will allow proper categorization of some events that fall in multiple event types.', + name: 'event.type', + type: 'keyword', + }, + 'event.url': { + category: 'event', + description: + 'URL linking to an external system to continue investigation of this event. This URL links to another system where in-depth investigation of the specific occurence of this event can take place. Alert events, indicated by `event.kind:alert`, are a common use case for this field.', + example: 'https://mysystem.mydomain.com/alert/5271dedb-f5b0-4218-87f0-4ac4870a38fe', + name: 'event.url', + type: 'keyword', + }, + 'file.accessed': { + category: 'file', + description: + 'Last time the file was accessed. Note that not all filesystems keep track of access time.', + name: 'file.accessed', + type: 'date', + }, + 'file.attributes': { + category: 'file', + description: + "Array of file attributes. Attributes names will vary by platform. Here's a non-exhaustive list of values that are expected in this field: archive, compressed, directory, encrypted, execute, hidden, read, readonly, system, write.", + example: '["readonly", "system"]', + name: 'file.attributes', + type: 'keyword', + }, + 'file.code_signature.exists': { + category: 'file', + description: 'Boolean to capture if a signature is present.', + example: 'true', + name: 'file.code_signature.exists', + type: 'boolean', + }, + 'file.code_signature.status': { + category: 'file', + description: + 'Additional information about the certificate status. This is useful for logging cryptographic errors with the certificate validity or trust status. Leave unpopulated if the validity or trust of the certificate was unchecked.', + example: 'ERROR_UNTRUSTED_ROOT', + name: 'file.code_signature.status', + type: 'keyword', + }, + 'file.code_signature.subject_name': { + category: 'file', + description: 'Subject name of the code signer', + example: 'Microsoft Corporation', + name: 'file.code_signature.subject_name', + type: 'keyword', + }, + 'file.code_signature.trusted': { + category: 'file', + description: + 'Stores the trust status of the certificate chain. Validating the trust of the certificate chain may be complicated, and this field should only be populated by tools that actively check the status.', + example: 'true', + name: 'file.code_signature.trusted', + type: 'boolean', + }, + 'file.code_signature.valid': { + category: 'file', + description: + 'Boolean to capture if the digital signature is verified against the binary content. Leave unpopulated if a certificate was unchecked.', + example: 'true', + name: 'file.code_signature.valid', + type: 'boolean', + }, + 'file.created': { + category: 'file', + description: 'File creation time. Note that not all filesystems store the creation time.', + name: 'file.created', + type: 'date', + }, + 'file.ctime': { + category: 'file', + description: + 'Last time the file attributes or metadata changed. Note that changes to the file content will update `mtime`. This implies `ctime` will be adjusted at the same time, since `mtime` is an attribute of the file.', + name: 'file.ctime', + type: 'date', + }, + 'file.device': { + category: 'file', + description: 'Device that is the source of the file.', + example: 'sda', + name: 'file.device', + type: 'keyword', + }, + 'file.directory': { + category: 'file', + description: + 'Directory where the file is located. It should include the drive letter, when appropriate.', + example: '/home/alice', + name: 'file.directory', + type: 'keyword', + }, + 'file.drive_letter': { + category: 'file', + description: + 'Drive letter where the file is located. This field is only relevant on Windows. The value should be uppercase, and not include the colon.', + example: 'C', + name: 'file.drive_letter', + type: 'keyword', + }, + 'file.extension': { + category: 'file', + description: 'File extension.', + example: 'png', + name: 'file.extension', + type: 'keyword', + }, + 'file.gid': { + category: 'file', + description: 'Primary group ID (GID) of the file.', + example: '1001', + name: 'file.gid', + type: 'keyword', + }, + 'file.group': { + category: 'file', + description: 'Primary group name of the file.', + example: 'alice', + name: 'file.group', + type: 'keyword', + }, + 'file.hash.md5': { + category: 'file', + description: 'MD5 hash.', + name: 'file.hash.md5', + type: 'keyword', + }, + 'file.hash.sha1': { + category: 'file', + description: 'SHA1 hash.', + name: 'file.hash.sha1', + type: 'keyword', + }, + 'file.hash.sha256': { + category: 'file', + description: 'SHA256 hash.', + name: 'file.hash.sha256', + type: 'keyword', + }, + 'file.hash.sha512': { + category: 'file', + description: 'SHA512 hash.', + name: 'file.hash.sha512', + type: 'keyword', + }, + 'file.inode': { + category: 'file', + description: 'Inode representing the file in the filesystem.', + example: '256383', + name: 'file.inode', + type: 'keyword', + }, + 'file.mime_type': { + category: 'file', + description: + 'MIME type should identify the format of the file or stream of bytes using https://www.iana.org/assignments/media-types/media-types.xhtml[IANA official types], where possible. When more than one type is applicable, the most specific type should be used.', + name: 'file.mime_type', + type: 'keyword', + }, + 'file.mode': { + category: 'file', + description: 'Mode of the file in octal representation.', + example: '0640', + name: 'file.mode', + type: 'keyword', + }, + 'file.mtime': { + category: 'file', + description: 'Last time the file content was modified.', + name: 'file.mtime', + type: 'date', + }, + 'file.name': { + category: 'file', + description: 'Name of the file including the extension, without the directory.', + example: 'example.png', + name: 'file.name', + type: 'keyword', + }, + 'file.owner': { + category: 'file', + description: "File owner's username.", + example: 'alice', + name: 'file.owner', + type: 'keyword', + }, + 'file.path': { + category: 'file', + description: + 'Full path to the file, including the file name. It should include the drive letter, when appropriate.', + example: '/home/alice/example.png', + name: 'file.path', + type: 'keyword', + }, + 'file.pe.company': { + category: 'file', + description: 'Internal company name of the file, provided at compile-time.', + example: 'Microsoft Corporation', + name: 'file.pe.company', + type: 'keyword', + }, + 'file.pe.description': { + category: 'file', + description: 'Internal description of the file, provided at compile-time.', + example: 'Paint', + name: 'file.pe.description', + type: 'keyword', + }, + 'file.pe.file_version': { + category: 'file', + description: 'Internal version of the file, provided at compile-time.', + example: '6.3.9600.17415', + name: 'file.pe.file_version', + type: 'keyword', + }, + 'file.pe.original_file_name': { + category: 'file', + description: 'Internal name of the file, provided at compile-time.', + example: 'MSPAINT.EXE', + name: 'file.pe.original_file_name', + type: 'keyword', + }, + 'file.pe.product': { + category: 'file', + description: 'Internal product name of the file, provided at compile-time.', + example: 'Microsoft® Windows® Operating System', + name: 'file.pe.product', + type: 'keyword', + }, + 'file.size': { + category: 'file', + description: 'File size in bytes. Only relevant when `file.type` is "file".', + example: 16384, + name: 'file.size', + type: 'long', + }, + 'file.target_path': { + category: 'file', + description: 'Target path for symlinks.', + name: 'file.target_path', + type: 'keyword', + }, + 'file.type': { + category: 'file', + description: 'File type (file, dir, or symlink).', + example: 'file', + name: 'file.type', + type: 'keyword', + }, + 'file.uid': { + category: 'file', + description: 'The user ID (UID) or security identifier (SID) of the file owner.', + example: '1001', + name: 'file.uid', + type: 'keyword', + }, + 'geo.city_name': { + category: 'geo', + description: 'City name.', + example: 'Montreal', + name: 'geo.city_name', + type: 'keyword', + }, + 'geo.continent_name': { + category: 'geo', + description: 'Name of the continent.', + example: 'North America', + name: 'geo.continent_name', + type: 'keyword', + }, + 'geo.country_iso_code': { + category: 'geo', + description: 'Country ISO code.', + example: 'CA', + name: 'geo.country_iso_code', + type: 'keyword', + }, + 'geo.country_name': { + category: 'geo', + description: 'Country name.', + example: 'Canada', + name: 'geo.country_name', + type: 'keyword', + }, + 'geo.location': { + category: 'geo', + description: 'Longitude and latitude.', + example: '{ "lon": -73.614830, "lat": 45.505918 }', + name: 'geo.location', + type: 'geo_point', + }, + 'geo.name': { + category: 'geo', + description: + 'User-defined description of a location, at the level of granularity they care about. Could be the name of their data centers, the floor number, if this describes a local physical entity, city names. Not typically used in automated geolocation.', + example: 'boston-dc', + name: 'geo.name', + type: 'keyword', + }, + 'geo.region_iso_code': { + category: 'geo', + description: 'Region ISO code.', + example: 'CA-QC', + name: 'geo.region_iso_code', + type: 'keyword', + }, + 'geo.region_name': { + category: 'geo', + description: 'Region name.', + example: 'Quebec', + name: 'geo.region_name', + type: 'keyword', + }, + 'group.domain': { + category: 'group', + description: + 'Name of the directory the group is a member of. For example, an LDAP or Active Directory domain name.', + name: 'group.domain', + type: 'keyword', + }, + 'group.id': { + category: 'group', + description: 'Unique identifier for the group on the system/platform.', + name: 'group.id', + type: 'keyword', + }, + 'group.name': { + category: 'group', + description: 'Name of the group.', + name: 'group.name', + type: 'keyword', + }, + 'hash.md5': { + category: 'hash', + description: 'MD5 hash.', + name: 'hash.md5', + type: 'keyword', + }, + 'hash.sha1': { + category: 'hash', + description: 'SHA1 hash.', + name: 'hash.sha1', + type: 'keyword', + }, + 'hash.sha256': { + category: 'hash', + description: 'SHA256 hash.', + name: 'hash.sha256', + type: 'keyword', + }, + 'hash.sha512': { + category: 'hash', + description: 'SHA512 hash.', + name: 'hash.sha512', + type: 'keyword', + }, + 'host.architecture': { + category: 'host', + description: 'Operating system architecture.', + example: 'x86_64', + name: 'host.architecture', + type: 'keyword', + }, + 'host.domain': { + category: 'host', + description: + "Name of the domain of which the host is a member. For example, on Windows this could be the host's Active Directory domain or NetBIOS domain name. For Linux this could be the domain of the host's LDAP provider.", + example: 'CONTOSO', + name: 'host.domain', + type: 'keyword', + }, + 'host.geo.city_name': { + category: 'host', + description: 'City name.', + example: 'Montreal', + name: 'host.geo.city_name', + type: 'keyword', + }, + 'host.geo.continent_name': { + category: 'host', + description: 'Name of the continent.', + example: 'North America', + name: 'host.geo.continent_name', + type: 'keyword', + }, + 'host.geo.country_iso_code': { + category: 'host', + description: 'Country ISO code.', + example: 'CA', + name: 'host.geo.country_iso_code', + type: 'keyword', + }, + 'host.geo.country_name': { + category: 'host', + description: 'Country name.', + example: 'Canada', + name: 'host.geo.country_name', + type: 'keyword', + }, + 'host.geo.location': { + category: 'host', + description: 'Longitude and latitude.', + example: '{ "lon": -73.614830, "lat": 45.505918 }', + name: 'host.geo.location', + type: 'geo_point', + }, + 'host.geo.name': { + category: 'host', + description: + 'User-defined description of a location, at the level of granularity they care about. Could be the name of their data centers, the floor number, if this describes a local physical entity, city names. Not typically used in automated geolocation.', + example: 'boston-dc', + name: 'host.geo.name', + type: 'keyword', + }, + 'host.geo.region_iso_code': { + category: 'host', + description: 'Region ISO code.', + example: 'CA-QC', + name: 'host.geo.region_iso_code', + type: 'keyword', + }, + 'host.geo.region_name': { + category: 'host', + description: 'Region name.', + example: 'Quebec', + name: 'host.geo.region_name', + type: 'keyword', + }, + 'host.hostname': { + category: 'host', + description: + 'Hostname of the host. It normally contains what the `hostname` command returns on the host machine.', + name: 'host.hostname', + type: 'keyword', + }, + 'host.id': { + category: 'host', + description: + 'Unique host id. As hostname is not always unique, use values that are meaningful in your environment. Example: The current usage of `beat.name`.', + name: 'host.id', + type: 'keyword', + }, + 'host.ip': { + category: 'host', + description: 'Host ip addresses.', + name: 'host.ip', + type: 'ip', + }, + 'host.mac': { + category: 'host', + description: 'Host mac addresses.', + name: 'host.mac', + type: 'keyword', + }, + 'host.name': { + category: 'host', + description: + 'Name of the host. It can contain what `hostname` returns on Unix systems, the fully qualified domain name, or a name specified by the user. The sender decides which value to use.', + name: 'host.name', + type: 'keyword', + }, + 'host.os.family': { + category: 'host', + description: 'OS family (such as redhat, debian, freebsd, windows).', + example: 'debian', + name: 'host.os.family', + type: 'keyword', + }, + 'host.os.full': { + category: 'host', + description: 'Operating system name, including the version or code name.', + example: 'Mac OS Mojave', + name: 'host.os.full', + type: 'keyword', + }, + 'host.os.kernel': { + category: 'host', + description: 'Operating system kernel version as a raw string.', + example: '4.4.0-112-generic', + name: 'host.os.kernel', + type: 'keyword', + }, + 'host.os.name': { + category: 'host', + description: 'Operating system name, without the version.', + example: 'Mac OS X', + name: 'host.os.name', + type: 'keyword', + }, + 'host.os.platform': { + category: 'host', + description: 'Operating system platform (such centos, ubuntu, windows).', + example: 'darwin', + name: 'host.os.platform', + type: 'keyword', + }, + 'host.os.version': { + category: 'host', + description: 'Operating system version as a raw string.', + example: '10.14.1', + name: 'host.os.version', + type: 'keyword', + }, + 'host.type': { + category: 'host', + description: + 'Type of host. For Cloud providers this can be the machine type like `t2.medium`. If vm, this could be the container, for example, or other information meaningful in your environment.', + name: 'host.type', + type: 'keyword', + }, + 'host.uptime': { + category: 'host', + description: 'Seconds the host has been up.', + example: 1325, + name: 'host.uptime', + type: 'long', + }, + 'host.user.domain': { + category: 'host', + description: + 'Name of the directory the user is a member of. For example, an LDAP or Active Directory domain name.', + name: 'host.user.domain', + type: 'keyword', + }, + 'host.user.email': { + category: 'host', + description: 'User email address.', + name: 'host.user.email', + type: 'keyword', + }, + 'host.user.full_name': { + category: 'host', + description: "User's full name, if available.", + example: 'Albert Einstein', + name: 'host.user.full_name', + type: 'keyword', + }, + 'host.user.group.domain': { + category: 'host', + description: + 'Name of the directory the group is a member of. For example, an LDAP or Active Directory domain name.', + name: 'host.user.group.domain', + type: 'keyword', + }, + 'host.user.group.id': { + category: 'host', + description: 'Unique identifier for the group on the system/platform.', + name: 'host.user.group.id', + type: 'keyword', + }, + 'host.user.group.name': { + category: 'host', + description: 'Name of the group.', + name: 'host.user.group.name', + type: 'keyword', + }, + 'host.user.hash': { + category: 'host', + description: + 'Unique user hash to correlate information for a user in anonymized form. Useful if `user.id` or `user.name` contain confidential information and cannot be used.', + name: 'host.user.hash', + type: 'keyword', + }, + 'host.user.id': { + category: 'host', + description: 'Unique identifiers of the user.', + name: 'host.user.id', + type: 'keyword', + }, + 'host.user.name': { + category: 'host', + description: 'Short name or login of the user.', + example: 'albert', + name: 'host.user.name', + type: 'keyword', + }, + 'http.request.body.bytes': { + category: 'http', + description: 'Size in bytes of the request body.', + example: 887, + name: 'http.request.body.bytes', + type: 'long', + format: 'bytes', + }, + 'http.request.body.content': { + category: 'http', + description: 'The full HTTP request body.', + example: 'Hello world', + name: 'http.request.body.content', + type: 'keyword', + }, + 'http.request.bytes': { + category: 'http', + description: 'Total size in bytes of the request (body and headers).', + example: 1437, + name: 'http.request.bytes', + type: 'long', + format: 'bytes', + }, + 'http.request.method': { + category: 'http', + description: + 'HTTP request method. The field value must be normalized to lowercase for querying. See the documentation section "Implementing ECS".', + example: 'get, post, put', + name: 'http.request.method', + type: 'keyword', + }, + 'http.request.referrer': { + category: 'http', + description: 'Referrer for this HTTP request.', + example: 'https://blog.example.com/', + name: 'http.request.referrer', + type: 'keyword', + }, + 'http.response.body.bytes': { + category: 'http', + description: 'Size in bytes of the response body.', + example: 887, + name: 'http.response.body.bytes', + type: 'long', + format: 'bytes', + }, + 'http.response.body.content': { + category: 'http', + description: 'The full HTTP response body.', + example: 'Hello world', + name: 'http.response.body.content', + type: 'keyword', + }, + 'http.response.bytes': { + category: 'http', + description: 'Total size in bytes of the response (body and headers).', + example: 1437, + name: 'http.response.bytes', + type: 'long', + format: 'bytes', + }, + 'http.response.status_code': { + category: 'http', + description: 'HTTP response status code.', + example: 404, + name: 'http.response.status_code', + type: 'long', + format: 'string', + }, + 'http.version': { + category: 'http', + description: 'HTTP version.', + example: 1.1, + name: 'http.version', + type: 'keyword', + }, + 'interface.alias': { + category: 'interface', + description: + 'Interface alias as reported by the system, typically used in firewall implementations for e.g. inside, outside, or dmz logical interface naming.', + example: 'outside', + name: 'interface.alias', + type: 'keyword', + }, + 'interface.id': { + category: 'interface', + description: 'Interface ID as reported by an observer (typically SNMP interface ID).', + example: 10, + name: 'interface.id', + type: 'keyword', + }, + 'interface.name': { + category: 'interface', + description: 'Interface name as reported by the system.', + example: 'eth0', + name: 'interface.name', + type: 'keyword', + }, + 'log.level': { + category: 'log', + description: + "Original log level of the log event. If the source of the event provides a log level or textual severity, this is the one that goes in `log.level`. If your source doesn't specify one, you may put your event transport's severity here (e.g. Syslog severity). Some examples are `warn`, `err`, `i`, `informational`.", + example: 'error', + name: 'log.level', + type: 'keyword', + }, + 'log.logger': { + category: 'log', + description: + 'The name of the logger inside an application. This is usually the name of the class which initialized the logger, or can be a custom name.', + example: 'org.elasticsearch.bootstrap.Bootstrap', + name: 'log.logger', + type: 'keyword', + }, + 'log.origin.file.line': { + category: 'log', + description: + 'The line number of the file containing the source code which originated the log event.', + example: 42, + name: 'log.origin.file.line', + type: 'integer', + }, + 'log.origin.file.name': { + category: 'log', + description: + 'The name of the file containing the source code which originated the log event. Note that this is not the name of the log file.', + example: 'Bootstrap.java', + name: 'log.origin.file.name', + type: 'keyword', + }, + 'log.origin.function': { + category: 'log', + description: 'The name of the function or method which originated the log event.', + example: 'init', + name: 'log.origin.function', + type: 'keyword', + }, + 'log.original': { + category: 'log', + description: + "This is the original log message and contains the full log message before splitting it up in multiple parts. In contrast to the `message` field which can contain an extracted part of the log message, this field contains the original, full log message. It can have already some modifications applied like encoding or new lines removed to clean up the log message. This field is not indexed and doc_values are disabled so it can't be queried but the value can be retrieved from `_source`.", + example: 'Sep 19 08:26:10 localhost My log', + name: 'log.original', + type: 'keyword', + }, + 'log.syslog': { + category: 'log', + description: + 'The Syslog metadata of the event, if the event was transmitted via Syslog. Please see RFCs 5424 or 3164.', + name: 'log.syslog', + type: 'object', + }, + 'log.syslog.facility.code': { + category: 'log', + description: + 'The Syslog numeric facility of the log event, if available. According to RFCs 5424 and 3164, this value should be an integer between 0 and 23.', + example: 23, + name: 'log.syslog.facility.code', + type: 'long', + format: 'string', + }, + 'log.syslog.facility.name': { + category: 'log', + description: 'The Syslog text-based facility of the log event, if available.', + example: 'local7', + name: 'log.syslog.facility.name', + type: 'keyword', + }, + 'log.syslog.priority': { + category: 'log', + description: + 'Syslog numeric priority of the event, if available. According to RFCs 5424 and 3164, the priority is 8 * facility + severity. This number is therefore expected to contain a value between 0 and 191.', + example: 135, + name: 'log.syslog.priority', + type: 'long', + format: 'string', + }, + 'log.syslog.severity.code': { + category: 'log', + description: + "The Syslog numeric severity of the log event, if available. If the event source publishing via Syslog provides a different numeric severity value (e.g. firewall, IDS), your source's numeric severity should go to `event.severity`. If the event source does not specify a distinct severity, you can optionally copy the Syslog severity to `event.severity`.", + example: 3, + name: 'log.syslog.severity.code', + type: 'long', + }, + 'log.syslog.severity.name': { + category: 'log', + description: + "The Syslog numeric severity of the log event, if available. If the event source publishing via Syslog provides a different severity value (e.g. firewall, IDS), your source's text severity should go to `log.level`. If the event source does not specify a distinct severity, you can optionally copy the Syslog severity to `log.level`.", + example: 'Error', + name: 'log.syslog.severity.name', + type: 'keyword', + }, + 'network.application': { + category: 'network', + description: + 'A name given to an application level protocol. This can be arbitrarily assigned for things like microservices, but also apply to things like skype, icq, facebook, twitter. This would be used in situations where the vendor or service can be decoded such as from the source/dest IP owners, ports, or wire format. The field value must be normalized to lowercase for querying. See the documentation section "Implementing ECS".', + example: 'aim', + name: 'network.application', + type: 'keyword', + }, + 'network.bytes': { + category: 'network', + description: + 'Total bytes transferred in both directions. If `source.bytes` and `destination.bytes` are known, `network.bytes` is their sum.', + example: 368, + name: 'network.bytes', + type: 'long', + format: 'bytes', + }, + 'network.community_id': { + category: 'network', + description: + 'A hash of source and destination IPs and ports, as well as the protocol used in a communication. This is a tool-agnostic standard to identify flows. Learn more at https://github.com/corelight/community-id-spec.', + example: '1:hO+sN4H+MG5MY/8hIrXPqc4ZQz0=', + name: 'network.community_id', + type: 'keyword', + }, + 'network.direction': { + category: 'network', + description: + "Direction of the network traffic. Recommended values are: * inbound * outbound * internal * external * unknown When mapping events from a host-based monitoring context, populate this field from the host's point of view. When mapping events from a network or perimeter-based monitoring context, populate this field from the point of view of your network perimeter.", + example: 'inbound', + name: 'network.direction', + type: 'keyword', + }, + 'network.forwarded_ip': { + category: 'network', + description: 'Host IP address when the source IP address is the proxy.', + example: '192.1.1.2', + name: 'network.forwarded_ip', + type: 'ip', + }, + 'network.iana_number': { + category: 'network', + description: + 'IANA Protocol Number (https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml). Standardized list of protocols. This aligns well with NetFlow and sFlow related logs which use the IANA Protocol Number.', + example: 6, + name: 'network.iana_number', + type: 'keyword', + }, + 'network.inner': { + category: 'network', + description: + 'Network.inner fields are added in addition to network.vlan fields to describe the innermost VLAN when q-in-q VLAN tagging is present. Allowed fields include vlan.id and vlan.name. Inner vlan fields are typically used when sending traffic with multiple 802.1q encapsulations to a network sensor (e.g. Zeek, Wireshark.)', + name: 'network.inner', + type: 'object', + }, + 'network.inner.vlan.id': { + category: 'network', + description: 'VLAN ID as reported by the observer.', + example: 10, + name: 'network.inner.vlan.id', + type: 'keyword', + }, + 'network.inner.vlan.name': { + category: 'network', + description: 'Optional VLAN name as reported by the observer.', + example: 'outside', + name: 'network.inner.vlan.name', + type: 'keyword', + }, + 'network.name': { + category: 'network', + description: 'Name given by operators to sections of their network.', + example: 'Guest Wifi', + name: 'network.name', + type: 'keyword', + }, + 'network.packets': { + category: 'network', + description: + 'Total packets transferred in both directions. If `source.packets` and `destination.packets` are known, `network.packets` is their sum.', + example: 24, + name: 'network.packets', + type: 'long', + }, + 'network.protocol': { + category: 'network', + description: + 'L7 Network protocol name. ex. http, lumberjack, transport protocol. The field value must be normalized to lowercase for querying. See the documentation section "Implementing ECS".', + example: 'http', + name: 'network.protocol', + type: 'keyword', + }, + 'network.transport': { + category: 'network', + description: + 'Same as network.iana_number, but instead using the Keyword name of the transport layer (udp, tcp, ipv6-icmp, etc.) The field value must be normalized to lowercase for querying. See the documentation section "Implementing ECS".', + example: 'tcp', + name: 'network.transport', + type: 'keyword', + }, + 'network.type': { + category: 'network', + description: + 'In the OSI Model this would be the Network Layer. ipv4, ipv6, ipsec, pim, etc The field value must be normalized to lowercase for querying. See the documentation section "Implementing ECS".', + example: 'ipv4', + name: 'network.type', + type: 'keyword', + }, + 'network.vlan.id': { + category: 'network', + description: 'VLAN ID as reported by the observer.', + example: 10, + name: 'network.vlan.id', + type: 'keyword', + }, + 'network.vlan.name': { + category: 'network', + description: 'Optional VLAN name as reported by the observer.', + example: 'outside', + name: 'network.vlan.name', + type: 'keyword', + }, + 'observer.egress': { + category: 'observer', + description: + 'Observer.egress holds information like interface number and name, vlan, and zone information to classify egress traffic. Single armed monitoring such as a network sensor on a span port should only use observer.ingress to categorize traffic.', + name: 'observer.egress', + type: 'object', + }, + 'observer.egress.interface.alias': { + category: 'observer', + description: + 'Interface alias as reported by the system, typically used in firewall implementations for e.g. inside, outside, or dmz logical interface naming.', + example: 'outside', + name: 'observer.egress.interface.alias', + type: 'keyword', + }, + 'observer.egress.interface.id': { + category: 'observer', + description: 'Interface ID as reported by an observer (typically SNMP interface ID).', + example: 10, + name: 'observer.egress.interface.id', + type: 'keyword', + }, + 'observer.egress.interface.name': { + category: 'observer', + description: 'Interface name as reported by the system.', + example: 'eth0', + name: 'observer.egress.interface.name', + type: 'keyword', + }, + 'observer.egress.vlan.id': { + category: 'observer', + description: 'VLAN ID as reported by the observer.', + example: 10, + name: 'observer.egress.vlan.id', + type: 'keyword', + }, + 'observer.egress.vlan.name': { + category: 'observer', + description: 'Optional VLAN name as reported by the observer.', + example: 'outside', + name: 'observer.egress.vlan.name', + type: 'keyword', + }, + 'observer.egress.zone': { + category: 'observer', + description: + 'Network zone of outbound traffic as reported by the observer to categorize the destination area of egress traffic, e.g. Internal, External, DMZ, HR, Legal, etc.', + example: 'Public_Internet', + name: 'observer.egress.zone', + type: 'keyword', + }, + 'observer.geo.city_name': { + category: 'observer', + description: 'City name.', + example: 'Montreal', + name: 'observer.geo.city_name', + type: 'keyword', + }, + 'observer.geo.continent_name': { + category: 'observer', + description: 'Name of the continent.', + example: 'North America', + name: 'observer.geo.continent_name', + type: 'keyword', + }, + 'observer.geo.country_iso_code': { + category: 'observer', + description: 'Country ISO code.', + example: 'CA', + name: 'observer.geo.country_iso_code', + type: 'keyword', + }, + 'observer.geo.country_name': { + category: 'observer', + description: 'Country name.', + example: 'Canada', + name: 'observer.geo.country_name', + type: 'keyword', + }, + 'observer.geo.location': { + category: 'observer', + description: 'Longitude and latitude.', + example: '{ "lon": -73.614830, "lat": 45.505918 }', + name: 'observer.geo.location', + type: 'geo_point', + }, + 'observer.geo.name': { + category: 'observer', + description: + 'User-defined description of a location, at the level of granularity they care about. Could be the name of their data centers, the floor number, if this describes a local physical entity, city names. Not typically used in automated geolocation.', + example: 'boston-dc', + name: 'observer.geo.name', + type: 'keyword', + }, + 'observer.geo.region_iso_code': { + category: 'observer', + description: 'Region ISO code.', + example: 'CA-QC', + name: 'observer.geo.region_iso_code', + type: 'keyword', + }, + 'observer.geo.region_name': { + category: 'observer', + description: 'Region name.', + example: 'Quebec', + name: 'observer.geo.region_name', + type: 'keyword', + }, + 'observer.hostname': { + category: 'observer', + description: 'Hostname of the observer.', + name: 'observer.hostname', + type: 'keyword', + }, + 'observer.ingress': { + category: 'observer', + description: + 'Observer.ingress holds information like interface number and name, vlan, and zone information to classify ingress traffic. Single armed monitoring such as a network sensor on a span port should only use observer.ingress to categorize traffic.', + name: 'observer.ingress', + type: 'object', + }, + 'observer.ingress.interface.alias': { + category: 'observer', + description: + 'Interface alias as reported by the system, typically used in firewall implementations for e.g. inside, outside, or dmz logical interface naming.', + example: 'outside', + name: 'observer.ingress.interface.alias', + type: 'keyword', + }, + 'observer.ingress.interface.id': { + category: 'observer', + description: 'Interface ID as reported by an observer (typically SNMP interface ID).', + example: 10, + name: 'observer.ingress.interface.id', + type: 'keyword', + }, + 'observer.ingress.interface.name': { + category: 'observer', + description: 'Interface name as reported by the system.', + example: 'eth0', + name: 'observer.ingress.interface.name', + type: 'keyword', + }, + 'observer.ingress.vlan.id': { + category: 'observer', + description: 'VLAN ID as reported by the observer.', + example: 10, + name: 'observer.ingress.vlan.id', + type: 'keyword', + }, + 'observer.ingress.vlan.name': { + category: 'observer', + description: 'Optional VLAN name as reported by the observer.', + example: 'outside', + name: 'observer.ingress.vlan.name', + type: 'keyword', + }, + 'observer.ingress.zone': { + category: 'observer', + description: + 'Network zone of incoming traffic as reported by the observer to categorize the source area of ingress traffic. e.g. internal, External, DMZ, HR, Legal, etc.', + example: 'DMZ', + name: 'observer.ingress.zone', + type: 'keyword', + }, + 'observer.ip': { + category: 'observer', + description: 'IP addresses of the observer.', + name: 'observer.ip', + type: 'ip', + }, + 'observer.mac': { + category: 'observer', + description: 'MAC addresses of the observer', + name: 'observer.mac', + type: 'keyword', + }, + 'observer.name': { + category: 'observer', + description: + 'Custom name of the observer. This is a name that can be given to an observer. This can be helpful for example if multiple firewalls of the same model are used in an organization. If no custom name is needed, the field can be left empty.', + example: '1_proxySG', + name: 'observer.name', + type: 'keyword', + }, + 'observer.os.family': { + category: 'observer', + description: 'OS family (such as redhat, debian, freebsd, windows).', + example: 'debian', + name: 'observer.os.family', + type: 'keyword', + }, + 'observer.os.full': { + category: 'observer', + description: 'Operating system name, including the version or code name.', + example: 'Mac OS Mojave', + name: 'observer.os.full', + type: 'keyword', + }, + 'observer.os.kernel': { + category: 'observer', + description: 'Operating system kernel version as a raw string.', + example: '4.4.0-112-generic', + name: 'observer.os.kernel', + type: 'keyword', + }, + 'observer.os.name': { + category: 'observer', + description: 'Operating system name, without the version.', + example: 'Mac OS X', + name: 'observer.os.name', + type: 'keyword', + }, + 'observer.os.platform': { + category: 'observer', + description: 'Operating system platform (such centos, ubuntu, windows).', + example: 'darwin', + name: 'observer.os.platform', + type: 'keyword', + }, + 'observer.os.version': { + category: 'observer', + description: 'Operating system version as a raw string.', + example: '10.14.1', + name: 'observer.os.version', + type: 'keyword', + }, + 'observer.product': { + category: 'observer', + description: 'The product name of the observer.', + example: 's200', + name: 'observer.product', + type: 'keyword', + }, + 'observer.serial_number': { + category: 'observer', + description: 'Observer serial number.', + name: 'observer.serial_number', + type: 'keyword', + }, + 'observer.type': { + category: 'observer', + description: + 'The type of the observer the data is coming from. There is no predefined list of observer types. Some examples are `forwarder`, `firewall`, `ids`, `ips`, `proxy`, `poller`, `sensor`, `APM server`.', + example: 'firewall', + name: 'observer.type', + type: 'keyword', + }, + 'observer.vendor': { + category: 'observer', + description: 'Vendor name of the observer.', + example: 'Symantec', + name: 'observer.vendor', + type: 'keyword', + }, + 'observer.version': { + category: 'observer', + description: 'Observer version.', + name: 'observer.version', + type: 'keyword', + }, + 'organization.id': { + category: 'organization', + description: 'Unique identifier for the organization.', + name: 'organization.id', + type: 'keyword', + }, + 'organization.name': { + category: 'organization', + description: 'Organization name.', + name: 'organization.name', + type: 'keyword', + }, + 'os.family': { + category: 'os', + description: 'OS family (such as redhat, debian, freebsd, windows).', + example: 'debian', + name: 'os.family', + type: 'keyword', + }, + 'os.full': { + category: 'os', + description: 'Operating system name, including the version or code name.', + example: 'Mac OS Mojave', + name: 'os.full', + type: 'keyword', + }, + 'os.kernel': { + category: 'os', + description: 'Operating system kernel version as a raw string.', + example: '4.4.0-112-generic', + name: 'os.kernel', + type: 'keyword', + }, + 'os.name': { + category: 'os', + description: 'Operating system name, without the version.', + example: 'Mac OS X', + name: 'os.name', + type: 'keyword', + }, + 'os.platform': { + category: 'os', + description: 'Operating system platform (such centos, ubuntu, windows).', + example: 'darwin', + name: 'os.platform', + type: 'keyword', + }, + 'os.version': { + category: 'os', + description: 'Operating system version as a raw string.', + example: '10.14.1', + name: 'os.version', + type: 'keyword', + }, + 'package.architecture': { + category: 'package', + description: 'Package architecture.', + example: 'x86_64', + name: 'package.architecture', + type: 'keyword', + }, + 'package.build_version': { + category: 'package', + description: + 'Additional information about the build version of the installed package. For example use the commit SHA of a non-released package.', + example: '36f4f7e89dd61b0988b12ee000b98966867710cd', + name: 'package.build_version', + type: 'keyword', + }, + 'package.checksum': { + category: 'package', + description: 'Checksum of the installed package for verification.', + example: '68b329da9893e34099c7d8ad5cb9c940', + name: 'package.checksum', + type: 'keyword', + }, + 'package.description': { + category: 'package', + description: 'Description of the package.', + example: 'Open source programming language to build simple/reliable/efficient software.', + name: 'package.description', + type: 'keyword', + }, + 'package.install_scope': { + category: 'package', + description: 'Indicating how the package was installed, e.g. user-local, global.', + example: 'global', + name: 'package.install_scope', + type: 'keyword', + }, + 'package.installed': { + category: 'package', + description: 'Time when package was installed.', + name: 'package.installed', + type: 'date', + }, + 'package.license': { + category: 'package', + description: + 'License under which the package was released. Use a short name, e.g. the license identifier from SPDX License List where possible (https://spdx.org/licenses/).', + example: 'Apache License 2.0', + name: 'package.license', + type: 'keyword', + }, + 'package.name': { + category: 'package', + description: 'Package name', + example: 'go', + name: 'package.name', + type: 'keyword', + }, + 'package.path': { + category: 'package', + description: 'Path where the package is installed.', + example: '/usr/local/Cellar/go/1.12.9/', + name: 'package.path', + type: 'keyword', + }, + 'package.reference': { + category: 'package', + description: 'Home page or reference URL of the software in this package, if available.', + example: 'https://golang.org', + name: 'package.reference', + type: 'keyword', + }, + 'package.size': { + category: 'package', + description: 'Package size in bytes.', + example: 62231, + name: 'package.size', + type: 'long', + format: 'string', + }, + 'package.type': { + category: 'package', + description: + 'Type of package. This should contain the package file type, rather than the package manager name. Examples: rpm, dpkg, brew, npm, gem, nupkg, jar.', + example: 'rpm', + name: 'package.type', + type: 'keyword', + }, + 'package.version': { + category: 'package', + description: 'Package version', + example: '1.12.9', + name: 'package.version', + type: 'keyword', + }, + 'pe.company': { + category: 'pe', + description: 'Internal company name of the file, provided at compile-time.', + example: 'Microsoft Corporation', + name: 'pe.company', + type: 'keyword', + }, + 'pe.description': { + category: 'pe', + description: 'Internal description of the file, provided at compile-time.', + example: 'Paint', + name: 'pe.description', + type: 'keyword', + }, + 'pe.file_version': { + category: 'pe', + description: 'Internal version of the file, provided at compile-time.', + example: '6.3.9600.17415', + name: 'pe.file_version', + type: 'keyword', + }, + 'pe.original_file_name': { + category: 'pe', + description: 'Internal name of the file, provided at compile-time.', + example: 'MSPAINT.EXE', + name: 'pe.original_file_name', + type: 'keyword', + }, + 'pe.product': { + category: 'pe', + description: 'Internal product name of the file, provided at compile-time.', + example: 'Microsoft® Windows® Operating System', + name: 'pe.product', + type: 'keyword', + }, + 'process.args': { + category: 'process', + description: + 'Array of process arguments, starting with the absolute path to the executable. May be filtered to protect sensitive information.', + example: '["/usr/bin/ssh","-l","user","10.0.0.16"]', + name: 'process.args', + type: 'keyword', + }, + 'process.args_count': { + category: 'process', + description: + 'Length of the process.args array. This field can be useful for querying or performing bucket analysis on how many arguments were provided to start a process. More arguments may be an indication of suspicious activity.', + example: 4, + name: 'process.args_count', + type: 'long', + }, + 'process.code_signature.exists': { + category: 'process', + description: 'Boolean to capture if a signature is present.', + example: 'true', + name: 'process.code_signature.exists', + type: 'boolean', + }, + 'process.code_signature.status': { + category: 'process', + description: + 'Additional information about the certificate status. This is useful for logging cryptographic errors with the certificate validity or trust status. Leave unpopulated if the validity or trust of the certificate was unchecked.', + example: 'ERROR_UNTRUSTED_ROOT', + name: 'process.code_signature.status', + type: 'keyword', + }, + 'process.code_signature.subject_name': { + category: 'process', + description: 'Subject name of the code signer', + example: 'Microsoft Corporation', + name: 'process.code_signature.subject_name', + type: 'keyword', + }, + 'process.code_signature.trusted': { + category: 'process', + description: + 'Stores the trust status of the certificate chain. Validating the trust of the certificate chain may be complicated, and this field should only be populated by tools that actively check the status.', + example: 'true', + name: 'process.code_signature.trusted', + type: 'boolean', + }, + 'process.code_signature.valid': { + category: 'process', + description: + 'Boolean to capture if the digital signature is verified against the binary content. Leave unpopulated if a certificate was unchecked.', + example: 'true', + name: 'process.code_signature.valid', + type: 'boolean', + }, + 'process.command_line': { + category: 'process', + description: + 'Full command line that started the process, including the absolute path to the executable, and all arguments. Some arguments may be filtered to protect sensitive information.', + example: '/usr/bin/ssh -l user 10.0.0.16', + name: 'process.command_line', + type: 'keyword', + }, + 'process.entity_id': { + category: 'process', + description: + 'Unique identifier for the process. The implementation of this is specified by the data source, but some examples of what could be used here are a process-generated UUID, Sysmon Process GUIDs, or a hash of some uniquely identifying components of a process. Constructing a globally unique identifier is a common practice to mitigate PID reuse as well as to identify a specific process over time, across multiple monitored hosts.', + example: 'c2c455d9f99375d', + name: 'process.entity_id', + type: 'keyword', + }, + 'process.executable': { + category: 'process', + description: 'Absolute path to the process executable.', + example: '/usr/bin/ssh', + name: 'process.executable', + type: 'keyword', + }, + 'process.exit_code': { + category: 'process', + description: + 'The exit code of the process, if this is a termination event. The field should be absent if there is no exit code for the event (e.g. process start).', + example: 137, + name: 'process.exit_code', + type: 'long', + }, + 'process.hash.md5': { + category: 'process', + description: 'MD5 hash.', + name: 'process.hash.md5', + type: 'keyword', + }, + 'process.hash.sha1': { + category: 'process', + description: 'SHA1 hash.', + name: 'process.hash.sha1', + type: 'keyword', + }, + 'process.hash.sha256': { + category: 'process', + description: 'SHA256 hash.', + name: 'process.hash.sha256', + type: 'keyword', + }, + 'process.hash.sha512': { + category: 'process', + description: 'SHA512 hash.', + name: 'process.hash.sha512', + type: 'keyword', + }, + 'process.name': { + category: 'process', + description: 'Process name. Sometimes called program name or similar.', + example: 'ssh', + name: 'process.name', + type: 'keyword', + }, + 'process.parent.args': { + category: 'process', + description: 'Array of process arguments. May be filtered to protect sensitive information.', + example: '["ssh","-l","user","10.0.0.16"]', + name: 'process.parent.args', + type: 'keyword', + }, + 'process.parent.args_count': { + category: 'process', + description: + 'Length of the process.args array. This field can be useful for querying or performing bucket analysis on how many arguments were provided to start a process. More arguments may be an indication of suspicious activity.', + example: 4, + name: 'process.parent.args_count', + type: 'long', + }, + 'process.parent.code_signature.exists': { + category: 'process', + description: 'Boolean to capture if a signature is present.', + example: 'true', + name: 'process.parent.code_signature.exists', + type: 'boolean', + }, + 'process.parent.code_signature.status': { + category: 'process', + description: + 'Additional information about the certificate status. This is useful for logging cryptographic errors with the certificate validity or trust status. Leave unpopulated if the validity or trust of the certificate was unchecked.', + example: 'ERROR_UNTRUSTED_ROOT', + name: 'process.parent.code_signature.status', + type: 'keyword', + }, + 'process.parent.code_signature.subject_name': { + category: 'process', + description: 'Subject name of the code signer', + example: 'Microsoft Corporation', + name: 'process.parent.code_signature.subject_name', + type: 'keyword', + }, + 'process.parent.code_signature.trusted': { + category: 'process', + description: + 'Stores the trust status of the certificate chain. Validating the trust of the certificate chain may be complicated, and this field should only be populated by tools that actively check the status.', + example: 'true', + name: 'process.parent.code_signature.trusted', + type: 'boolean', + }, + 'process.parent.code_signature.valid': { + category: 'process', + description: + 'Boolean to capture if the digital signature is verified against the binary content. Leave unpopulated if a certificate was unchecked.', + example: 'true', + name: 'process.parent.code_signature.valid', + type: 'boolean', + }, + 'process.parent.command_line': { + category: 'process', + description: + 'Full command line that started the process, including the absolute path to the executable, and all arguments. Some arguments may be filtered to protect sensitive information.', + example: '/usr/bin/ssh -l user 10.0.0.16', + name: 'process.parent.command_line', + type: 'keyword', + }, + 'process.parent.entity_id': { + category: 'process', + description: + 'Unique identifier for the process. The implementation of this is specified by the data source, but some examples of what could be used here are a process-generated UUID, Sysmon Process GUIDs, or a hash of some uniquely identifying components of a process. Constructing a globally unique identifier is a common practice to mitigate PID reuse as well as to identify a specific process over time, across multiple monitored hosts.', + example: 'c2c455d9f99375d', + name: 'process.parent.entity_id', + type: 'keyword', + }, + 'process.parent.executable': { + category: 'process', + description: 'Absolute path to the process executable.', + example: '/usr/bin/ssh', + name: 'process.parent.executable', + type: 'keyword', + }, + 'process.parent.exit_code': { + category: 'process', + description: + 'The exit code of the process, if this is a termination event. The field should be absent if there is no exit code for the event (e.g. process start).', + example: 137, + name: 'process.parent.exit_code', + type: 'long', + }, + 'process.parent.hash.md5': { + category: 'process', + description: 'MD5 hash.', + name: 'process.parent.hash.md5', + type: 'keyword', + }, + 'process.parent.hash.sha1': { + category: 'process', + description: 'SHA1 hash.', + name: 'process.parent.hash.sha1', + type: 'keyword', + }, + 'process.parent.hash.sha256': { + category: 'process', + description: 'SHA256 hash.', + name: 'process.parent.hash.sha256', + type: 'keyword', + }, + 'process.parent.hash.sha512': { + category: 'process', + description: 'SHA512 hash.', + name: 'process.parent.hash.sha512', + type: 'keyword', + }, + 'process.parent.name': { + category: 'process', + description: 'Process name. Sometimes called program name or similar.', + example: 'ssh', + name: 'process.parent.name', + type: 'keyword', + }, + 'process.parent.pgid': { + category: 'process', + description: 'Identifier of the group of processes the process belongs to.', + name: 'process.parent.pgid', + type: 'long', + format: 'string', + }, + 'process.parent.pid': { + category: 'process', + description: 'Process id.', + example: 4242, + name: 'process.parent.pid', + type: 'long', + format: 'string', + }, + 'process.parent.ppid': { + category: 'process', + description: "Parent process' pid.", + example: 4241, + name: 'process.parent.ppid', + type: 'long', + format: 'string', + }, + 'process.parent.start': { + category: 'process', + description: 'The time the process started.', + example: '2016-05-23T08:05:34.853Z', + name: 'process.parent.start', + type: 'date', + }, + 'process.parent.thread.id': { + category: 'process', + description: 'Thread ID.', + example: 4242, + name: 'process.parent.thread.id', + type: 'long', + format: 'string', + }, + 'process.parent.thread.name': { + category: 'process', + description: 'Thread name.', + example: 'thread-0', + name: 'process.parent.thread.name', + type: 'keyword', + }, + 'process.parent.title': { + category: 'process', + description: + 'Process title. The proctitle, some times the same as process name. Can also be different: for example a browser setting its title to the web page currently opened.', + name: 'process.parent.title', + type: 'keyword', + }, + 'process.parent.uptime': { + category: 'process', + description: 'Seconds the process has been up.', + example: 1325, + name: 'process.parent.uptime', + type: 'long', + }, + 'process.parent.working_directory': { + category: 'process', + description: 'The working directory of the process.', + example: '/home/alice', + name: 'process.parent.working_directory', + type: 'keyword', + }, + 'process.pe.company': { + category: 'process', + description: 'Internal company name of the file, provided at compile-time.', + example: 'Microsoft Corporation', + name: 'process.pe.company', + type: 'keyword', + }, + 'process.pe.description': { + category: 'process', + description: 'Internal description of the file, provided at compile-time.', + example: 'Paint', + name: 'process.pe.description', + type: 'keyword', + }, + 'process.pe.file_version': { + category: 'process', + description: 'Internal version of the file, provided at compile-time.', + example: '6.3.9600.17415', + name: 'process.pe.file_version', + type: 'keyword', + }, + 'process.pe.original_file_name': { + category: 'process', + description: 'Internal name of the file, provided at compile-time.', + example: 'MSPAINT.EXE', + name: 'process.pe.original_file_name', + type: 'keyword', + }, + 'process.pe.product': { + category: 'process', + description: 'Internal product name of the file, provided at compile-time.', + example: 'Microsoft® Windows® Operating System', + name: 'process.pe.product', + type: 'keyword', + }, + 'process.pgid': { + category: 'process', + description: 'Identifier of the group of processes the process belongs to.', + name: 'process.pgid', + type: 'long', + format: 'string', + }, + 'process.pid': { + category: 'process', + description: 'Process id.', + example: 4242, + name: 'process.pid', + type: 'long', + format: 'string', + }, + 'process.ppid': { + category: 'process', + description: "Parent process' pid.", + example: 4241, + name: 'process.ppid', + type: 'long', + format: 'string', + }, + 'process.start': { + category: 'process', + description: 'The time the process started.', + example: '2016-05-23T08:05:34.853Z', + name: 'process.start', + type: 'date', + }, + 'process.thread.id': { + category: 'process', + description: 'Thread ID.', + example: 4242, + name: 'process.thread.id', + type: 'long', + format: 'string', + }, + 'process.thread.name': { + category: 'process', + description: 'Thread name.', + example: 'thread-0', + name: 'process.thread.name', + type: 'keyword', + }, + 'process.title': { + category: 'process', + description: + 'Process title. The proctitle, some times the same as process name. Can also be different: for example a browser setting its title to the web page currently opened.', + name: 'process.title', + type: 'keyword', + }, + 'process.uptime': { + category: 'process', + description: 'Seconds the process has been up.', + example: 1325, + name: 'process.uptime', + type: 'long', + }, + 'process.working_directory': { + category: 'process', + description: 'The working directory of the process.', + example: '/home/alice', + name: 'process.working_directory', + type: 'keyword', + }, + 'registry.data.bytes': { + category: 'registry', + description: + 'Original bytes written with base64 encoding. For Windows registry operations, such as SetValueEx and RegQueryValueEx, this corresponds to the data pointed by `lp_data`. This is optional but provides better recoverability and should be populated for REG_BINARY encoded values.', + example: 'ZQBuAC0AVQBTAAAAZQBuAAAAAAA=', + name: 'registry.data.bytes', + type: 'keyword', + }, + 'registry.data.strings': { + category: 'registry', + description: + 'Content when writing string types. Populated as an array when writing string data to the registry. For single string registry types (REG_SZ, REG_EXPAND_SZ), this should be an array with one string. For sequences of string with REG_MULTI_SZ, this array will be variable length. For numeric data, such as REG_DWORD and REG_QWORD, this should be populated with the decimal representation (e.g `"1"`).', + example: '["C:\\rta\\red_ttp\\bin\\myapp.exe"]', + name: 'registry.data.strings', + type: 'keyword', + }, + 'registry.data.type': { + category: 'registry', + description: 'Standard registry type for encoding contents', + example: 'REG_SZ', + name: 'registry.data.type', + type: 'keyword', + }, + 'registry.hive': { + category: 'registry', + description: 'Abbreviated name for the hive.', + example: 'HKLM', + name: 'registry.hive', + type: 'keyword', + }, + 'registry.key': { + category: 'registry', + description: 'Hive-relative path of keys.', + example: + 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options\\winword.exe', + name: 'registry.key', + type: 'keyword', + }, + 'registry.path': { + category: 'registry', + description: 'Full path, including hive, key and value', + example: + 'HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options\\winword.exe\\Debugger', + name: 'registry.path', + type: 'keyword', + }, + 'registry.value': { + category: 'registry', + description: 'Name of the value written.', + example: 'Debugger', + name: 'registry.value', + type: 'keyword', + }, + 'related.hash': { + category: 'related', + description: + "All the hashes seen on your event. Populating this field, then using it to search for hashes can help in situations where you're unsure what the hash algorithm is (and therefore which key name to search).", + name: 'related.hash', + type: 'keyword', + }, + 'related.ip': { + category: 'related', + description: 'All of the IPs seen on your event.', + name: 'related.ip', + type: 'ip', + }, + 'related.user': { + category: 'related', + description: 'All the user names seen on your event.', + name: 'related.user', + type: 'keyword', + }, + 'rule.author': { + category: 'rule', + description: + 'Name, organization, or pseudonym of the author or authors who created the rule used to generate this event.', + example: '["Star-Lord"]', + name: 'rule.author', + type: 'keyword', + }, + 'rule.category': { + category: 'rule', + description: + 'A categorization value keyword used by the entity using the rule for detection of this event.', + example: 'Attempted Information Leak', + name: 'rule.category', + type: 'keyword', + }, + 'rule.description': { + category: 'rule', + description: 'The description of the rule generating the event.', + example: 'Block requests to public DNS over HTTPS / TLS protocols', + name: 'rule.description', + type: 'keyword', + }, + 'rule.id': { + category: 'rule', + description: + 'A rule ID that is unique within the scope of an agent, observer, or other entity using the rule for detection of this event.', + example: 101, + name: 'rule.id', + type: 'keyword', + }, + 'rule.license': { + category: 'rule', + description: + 'Name of the license under which the rule used to generate this event is made available.', + example: 'Apache 2.0', + name: 'rule.license', + type: 'keyword', + }, + 'rule.name': { + category: 'rule', + description: 'The name of the rule or signature generating the event.', + example: 'BLOCK_DNS_over_TLS', + name: 'rule.name', + type: 'keyword', + }, + 'rule.reference': { + category: 'rule', + description: + "Reference URL to additional information about the rule used to generate this event. The URL can point to the vendor's documentation about the rule. If that's not available, it can also be a link to a more general page describing this type of alert.", + example: 'https://en.wikipedia.org/wiki/DNS_over_TLS', + name: 'rule.reference', + type: 'keyword', + }, + 'rule.ruleset': { + category: 'rule', + description: + 'Name of the ruleset, policy, group, or parent category in which the rule used to generate this event is a member.', + example: 'Standard_Protocol_Filters', + name: 'rule.ruleset', + type: 'keyword', + }, + 'rule.uuid': { + category: 'rule', + description: + 'A rule ID that is unique within the scope of a set or group of agents, observers, or other entities using the rule for detection of this event.', + example: 1100110011, + name: 'rule.uuid', + type: 'keyword', + }, + 'rule.version': { + category: 'rule', + description: 'The version / revision of the rule being used for analysis.', + example: 1.1, + name: 'rule.version', + type: 'keyword', + }, + 'server.address': { + category: 'server', + description: + 'Some event server addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + name: 'server.address', + type: 'keyword', + }, + 'server.as.number': { + category: 'server', + description: + 'Unique number allocated to the autonomous system. The autonomous system number (ASN) uniquely identifies each network on the Internet.', + example: 15169, + name: 'server.as.number', + type: 'long', + }, + 'server.as.organization.name': { + category: 'server', + description: 'Organization name.', + example: 'Google LLC', + name: 'server.as.organization.name', + type: 'keyword', + }, + 'server.bytes': { + category: 'server', + description: 'Bytes sent from the server to the client.', + example: 184, + name: 'server.bytes', + type: 'long', + format: 'bytes', + }, + 'server.domain': { + category: 'server', + description: 'Server domain.', + name: 'server.domain', + type: 'keyword', + }, + 'server.geo.city_name': { + category: 'server', + description: 'City name.', + example: 'Montreal', + name: 'server.geo.city_name', + type: 'keyword', + }, + 'server.geo.continent_name': { + category: 'server', + description: 'Name of the continent.', + example: 'North America', + name: 'server.geo.continent_name', + type: 'keyword', + }, + 'server.geo.country_iso_code': { + category: 'server', + description: 'Country ISO code.', + example: 'CA', + name: 'server.geo.country_iso_code', + type: 'keyword', + }, + 'server.geo.country_name': { + category: 'server', + description: 'Country name.', + example: 'Canada', + name: 'server.geo.country_name', + type: 'keyword', + }, + 'server.geo.location': { + category: 'server', + description: 'Longitude and latitude.', + example: '{ "lon": -73.614830, "lat": 45.505918 }', + name: 'server.geo.location', + type: 'geo_point', + }, + 'server.geo.name': { + category: 'server', + description: + 'User-defined description of a location, at the level of granularity they care about. Could be the name of their data centers, the floor number, if this describes a local physical entity, city names. Not typically used in automated geolocation.', + example: 'boston-dc', + name: 'server.geo.name', + type: 'keyword', + }, + 'server.geo.region_iso_code': { + category: 'server', + description: 'Region ISO code.', + example: 'CA-QC', + name: 'server.geo.region_iso_code', + type: 'keyword', + }, + 'server.geo.region_name': { + category: 'server', + description: 'Region name.', + example: 'Quebec', + name: 'server.geo.region_name', + type: 'keyword', + }, + 'server.ip': { + category: 'server', + description: 'IP address of the server. Can be one or multiple IPv4 or IPv6 addresses.', + name: 'server.ip', + type: 'ip', + }, + 'server.mac': { + category: 'server', + description: 'MAC address of the server.', + name: 'server.mac', + type: 'keyword', + }, + 'server.nat.ip': { + category: 'server', + description: + 'Translated ip of destination based NAT sessions (e.g. internet to private DMZ) Typically used with load balancers, firewalls, or routers.', + name: 'server.nat.ip', + type: 'ip', + }, + 'server.nat.port': { + category: 'server', + description: + 'Translated port of destination based NAT sessions (e.g. internet to private DMZ) Typically used with load balancers, firewalls, or routers.', + name: 'server.nat.port', + type: 'long', + format: 'string', + }, + 'server.packets': { + category: 'server', + description: 'Packets sent from the server to the client.', + example: 12, + name: 'server.packets', + type: 'long', + }, + 'server.port': { + category: 'server', + description: 'Port of the server.', + name: 'server.port', + type: 'long', + format: 'string', + }, + 'server.registered_domain': { + category: 'server', + description: + 'The highest registered server domain, stripped of the subdomain. For example, the registered domain for "foo.google.com" is "google.com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last two labels will not work well for TLDs such as "co.uk".', + example: 'google.com', + name: 'server.registered_domain', + type: 'keyword', + }, + 'server.top_level_domain': { + category: 'server', + description: + 'The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for google.com is "com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last label will not work well for effective TLDs such as "co.uk".', + example: 'co.uk', + name: 'server.top_level_domain', + type: 'keyword', + }, + 'server.user.domain': { + category: 'server', + description: + 'Name of the directory the user is a member of. For example, an LDAP or Active Directory domain name.', + name: 'server.user.domain', + type: 'keyword', + }, + 'server.user.email': { + category: 'server', + description: 'User email address.', + name: 'server.user.email', + type: 'keyword', + }, + 'server.user.full_name': { + category: 'server', + description: "User's full name, if available.", + example: 'Albert Einstein', + name: 'server.user.full_name', + type: 'keyword', + }, + 'server.user.group.domain': { + category: 'server', + description: + 'Name of the directory the group is a member of. For example, an LDAP or Active Directory domain name.', + name: 'server.user.group.domain', + type: 'keyword', + }, + 'server.user.group.id': { + category: 'server', + description: 'Unique identifier for the group on the system/platform.', + name: 'server.user.group.id', + type: 'keyword', + }, + 'server.user.group.name': { + category: 'server', + description: 'Name of the group.', + name: 'server.user.group.name', + type: 'keyword', + }, + 'server.user.hash': { + category: 'server', + description: + 'Unique user hash to correlate information for a user in anonymized form. Useful if `user.id` or `user.name` contain confidential information and cannot be used.', + name: 'server.user.hash', + type: 'keyword', + }, + 'server.user.id': { + category: 'server', + description: 'Unique identifiers of the user.', + name: 'server.user.id', + type: 'keyword', + }, + 'server.user.name': { + category: 'server', + description: 'Short name or login of the user.', + example: 'albert', + name: 'server.user.name', + type: 'keyword', + }, + 'service.ephemeral_id': { + category: 'service', + description: + 'Ephemeral identifier of this service (if one exists). This id normally changes across restarts, but `service.id` does not.', + example: '8a4f500f', + name: 'service.ephemeral_id', + type: 'keyword', + }, + 'service.id': { + category: 'service', + description: + 'Unique identifier of the running service. If the service is comprised of many nodes, the `service.id` should be the same for all nodes. This id should uniquely identify the service. This makes it possible to correlate logs and metrics for one specific service, no matter which particular node emitted the event. Note that if you need to see the events from one specific host of the service, you should filter on that `host.name` or `host.id` instead.', + example: 'd37e5ebfe0ae6c4972dbe9f0174a1637bb8247f6', + name: 'service.id', + type: 'keyword', + }, + 'service.name': { + category: 'service', + description: + 'Name of the service data is collected from. The name of the service is normally user given. This allows for distributed services that run on multiple hosts to correlate the related instances based on the name. In the case of Elasticsearch the `service.name` could contain the cluster name. For Beats the `service.name` is by default a copy of the `service.type` field if no name is specified.', + example: 'elasticsearch-metrics', + name: 'service.name', + type: 'keyword', + }, + 'service.node.name': { + category: 'service', + description: + "Name of a service node. This allows for two nodes of the same service running on the same host to be differentiated. Therefore, `service.node.name` should typically be unique across nodes of a given service. In the case of Elasticsearch, the `service.node.name` could contain the unique node name within the Elasticsearch cluster. In cases where the service doesn't have the concept of a node name, the host name or container name can be used to distinguish running instances that make up this service. If those do not provide uniqueness (e.g. multiple instances of the service running on the same host) - the node name can be manually set.", + example: 'instance-0000000016', + name: 'service.node.name', + type: 'keyword', + }, + 'service.state': { + category: 'service', + description: 'Current state of the service.', + name: 'service.state', + type: 'keyword', + }, + 'service.type': { + category: 'service', + description: + 'The type of the service data is collected from. The type can be used to group and correlate logs and metrics from one service type. Example: If logs or metrics are collected from Elasticsearch, `service.type` would be `elasticsearch`.', + example: 'elasticsearch', + name: 'service.type', + type: 'keyword', + }, + 'service.version': { + category: 'service', + description: + 'Version of the service the data was collected from. This allows to look at a data set only for a specific version of a service.', + example: '3.2.4', + name: 'service.version', + type: 'keyword', + }, + 'source.address': { + category: 'source', + description: + 'Some event source addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + name: 'source.address', + type: 'keyword', + }, + 'source.as.number': { + category: 'source', + description: + 'Unique number allocated to the autonomous system. The autonomous system number (ASN) uniquely identifies each network on the Internet.', + example: 15169, + name: 'source.as.number', + type: 'long', + }, + 'source.as.organization.name': { + category: 'source', + description: 'Organization name.', + example: 'Google LLC', + name: 'source.as.organization.name', + type: 'keyword', + }, + 'source.bytes': { + category: 'source', + description: 'Bytes sent from the source to the destination.', + example: 184, + name: 'source.bytes', + type: 'long', + format: 'bytes', + }, + 'source.domain': { + category: 'source', + description: 'Source domain.', + name: 'source.domain', + type: 'keyword', + }, + 'source.geo.city_name': { + category: 'source', + description: 'City name.', + example: 'Montreal', + name: 'source.geo.city_name', + type: 'keyword', + }, + 'source.geo.continent_name': { + category: 'source', + description: 'Name of the continent.', + example: 'North America', + name: 'source.geo.continent_name', + type: 'keyword', + }, + 'source.geo.country_iso_code': { + category: 'source', + description: 'Country ISO code.', + example: 'CA', + name: 'source.geo.country_iso_code', + type: 'keyword', + }, + 'source.geo.country_name': { + category: 'source', + description: 'Country name.', + example: 'Canada', + name: 'source.geo.country_name', + type: 'keyword', + }, + 'source.geo.location': { + category: 'source', + description: 'Longitude and latitude.', + example: '{ "lon": -73.614830, "lat": 45.505918 }', + name: 'source.geo.location', + type: 'geo_point', + }, + 'source.geo.name': { + category: 'source', + description: + 'User-defined description of a location, at the level of granularity they care about. Could be the name of their data centers, the floor number, if this describes a local physical entity, city names. Not typically used in automated geolocation.', + example: 'boston-dc', + name: 'source.geo.name', + type: 'keyword', + }, + 'source.geo.region_iso_code': { + category: 'source', + description: 'Region ISO code.', + example: 'CA-QC', + name: 'source.geo.region_iso_code', + type: 'keyword', + }, + 'source.geo.region_name': { + category: 'source', + description: 'Region name.', + example: 'Quebec', + name: 'source.geo.region_name', + type: 'keyword', + }, + 'source.ip': { + category: 'source', + description: 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', + name: 'source.ip', + type: 'ip', + }, + 'source.mac': { + category: 'source', + description: 'MAC address of the source.', + name: 'source.mac', + type: 'keyword', + }, + 'source.nat.ip': { + category: 'source', + description: + 'Translated ip of source based NAT sessions (e.g. internal client to internet) Typically connections traversing load balancers, firewalls, or routers.', + name: 'source.nat.ip', + type: 'ip', + }, + 'source.nat.port': { + category: 'source', + description: + 'Translated port of source based NAT sessions. (e.g. internal client to internet) Typically used with load balancers, firewalls, or routers.', + name: 'source.nat.port', + type: 'long', + format: 'string', + }, + 'source.packets': { + category: 'source', + description: 'Packets sent from the source to the destination.', + example: 12, + name: 'source.packets', + type: 'long', + }, + 'source.port': { + category: 'source', + description: 'Port of the source.', + name: 'source.port', + type: 'long', + format: 'string', + }, + 'source.registered_domain': { + category: 'source', + description: + 'The highest registered source domain, stripped of the subdomain. For example, the registered domain for "foo.google.com" is "google.com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last two labels will not work well for TLDs such as "co.uk".', + example: 'google.com', + name: 'source.registered_domain', + type: 'keyword', + }, + 'source.top_level_domain': { + category: 'source', + description: + 'The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for google.com is "com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last label will not work well for effective TLDs such as "co.uk".', + example: 'co.uk', + name: 'source.top_level_domain', + type: 'keyword', + }, + 'source.user.domain': { + category: 'source', + description: + 'Name of the directory the user is a member of. For example, an LDAP or Active Directory domain name.', + name: 'source.user.domain', + type: 'keyword', + }, + 'source.user.email': { + category: 'source', + description: 'User email address.', + name: 'source.user.email', + type: 'keyword', + }, + 'source.user.full_name': { + category: 'source', + description: "User's full name, if available.", + example: 'Albert Einstein', + name: 'source.user.full_name', + type: 'keyword', + }, + 'source.user.group.domain': { + category: 'source', + description: + 'Name of the directory the group is a member of. For example, an LDAP or Active Directory domain name.', + name: 'source.user.group.domain', + type: 'keyword', + }, + 'source.user.group.id': { + category: 'source', + description: 'Unique identifier for the group on the system/platform.', + name: 'source.user.group.id', + type: 'keyword', + }, + 'source.user.group.name': { + category: 'source', + description: 'Name of the group.', + name: 'source.user.group.name', + type: 'keyword', + }, + 'source.user.hash': { + category: 'source', + description: + 'Unique user hash to correlate information for a user in anonymized form. Useful if `user.id` or `user.name` contain confidential information and cannot be used.', + name: 'source.user.hash', + type: 'keyword', + }, + 'source.user.id': { + category: 'source', + description: 'Unique identifiers of the user.', + name: 'source.user.id', + type: 'keyword', + }, + 'source.user.name': { + category: 'source', + description: 'Short name or login of the user.', + example: 'albert', + name: 'source.user.name', + type: 'keyword', + }, + 'threat.framework': { + category: 'threat', + description: + 'Name of the threat framework used to further categorize and classify the tactic and technique of the reported threat. Framework classification can be provided by detecting systems, evaluated at ingest time, or retrospectively tagged to events.', + example: 'MITRE ATT&CK', + name: 'threat.framework', + type: 'keyword', + }, + 'threat.tactic.id': { + category: 'threat', + description: + 'The id of tactic used by this threat. You can use the Mitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/tactics/TA0040/ )', + example: 'TA0040', + name: 'threat.tactic.id', + type: 'keyword', + }, + 'threat.tactic.name': { + category: 'threat', + description: + 'Name of the type of tactic used by this threat. You can use the Mitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/tactics/TA0040/ )', + example: 'impact', + name: 'threat.tactic.name', + type: 'keyword', + }, + 'threat.tactic.reference': { + category: 'threat', + description: + 'The reference url of tactic used by this threat. You can use the Mitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/tactics/TA0040/ )', + example: 'https://attack.mitre.org/tactics/TA0040/', + name: 'threat.tactic.reference', + type: 'keyword', + }, + 'threat.technique.id': { + category: 'threat', + description: + 'The id of technique used by this tactic. You can use the Mitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/techniques/T1499/ )', + example: 'T1499', + name: 'threat.technique.id', + type: 'keyword', + }, + 'threat.technique.name': { + category: 'threat', + description: + 'The name of technique used by this tactic. You can use the Mitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/techniques/T1499/ )', + example: 'endpoint denial of service', + name: 'threat.technique.name', + type: 'keyword', + }, + 'threat.technique.reference': { + category: 'threat', + description: + 'The reference url of technique used by this tactic. You can use the Mitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/techniques/T1499/ )', + example: 'https://attack.mitre.org/techniques/T1499/', + name: 'threat.technique.reference', + type: 'keyword', + }, + 'tls.cipher': { + category: 'tls', + description: 'String indicating the cipher used during the current connection.', + example: 'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256', + name: 'tls.cipher', + type: 'keyword', + }, + 'tls.client.certificate': { + category: 'tls', + description: + 'PEM-encoded stand-alone certificate offered by the client. This is usually mutually-exclusive of `client.certificate_chain` since this value also exists in that list.', + example: 'MII...', + name: 'tls.client.certificate', + type: 'keyword', + }, + 'tls.client.certificate_chain': { + category: 'tls', + description: + 'Array of PEM-encoded certificates that make up the certificate chain offered by the client. This is usually mutually-exclusive of `client.certificate` since that value should be the first certificate in the chain.', + example: '["MII...","MII..."]', + name: 'tls.client.certificate_chain', + type: 'keyword', + }, + 'tls.client.hash.md5': { + category: 'tls', + description: + 'Certificate fingerprint using the MD5 digest of DER-encoded version of certificate offered by the client. For consistency with other hash values, this value should be formatted as an uppercase hash.', + example: '0F76C7F2C55BFD7D8E8B8F4BFBF0C9EC', + name: 'tls.client.hash.md5', + type: 'keyword', + }, + 'tls.client.hash.sha1': { + category: 'tls', + description: + 'Certificate fingerprint using the SHA1 digest of DER-encoded version of certificate offered by the client. For consistency with other hash values, this value should be formatted as an uppercase hash.', + example: '9E393D93138888D288266C2D915214D1D1CCEB2A', + name: 'tls.client.hash.sha1', + type: 'keyword', + }, + 'tls.client.hash.sha256': { + category: 'tls', + description: + 'Certificate fingerprint using the SHA256 digest of DER-encoded version of certificate offered by the client. For consistency with other hash values, this value should be formatted as an uppercase hash.', + example: '0687F666A054EF17A08E2F2162EAB4CBC0D265E1D7875BE74BF3C712CA92DAF0', + name: 'tls.client.hash.sha256', + type: 'keyword', + }, + 'tls.client.issuer': { + category: 'tls', + description: + 'Distinguished name of subject of the issuer of the x.509 certificate presented by the client.', + example: 'CN=MyDomain Root CA, OU=Infrastructure Team, DC=mydomain, DC=com', + name: 'tls.client.issuer', + type: 'keyword', + }, + 'tls.client.ja3': { + category: 'tls', + description: 'A hash that identifies clients based on how they perform an SSL/TLS handshake.', + example: 'd4e5b18d6b55c71272893221c96ba240', + name: 'tls.client.ja3', + type: 'keyword', + }, + 'tls.client.not_after': { + category: 'tls', + description: 'Date/Time indicating when client certificate is no longer considered valid.', + example: '2021-01-01T00:00:00.000Z', + name: 'tls.client.not_after', + type: 'date', + }, + 'tls.client.not_before': { + category: 'tls', + description: 'Date/Time indicating when client certificate is first considered valid.', + example: '1970-01-01T00:00:00.000Z', + name: 'tls.client.not_before', + type: 'date', + }, + 'tls.client.server_name': { + category: 'tls', + description: + 'Also called an SNI, this tells the server which hostname to which the client is attempting to connect. When this value is available, it should get copied to `destination.domain`.', + example: 'www.elastic.co', + name: 'tls.client.server_name', + type: 'keyword', + }, + 'tls.client.subject': { + category: 'tls', + description: 'Distinguished name of subject of the x.509 certificate presented by the client.', + example: 'CN=myclient, OU=Documentation Team, DC=mydomain, DC=com', + name: 'tls.client.subject', + type: 'keyword', + }, + 'tls.client.supported_ciphers': { + category: 'tls', + description: 'Array of ciphers offered by the client during the client hello.', + example: + '["TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384","..."]', + name: 'tls.client.supported_ciphers', + type: 'keyword', + }, + 'tls.curve': { + category: 'tls', + description: 'String indicating the curve used for the given cipher, when applicable.', + example: 'secp256r1', + name: 'tls.curve', + type: 'keyword', + }, + 'tls.established': { + category: 'tls', + description: + 'Boolean flag indicating if the TLS negotiation was successful and transitioned to an encrypted tunnel.', + name: 'tls.established', + type: 'boolean', + }, + 'tls.next_protocol': { + category: 'tls', + description: + 'String indicating the protocol being tunneled. Per the values in the IANA registry (https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids), this string should be lower case.', + example: 'http/1.1', + name: 'tls.next_protocol', + type: 'keyword', + }, + 'tls.resumed': { + category: 'tls', + description: + 'Boolean flag indicating if this TLS connection was resumed from an existing TLS negotiation.', + name: 'tls.resumed', + type: 'boolean', + }, + 'tls.server.certificate': { + category: 'tls', + description: + 'PEM-encoded stand-alone certificate offered by the server. This is usually mutually-exclusive of `server.certificate_chain` since this value also exists in that list.', + example: 'MII...', + name: 'tls.server.certificate', + type: 'keyword', + }, + 'tls.server.certificate_chain': { + category: 'tls', + description: + 'Array of PEM-encoded certificates that make up the certificate chain offered by the server. This is usually mutually-exclusive of `server.certificate` since that value should be the first certificate in the chain.', + example: '["MII...","MII..."]', + name: 'tls.server.certificate_chain', + type: 'keyword', + }, + 'tls.server.hash.md5': { + category: 'tls', + description: + 'Certificate fingerprint using the MD5 digest of DER-encoded version of certificate offered by the server. For consistency with other hash values, this value should be formatted as an uppercase hash.', + example: '0F76C7F2C55BFD7D8E8B8F4BFBF0C9EC', + name: 'tls.server.hash.md5', + type: 'keyword', + }, + 'tls.server.hash.sha1': { + category: 'tls', + description: + 'Certificate fingerprint using the SHA1 digest of DER-encoded version of certificate offered by the server. For consistency with other hash values, this value should be formatted as an uppercase hash.', + example: '9E393D93138888D288266C2D915214D1D1CCEB2A', + name: 'tls.server.hash.sha1', + type: 'keyword', + }, + 'tls.server.hash.sha256': { + category: 'tls', + description: + 'Certificate fingerprint using the SHA256 digest of DER-encoded version of certificate offered by the server. For consistency with other hash values, this value should be formatted as an uppercase hash.', + example: '0687F666A054EF17A08E2F2162EAB4CBC0D265E1D7875BE74BF3C712CA92DAF0', + name: 'tls.server.hash.sha256', + type: 'keyword', + }, + 'tls.server.issuer': { + category: 'tls', + description: 'Subject of the issuer of the x.509 certificate presented by the server.', + example: 'CN=MyDomain Root CA, OU=Infrastructure Team, DC=mydomain, DC=com', + name: 'tls.server.issuer', + type: 'keyword', + }, + 'tls.server.ja3s': { + category: 'tls', + description: 'A hash that identifies servers based on how they perform an SSL/TLS handshake.', + example: '394441ab65754e2207b1e1b457b3641d', + name: 'tls.server.ja3s', + type: 'keyword', + }, + 'tls.server.not_after': { + category: 'tls', + description: 'Timestamp indicating when server certificate is no longer considered valid.', + example: '2021-01-01T00:00:00.000Z', + name: 'tls.server.not_after', + type: 'date', + }, + 'tls.server.not_before': { + category: 'tls', + description: 'Timestamp indicating when server certificate is first considered valid.', + example: '1970-01-01T00:00:00.000Z', + name: 'tls.server.not_before', + type: 'date', + }, + 'tls.server.subject': { + category: 'tls', + description: 'Subject of the x.509 certificate presented by the server.', + example: 'CN=www.mydomain.com, OU=Infrastructure Team, DC=mydomain, DC=com', + name: 'tls.server.subject', + type: 'keyword', + }, + 'tls.version': { + category: 'tls', + description: 'Numeric part of the version parsed from the original string.', + example: '1.2', + name: 'tls.version', + type: 'keyword', + }, + 'tls.version_protocol': { + category: 'tls', + description: 'Normalized lowercase protocol name parsed from original string.', + example: 'tls', + name: 'tls.version_protocol', + type: 'keyword', + }, + 'tracing.trace.id': { + category: 'tracing', + description: + 'Unique identifier of the trace. A trace groups multiple events like transactions that belong together. For example, a user request handled by multiple inter-connected services.', + example: '4bf92f3577b34da6a3ce929d0e0e4736', + name: 'tracing.trace.id', + type: 'keyword', + }, + 'tracing.transaction.id': { + category: 'tracing', + description: + 'Unique identifier of the transaction. A transaction is the highest level of work measured within a service, such as a request to a server.', + example: '00f067aa0ba902b7', + name: 'tracing.transaction.id', + type: 'keyword', + }, + 'url.domain': { + category: 'url', + description: + 'Domain of the url, such as "www.elastic.co". In some cases a URL may refer to an IP and/or port directly, without a domain name. In this case, the IP address would go to the `domain` field.', + example: 'www.elastic.co', + name: 'url.domain', + type: 'keyword', + }, + 'url.extension': { + category: 'url', + description: + 'The field contains the file extension from the original request url. The file extension is only set if it exists, as not every url has a file extension. The leading period must not be included. For example, the value must be "png", not ".png".', + example: 'png', + name: 'url.extension', + type: 'keyword', + }, + 'url.fragment': { + category: 'url', + description: + 'Portion of the url after the `#`, such as "top". The `#` is not part of the fragment.', + name: 'url.fragment', + type: 'keyword', + }, + 'url.full': { + category: 'url', + description: + 'If full URLs are important to your use case, they should be stored in `url.full`, whether this field is reconstructed or present in the event source.', + example: 'https://www.elastic.co:443/search?q=elasticsearch#top', + name: 'url.full', + type: 'keyword', + }, + 'url.original': { + category: 'url', + description: + 'Unmodified original url as seen in the event source. Note that in network monitoring, the observed URL may be a full URL, whereas in access logs, the URL is often just represented as a path. This field is meant to represent the URL as it was observed, complete or not.', + example: 'https://www.elastic.co:443/search?q=elasticsearch#top or /search?q=elasticsearch', + name: 'url.original', + type: 'keyword', + }, + 'url.password': { + category: 'url', + description: 'Password of the request.', + name: 'url.password', + type: 'keyword', + }, + 'url.path': { + category: 'url', + description: 'Path of the request, such as "/search".', + name: 'url.path', + type: 'keyword', + }, + 'url.port': { + category: 'url', + description: 'Port of the request, such as 443.', + example: 443, + name: 'url.port', + type: 'long', + format: 'string', + }, + 'url.query': { + category: 'url', + description: + 'The query field describes the query string of the request, such as "q=elasticsearch". The `?` is excluded from the query string. If a URL contains no `?`, there is no query field. If there is a `?` but no query, the query field exists with an empty string. The `exists` query can be used to differentiate between the two cases.', + name: 'url.query', + type: 'keyword', + }, + 'url.registered_domain': { + category: 'url', + description: + 'The highest registered url domain, stripped of the subdomain. For example, the registered domain for "foo.google.com" is "google.com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last two labels will not work well for TLDs such as "co.uk".', + example: 'google.com', + name: 'url.registered_domain', + type: 'keyword', + }, + 'url.scheme': { + category: 'url', + description: 'Scheme of the request, such as "https". Note: The `:` is not part of the scheme.', + example: 'https', + name: 'url.scheme', + type: 'keyword', + }, + 'url.top_level_domain': { + category: 'url', + description: + 'The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for google.com is "com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last label will not work well for effective TLDs such as "co.uk".', + example: 'co.uk', + name: 'url.top_level_domain', + type: 'keyword', + }, + 'url.username': { + category: 'url', + description: 'Username of the request.', + name: 'url.username', + type: 'keyword', + }, + 'user.domain': { + category: 'user', + description: + 'Name of the directory the user is a member of. For example, an LDAP or Active Directory domain name.', + name: 'user.domain', + type: 'keyword', + }, + 'user.email': { + category: 'user', + description: 'User email address.', + name: 'user.email', + type: 'keyword', + }, + 'user.full_name': { + category: 'user', + description: "User's full name, if available.", + example: 'Albert Einstein', + name: 'user.full_name', + type: 'keyword', + }, + 'user.group.domain': { + category: 'user', + description: + 'Name of the directory the group is a member of. For example, an LDAP or Active Directory domain name.', + name: 'user.group.domain', + type: 'keyword', + }, + 'user.group.id': { + category: 'user', + description: 'Unique identifier for the group on the system/platform.', + name: 'user.group.id', + type: 'keyword', + }, + 'user.group.name': { + category: 'user', + description: 'Name of the group.', + name: 'user.group.name', + type: 'keyword', + }, + 'user.hash': { + category: 'user', + description: + 'Unique user hash to correlate information for a user in anonymized form. Useful if `user.id` or `user.name` contain confidential information and cannot be used.', + name: 'user.hash', + type: 'keyword', + }, + 'user.id': { + category: 'user', + description: 'Unique identifiers of the user.', + name: 'user.id', + type: 'keyword', + }, + 'user.name': { + category: 'user', + description: 'Short name or login of the user.', + example: 'albert', + name: 'user.name', + type: 'keyword', + }, + 'user_agent.device.name': { + category: 'user_agent', + description: 'Name of the device.', + example: 'iPhone', + name: 'user_agent.device.name', + type: 'keyword', + }, + 'user_agent.name': { + category: 'user_agent', + description: 'Name of the user agent.', + example: 'Safari', + name: 'user_agent.name', + type: 'keyword', + }, + 'user_agent.original': { + category: 'user_agent', + description: 'Unparsed user_agent string.', + example: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1', + name: 'user_agent.original', + type: 'keyword', + }, + 'user_agent.os.family': { + category: 'user_agent', + description: 'OS family (such as redhat, debian, freebsd, windows).', + example: 'debian', + name: 'user_agent.os.family', + type: 'keyword', + }, + 'user_agent.os.full': { + category: 'user_agent', + description: 'Operating system name, including the version or code name.', + example: 'Mac OS Mojave', + name: 'user_agent.os.full', + type: 'keyword', + }, + 'user_agent.os.kernel': { + category: 'user_agent', + description: 'Operating system kernel version as a raw string.', + example: '4.4.0-112-generic', + name: 'user_agent.os.kernel', + type: 'keyword', + }, + 'user_agent.os.name': { + category: 'user_agent', + description: 'Operating system name, without the version.', + example: 'Mac OS X', + name: 'user_agent.os.name', + type: 'keyword', + }, + 'user_agent.os.platform': { + category: 'user_agent', + description: 'Operating system platform (such centos, ubuntu, windows).', + example: 'darwin', + name: 'user_agent.os.platform', + type: 'keyword', + }, + 'user_agent.os.version': { + category: 'user_agent', + description: 'Operating system version as a raw string.', + example: '10.14.1', + name: 'user_agent.os.version', + type: 'keyword', + }, + 'user_agent.version': { + category: 'user_agent', + description: 'Version of the user agent.', + example: 12, + name: 'user_agent.version', + type: 'keyword', + }, + 'vlan.id': { + category: 'vlan', + description: 'VLAN ID as reported by the observer.', + example: 10, + name: 'vlan.id', + type: 'keyword', + }, + 'vlan.name': { + category: 'vlan', + description: 'Optional VLAN name as reported by the observer.', + example: 'outside', + name: 'vlan.name', + type: 'keyword', + }, + 'vulnerability.category': { + category: 'vulnerability', + description: + 'The type of system or architecture that the vulnerability affects. These may be platform-specific (for example, Debian or SUSE) or general (for example, Database or Firewall). For example (https://qualysguard.qualys.com/qwebhelp/fo_portal/knowledgebase/vulnerability_categories.htm[Qualys vulnerability categories]) This field must be an array.', + example: '["Firewall"]', + name: 'vulnerability.category', + type: 'keyword', + }, + 'vulnerability.classification': { + category: 'vulnerability', + description: + 'The classification of the vulnerability scoring system. For example (https://www.first.org/cvss/)', + example: 'CVSS', + name: 'vulnerability.classification', + type: 'keyword', + }, + 'vulnerability.description': { + category: 'vulnerability', + description: + 'The description of the vulnerability that provides additional context of the vulnerability. For example (https://cve.mitre.org/about/faqs.html#cve_entry_descriptions_created[Common Vulnerabilities and Exposure CVE description])', + example: 'In macOS before 2.12.6, there is a vulnerability in the RPC...', + name: 'vulnerability.description', + type: 'keyword', + }, + 'vulnerability.enumeration': { + category: 'vulnerability', + description: + 'The type of identifier used for this vulnerability. For example (https://cve.mitre.org/about/)', + example: 'CVE', + name: 'vulnerability.enumeration', + type: 'keyword', + }, + 'vulnerability.id': { + category: 'vulnerability', + description: + 'The identification (ID) is the number portion of a vulnerability entry. It includes a unique identification number for the vulnerability. For example (https://cve.mitre.org/about/faqs.html#what_is_cve_id)[Common Vulnerabilities and Exposure CVE ID]', + example: 'CVE-2019-00001', + name: 'vulnerability.id', + type: 'keyword', + }, + 'vulnerability.reference': { + category: 'vulnerability', + description: + 'A resource that provides additional information, context, and mitigations for the identified vulnerability.', + example: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-6111', + name: 'vulnerability.reference', + type: 'keyword', + }, + 'vulnerability.report_id': { + category: 'vulnerability', + description: 'The report or scan identification number.', + example: 20191018.0001, + name: 'vulnerability.report_id', + type: 'keyword', + }, + 'vulnerability.scanner.vendor': { + category: 'vulnerability', + description: 'The name of the vulnerability scanner vendor.', + example: 'Tenable', + name: 'vulnerability.scanner.vendor', + type: 'keyword', + }, + 'vulnerability.score.base': { + category: 'vulnerability', + description: + 'Scores can range from 0.0 to 10.0, with 10.0 being the most severe. Base scores cover an assessment for exploitability metrics (attack vector, complexity, privileges, and user interaction), impact metrics (confidentiality, integrity, and availability), and scope. For example (https://www.first.org/cvss/specification-document)', + example: 5.5, + name: 'vulnerability.score.base', + type: 'float', + }, + 'vulnerability.score.environmental': { + category: 'vulnerability', + description: + 'Scores can range from 0.0 to 10.0, with 10.0 being the most severe. Environmental scores cover an assessment for any modified Base metrics, confidentiality, integrity, and availability requirements. For example (https://www.first.org/cvss/specification-document)', + example: 5.5, + name: 'vulnerability.score.environmental', + type: 'float', + }, + 'vulnerability.score.temporal': { + category: 'vulnerability', + description: + 'Scores can range from 0.0 to 10.0, with 10.0 being the most severe. Temporal scores cover an assessment for code maturity, remediation level, and confidence. For example (https://www.first.org/cvss/specification-document)', + name: 'vulnerability.score.temporal', + type: 'float', + }, + 'vulnerability.score.version': { + category: 'vulnerability', + description: + 'The National Vulnerability Database (NVD) provides qualitative severity rankings of "Low", "Medium", and "High" for CVSS v2.0 base score ranges in addition to the severity ratings for CVSS v3.0 as they are defined in the CVSS v3.0 specification. CVSS is owned and managed by FIRST.Org, Inc. (FIRST), a US-based non-profit organization, whose mission is to help computer security incident response teams across the world. For example (https://nvd.nist.gov/vuln-metrics/cvss)', + example: 2, + name: 'vulnerability.score.version', + type: 'keyword', + }, + 'vulnerability.severity': { + category: 'vulnerability', + description: + 'The severity of the vulnerability can help with metrics and internal prioritization regarding remediation. For example (https://nvd.nist.gov/vuln-metrics/cvss)', + example: 'Critical', + name: 'vulnerability.severity', + type: 'keyword', + }, + 'agent.hostname': { + category: 'agent', + description: + 'Deprecated - use agent.name or agent.id to identify an agent. Hostname of the agent. ', + name: 'agent.hostname', + type: 'keyword', + }, + 'beat.timezone': { + category: 'beat', + name: 'beat.timezone', + type: 'alias', + }, + fields: { + category: 'base', + description: 'Contains user configurable fields. ', + name: 'fields', + type: 'object', + }, + 'beat.name': { + category: 'beat', + name: 'beat.name', + type: 'alias', + }, + 'beat.hostname': { + category: 'beat', + name: 'beat.hostname', + type: 'alias', + }, + 'timeseries.instance': { + category: 'timeseries', + description: 'Time series instance id', + name: 'timeseries.instance', + type: 'keyword', + }, + 'cloud.project.id': { + category: 'cloud', + description: 'Name of the project in Google Cloud. ', + example: 'project-x', + name: 'cloud.project.id', + }, + 'cloud.image.id': { + category: 'cloud', + description: 'Image ID for the cloud instance. ', + example: 'ami-abcd1234', + name: 'cloud.image.id', + }, + 'meta.cloud.provider': { + category: 'meta', + name: 'meta.cloud.provider', + type: 'alias', + }, + 'meta.cloud.instance_id': { + category: 'meta', + name: 'meta.cloud.instance_id', + type: 'alias', + }, + 'meta.cloud.instance_name': { + category: 'meta', + name: 'meta.cloud.instance_name', + type: 'alias', + }, + 'meta.cloud.machine_type': { + category: 'meta', + name: 'meta.cloud.machine_type', + type: 'alias', + }, + 'meta.cloud.availability_zone': { + category: 'meta', + name: 'meta.cloud.availability_zone', + type: 'alias', + }, + 'meta.cloud.project_id': { + category: 'meta', + name: 'meta.cloud.project_id', + type: 'alias', + }, + 'meta.cloud.region': { + category: 'meta', + name: 'meta.cloud.region', + type: 'alias', + }, + 'docker.container.id': { + category: 'docker', + name: 'docker.container.id', + type: 'alias', + }, + 'docker.container.image': { + category: 'docker', + name: 'docker.container.image', + type: 'alias', + }, + 'docker.container.name': { + category: 'docker', + name: 'docker.container.name', + type: 'alias', + }, + 'docker.container.labels': { + category: 'docker', + description: 'Image labels. ', + name: 'docker.container.labels', + type: 'object', + }, + 'host.containerized': { + category: 'host', + description: 'If the host is a container. ', + name: 'host.containerized', + type: 'boolean', + }, + 'host.os.build': { + category: 'host', + description: 'OS build information. ', + example: '18D109', + name: 'host.os.build', + type: 'keyword', + }, + 'host.os.codename': { + category: 'host', + description: 'OS codename, if any. ', + example: 'stretch', + name: 'host.os.codename', + type: 'keyword', + }, + 'kubernetes.pod.name': { + category: 'kubernetes', + description: 'Kubernetes pod name ', + name: 'kubernetes.pod.name', + type: 'keyword', + }, + 'kubernetes.pod.uid': { + category: 'kubernetes', + description: 'Kubernetes Pod UID ', + name: 'kubernetes.pod.uid', + type: 'keyword', + }, + 'kubernetes.namespace': { + category: 'kubernetes', + description: 'Kubernetes namespace ', + name: 'kubernetes.namespace', + type: 'keyword', + }, + 'kubernetes.node.name': { + category: 'kubernetes', + description: 'Kubernetes node name ', + name: 'kubernetes.node.name', + type: 'keyword', + }, + 'kubernetes.labels.*': { + category: 'kubernetes', + description: 'Kubernetes labels map ', + name: 'kubernetes.labels.*', + type: 'object', + }, + 'kubernetes.annotations.*': { + category: 'kubernetes', + description: 'Kubernetes annotations map ', + name: 'kubernetes.annotations.*', + type: 'object', + }, + 'kubernetes.replicaset.name': { + category: 'kubernetes', + description: 'Kubernetes replicaset name ', + name: 'kubernetes.replicaset.name', + type: 'keyword', + }, + 'kubernetes.deployment.name': { + category: 'kubernetes', + description: 'Kubernetes deployment name ', + name: 'kubernetes.deployment.name', + type: 'keyword', + }, + 'kubernetes.statefulset.name': { + category: 'kubernetes', + description: 'Kubernetes statefulset name ', + name: 'kubernetes.statefulset.name', + type: 'keyword', + }, + 'kubernetes.container.name': { + category: 'kubernetes', + description: 'Kubernetes container name ', + name: 'kubernetes.container.name', + type: 'keyword', + }, + 'kubernetes.container.image': { + category: 'kubernetes', + description: 'Kubernetes container image ', + name: 'kubernetes.container.image', + type: 'keyword', + }, + 'process.exe': { + category: 'process', + name: 'process.exe', + type: 'alias', + }, + 'jolokia.agent.version': { + category: 'jolokia', + description: 'Version number of jolokia agent. ', + name: 'jolokia.agent.version', + type: 'keyword', + }, + 'jolokia.agent.id': { + category: 'jolokia', + description: + 'Each agent has a unique id which can be either provided during startup of the agent in form of a configuration parameter or being autodetected. If autodected, the id has several parts: The IP, the process id, hashcode of the agent and its type. ', + name: 'jolokia.agent.id', + type: 'keyword', + }, + 'jolokia.server.product': { + category: 'jolokia', + description: 'The container product if detected. ', + name: 'jolokia.server.product', + type: 'keyword', + }, + 'jolokia.server.version': { + category: 'jolokia', + description: "The container's version (if detected). ", + name: 'jolokia.server.version', + type: 'keyword', + }, + 'jolokia.server.vendor': { + category: 'jolokia', + description: 'The vendor of the container the agent is running in. ', + name: 'jolokia.server.vendor', + type: 'keyword', + }, + 'jolokia.url': { + category: 'jolokia', + description: 'The URL how this agent can be contacted. ', + name: 'jolokia.url', + type: 'keyword', + }, + 'jolokia.secured': { + category: 'jolokia', + description: 'Whether the agent was configured for authentication or not. ', + name: 'jolokia.secured', + type: 'boolean', + }, + 'file.setuid': { + category: 'file', + description: 'Set if the file has the `setuid` bit set. Omitted otherwise.', + example: 'true', + name: 'file.setuid', + type: 'boolean', + }, + 'file.setgid': { + category: 'file', + description: 'Set if the file has the `setgid` bit set. Omitted otherwise.', + example: 'true', + name: 'file.setgid', + type: 'boolean', + }, + 'file.origin': { + category: 'file', + description: + 'An array of strings describing a possible external origin for this file. For example, the URL it was downloaded from. Only supported in macOS, via the kMDItemWhereFroms attribute. Omitted if origin information is not available. ', + name: 'file.origin', + type: 'keyword', + }, + 'file.selinux.user': { + category: 'file', + description: 'The owner of the object.', + name: 'file.selinux.user', + type: 'keyword', + }, + 'file.selinux.role': { + category: 'file', + description: "The object's SELinux role.", + name: 'file.selinux.role', + type: 'keyword', + }, + 'file.selinux.domain': { + category: 'file', + description: "The object's SELinux domain or type.", + name: 'file.selinux.domain', + type: 'keyword', + }, + 'file.selinux.level': { + category: 'file', + description: "The object's SELinux level.", + example: 's0', + name: 'file.selinux.level', + type: 'keyword', + }, + 'user.audit.id': { + category: 'user', + description: 'Audit user ID.', + name: 'user.audit.id', + type: 'keyword', + }, + 'user.audit.name': { + category: 'user', + description: 'Audit user name.', + name: 'user.audit.name', + type: 'keyword', + }, + 'user.effective.id': { + category: 'user', + description: 'Effective user ID.', + name: 'user.effective.id', + type: 'keyword', + }, + 'user.effective.name': { + category: 'user', + description: 'Effective user name.', + name: 'user.effective.name', + type: 'keyword', + }, + 'user.effective.group.id': { + category: 'user', + description: 'Effective group ID.', + name: 'user.effective.group.id', + type: 'keyword', + }, + 'user.effective.group.name': { + category: 'user', + description: 'Effective group name.', + name: 'user.effective.group.name', + type: 'keyword', + }, + 'user.filesystem.id': { + category: 'user', + description: 'Filesystem user ID.', + name: 'user.filesystem.id', + type: 'keyword', + }, + 'user.filesystem.name': { + category: 'user', + description: 'Filesystem user name.', + name: 'user.filesystem.name', + type: 'keyword', + }, + 'user.filesystem.group.id': { + category: 'user', + description: 'Filesystem group ID.', + name: 'user.filesystem.group.id', + type: 'keyword', + }, + 'user.filesystem.group.name': { + category: 'user', + description: 'Filesystem group name.', + name: 'user.filesystem.group.name', + type: 'keyword', + }, + 'user.saved.id': { + category: 'user', + description: 'Saved user ID.', + name: 'user.saved.id', + type: 'keyword', + }, + 'user.saved.name': { + category: 'user', + description: 'Saved user name.', + name: 'user.saved.name', + type: 'keyword', + }, + 'user.saved.group.id': { + category: 'user', + description: 'Saved group ID.', + name: 'user.saved.group.id', + type: 'keyword', + }, + 'user.saved.group.name': { + category: 'user', + description: 'Saved group name.', + name: 'user.saved.group.name', + type: 'keyword', + }, + 'user.auid': { + category: 'user', + name: 'user.auid', + type: 'alias', + }, + 'user.uid': { + category: 'user', + name: 'user.uid', + type: 'alias', + }, + 'user.euid': { + category: 'user', + name: 'user.euid', + type: 'alias', + }, + 'user.fsuid': { + category: 'user', + name: 'user.fsuid', + type: 'alias', + }, + 'user.suid': { + category: 'user', + name: 'user.suid', + type: 'alias', + }, + 'user.gid': { + category: 'user', + name: 'user.gid', + type: 'alias', + }, + 'user.egid': { + category: 'user', + name: 'user.egid', + type: 'alias', + }, + 'user.sgid': { + category: 'user', + name: 'user.sgid', + type: 'alias', + }, + 'user.fsgid': { + category: 'user', + name: 'user.fsgid', + type: 'alias', + }, + 'user.name_map.auid': { + category: 'user', + name: 'user.name_map.auid', + type: 'alias', + }, + 'user.name_map.uid': { + category: 'user', + name: 'user.name_map.uid', + type: 'alias', + }, + 'user.name_map.euid': { + category: 'user', + name: 'user.name_map.euid', + type: 'alias', + }, + 'user.name_map.fsuid': { + category: 'user', + name: 'user.name_map.fsuid', + type: 'alias', + }, + 'user.name_map.suid': { + category: 'user', + name: 'user.name_map.suid', + type: 'alias', + }, + 'user.name_map.gid': { + category: 'user', + name: 'user.name_map.gid', + type: 'alias', + }, + 'user.name_map.egid': { + category: 'user', + name: 'user.name_map.egid', + type: 'alias', + }, + 'user.name_map.sgid': { + category: 'user', + name: 'user.name_map.sgid', + type: 'alias', + }, + 'user.name_map.fsgid': { + category: 'user', + name: 'user.name_map.fsgid', + type: 'alias', + }, + 'user.selinux.user': { + category: 'user', + description: 'account submitted for authentication', + name: 'user.selinux.user', + type: 'keyword', + }, + 'user.selinux.role': { + category: 'user', + description: "user's SELinux role", + name: 'user.selinux.role', + type: 'keyword', + }, + 'user.selinux.domain': { + category: 'user', + description: "The actor's SELinux domain or type.", + name: 'user.selinux.domain', + type: 'keyword', + }, + 'user.selinux.level': { + category: 'user', + description: "The actor's SELinux level.", + example: 's0', + name: 'user.selinux.level', + type: 'keyword', + }, + 'user.selinux.category': { + category: 'user', + description: "The actor's SELinux category or compartments.", + name: 'user.selinux.category', + type: 'keyword', + }, + 'process.cwd': { + category: 'process', + description: 'The current working directory.', + name: 'process.cwd', + type: 'alias', + }, + 'source.path': { + category: 'source', + description: 'This is the path associated with a unix socket.', + name: 'source.path', + type: 'keyword', + }, + 'destination.path': { + category: 'destination', + description: 'This is the path associated with a unix socket.', + name: 'destination.path', + type: 'keyword', + }, + 'auditd.message_type': { + category: 'auditd', + description: 'The audit message type (e.g. syscall or apparmor_denied). ', + example: 'syscall', + name: 'auditd.message_type', + type: 'keyword', + }, + 'auditd.sequence': { + category: 'auditd', + description: + 'The sequence number of the event as assigned by the kernel. Sequence numbers are stored as a uint32 in the kernel and can rollover. ', + name: 'auditd.sequence', + type: 'long', + }, + 'auditd.session': { + category: 'auditd', + description: + 'The session ID assigned to a login. All events related to a login session will have the same value. ', + name: 'auditd.session', + type: 'keyword', + }, + 'auditd.result': { + category: 'auditd', + description: 'The result of the audited operation (success/fail).', + example: 'success or fail', + name: 'auditd.result', + type: 'keyword', + }, + 'auditd.summary.actor.primary': { + category: 'auditd', + description: + "The primary identity of the actor. This is the actor's original login ID. It will not change even if the user changes to another account. ", + name: 'auditd.summary.actor.primary', + type: 'keyword', + }, + 'auditd.summary.actor.secondary': { + category: 'auditd', + description: + 'The secondary identity of the actor. This is typically the same as the primary, except for when the user has used `su`.', + name: 'auditd.summary.actor.secondary', + type: 'keyword', + }, + 'auditd.summary.object.type': { + category: 'auditd', + description: 'A description of the what the "thing" is (e.g. file, socket, user-session). ', + name: 'auditd.summary.object.type', + type: 'keyword', + }, + 'auditd.summary.object.primary': { + category: 'auditd', + description: '', + name: 'auditd.summary.object.primary', + type: 'keyword', + }, + 'auditd.summary.object.secondary': { + category: 'auditd', + description: '', + name: 'auditd.summary.object.secondary', + type: 'keyword', + }, + 'auditd.summary.how': { + category: 'auditd', + description: + 'This describes how the action was performed. Usually this is the exe or command that was being executed that triggered the event. ', + name: 'auditd.summary.how', + type: 'keyword', + }, + 'auditd.paths.inode': { + category: 'auditd', + description: 'inode number', + name: 'auditd.paths.inode', + type: 'keyword', + }, + 'auditd.paths.dev': { + category: 'auditd', + description: 'device name as found in /dev', + name: 'auditd.paths.dev', + type: 'keyword', + }, + 'auditd.paths.obj_user': { + category: 'auditd', + description: '', + name: 'auditd.paths.obj_user', + type: 'keyword', + }, + 'auditd.paths.obj_role': { + category: 'auditd', + description: '', + name: 'auditd.paths.obj_role', + type: 'keyword', + }, + 'auditd.paths.obj_domain': { + category: 'auditd', + description: '', + name: 'auditd.paths.obj_domain', + type: 'keyword', + }, + 'auditd.paths.obj_level': { + category: 'auditd', + description: '', + name: 'auditd.paths.obj_level', + type: 'keyword', + }, + 'auditd.paths.objtype': { + category: 'auditd', + description: '', + name: 'auditd.paths.objtype', + type: 'keyword', + }, + 'auditd.paths.ouid': { + category: 'auditd', + description: 'file owner user ID', + name: 'auditd.paths.ouid', + type: 'keyword', + }, + 'auditd.paths.rdev': { + category: 'auditd', + description: 'the device identifier (special files only)', + name: 'auditd.paths.rdev', + type: 'keyword', + }, + 'auditd.paths.nametype': { + category: 'auditd', + description: 'kind of file operation being referenced', + name: 'auditd.paths.nametype', + type: 'keyword', + }, + 'auditd.paths.ogid': { + category: 'auditd', + description: 'file owner group ID', + name: 'auditd.paths.ogid', + type: 'keyword', + }, + 'auditd.paths.item': { + category: 'auditd', + description: 'which item is being recorded', + name: 'auditd.paths.item', + type: 'keyword', + }, + 'auditd.paths.mode': { + category: 'auditd', + description: 'mode flags on a file', + name: 'auditd.paths.mode', + type: 'keyword', + }, + 'auditd.paths.name': { + category: 'auditd', + description: 'file name in avcs', + name: 'auditd.paths.name', + type: 'keyword', + }, + 'auditd.data.action': { + category: 'auditd', + description: 'netfilter packet disposition', + name: 'auditd.data.action', + type: 'keyword', + }, + 'auditd.data.minor': { + category: 'auditd', + description: 'device minor number', + name: 'auditd.data.minor', + type: 'keyword', + }, + 'auditd.data.acct': { + category: 'auditd', + description: "a user's account name", + name: 'auditd.data.acct', + type: 'keyword', + }, + 'auditd.data.addr': { + category: 'auditd', + description: 'the remote address that the user is connecting from', + name: 'auditd.data.addr', + type: 'keyword', + }, + 'auditd.data.cipher': { + category: 'auditd', + description: 'name of crypto cipher selected', + name: 'auditd.data.cipher', + type: 'keyword', + }, + 'auditd.data.id': { + category: 'auditd', + description: 'during account changes', + name: 'auditd.data.id', + type: 'keyword', + }, + 'auditd.data.entries': { + category: 'auditd', + description: 'number of entries in the netfilter table', + name: 'auditd.data.entries', + type: 'keyword', + }, + 'auditd.data.kind': { + category: 'auditd', + description: 'server or client in crypto operation', + name: 'auditd.data.kind', + type: 'keyword', + }, + 'auditd.data.ksize': { + category: 'auditd', + description: 'key size for crypto operation', + name: 'auditd.data.ksize', + type: 'keyword', + }, + 'auditd.data.spid': { + category: 'auditd', + description: 'sent process ID', + name: 'auditd.data.spid', + type: 'keyword', + }, + 'auditd.data.arch': { + category: 'auditd', + description: 'the elf architecture flags', + name: 'auditd.data.arch', + type: 'keyword', + }, + 'auditd.data.argc': { + category: 'auditd', + description: 'the number of arguments to an execve syscall', + name: 'auditd.data.argc', + type: 'keyword', + }, + 'auditd.data.major': { + category: 'auditd', + description: 'device major number', + name: 'auditd.data.major', + type: 'keyword', + }, + 'auditd.data.unit': { + category: 'auditd', + description: 'systemd unit', + name: 'auditd.data.unit', + type: 'keyword', + }, + 'auditd.data.table': { + category: 'auditd', + description: 'netfilter table name', + name: 'auditd.data.table', + type: 'keyword', + }, + 'auditd.data.terminal': { + category: 'auditd', + description: 'terminal name the user is running programs on', + name: 'auditd.data.terminal', + type: 'keyword', + }, + 'auditd.data.grantors': { + category: 'auditd', + description: 'pam modules approving the action', + name: 'auditd.data.grantors', + type: 'keyword', + }, + 'auditd.data.direction': { + category: 'auditd', + description: 'direction of crypto operation', + name: 'auditd.data.direction', + type: 'keyword', + }, + 'auditd.data.op': { + category: 'auditd', + description: 'the operation being performed that is audited', + name: 'auditd.data.op', + type: 'keyword', + }, + 'auditd.data.tty': { + category: 'auditd', + description: 'tty udevice the user is running programs on', + name: 'auditd.data.tty', + type: 'keyword', + }, + 'auditd.data.syscall': { + category: 'auditd', + description: 'syscall number in effect when the event occurred', + name: 'auditd.data.syscall', + type: 'keyword', + }, + 'auditd.data.data': { + category: 'auditd', + description: 'TTY text', + name: 'auditd.data.data', + type: 'keyword', + }, + 'auditd.data.family': { + category: 'auditd', + description: 'netfilter protocol', + name: 'auditd.data.family', + type: 'keyword', + }, + 'auditd.data.mac': { + category: 'auditd', + description: 'crypto MAC algorithm selected', + name: 'auditd.data.mac', + type: 'keyword', + }, + 'auditd.data.pfs': { + category: 'auditd', + description: 'perfect forward secrecy method', + name: 'auditd.data.pfs', + type: 'keyword', + }, + 'auditd.data.items': { + category: 'auditd', + description: 'the number of path records in the event', + name: 'auditd.data.items', + type: 'keyword', + }, + 'auditd.data.a0': { + category: 'auditd', + description: '', + name: 'auditd.data.a0', + type: 'keyword', + }, + 'auditd.data.a1': { + category: 'auditd', + description: '', + name: 'auditd.data.a1', + type: 'keyword', + }, + 'auditd.data.a2': { + category: 'auditd', + description: '', + name: 'auditd.data.a2', + type: 'keyword', + }, + 'auditd.data.a3': { + category: 'auditd', + description: '', + name: 'auditd.data.a3', + type: 'keyword', + }, + 'auditd.data.hostname': { + category: 'auditd', + description: 'the hostname that the user is connecting from', + name: 'auditd.data.hostname', + type: 'keyword', + }, + 'auditd.data.lport': { + category: 'auditd', + description: 'local network port', + name: 'auditd.data.lport', + type: 'keyword', + }, + 'auditd.data.rport': { + category: 'auditd', + description: 'remote port number', + name: 'auditd.data.rport', + type: 'keyword', + }, + 'auditd.data.exit': { + category: 'auditd', + description: 'syscall exit code', + name: 'auditd.data.exit', + type: 'keyword', + }, + 'auditd.data.fp': { + category: 'auditd', + description: 'crypto key finger print', + name: 'auditd.data.fp', + type: 'keyword', + }, + 'auditd.data.laddr': { + category: 'auditd', + description: 'local network address', + name: 'auditd.data.laddr', + type: 'keyword', + }, + 'auditd.data.sport': { + category: 'auditd', + description: 'local port number', + name: 'auditd.data.sport', + type: 'keyword', + }, + 'auditd.data.capability': { + category: 'auditd', + description: 'posix capabilities', + name: 'auditd.data.capability', + type: 'keyword', + }, + 'auditd.data.nargs': { + category: 'auditd', + description: 'the number of arguments to a socket call', + name: 'auditd.data.nargs', + type: 'keyword', + }, + 'auditd.data.new-enabled': { + category: 'auditd', + description: 'new TTY audit enabled setting', + name: 'auditd.data.new-enabled', + type: 'keyword', + }, + 'auditd.data.audit_backlog_limit': { + category: 'auditd', + description: "audit system's backlog queue size", + name: 'auditd.data.audit_backlog_limit', + type: 'keyword', + }, + 'auditd.data.dir': { + category: 'auditd', + description: 'directory name', + name: 'auditd.data.dir', + type: 'keyword', + }, + 'auditd.data.cap_pe': { + category: 'auditd', + description: 'process effective capability map', + name: 'auditd.data.cap_pe', + type: 'keyword', + }, + 'auditd.data.model': { + category: 'auditd', + description: 'security model being used for virt', + name: 'auditd.data.model', + type: 'keyword', + }, + 'auditd.data.new_pp': { + category: 'auditd', + description: 'new process permitted capability map', + name: 'auditd.data.new_pp', + type: 'keyword', + }, + 'auditd.data.old-enabled': { + category: 'auditd', + description: 'present TTY audit enabled setting', + name: 'auditd.data.old-enabled', + type: 'keyword', + }, + 'auditd.data.oauid': { + category: 'auditd', + description: "object's login user ID", + name: 'auditd.data.oauid', + type: 'keyword', + }, + 'auditd.data.old': { + category: 'auditd', + description: 'old value', + name: 'auditd.data.old', + type: 'keyword', + }, + 'auditd.data.banners': { + category: 'auditd', + description: 'banners used on printed page', + name: 'auditd.data.banners', + type: 'keyword', + }, + 'auditd.data.feature': { + category: 'auditd', + description: 'kernel feature being changed', + name: 'auditd.data.feature', + type: 'keyword', + }, + 'auditd.data.vm-ctx': { + category: 'auditd', + description: "the vm's context string", + name: 'auditd.data.vm-ctx', + type: 'keyword', + }, + 'auditd.data.opid': { + category: 'auditd', + description: "object's process ID", + name: 'auditd.data.opid', + type: 'keyword', + }, + 'auditd.data.seperms': { + category: 'auditd', + description: 'SELinux permissions being used', + name: 'auditd.data.seperms', + type: 'keyword', + }, + 'auditd.data.seresult': { + category: 'auditd', + description: 'SELinux AVC decision granted/denied', + name: 'auditd.data.seresult', + type: 'keyword', + }, + 'auditd.data.new-rng': { + category: 'auditd', + description: 'device name of rng being added from a vm', + name: 'auditd.data.new-rng', + type: 'keyword', + }, + 'auditd.data.old-net': { + category: 'auditd', + description: 'present MAC address assigned to vm', + name: 'auditd.data.old-net', + type: 'keyword', + }, + 'auditd.data.sigev_signo': { + category: 'auditd', + description: 'signal number', + name: 'auditd.data.sigev_signo', + type: 'keyword', + }, + 'auditd.data.ino': { + category: 'auditd', + description: 'inode number', + name: 'auditd.data.ino', + type: 'keyword', + }, + 'auditd.data.old_enforcing': { + category: 'auditd', + description: 'old MAC enforcement status', + name: 'auditd.data.old_enforcing', + type: 'keyword', + }, + 'auditd.data.old-vcpu': { + category: 'auditd', + description: 'present number of CPU cores', + name: 'auditd.data.old-vcpu', + type: 'keyword', + }, + 'auditd.data.range': { + category: 'auditd', + description: "user's SE Linux range", + name: 'auditd.data.range', + type: 'keyword', + }, + 'auditd.data.res': { + category: 'auditd', + description: 'result of the audited operation(success/fail)', + name: 'auditd.data.res', + type: 'keyword', + }, + 'auditd.data.added': { + category: 'auditd', + description: 'number of new files detected', + name: 'auditd.data.added', + type: 'keyword', + }, + 'auditd.data.fam': { + category: 'auditd', + description: 'socket address family', + name: 'auditd.data.fam', + type: 'keyword', + }, + 'auditd.data.nlnk-pid': { + category: 'auditd', + description: 'pid of netlink packet sender', + name: 'auditd.data.nlnk-pid', + type: 'keyword', + }, + 'auditd.data.subj': { + category: 'auditd', + description: "lspp subject's context string", + name: 'auditd.data.subj', + type: 'keyword', + }, + 'auditd.data.a[0-3]': { + category: 'auditd', + description: 'the arguments to a syscall', + name: 'auditd.data.a[0-3]', + type: 'keyword', + }, + 'auditd.data.cgroup': { + category: 'auditd', + description: 'path to cgroup in sysfs', + name: 'auditd.data.cgroup', + type: 'keyword', + }, + 'auditd.data.kernel': { + category: 'auditd', + description: "kernel's version number", + name: 'auditd.data.kernel', + type: 'keyword', + }, + 'auditd.data.ocomm': { + category: 'auditd', + description: "object's command line name", + name: 'auditd.data.ocomm', + type: 'keyword', + }, + 'auditd.data.new-net': { + category: 'auditd', + description: 'MAC address being assigned to vm', + name: 'auditd.data.new-net', + type: 'keyword', + }, + 'auditd.data.permissive': { + category: 'auditd', + description: 'SELinux is in permissive mode', + name: 'auditd.data.permissive', + type: 'keyword', + }, + 'auditd.data.class': { + category: 'auditd', + description: 'resource class assigned to vm', + name: 'auditd.data.class', + type: 'keyword', + }, + 'auditd.data.compat': { + category: 'auditd', + description: 'is_compat_task result', + name: 'auditd.data.compat', + type: 'keyword', + }, + 'auditd.data.fi': { + category: 'auditd', + description: 'file assigned inherited capability map', + name: 'auditd.data.fi', + type: 'keyword', + }, + 'auditd.data.changed': { + category: 'auditd', + description: 'number of changed files', + name: 'auditd.data.changed', + type: 'keyword', + }, + 'auditd.data.msg': { + category: 'auditd', + description: 'the payload of the audit record', + name: 'auditd.data.msg', + type: 'keyword', + }, + 'auditd.data.dport': { + category: 'auditd', + description: 'remote port number', + name: 'auditd.data.dport', + type: 'keyword', + }, + 'auditd.data.new-seuser': { + category: 'auditd', + description: 'new SELinux user', + name: 'auditd.data.new-seuser', + type: 'keyword', + }, + 'auditd.data.invalid_context': { + category: 'auditd', + description: 'SELinux context', + name: 'auditd.data.invalid_context', + type: 'keyword', + }, + 'auditd.data.dmac': { + category: 'auditd', + description: 'remote MAC address', + name: 'auditd.data.dmac', + type: 'keyword', + }, + 'auditd.data.ipx-net': { + category: 'auditd', + description: 'IPX network number', + name: 'auditd.data.ipx-net', + type: 'keyword', + }, + 'auditd.data.iuid': { + category: 'auditd', + description: "ipc object's user ID", + name: 'auditd.data.iuid', + type: 'keyword', + }, + 'auditd.data.macproto': { + category: 'auditd', + description: 'ethernet packet type ID field', + name: 'auditd.data.macproto', + type: 'keyword', + }, + 'auditd.data.obj': { + category: 'auditd', + description: 'lspp object context string', + name: 'auditd.data.obj', + type: 'keyword', + }, + 'auditd.data.ipid': { + category: 'auditd', + description: 'IP datagram fragment identifier', + name: 'auditd.data.ipid', + type: 'keyword', + }, + 'auditd.data.new-fs': { + category: 'auditd', + description: 'file system being added to vm', + name: 'auditd.data.new-fs', + type: 'keyword', + }, + 'auditd.data.vm-pid': { + category: 'auditd', + description: "vm's process ID", + name: 'auditd.data.vm-pid', + type: 'keyword', + }, + 'auditd.data.cap_pi': { + category: 'auditd', + description: 'process inherited capability map', + name: 'auditd.data.cap_pi', + type: 'keyword', + }, + 'auditd.data.old-auid': { + category: 'auditd', + description: 'previous auid value', + name: 'auditd.data.old-auid', + type: 'keyword', + }, + 'auditd.data.oses': { + category: 'auditd', + description: "object's session ID", + name: 'auditd.data.oses', + type: 'keyword', + }, + 'auditd.data.fd': { + category: 'auditd', + description: 'file descriptor number', + name: 'auditd.data.fd', + type: 'keyword', + }, + 'auditd.data.igid': { + category: 'auditd', + description: "ipc object's group ID", + name: 'auditd.data.igid', + type: 'keyword', + }, + 'auditd.data.new-disk': { + category: 'auditd', + description: 'disk being added to vm', + name: 'auditd.data.new-disk', + type: 'keyword', + }, + 'auditd.data.parent': { + category: 'auditd', + description: 'the inode number of the parent file', + name: 'auditd.data.parent', + type: 'keyword', + }, + 'auditd.data.len': { + category: 'auditd', + description: 'length', + name: 'auditd.data.len', + type: 'keyword', + }, + 'auditd.data.oflag': { + category: 'auditd', + description: 'open syscall flags', + name: 'auditd.data.oflag', + type: 'keyword', + }, + 'auditd.data.uuid': { + category: 'auditd', + description: 'a UUID', + name: 'auditd.data.uuid', + type: 'keyword', + }, + 'auditd.data.code': { + category: 'auditd', + description: 'seccomp action code', + name: 'auditd.data.code', + type: 'keyword', + }, + 'auditd.data.nlnk-grp': { + category: 'auditd', + description: 'netlink group number', + name: 'auditd.data.nlnk-grp', + type: 'keyword', + }, + 'auditd.data.cap_fp': { + category: 'auditd', + description: 'file permitted capability map', + name: 'auditd.data.cap_fp', + type: 'keyword', + }, + 'auditd.data.new-mem': { + category: 'auditd', + description: 'new amount of memory in KB', + name: 'auditd.data.new-mem', + type: 'keyword', + }, + 'auditd.data.seperm': { + category: 'auditd', + description: 'SELinux permission being decided on', + name: 'auditd.data.seperm', + type: 'keyword', + }, + 'auditd.data.enforcing': { + category: 'auditd', + description: 'new MAC enforcement status', + name: 'auditd.data.enforcing', + type: 'keyword', + }, + 'auditd.data.new-chardev': { + category: 'auditd', + description: 'new character device being assigned to vm', + name: 'auditd.data.new-chardev', + type: 'keyword', + }, + 'auditd.data.old-rng': { + category: 'auditd', + description: 'device name of rng being removed from a vm', + name: 'auditd.data.old-rng', + type: 'keyword', + }, + 'auditd.data.outif': { + category: 'auditd', + description: 'out interface number', + name: 'auditd.data.outif', + type: 'keyword', + }, + 'auditd.data.cmd': { + category: 'auditd', + description: 'command being executed', + name: 'auditd.data.cmd', + type: 'keyword', + }, + 'auditd.data.hook': { + category: 'auditd', + description: 'netfilter hook that packet came from', + name: 'auditd.data.hook', + type: 'keyword', + }, + 'auditd.data.new-level': { + category: 'auditd', + description: 'new run level', + name: 'auditd.data.new-level', + type: 'keyword', + }, + 'auditd.data.sauid': { + category: 'auditd', + description: 'sent login user ID', + name: 'auditd.data.sauid', + type: 'keyword', + }, + 'auditd.data.sig': { + category: 'auditd', + description: 'signal number', + name: 'auditd.data.sig', + type: 'keyword', + }, + 'auditd.data.audit_backlog_wait_time': { + category: 'auditd', + description: "audit system's backlog wait time", + name: 'auditd.data.audit_backlog_wait_time', + type: 'keyword', + }, + 'auditd.data.printer': { + category: 'auditd', + description: 'printer name', + name: 'auditd.data.printer', + type: 'keyword', + }, + 'auditd.data.old-mem': { + category: 'auditd', + description: 'present amount of memory in KB', + name: 'auditd.data.old-mem', + type: 'keyword', + }, + 'auditd.data.perm': { + category: 'auditd', + description: 'the file permission being used', + name: 'auditd.data.perm', + type: 'keyword', + }, + 'auditd.data.old_pi': { + category: 'auditd', + description: 'old process inherited capability map', + name: 'auditd.data.old_pi', + type: 'keyword', + }, + 'auditd.data.state': { + category: 'auditd', + description: 'audit daemon configuration resulting state', + name: 'auditd.data.state', + type: 'keyword', + }, + 'auditd.data.format': { + category: 'auditd', + description: "audit log's format", + name: 'auditd.data.format', + type: 'keyword', + }, + 'auditd.data.new_gid': { + category: 'auditd', + description: 'new group ID being assigned', + name: 'auditd.data.new_gid', + type: 'keyword', + }, + 'auditd.data.tcontext': { + category: 'auditd', + description: "the target's or object's context string", + name: 'auditd.data.tcontext', + type: 'keyword', + }, + 'auditd.data.maj': { + category: 'auditd', + description: 'device major number', + name: 'auditd.data.maj', + type: 'keyword', + }, + 'auditd.data.watch': { + category: 'auditd', + description: 'file name in a watch record', + name: 'auditd.data.watch', + type: 'keyword', + }, + 'auditd.data.device': { + category: 'auditd', + description: 'device name', + name: 'auditd.data.device', + type: 'keyword', + }, + 'auditd.data.grp': { + category: 'auditd', + description: 'group name', + name: 'auditd.data.grp', + type: 'keyword', + }, + 'auditd.data.bool': { + category: 'auditd', + description: 'name of SELinux boolean', + name: 'auditd.data.bool', + type: 'keyword', + }, + 'auditd.data.icmp_type': { + category: 'auditd', + description: 'type of icmp message', + name: 'auditd.data.icmp_type', + type: 'keyword', + }, + 'auditd.data.new_lock': { + category: 'auditd', + description: 'new value of feature lock', + name: 'auditd.data.new_lock', + type: 'keyword', + }, + 'auditd.data.old_prom': { + category: 'auditd', + description: 'network promiscuity flag', + name: 'auditd.data.old_prom', + type: 'keyword', + }, + 'auditd.data.acl': { + category: 'auditd', + description: 'access mode of resource assigned to vm', + name: 'auditd.data.acl', + type: 'keyword', + }, + 'auditd.data.ip': { + category: 'auditd', + description: 'network address of a printer', + name: 'auditd.data.ip', + type: 'keyword', + }, + 'auditd.data.new_pi': { + category: 'auditd', + description: 'new process inherited capability map', + name: 'auditd.data.new_pi', + type: 'keyword', + }, + 'auditd.data.default-context': { + category: 'auditd', + description: 'default MAC context', + name: 'auditd.data.default-context', + type: 'keyword', + }, + 'auditd.data.inode_gid': { + category: 'auditd', + description: "group ID of the inode's owner", + name: 'auditd.data.inode_gid', + type: 'keyword', + }, + 'auditd.data.new-log_passwd': { + category: 'auditd', + description: 'new value for TTY password logging', + name: 'auditd.data.new-log_passwd', + type: 'keyword', + }, + 'auditd.data.new_pe': { + category: 'auditd', + description: 'new process effective capability map', + name: 'auditd.data.new_pe', + type: 'keyword', + }, + 'auditd.data.selected-context': { + category: 'auditd', + description: 'new MAC context assigned to session', + name: 'auditd.data.selected-context', + type: 'keyword', + }, + 'auditd.data.cap_fver': { + category: 'auditd', + description: 'file system capabilities version number', + name: 'auditd.data.cap_fver', + type: 'keyword', + }, + 'auditd.data.file': { + category: 'auditd', + description: 'file name', + name: 'auditd.data.file', + type: 'keyword', + }, + 'auditd.data.net': { + category: 'auditd', + description: 'network MAC address', + name: 'auditd.data.net', + type: 'keyword', + }, + 'auditd.data.virt': { + category: 'auditd', + description: 'kind of virtualization being referenced', + name: 'auditd.data.virt', + type: 'keyword', + }, + 'auditd.data.cap_pp': { + category: 'auditd', + description: 'process permitted capability map', + name: 'auditd.data.cap_pp', + type: 'keyword', + }, + 'auditd.data.old-range': { + category: 'auditd', + description: 'present SELinux range', + name: 'auditd.data.old-range', + type: 'keyword', + }, + 'auditd.data.resrc': { + category: 'auditd', + description: 'resource being assigned', + name: 'auditd.data.resrc', + type: 'keyword', + }, + 'auditd.data.new-range': { + category: 'auditd', + description: 'new SELinux range', + name: 'auditd.data.new-range', + type: 'keyword', + }, + 'auditd.data.obj_gid': { + category: 'auditd', + description: 'group ID of object', + name: 'auditd.data.obj_gid', + type: 'keyword', + }, + 'auditd.data.proto': { + category: 'auditd', + description: 'network protocol', + name: 'auditd.data.proto', + type: 'keyword', + }, + 'auditd.data.old-disk': { + category: 'auditd', + description: 'disk being removed from vm', + name: 'auditd.data.old-disk', + type: 'keyword', + }, + 'auditd.data.audit_failure': { + category: 'auditd', + description: "audit system's failure mode", + name: 'auditd.data.audit_failure', + type: 'keyword', + }, + 'auditd.data.inif': { + category: 'auditd', + description: 'in interface number', + name: 'auditd.data.inif', + type: 'keyword', + }, + 'auditd.data.vm': { + category: 'auditd', + description: 'virtual machine name', + name: 'auditd.data.vm', + type: 'keyword', + }, + 'auditd.data.flags': { + category: 'auditd', + description: 'mmap syscall flags', + name: 'auditd.data.flags', + type: 'keyword', + }, + 'auditd.data.nlnk-fam': { + category: 'auditd', + description: 'netlink protocol number', + name: 'auditd.data.nlnk-fam', + type: 'keyword', + }, + 'auditd.data.old-fs': { + category: 'auditd', + description: 'file system being removed from vm', + name: 'auditd.data.old-fs', + type: 'keyword', + }, + 'auditd.data.old-ses': { + category: 'auditd', + description: 'previous ses value', + name: 'auditd.data.old-ses', + type: 'keyword', + }, + 'auditd.data.seqno': { + category: 'auditd', + description: 'sequence number', + name: 'auditd.data.seqno', + type: 'keyword', + }, + 'auditd.data.fver': { + category: 'auditd', + description: 'file system capabilities version number', + name: 'auditd.data.fver', + type: 'keyword', + }, + 'auditd.data.qbytes': { + category: 'auditd', + description: 'ipc objects quantity of bytes', + name: 'auditd.data.qbytes', + type: 'keyword', + }, + 'auditd.data.seuser': { + category: 'auditd', + description: "user's SE Linux user acct", + name: 'auditd.data.seuser', + type: 'keyword', + }, + 'auditd.data.cap_fe': { + category: 'auditd', + description: 'file assigned effective capability map', + name: 'auditd.data.cap_fe', + type: 'keyword', + }, + 'auditd.data.new-vcpu': { + category: 'auditd', + description: 'new number of CPU cores', + name: 'auditd.data.new-vcpu', + type: 'keyword', + }, + 'auditd.data.old-level': { + category: 'auditd', + description: 'old run level', + name: 'auditd.data.old-level', + type: 'keyword', + }, + 'auditd.data.old_pp': { + category: 'auditd', + description: 'old process permitted capability map', + name: 'auditd.data.old_pp', + type: 'keyword', + }, + 'auditd.data.daddr': { + category: 'auditd', + description: 'remote IP address', + name: 'auditd.data.daddr', + type: 'keyword', + }, + 'auditd.data.old-role': { + category: 'auditd', + description: 'present SELinux role', + name: 'auditd.data.old-role', + type: 'keyword', + }, + 'auditd.data.ioctlcmd': { + category: 'auditd', + description: 'The request argument to the ioctl syscall', + name: 'auditd.data.ioctlcmd', + type: 'keyword', + }, + 'auditd.data.smac': { + category: 'auditd', + description: 'local MAC address', + name: 'auditd.data.smac', + type: 'keyword', + }, + 'auditd.data.apparmor': { + category: 'auditd', + description: 'apparmor event information', + name: 'auditd.data.apparmor', + type: 'keyword', + }, + 'auditd.data.fe': { + category: 'auditd', + description: 'file assigned effective capability map', + name: 'auditd.data.fe', + type: 'keyword', + }, + 'auditd.data.perm_mask': { + category: 'auditd', + description: 'file permission mask that triggered a watch event', + name: 'auditd.data.perm_mask', + type: 'keyword', + }, + 'auditd.data.ses': { + category: 'auditd', + description: 'login session ID', + name: 'auditd.data.ses', + type: 'keyword', + }, + 'auditd.data.cap_fi': { + category: 'auditd', + description: 'file inherited capability map', + name: 'auditd.data.cap_fi', + type: 'keyword', + }, + 'auditd.data.obj_uid': { + category: 'auditd', + description: 'user ID of object', + name: 'auditd.data.obj_uid', + type: 'keyword', + }, + 'auditd.data.reason': { + category: 'auditd', + description: 'text string denoting a reason for the action', + name: 'auditd.data.reason', + type: 'keyword', + }, + 'auditd.data.list': { + category: 'auditd', + description: "the audit system's filter list number", + name: 'auditd.data.list', + type: 'keyword', + }, + 'auditd.data.old_lock': { + category: 'auditd', + description: 'present value of feature lock', + name: 'auditd.data.old_lock', + type: 'keyword', + }, + 'auditd.data.bus': { + category: 'auditd', + description: 'name of subsystem bus a vm resource belongs to', + name: 'auditd.data.bus', + type: 'keyword', + }, + 'auditd.data.old_pe': { + category: 'auditd', + description: 'old process effective capability map', + name: 'auditd.data.old_pe', + type: 'keyword', + }, + 'auditd.data.new-role': { + category: 'auditd', + description: 'new SELinux role', + name: 'auditd.data.new-role', + type: 'keyword', + }, + 'auditd.data.prom': { + category: 'auditd', + description: 'network promiscuity flag', + name: 'auditd.data.prom', + type: 'keyword', + }, + 'auditd.data.uri': { + category: 'auditd', + description: 'URI pointing to a printer', + name: 'auditd.data.uri', + type: 'keyword', + }, + 'auditd.data.audit_enabled': { + category: 'auditd', + description: "audit systems's enable/disable status", + name: 'auditd.data.audit_enabled', + type: 'keyword', + }, + 'auditd.data.old-log_passwd': { + category: 'auditd', + description: 'present value for TTY password logging', + name: 'auditd.data.old-log_passwd', + type: 'keyword', + }, + 'auditd.data.old-seuser': { + category: 'auditd', + description: 'present SELinux user', + name: 'auditd.data.old-seuser', + type: 'keyword', + }, + 'auditd.data.per': { + category: 'auditd', + description: 'linux personality', + name: 'auditd.data.per', + type: 'keyword', + }, + 'auditd.data.scontext': { + category: 'auditd', + description: "the subject's context string", + name: 'auditd.data.scontext', + type: 'keyword', + }, + 'auditd.data.tclass': { + category: 'auditd', + description: "target's object classification", + name: 'auditd.data.tclass', + type: 'keyword', + }, + 'auditd.data.ver': { + category: 'auditd', + description: "audit daemon's version number", + name: 'auditd.data.ver', + type: 'keyword', + }, + 'auditd.data.new': { + category: 'auditd', + description: 'value being set in feature', + name: 'auditd.data.new', + type: 'keyword', + }, + 'auditd.data.val': { + category: 'auditd', + description: 'generic value associated with the operation', + name: 'auditd.data.val', + type: 'keyword', + }, + 'auditd.data.img-ctx': { + category: 'auditd', + description: "the vm's disk image context string", + name: 'auditd.data.img-ctx', + type: 'keyword', + }, + 'auditd.data.old-chardev': { + category: 'auditd', + description: 'present character device assigned to vm', + name: 'auditd.data.old-chardev', + type: 'keyword', + }, + 'auditd.data.old_val': { + category: 'auditd', + description: 'current value of SELinux boolean', + name: 'auditd.data.old_val', + type: 'keyword', + }, + 'auditd.data.success': { + category: 'auditd', + description: 'whether the syscall was successful or not', + name: 'auditd.data.success', + type: 'keyword', + }, + 'auditd.data.inode_uid': { + category: 'auditd', + description: "user ID of the inode's owner", + name: 'auditd.data.inode_uid', + type: 'keyword', + }, + 'auditd.data.removed': { + category: 'auditd', + description: 'number of deleted files', + name: 'auditd.data.removed', + type: 'keyword', + }, + 'auditd.data.socket.port': { + category: 'auditd', + description: 'The port number.', + name: 'auditd.data.socket.port', + type: 'keyword', + }, + 'auditd.data.socket.saddr': { + category: 'auditd', + description: 'The raw socket address structure.', + name: 'auditd.data.socket.saddr', + type: 'keyword', + }, + 'auditd.data.socket.addr': { + category: 'auditd', + description: 'The remote address.', + name: 'auditd.data.socket.addr', + type: 'keyword', + }, + 'auditd.data.socket.family': { + category: 'auditd', + description: 'The socket family (unix, ipv4, ipv6, netlink).', + example: 'unix', + name: 'auditd.data.socket.family', + type: 'keyword', + }, + 'auditd.data.socket.path': { + category: 'auditd', + description: 'This is the path associated with a unix socket.', + name: 'auditd.data.socket.path', + type: 'keyword', + }, + 'auditd.messages': { + category: 'auditd', + description: + 'An ordered list of the raw messages received from the kernel that were used to construct this document. This field is present if an error occurred processing the data or if `include_raw_message` is set in the config. ', + name: 'auditd.messages', + type: 'alias', + }, + 'auditd.warnings': { + category: 'auditd', + description: + 'The warnings generated by the Beat during the construction of the event. These are disabled by default and are used for development and debug purposes only. ', + name: 'auditd.warnings', + type: 'alias', + }, + 'geoip.continent_name': { + category: 'geoip', + description: 'The name of the continent. ', + name: 'geoip.continent_name', + type: 'keyword', + }, + 'geoip.city_name': { + category: 'geoip', + description: 'The name of the city. ', + name: 'geoip.city_name', + type: 'keyword', + }, + 'geoip.region_name': { + category: 'geoip', + description: 'The name of the region. ', + name: 'geoip.region_name', + type: 'keyword', + }, + 'geoip.country_iso_code': { + category: 'geoip', + description: 'Country ISO code. ', + name: 'geoip.country_iso_code', + type: 'keyword', + }, + 'geoip.location': { + category: 'geoip', + description: 'The longitude and latitude. ', + name: 'geoip.location', + type: 'geo_point', + }, + 'hash.blake2b_256': { + category: 'hash', + description: 'BLAKE2b-256 hash of the file.', + name: 'hash.blake2b_256', + type: 'keyword', + }, + 'hash.blake2b_384': { + category: 'hash', + description: 'BLAKE2b-384 hash of the file.', + name: 'hash.blake2b_384', + type: 'keyword', + }, + 'hash.blake2b_512': { + category: 'hash', + description: 'BLAKE2b-512 hash of the file.', + name: 'hash.blake2b_512', + type: 'keyword', + }, + 'hash.sha224': { + category: 'hash', + description: 'SHA224 hash of the file.', + name: 'hash.sha224', + type: 'keyword', + }, + 'hash.sha384': { + category: 'hash', + description: 'SHA384 hash of the file.', + name: 'hash.sha384', + type: 'keyword', + }, + 'hash.sha3_224': { + category: 'hash', + description: 'SHA3_224 hash of the file.', + name: 'hash.sha3_224', + type: 'keyword', + }, + 'hash.sha3_256': { + category: 'hash', + description: 'SHA3_256 hash of the file.', + name: 'hash.sha3_256', + type: 'keyword', + }, + 'hash.sha3_384': { + category: 'hash', + description: 'SHA3_384 hash of the file.', + name: 'hash.sha3_384', + type: 'keyword', + }, + 'hash.sha3_512': { + category: 'hash', + description: 'SHA3_512 hash of the file.', + name: 'hash.sha3_512', + type: 'keyword', + }, + 'hash.sha512_224': { + category: 'hash', + description: 'SHA512/224 hash of the file.', + name: 'hash.sha512_224', + type: 'keyword', + }, + 'hash.sha512_256': { + category: 'hash', + description: 'SHA512/256 hash of the file.', + name: 'hash.sha512_256', + type: 'keyword', + }, + 'hash.xxh64': { + category: 'hash', + description: 'XX64 hash of the file.', + name: 'hash.xxh64', + type: 'keyword', + }, + 'event.origin': { + category: 'event', + description: + 'Origin of the event. This can be a file path (e.g. `/var/log/log.1`), or the name of the system component that supplied the data (e.g. `netlink`). ', + name: 'event.origin', + type: 'keyword', + }, + 'user.entity_id': { + category: 'user', + description: + 'ID uniquely identifying the user on a host. It is computed as a SHA-256 hash of the host ID, user ID, and user name. ', + name: 'user.entity_id', + type: 'keyword', + }, + 'user.terminal': { + category: 'user', + description: 'Terminal of the user. ', + name: 'user.terminal', + type: 'keyword', + }, + 'process.hash.blake2b_256': { + category: 'process', + description: 'BLAKE2b-256 hash of the executable.', + name: 'process.hash.blake2b_256', + type: 'keyword', + }, + 'process.hash.blake2b_384': { + category: 'process', + description: 'BLAKE2b-384 hash of the executable.', + name: 'process.hash.blake2b_384', + type: 'keyword', + }, + 'process.hash.blake2b_512': { + category: 'process', + description: 'BLAKE2b-512 hash of the executable.', + name: 'process.hash.blake2b_512', + type: 'keyword', + }, + 'process.hash.sha224': { + category: 'process', + description: 'SHA224 hash of the executable.', + name: 'process.hash.sha224', + type: 'keyword', + }, + 'process.hash.sha384': { + category: 'process', + description: 'SHA384 hash of the executable.', + name: 'process.hash.sha384', + type: 'keyword', + }, + 'process.hash.sha3_224': { + category: 'process', + description: 'SHA3_224 hash of the executable.', + name: 'process.hash.sha3_224', + type: 'keyword', + }, + 'process.hash.sha3_256': { + category: 'process', + description: 'SHA3_256 hash of the executable.', + name: 'process.hash.sha3_256', + type: 'keyword', + }, + 'process.hash.sha3_384': { + category: 'process', + description: 'SHA3_384 hash of the executable.', + name: 'process.hash.sha3_384', + type: 'keyword', + }, + 'process.hash.sha3_512': { + category: 'process', + description: 'SHA3_512 hash of the executable.', + name: 'process.hash.sha3_512', + type: 'keyword', + }, + 'process.hash.sha512_224': { + category: 'process', + description: 'SHA512/224 hash of the executable.', + name: 'process.hash.sha512_224', + type: 'keyword', + }, + 'process.hash.sha512_256': { + category: 'process', + description: 'SHA512/256 hash of the executable.', + name: 'process.hash.sha512_256', + type: 'keyword', + }, + 'process.hash.xxh64': { + category: 'process', + description: 'XX64 hash of the executable.', + name: 'process.hash.xxh64', + type: 'keyword', + }, + 'socket.entity_id': { + category: 'socket', + description: + 'ID uniquely identifying the socket. It is computed as a SHA-256 hash of the host ID, socket inode, local IP, local port, remote IP, and remote port. ', + name: 'socket.entity_id', + type: 'keyword', + }, + 'system.audit.host.uptime': { + category: 'system', + description: 'Uptime in nanoseconds. ', + name: 'system.audit.host.uptime', + type: 'long', + format: 'duration', + }, + 'system.audit.host.boottime': { + category: 'system', + description: 'Boot time. ', + name: 'system.audit.host.boottime', + type: 'date', + }, + 'system.audit.host.containerized': { + category: 'system', + description: 'Set if host is a container. ', + name: 'system.audit.host.containerized', + type: 'boolean', + }, + 'system.audit.host.timezone.name': { + category: 'system', + description: 'Name of the timezone of the host, e.g. BST. ', + name: 'system.audit.host.timezone.name', + type: 'keyword', + }, + 'system.audit.host.timezone.offset.sec': { + category: 'system', + description: 'Timezone offset in seconds. ', + name: 'system.audit.host.timezone.offset.sec', + type: 'long', + }, + 'system.audit.host.hostname': { + category: 'system', + description: 'Hostname. ', + name: 'system.audit.host.hostname', + type: 'keyword', + }, + 'system.audit.host.id': { + category: 'system', + description: 'Host ID. ', + name: 'system.audit.host.id', + type: 'keyword', + }, + 'system.audit.host.architecture': { + category: 'system', + description: 'Host architecture (e.g. x86_64). ', + name: 'system.audit.host.architecture', + type: 'keyword', + }, + 'system.audit.host.mac': { + category: 'system', + description: 'MAC addresses. ', + name: 'system.audit.host.mac', + type: 'keyword', + }, + 'system.audit.host.ip': { + category: 'system', + description: 'IP addresses. ', + name: 'system.audit.host.ip', + type: 'ip', + }, + 'system.audit.host.os.codename': { + category: 'system', + description: 'OS codename, if any (e.g. stretch). ', + name: 'system.audit.host.os.codename', + type: 'keyword', + }, + 'system.audit.host.os.platform': { + category: 'system', + description: 'OS platform (e.g. centos, ubuntu, windows). ', + name: 'system.audit.host.os.platform', + type: 'keyword', + }, + 'system.audit.host.os.name': { + category: 'system', + description: 'OS name (e.g. Mac OS X). ', + name: 'system.audit.host.os.name', + type: 'keyword', + }, + 'system.audit.host.os.family': { + category: 'system', + description: 'OS family (e.g. redhat, debian, freebsd, windows). ', + name: 'system.audit.host.os.family', + type: 'keyword', + }, + 'system.audit.host.os.version': { + category: 'system', + description: 'OS version. ', + name: 'system.audit.host.os.version', + type: 'keyword', + }, + 'system.audit.host.os.kernel': { + category: 'system', + description: "The operating system's kernel version. ", + name: 'system.audit.host.os.kernel', + type: 'keyword', + }, + 'system.audit.package.entity_id': { + category: 'system', + description: + 'ID uniquely identifying the package. It is computed as a SHA-256 hash of the host ID, package name, and package version. ', + name: 'system.audit.package.entity_id', + type: 'keyword', + }, + 'system.audit.package.name': { + category: 'system', + description: 'Package name. ', + name: 'system.audit.package.name', + type: 'keyword', + }, + 'system.audit.package.version': { + category: 'system', + description: 'Package version. ', + name: 'system.audit.package.version', + type: 'keyword', + }, + 'system.audit.package.release': { + category: 'system', + description: 'Package release. ', + name: 'system.audit.package.release', + type: 'keyword', + }, + 'system.audit.package.arch': { + category: 'system', + description: 'Package architecture. ', + name: 'system.audit.package.arch', + type: 'keyword', + }, + 'system.audit.package.license': { + category: 'system', + description: 'Package license. ', + name: 'system.audit.package.license', + type: 'keyword', + }, + 'system.audit.package.installtime': { + category: 'system', + description: 'Package install time. ', + name: 'system.audit.package.installtime', + type: 'date', + }, + 'system.audit.package.size': { + category: 'system', + description: 'Package size. ', + name: 'system.audit.package.size', + type: 'long', + }, + 'system.audit.package.summary': { + category: 'system', + description: 'Package summary. ', + name: 'system.audit.package.summary', + }, + 'system.audit.package.url': { + category: 'system', + description: 'Package URL. ', + name: 'system.audit.package.url', + type: 'keyword', + }, + 'system.audit.user.name': { + category: 'system', + description: 'User name. ', + name: 'system.audit.user.name', + type: 'keyword', + }, + 'system.audit.user.uid': { + category: 'system', + description: 'User ID. ', + name: 'system.audit.user.uid', + type: 'keyword', + }, + 'system.audit.user.gid': { + category: 'system', + description: 'Group ID. ', + name: 'system.audit.user.gid', + type: 'keyword', + }, + 'system.audit.user.dir': { + category: 'system', + description: "User's home directory. ", + name: 'system.audit.user.dir', + type: 'keyword', + }, + 'system.audit.user.shell': { + category: 'system', + description: 'Program to run at login. ', + name: 'system.audit.user.shell', + type: 'keyword', + }, + 'system.audit.user.user_information': { + category: 'system', + description: 'General user information. On Linux, this is the gecos field. ', + name: 'system.audit.user.user_information', + type: 'keyword', + }, + 'system.audit.user.group.name': { + category: 'system', + description: 'Group name. ', + name: 'system.audit.user.group.name', + type: 'keyword', + }, + 'system.audit.user.group.gid': { + category: 'system', + description: 'Group ID. ', + name: 'system.audit.user.group.gid', + type: 'integer', + }, + 'system.audit.user.password.type': { + category: 'system', + description: + "A user's password type. Possible values are `shadow_password` (the password hash is in the shadow file), `password_disabled`, `no_password` (this is dangerous as anyone can log in), and `crypt_password` (when the password field in /etc/passwd seems to contain an encrypted password). ", + name: 'system.audit.user.password.type', + type: 'keyword', + }, + 'system.audit.user.password.last_changed': { + category: 'system', + description: "The day the user's password was last changed. ", + name: 'system.audit.user.password.last_changed', + type: 'date', + }, + 'log.file.path': { + category: 'log', + description: + 'The file from which the line was read. This field contains the absolute path to the file. For example: `/var/log/system.log`. ', + name: 'log.file.path', + type: 'keyword', + }, + 'log.source.address': { + category: 'log', + description: 'Source address from which the log event was read / sent from. ', + name: 'log.source.address', + type: 'keyword', + }, + 'log.offset': { + category: 'log', + description: 'The file offset the reported line starts at. ', + name: 'log.offset', + type: 'long', + }, + stream: { + category: 'base', + description: "Log stream when reading container logs, can be 'stdout' or 'stderr' ", + name: 'stream', + type: 'keyword', + }, + 'input.type': { + category: 'input', + description: + 'The input type from which the event was generated. This field is set to the value specified for the `type` option in the input section of the Filebeat config file. ', + name: 'input.type', + }, + 'syslog.facility': { + category: 'syslog', + description: 'The facility extracted from the priority. ', + name: 'syslog.facility', + type: 'long', + }, + 'syslog.priority': { + category: 'syslog', + description: 'The priority of the syslog event. ', + name: 'syslog.priority', + type: 'long', + }, + 'syslog.severity_label': { + category: 'syslog', + description: 'The human readable severity. ', + name: 'syslog.severity_label', + type: 'keyword', + }, + 'syslog.facility_label': { + category: 'syslog', + description: 'The human readable facility. ', + name: 'syslog.facility_label', + type: 'keyword', + }, + 'process.program': { + category: 'process', + description: 'The name of the program. ', + name: 'process.program', + type: 'keyword', + }, + 'log.flags': { + category: 'log', + description: 'This field contains the flags of the event. ', + name: 'log.flags', + }, + 'http.response.content_length': { + category: 'http', + name: 'http.response.content_length', + type: 'alias', + }, + 'user_agent.os.full_name': { + category: 'user_agent', + name: 'user_agent.os.full_name', + type: 'keyword', + }, + 'fileset.name': { + category: 'fileset', + description: 'The Filebeat fileset that generated this event. ', + name: 'fileset.name', + type: 'keyword', + }, + 'fileset.module': { + category: 'fileset', + name: 'fileset.module', + type: 'alias', + }, + read_timestamp: { + category: 'base', + name: 'read_timestamp', + type: 'alias', + }, + 'docker.attrs': { + category: 'docker', + description: + "docker.attrs contains labels and environment variables written by docker's JSON File logging driver. These fields are only available when they are configured in the logging driver options. ", + name: 'docker.attrs', + type: 'object', + }, + 'icmp.code': { + category: 'icmp', + description: 'ICMP code. ', + name: 'icmp.code', + type: 'keyword', + }, + 'icmp.type': { + category: 'icmp', + description: 'ICMP type. ', + name: 'icmp.type', + type: 'keyword', + }, + 'igmp.type': { + category: 'igmp', + description: 'IGMP type. ', + name: 'igmp.type', + type: 'keyword', + }, + 'azure.eventhub': { + category: 'azure', + description: 'Name of the eventhub. ', + name: 'azure.eventhub', + type: 'keyword', + }, + 'azure.offset': { + category: 'azure', + description: 'The offset. ', + name: 'azure.offset', + type: 'long', + }, + 'azure.enqueued_time': { + category: 'azure', + description: 'The enqueued time. ', + name: 'azure.enqueued_time', + type: 'date', + }, + 'azure.partition_id': { + category: 'azure', + description: 'The partition id. ', + name: 'azure.partition_id', + type: 'long', + }, + 'azure.consumer_group': { + category: 'azure', + description: 'The consumer group. ', + name: 'azure.consumer_group', + type: 'keyword', + }, + 'azure.sequence_number': { + category: 'azure', + description: 'The sequence number. ', + name: 'azure.sequence_number', + type: 'long', + }, + 'kafka.topic': { + category: 'kafka', + description: 'Kafka topic ', + name: 'kafka.topic', + type: 'keyword', + }, + 'kafka.partition': { + category: 'kafka', + description: 'Kafka partition number ', + name: 'kafka.partition', + type: 'long', + }, + 'kafka.offset': { + category: 'kafka', + description: 'Kafka offset of this message ', + name: 'kafka.offset', + type: 'long', + }, + 'kafka.key': { + category: 'kafka', + description: 'Kafka key, corresponding to the Kafka value stored in the message ', + name: 'kafka.key', + type: 'keyword', + }, + 'kafka.block_timestamp': { + category: 'kafka', + description: 'Kafka outer (compressed) block timestamp ', + name: 'kafka.block_timestamp', + type: 'date', + }, + 'kafka.headers': { + category: 'kafka', + description: + 'An array of Kafka header strings for this message, in the form "<key>: <value>". ', + name: 'kafka.headers', + type: 'array', + }, + 'apache2.access.remote_ip': { + category: 'apache2', + name: 'apache2.access.remote_ip', + type: 'alias', + }, + 'apache2.access.ssl.protocol': { + category: 'apache2', + name: 'apache2.access.ssl.protocol', + type: 'alias', + }, + 'apache2.access.ssl.cipher': { + category: 'apache2', + name: 'apache2.access.ssl.cipher', + type: 'alias', + }, + 'apache2.access.body_sent.bytes': { + category: 'apache2', + name: 'apache2.access.body_sent.bytes', + type: 'alias', + }, + 'apache2.access.user_name': { + category: 'apache2', + name: 'apache2.access.user_name', + type: 'alias', + }, + 'apache2.access.method': { + category: 'apache2', + name: 'apache2.access.method', + type: 'alias', + }, + 'apache2.access.url': { + category: 'apache2', + name: 'apache2.access.url', + type: 'alias', + }, + 'apache2.access.http_version': { + category: 'apache2', + name: 'apache2.access.http_version', + type: 'alias', + }, + 'apache2.access.response_code': { + category: 'apache2', + name: 'apache2.access.response_code', + type: 'alias', + }, + 'apache2.access.referrer': { + category: 'apache2', + name: 'apache2.access.referrer', + type: 'alias', + }, + 'apache2.access.agent': { + category: 'apache2', + name: 'apache2.access.agent', + type: 'alias', + }, + 'apache2.access.user_agent.device': { + category: 'apache2', + name: 'apache2.access.user_agent.device', + type: 'alias', + }, + 'apache2.access.user_agent.name': { + category: 'apache2', + name: 'apache2.access.user_agent.name', + type: 'alias', + }, + 'apache2.access.user_agent.os': { + category: 'apache2', + name: 'apache2.access.user_agent.os', + type: 'alias', + }, + 'apache2.access.user_agent.os_name': { + category: 'apache2', + name: 'apache2.access.user_agent.os_name', + type: 'alias', + }, + 'apache2.access.user_agent.original': { + category: 'apache2', + name: 'apache2.access.user_agent.original', + type: 'alias', + }, + 'apache2.access.geoip.continent_name': { + category: 'apache2', + name: 'apache2.access.geoip.continent_name', + type: 'alias', + }, + 'apache2.access.geoip.country_iso_code': { + category: 'apache2', + name: 'apache2.access.geoip.country_iso_code', + type: 'alias', + }, + 'apache2.access.geoip.location': { + category: 'apache2', + name: 'apache2.access.geoip.location', + type: 'alias', + }, + 'apache2.access.geoip.region_name': { + category: 'apache2', + name: 'apache2.access.geoip.region_name', + type: 'alias', + }, + 'apache2.access.geoip.city_name': { + category: 'apache2', + name: 'apache2.access.geoip.city_name', + type: 'alias', + }, + 'apache2.access.geoip.region_iso_code': { + category: 'apache2', + name: 'apache2.access.geoip.region_iso_code', + type: 'alias', + }, + 'apache2.error.level': { + category: 'apache2', + name: 'apache2.error.level', + type: 'alias', + }, + 'apache2.error.message': { + category: 'apache2', + name: 'apache2.error.message', + type: 'alias', + }, + 'apache2.error.pid': { + category: 'apache2', + name: 'apache2.error.pid', + type: 'alias', + }, + 'apache2.error.tid': { + category: 'apache2', + name: 'apache2.error.tid', + type: 'alias', + }, + 'apache2.error.module': { + category: 'apache2', + name: 'apache2.error.module', + type: 'alias', + }, + 'apache.access.ssl.protocol': { + category: 'apache', + description: 'SSL protocol version. ', + name: 'apache.access.ssl.protocol', + type: 'keyword', + }, + 'apache.access.ssl.cipher': { + category: 'apache', + description: 'SSL cipher name. ', + name: 'apache.access.ssl.cipher', + type: 'keyword', + }, + 'apache.error.module': { + category: 'apache', + description: 'The module producing the logged message. ', + name: 'apache.error.module', + type: 'keyword', + }, + 'user.audit.group.id': { + category: 'user', + description: 'Unique identifier for the group on the system/platform. ', + name: 'user.audit.group.id', + type: 'keyword', + }, + 'user.audit.group.name': { + category: 'user', + description: 'Name of the group. ', + name: 'user.audit.group.name', + type: 'keyword', + }, + 'user.owner.id': { + category: 'user', + description: 'One or multiple unique identifiers of the user. ', + name: 'user.owner.id', + type: 'keyword', + }, + 'user.owner.name': { + category: 'user', + description: 'Short name or login of the user. ', + example: 'albert', + name: 'user.owner.name', + type: 'keyword', + }, + 'user.owner.group.id': { + category: 'user', + description: 'Unique identifier for the group on the system/platform. ', + name: 'user.owner.group.id', + type: 'keyword', + }, + 'user.owner.group.name': { + category: 'user', + description: 'Name of the group. ', + name: 'user.owner.group.name', + type: 'keyword', + }, + 'auditd.log.old_auid': { + category: 'auditd', + description: + 'For login events this is the old audit ID used for the user prior to this login. ', + name: 'auditd.log.old_auid', + }, + 'auditd.log.new_auid': { + category: 'auditd', + description: + 'For login events this is the new audit ID. The audit ID can be used to trace future events to the user even if their identity changes (like becoming root). ', + name: 'auditd.log.new_auid', + }, + 'auditd.log.old_ses': { + category: 'auditd', + description: + 'For login events this is the old session ID used for the user prior to this login. ', + name: 'auditd.log.old_ses', + }, + 'auditd.log.new_ses': { + category: 'auditd', + description: + 'For login events this is the new session ID. It can be used to tie a user to future events by session ID. ', + name: 'auditd.log.new_ses', + }, + 'auditd.log.sequence': { + category: 'auditd', + description: 'The audit event sequence number. ', + name: 'auditd.log.sequence', + type: 'long', + }, + 'auditd.log.items': { + category: 'auditd', + description: 'The number of items in an event. ', + name: 'auditd.log.items', + }, + 'auditd.log.item': { + category: 'auditd', + description: + 'The item field indicates which item out of the total number of items. This number is zero-based; a value of 0 means it is the first item. ', + name: 'auditd.log.item', + }, + 'auditd.log.tty': { + category: 'auditd', + name: 'auditd.log.tty', + type: 'keyword', + }, + 'auditd.log.a0': { + category: 'auditd', + description: 'The first argument to the system call. ', + name: 'auditd.log.a0', + }, + 'auditd.log.addr': { + category: 'auditd', + name: 'auditd.log.addr', + type: 'ip', + }, + 'auditd.log.rport': { + category: 'auditd', + name: 'auditd.log.rport', + type: 'long', + }, + 'auditd.log.laddr': { + category: 'auditd', + name: 'auditd.log.laddr', + type: 'ip', + }, + 'auditd.log.lport': { + category: 'auditd', + name: 'auditd.log.lport', + type: 'long', + }, + 'auditd.log.acct': { + category: 'auditd', + name: 'auditd.log.acct', + type: 'alias', + }, + 'auditd.log.pid': { + category: 'auditd', + name: 'auditd.log.pid', + type: 'alias', + }, + 'auditd.log.ppid': { + category: 'auditd', + name: 'auditd.log.ppid', + type: 'alias', + }, + 'auditd.log.res': { + category: 'auditd', + name: 'auditd.log.res', + type: 'alias', + }, + 'auditd.log.record_type': { + category: 'auditd', + name: 'auditd.log.record_type', + type: 'alias', + }, + 'auditd.log.geoip.continent_name': { + category: 'auditd', + name: 'auditd.log.geoip.continent_name', + type: 'alias', + }, + 'auditd.log.geoip.country_iso_code': { + category: 'auditd', + name: 'auditd.log.geoip.country_iso_code', + type: 'alias', + }, + 'auditd.log.geoip.location': { + category: 'auditd', + name: 'auditd.log.geoip.location', + type: 'alias', + }, + 'auditd.log.geoip.region_name': { + category: 'auditd', + name: 'auditd.log.geoip.region_name', + type: 'alias', + }, + 'auditd.log.geoip.city_name': { + category: 'auditd', + name: 'auditd.log.geoip.city_name', + type: 'alias', + }, + 'auditd.log.geoip.region_iso_code': { + category: 'auditd', + name: 'auditd.log.geoip.region_iso_code', + type: 'alias', + }, + 'auditd.log.arch': { + category: 'auditd', + name: 'auditd.log.arch', + type: 'alias', + }, + 'auditd.log.gid': { + category: 'auditd', + name: 'auditd.log.gid', + type: 'alias', + }, + 'auditd.log.uid': { + category: 'auditd', + name: 'auditd.log.uid', + type: 'alias', + }, + 'auditd.log.agid': { + category: 'auditd', + name: 'auditd.log.agid', + type: 'alias', + }, + 'auditd.log.auid': { + category: 'auditd', + name: 'auditd.log.auid', + type: 'alias', + }, + 'auditd.log.fsgid': { + category: 'auditd', + name: 'auditd.log.fsgid', + type: 'alias', + }, + 'auditd.log.fsuid': { + category: 'auditd', + name: 'auditd.log.fsuid', + type: 'alias', + }, + 'auditd.log.egid': { + category: 'auditd', + name: 'auditd.log.egid', + type: 'alias', + }, + 'auditd.log.euid': { + category: 'auditd', + name: 'auditd.log.euid', + type: 'alias', + }, + 'auditd.log.sgid': { + category: 'auditd', + name: 'auditd.log.sgid', + type: 'alias', + }, + 'auditd.log.suid': { + category: 'auditd', + name: 'auditd.log.suid', + type: 'alias', + }, + 'auditd.log.ogid': { + category: 'auditd', + name: 'auditd.log.ogid', + type: 'alias', + }, + 'auditd.log.ouid': { + category: 'auditd', + name: 'auditd.log.ouid', + type: 'alias', + }, + 'auditd.log.comm': { + category: 'auditd', + name: 'auditd.log.comm', + type: 'alias', + }, + 'auditd.log.exe': { + category: 'auditd', + name: 'auditd.log.exe', + type: 'alias', + }, + 'auditd.log.terminal': { + category: 'auditd', + name: 'auditd.log.terminal', + type: 'alias', + }, + 'auditd.log.msg': { + category: 'auditd', + name: 'auditd.log.msg', + type: 'alias', + }, + 'auditd.log.src': { + category: 'auditd', + name: 'auditd.log.src', + type: 'alias', + }, + 'auditd.log.dst': { + category: 'auditd', + name: 'auditd.log.dst', + type: 'alias', + }, + 'elasticsearch.component': { + category: 'elasticsearch', + description: 'Elasticsearch component from where the log event originated', + example: 'o.e.c.m.MetaDataCreateIndexService', + name: 'elasticsearch.component', + type: 'keyword', + }, + 'elasticsearch.cluster.uuid': { + category: 'elasticsearch', + description: 'UUID of the cluster', + example: 'GmvrbHlNTiSVYiPf8kxg9g', + name: 'elasticsearch.cluster.uuid', + type: 'keyword', + }, + 'elasticsearch.cluster.name': { + category: 'elasticsearch', + description: 'Name of the cluster', + example: 'docker-cluster', + name: 'elasticsearch.cluster.name', + type: 'keyword', + }, + 'elasticsearch.node.id': { + category: 'elasticsearch', + description: 'ID of the node', + example: 'DSiWcTyeThWtUXLB9J0BMw', + name: 'elasticsearch.node.id', + type: 'keyword', + }, + 'elasticsearch.node.name': { + category: 'elasticsearch', + description: 'Name of the node', + example: 'vWNJsZ3', + name: 'elasticsearch.node.name', + type: 'keyword', + }, + 'elasticsearch.index.name': { + category: 'elasticsearch', + description: 'Index name', + example: 'filebeat-test-input', + name: 'elasticsearch.index.name', + type: 'keyword', + }, + 'elasticsearch.index.id': { + category: 'elasticsearch', + description: 'Index id', + example: 'aOGgDwbURfCV57AScqbCgw', + name: 'elasticsearch.index.id', + type: 'keyword', + }, + 'elasticsearch.shard.id': { + category: 'elasticsearch', + description: 'Id of the shard', + example: '0', + name: 'elasticsearch.shard.id', + type: 'keyword', + }, + 'elasticsearch.audit.layer': { + category: 'elasticsearch', + description: 'The layer from which this event originated: rest, transport or ip_filter', + example: 'rest', + name: 'elasticsearch.audit.layer', + type: 'keyword', + }, + 'elasticsearch.audit.event_type': { + category: 'elasticsearch', + description: + 'The type of event that occurred: anonymous_access_denied, authentication_failed, access_denied, access_granted, connection_granted, connection_denied, tampered_request, run_as_granted, run_as_denied', + example: 'access_granted', + name: 'elasticsearch.audit.event_type', + type: 'keyword', + }, + 'elasticsearch.audit.origin.type': { + category: 'elasticsearch', + description: + 'Where the request originated: rest (request originated from a REST API request), transport (request was received on the transport channel), local_node (the local node issued the request)', + example: 'local_node', + name: 'elasticsearch.audit.origin.type', + type: 'keyword', + }, + 'elasticsearch.audit.realm': { + category: 'elasticsearch', + description: 'The authentication realm the authentication was validated against', + name: 'elasticsearch.audit.realm', + type: 'keyword', + }, + 'elasticsearch.audit.user.realm': { + category: 'elasticsearch', + description: "The user's authentication realm, if authenticated", + name: 'elasticsearch.audit.user.realm', + type: 'keyword', + }, + 'elasticsearch.audit.user.roles': { + category: 'elasticsearch', + description: 'Roles to which the principal belongs', + example: '["kibana_admin","beats_admin"]', + name: 'elasticsearch.audit.user.roles', + type: 'keyword', + }, + 'elasticsearch.audit.action': { + category: 'elasticsearch', + description: 'The name of the action that was executed', + example: 'cluster:monitor/main', + name: 'elasticsearch.audit.action', + type: 'keyword', + }, + 'elasticsearch.audit.url.params': { + category: 'elasticsearch', + description: 'REST URI parameters', + example: '{username=jacknich2}', + name: 'elasticsearch.audit.url.params', + }, + 'elasticsearch.audit.indices': { + category: 'elasticsearch', + description: 'Indices accessed by action', + example: '["foo-2019.01.04","foo-2019.01.03","foo-2019.01.06"]', + name: 'elasticsearch.audit.indices', + type: 'keyword', + }, + 'elasticsearch.audit.request.id': { + category: 'elasticsearch', + description: 'Unique ID of request', + example: 'WzL_kb6VSvOhAq0twPvHOQ', + name: 'elasticsearch.audit.request.id', + type: 'keyword', + }, + 'elasticsearch.audit.request.name': { + category: 'elasticsearch', + description: 'The type of request that was executed', + example: 'ClearScrollRequest', + name: 'elasticsearch.audit.request.name', + type: 'keyword', + }, + 'elasticsearch.audit.request_body': { + category: 'elasticsearch', + name: 'elasticsearch.audit.request_body', + type: 'alias', + }, + 'elasticsearch.audit.origin_address': { + category: 'elasticsearch', + name: 'elasticsearch.audit.origin_address', + type: 'alias', + }, + 'elasticsearch.audit.uri': { + category: 'elasticsearch', + name: 'elasticsearch.audit.uri', + type: 'alias', + }, + 'elasticsearch.audit.principal': { + category: 'elasticsearch', + name: 'elasticsearch.audit.principal', + type: 'alias', + }, + 'elasticsearch.audit.message': { + category: 'elasticsearch', + name: 'elasticsearch.audit.message', + type: 'text', + }, + 'elasticsearch.deprecation': { + category: 'elasticsearch', + description: '', + name: 'elasticsearch.deprecation', + type: 'group', + }, + 'elasticsearch.gc.phase.name': { + category: 'elasticsearch', + description: 'Name of the GC collection phase. ', + name: 'elasticsearch.gc.phase.name', + type: 'keyword', + }, + 'elasticsearch.gc.phase.duration_sec': { + category: 'elasticsearch', + description: 'Collection phase duration according to the Java virtual machine. ', + name: 'elasticsearch.gc.phase.duration_sec', + type: 'float', + }, + 'elasticsearch.gc.phase.scrub_symbol_table_time_sec': { + category: 'elasticsearch', + description: 'Pause time in seconds cleaning up symbol tables. ', + name: 'elasticsearch.gc.phase.scrub_symbol_table_time_sec', + type: 'float', + }, + 'elasticsearch.gc.phase.scrub_string_table_time_sec': { + category: 'elasticsearch', + description: 'Pause time in seconds cleaning up string tables. ', + name: 'elasticsearch.gc.phase.scrub_string_table_time_sec', + type: 'float', + }, + 'elasticsearch.gc.phase.weak_refs_processing_time_sec': { + category: 'elasticsearch', + description: 'Time spent processing weak references in seconds. ', + name: 'elasticsearch.gc.phase.weak_refs_processing_time_sec', + type: 'float', + }, + 'elasticsearch.gc.phase.parallel_rescan_time_sec': { + category: 'elasticsearch', + description: 'Time spent in seconds marking live objects while application is stopped. ', + name: 'elasticsearch.gc.phase.parallel_rescan_time_sec', + type: 'float', + }, + 'elasticsearch.gc.phase.class_unload_time_sec': { + category: 'elasticsearch', + description: 'Time spent unloading unused classes in seconds. ', + name: 'elasticsearch.gc.phase.class_unload_time_sec', + type: 'float', + }, + 'elasticsearch.gc.phase.cpu_time.user_sec': { + category: 'elasticsearch', + description: 'CPU time spent outside the kernel. ', + name: 'elasticsearch.gc.phase.cpu_time.user_sec', + type: 'float', + }, + 'elasticsearch.gc.phase.cpu_time.sys_sec': { + category: 'elasticsearch', + description: 'CPU time spent inside the kernel. ', + name: 'elasticsearch.gc.phase.cpu_time.sys_sec', + type: 'float', + }, + 'elasticsearch.gc.phase.cpu_time.real_sec': { + category: 'elasticsearch', + description: 'Total elapsed CPU time spent to complete the collection from start to finish. ', + name: 'elasticsearch.gc.phase.cpu_time.real_sec', + type: 'float', + }, + 'elasticsearch.gc.jvm_runtime_sec': { + category: 'elasticsearch', + description: 'The time from JVM start up in seconds, as a floating point number. ', + name: 'elasticsearch.gc.jvm_runtime_sec', + type: 'float', + }, + 'elasticsearch.gc.threads_total_stop_time_sec': { + category: 'elasticsearch', + description: 'Garbage collection threads total stop time seconds. ', + name: 'elasticsearch.gc.threads_total_stop_time_sec', + type: 'float', + }, + 'elasticsearch.gc.stopping_threads_time_sec': { + category: 'elasticsearch', + description: 'Time took to stop threads seconds. ', + name: 'elasticsearch.gc.stopping_threads_time_sec', + type: 'float', + }, + 'elasticsearch.gc.tags': { + category: 'elasticsearch', + description: 'GC logging tags. ', + name: 'elasticsearch.gc.tags', + type: 'keyword', + }, + 'elasticsearch.gc.heap.size_kb': { + category: 'elasticsearch', + description: 'Total heap size in kilobytes. ', + name: 'elasticsearch.gc.heap.size_kb', + type: 'integer', + }, + 'elasticsearch.gc.heap.used_kb': { + category: 'elasticsearch', + description: 'Used heap in kilobytes. ', + name: 'elasticsearch.gc.heap.used_kb', + type: 'integer', + }, + 'elasticsearch.gc.old_gen.size_kb': { + category: 'elasticsearch', + description: 'Total size of old generation in kilobytes. ', + name: 'elasticsearch.gc.old_gen.size_kb', + type: 'integer', + }, + 'elasticsearch.gc.old_gen.used_kb': { + category: 'elasticsearch', + description: 'Old generation occupancy in kilobytes. ', + name: 'elasticsearch.gc.old_gen.used_kb', + type: 'integer', + }, + 'elasticsearch.gc.young_gen.size_kb': { + category: 'elasticsearch', + description: 'Total size of young generation in kilobytes. ', + name: 'elasticsearch.gc.young_gen.size_kb', + type: 'integer', + }, + 'elasticsearch.gc.young_gen.used_kb': { + category: 'elasticsearch', + description: 'Young generation occupancy in kilobytes. ', + name: 'elasticsearch.gc.young_gen.used_kb', + type: 'integer', + }, + 'elasticsearch.server.stacktrace': { + category: 'elasticsearch', + name: 'elasticsearch.server.stacktrace', + }, + 'elasticsearch.server.gc.young.one': { + category: 'elasticsearch', + description: '', + example: '', + name: 'elasticsearch.server.gc.young.one', + type: 'long', + }, + 'elasticsearch.server.gc.young.two': { + category: 'elasticsearch', + description: '', + example: '', + name: 'elasticsearch.server.gc.young.two', + type: 'long', + }, + 'elasticsearch.server.gc.overhead_seq': { + category: 'elasticsearch', + description: 'Sequence number', + example: 3449992, + name: 'elasticsearch.server.gc.overhead_seq', + type: 'long', + }, + 'elasticsearch.server.gc.collection_duration.ms': { + category: 'elasticsearch', + description: 'Time spent in GC, in milliseconds', + example: 1600, + name: 'elasticsearch.server.gc.collection_duration.ms', + type: 'float', + }, + 'elasticsearch.server.gc.observation_duration.ms': { + category: 'elasticsearch', + description: 'Total time over which collection was observed, in milliseconds', + example: 1800, + name: 'elasticsearch.server.gc.observation_duration.ms', + type: 'float', + }, + 'elasticsearch.slowlog.logger': { + category: 'elasticsearch', + description: 'Logger name', + example: 'index.search.slowlog.fetch', + name: 'elasticsearch.slowlog.logger', + type: 'keyword', + }, + 'elasticsearch.slowlog.took': { + category: 'elasticsearch', + description: 'Time it took to execute the query', + example: '300ms', + name: 'elasticsearch.slowlog.took', + type: 'keyword', + }, + 'elasticsearch.slowlog.types': { + category: 'elasticsearch', + description: 'Types', + example: '', + name: 'elasticsearch.slowlog.types', + type: 'keyword', + }, + 'elasticsearch.slowlog.stats': { + category: 'elasticsearch', + description: 'Stats groups', + example: 'group1', + name: 'elasticsearch.slowlog.stats', + type: 'keyword', + }, + 'elasticsearch.slowlog.search_type': { + category: 'elasticsearch', + description: 'Search type', + example: 'QUERY_THEN_FETCH', + name: 'elasticsearch.slowlog.search_type', + type: 'keyword', + }, + 'elasticsearch.slowlog.source_query': { + category: 'elasticsearch', + description: 'Slow query', + example: '{"query":{"match_all":{"boost":1.0}}}', + name: 'elasticsearch.slowlog.source_query', + type: 'keyword', + }, + 'elasticsearch.slowlog.extra_source': { + category: 'elasticsearch', + description: 'Extra source information', + example: '', + name: 'elasticsearch.slowlog.extra_source', + type: 'keyword', + }, + 'elasticsearch.slowlog.total_hits': { + category: 'elasticsearch', + description: 'Total hits', + example: 42, + name: 'elasticsearch.slowlog.total_hits', + type: 'keyword', + }, + 'elasticsearch.slowlog.total_shards': { + category: 'elasticsearch', + description: 'Total queried shards', + example: 22, + name: 'elasticsearch.slowlog.total_shards', + type: 'keyword', + }, + 'elasticsearch.slowlog.routing': { + category: 'elasticsearch', + description: 'Routing', + example: 's01HZ2QBk9jw4gtgaFtn', + name: 'elasticsearch.slowlog.routing', + type: 'keyword', + }, + 'elasticsearch.slowlog.id': { + category: 'elasticsearch', + description: 'Id', + example: '', + name: 'elasticsearch.slowlog.id', + type: 'keyword', + }, + 'elasticsearch.slowlog.type': { + category: 'elasticsearch', + description: 'Type', + example: 'doc', + name: 'elasticsearch.slowlog.type', + type: 'keyword', + }, + 'elasticsearch.slowlog.source': { + category: 'elasticsearch', + description: 'Source of document that was indexed', + name: 'elasticsearch.slowlog.source', + type: 'keyword', + }, + 'haproxy.frontend_name': { + category: 'haproxy', + description: 'Name of the frontend (or listener) which received and processed the connection.', + name: 'haproxy.frontend_name', + }, + 'haproxy.backend_name': { + category: 'haproxy', + description: + 'Name of the backend (or listener) which was selected to manage the connection to the server.', + name: 'haproxy.backend_name', + }, + 'haproxy.server_name': { + category: 'haproxy', + description: 'Name of the last server to which the connection was sent.', + name: 'haproxy.server_name', + }, + 'haproxy.total_waiting_time_ms': { + category: 'haproxy', + description: 'Total time in milliseconds spent waiting in the various queues', + name: 'haproxy.total_waiting_time_ms', + type: 'long', + }, + 'haproxy.connection_wait_time_ms': { + category: 'haproxy', + description: + 'Total time in milliseconds spent waiting for the connection to establish to the final server', + name: 'haproxy.connection_wait_time_ms', + type: 'long', + }, + 'haproxy.bytes_read': { + category: 'haproxy', + description: 'Total number of bytes transmitted to the client when the log is emitted.', + name: 'haproxy.bytes_read', + type: 'long', + }, + 'haproxy.time_queue': { + category: 'haproxy', + description: 'Total time in milliseconds spent waiting in the various queues.', + name: 'haproxy.time_queue', + type: 'long', + }, + 'haproxy.time_backend_connect': { + category: 'haproxy', + description: + 'Total time in milliseconds spent waiting for the connection to establish to the final server, including retries.', + name: 'haproxy.time_backend_connect', + type: 'long', + }, + 'haproxy.server_queue': { + category: 'haproxy', + description: + 'Total number of requests which were processed before this one in the server queue.', + name: 'haproxy.server_queue', + type: 'long', + }, + 'haproxy.backend_queue': { + category: 'haproxy', + description: + "Total number of requests which were processed before this one in the backend's global queue.", + name: 'haproxy.backend_queue', + type: 'long', + }, + 'haproxy.bind_name': { + category: 'haproxy', + description: 'Name of the listening address which received the connection.', + name: 'haproxy.bind_name', + }, + 'haproxy.error_message': { + category: 'haproxy', + description: 'Error message logged by HAProxy in case of error.', + name: 'haproxy.error_message', + type: 'text', + }, + 'haproxy.source': { + category: 'haproxy', + description: 'The HAProxy source of the log', + name: 'haproxy.source', + type: 'keyword', + }, + 'haproxy.termination_state': { + category: 'haproxy', + description: 'Condition the session was in when the session ended.', + name: 'haproxy.termination_state', + }, + 'haproxy.mode': { + category: 'haproxy', + description: 'mode that the frontend is operating (TCP or HTTP)', + name: 'haproxy.mode', + type: 'keyword', + }, + 'haproxy.connections.active': { + category: 'haproxy', + description: + 'Total number of concurrent connections on the process when the session was logged.', + name: 'haproxy.connections.active', + type: 'long', + }, + 'haproxy.connections.frontend': { + category: 'haproxy', + description: + 'Total number of concurrent connections on the frontend when the session was logged.', + name: 'haproxy.connections.frontend', + type: 'long', + }, + 'haproxy.connections.backend': { + category: 'haproxy', + description: + 'Total number of concurrent connections handled by the backend when the session was logged.', + name: 'haproxy.connections.backend', + type: 'long', + }, + 'haproxy.connections.server': { + category: 'haproxy', + description: + 'Total number of concurrent connections still active on the server when the session was logged.', + name: 'haproxy.connections.server', + type: 'long', + }, + 'haproxy.connections.retries': { + category: 'haproxy', + description: + 'Number of connection retries experienced by this session when trying to connect to the server.', + name: 'haproxy.connections.retries', + type: 'long', + }, + 'haproxy.client.ip': { + category: 'haproxy', + name: 'haproxy.client.ip', + type: 'alias', + }, + 'haproxy.client.port': { + category: 'haproxy', + name: 'haproxy.client.port', + type: 'alias', + }, + 'haproxy.process_name': { + category: 'haproxy', + name: 'haproxy.process_name', + type: 'alias', + }, + 'haproxy.pid': { + category: 'haproxy', + name: 'haproxy.pid', + type: 'alias', + }, + 'haproxy.destination.port': { + category: 'haproxy', + name: 'haproxy.destination.port', + type: 'alias', + }, + 'haproxy.destination.ip': { + category: 'haproxy', + name: 'haproxy.destination.ip', + type: 'alias', + }, + 'haproxy.geoip.continent_name': { + category: 'haproxy', + name: 'haproxy.geoip.continent_name', + type: 'alias', + }, + 'haproxy.geoip.country_iso_code': { + category: 'haproxy', + name: 'haproxy.geoip.country_iso_code', + type: 'alias', + }, + 'haproxy.geoip.location': { + category: 'haproxy', + name: 'haproxy.geoip.location', + type: 'alias', + }, + 'haproxy.geoip.region_name': { + category: 'haproxy', + name: 'haproxy.geoip.region_name', + type: 'alias', + }, + 'haproxy.geoip.city_name': { + category: 'haproxy', + name: 'haproxy.geoip.city_name', + type: 'alias', + }, + 'haproxy.geoip.region_iso_code': { + category: 'haproxy', + name: 'haproxy.geoip.region_iso_code', + type: 'alias', + }, + 'haproxy.http.response.captured_cookie': { + category: 'haproxy', + description: + 'Optional "name=value" entry indicating that the client had this cookie in the response. ', + name: 'haproxy.http.response.captured_cookie', + }, + 'haproxy.http.response.captured_headers': { + category: 'haproxy', + description: + 'List of headers captured in the response due to the presence of the "capture response header" statement in the frontend. ', + name: 'haproxy.http.response.captured_headers', + type: 'keyword', + }, + 'haproxy.http.response.status_code': { + category: 'haproxy', + name: 'haproxy.http.response.status_code', + type: 'alias', + }, + 'haproxy.http.request.captured_cookie': { + category: 'haproxy', + description: + 'Optional "name=value" entry indicating that the server has returned a cookie with its request. ', + name: 'haproxy.http.request.captured_cookie', + }, + 'haproxy.http.request.captured_headers': { + category: 'haproxy', + description: + 'List of headers captured in the request due to the presence of the "capture request header" statement in the frontend. ', + name: 'haproxy.http.request.captured_headers', + type: 'keyword', + }, + 'haproxy.http.request.raw_request_line': { + category: 'haproxy', + description: + 'Complete HTTP request line, including the method, request and HTTP version string.', + name: 'haproxy.http.request.raw_request_line', + type: 'keyword', + }, + 'haproxy.http.request.time_wait_without_data_ms': { + category: 'haproxy', + description: + 'Total time in milliseconds spent waiting for the server to send a full HTTP response, not counting data.', + name: 'haproxy.http.request.time_wait_without_data_ms', + type: 'long', + }, + 'haproxy.http.request.time_wait_ms': { + category: 'haproxy', + description: + 'Total time in milliseconds spent waiting for a full HTTP request from the client (not counting body) after the first byte was received.', + name: 'haproxy.http.request.time_wait_ms', + type: 'long', + }, + 'haproxy.tcp.connection_waiting_time_ms': { + category: 'haproxy', + description: 'Total time in milliseconds elapsed between the accept and the last close', + name: 'haproxy.tcp.connection_waiting_time_ms', + type: 'long', + }, + 'icinga.debug.facility': { + category: 'icinga', + description: 'Specifies what component of Icinga logged the message. ', + name: 'icinga.debug.facility', + type: 'keyword', + }, + 'icinga.debug.severity': { + category: 'icinga', + name: 'icinga.debug.severity', + type: 'alias', + }, + 'icinga.debug.message': { + category: 'icinga', + name: 'icinga.debug.message', + type: 'alias', + }, + 'icinga.main.facility': { + category: 'icinga', + description: 'Specifies what component of Icinga logged the message. ', + name: 'icinga.main.facility', + type: 'keyword', + }, + 'icinga.main.severity': { + category: 'icinga', + name: 'icinga.main.severity', + type: 'alias', + }, + 'icinga.main.message': { + category: 'icinga', + name: 'icinga.main.message', + type: 'alias', + }, + 'icinga.startup.facility': { + category: 'icinga', + description: 'Specifies what component of Icinga logged the message. ', + name: 'icinga.startup.facility', + type: 'keyword', + }, + 'icinga.startup.severity': { + category: 'icinga', + name: 'icinga.startup.severity', + type: 'alias', + }, + 'icinga.startup.message': { + category: 'icinga', + name: 'icinga.startup.message', + type: 'alias', + }, + 'iis.access.sub_status': { + category: 'iis', + description: 'The HTTP substatus code. ', + name: 'iis.access.sub_status', + type: 'long', + }, + 'iis.access.win32_status': { + category: 'iis', + description: 'The Windows status code. ', + name: 'iis.access.win32_status', + type: 'long', + }, + 'iis.access.site_name': { + category: 'iis', + description: 'The site name and instance number. ', + name: 'iis.access.site_name', + type: 'keyword', + }, + 'iis.access.server_name': { + category: 'iis', + description: 'The name of the server on which the log file entry was generated. ', + name: 'iis.access.server_name', + type: 'keyword', + }, + 'iis.access.cookie': { + category: 'iis', + description: 'The content of the cookie sent or received, if any. ', + name: 'iis.access.cookie', + type: 'keyword', + }, + 'iis.access.body_received.bytes': { + category: 'iis', + name: 'iis.access.body_received.bytes', + type: 'alias', + }, + 'iis.access.body_sent.bytes': { + category: 'iis', + name: 'iis.access.body_sent.bytes', + type: 'alias', + }, + 'iis.access.server_ip': { + category: 'iis', + name: 'iis.access.server_ip', + type: 'alias', + }, + 'iis.access.method': { + category: 'iis', + name: 'iis.access.method', + type: 'alias', + }, + 'iis.access.url': { + category: 'iis', + name: 'iis.access.url', + type: 'alias', + }, + 'iis.access.query_string': { + category: 'iis', + name: 'iis.access.query_string', + type: 'alias', + }, + 'iis.access.port': { + category: 'iis', + name: 'iis.access.port', + type: 'alias', + }, + 'iis.access.user_name': { + category: 'iis', + name: 'iis.access.user_name', + type: 'alias', + }, + 'iis.access.remote_ip': { + category: 'iis', + name: 'iis.access.remote_ip', + type: 'alias', + }, + 'iis.access.referrer': { + category: 'iis', + name: 'iis.access.referrer', + type: 'alias', + }, + 'iis.access.response_code': { + category: 'iis', + name: 'iis.access.response_code', + type: 'alias', + }, + 'iis.access.http_version': { + category: 'iis', + name: 'iis.access.http_version', + type: 'alias', + }, + 'iis.access.hostname': { + category: 'iis', + name: 'iis.access.hostname', + type: 'alias', + }, + 'iis.access.user_agent.device': { + category: 'iis', + name: 'iis.access.user_agent.device', + type: 'alias', + }, + 'iis.access.user_agent.name': { + category: 'iis', + name: 'iis.access.user_agent.name', + type: 'alias', + }, + 'iis.access.user_agent.os': { + category: 'iis', + name: 'iis.access.user_agent.os', + type: 'alias', + }, + 'iis.access.user_agent.os_name': { + category: 'iis', + name: 'iis.access.user_agent.os_name', + type: 'alias', + }, + 'iis.access.user_agent.original': { + category: 'iis', + name: 'iis.access.user_agent.original', + type: 'alias', + }, + 'iis.access.geoip.continent_name': { + category: 'iis', + name: 'iis.access.geoip.continent_name', + type: 'alias', + }, + 'iis.access.geoip.country_iso_code': { + category: 'iis', + name: 'iis.access.geoip.country_iso_code', + type: 'alias', + }, + 'iis.access.geoip.location': { + category: 'iis', + name: 'iis.access.geoip.location', + type: 'alias', + }, + 'iis.access.geoip.region_name': { + category: 'iis', + name: 'iis.access.geoip.region_name', + type: 'alias', + }, + 'iis.access.geoip.city_name': { + category: 'iis', + name: 'iis.access.geoip.city_name', + type: 'alias', + }, + 'iis.access.geoip.region_iso_code': { + category: 'iis', + name: 'iis.access.geoip.region_iso_code', + type: 'alias', + }, + 'iis.error.reason_phrase': { + category: 'iis', + description: 'The HTTP reason phrase. ', + name: 'iis.error.reason_phrase', + type: 'keyword', + }, + 'iis.error.queue_name': { + category: 'iis', + description: 'The IIS application pool name. ', + name: 'iis.error.queue_name', + type: 'keyword', + }, + 'iis.error.remote_ip': { + category: 'iis', + name: 'iis.error.remote_ip', + type: 'alias', + }, + 'iis.error.remote_port': { + category: 'iis', + name: 'iis.error.remote_port', + type: 'alias', + }, + 'iis.error.server_ip': { + category: 'iis', + name: 'iis.error.server_ip', + type: 'alias', + }, + 'iis.error.server_port': { + category: 'iis', + name: 'iis.error.server_port', + type: 'alias', + }, + 'iis.error.http_version': { + category: 'iis', + name: 'iis.error.http_version', + type: 'alias', + }, + 'iis.error.method': { + category: 'iis', + name: 'iis.error.method', + type: 'alias', + }, + 'iis.error.url': { + category: 'iis', + name: 'iis.error.url', + type: 'alias', + }, + 'iis.error.response_code': { + category: 'iis', + name: 'iis.error.response_code', + type: 'alias', + }, + 'iis.error.geoip.continent_name': { + category: 'iis', + name: 'iis.error.geoip.continent_name', + type: 'alias', + }, + 'iis.error.geoip.country_iso_code': { + category: 'iis', + name: 'iis.error.geoip.country_iso_code', + type: 'alias', + }, + 'iis.error.geoip.location': { + category: 'iis', + name: 'iis.error.geoip.location', + type: 'alias', + }, + 'iis.error.geoip.region_name': { + category: 'iis', + name: 'iis.error.geoip.region_name', + type: 'alias', + }, + 'iis.error.geoip.city_name': { + category: 'iis', + name: 'iis.error.geoip.city_name', + type: 'alias', + }, + 'iis.error.geoip.region_iso_code': { + category: 'iis', + name: 'iis.error.geoip.region_iso_code', + type: 'alias', + }, + 'kafka.log.level': { + category: 'kafka', + name: 'kafka.log.level', + type: 'alias', + }, + 'kafka.log.message': { + category: 'kafka', + name: 'kafka.log.message', + type: 'alias', + }, + 'kafka.log.component': { + category: 'kafka', + description: 'Component the log is coming from. ', + name: 'kafka.log.component', + type: 'keyword', + }, + 'kafka.log.class': { + category: 'kafka', + description: 'Java class the log is coming from. ', + name: 'kafka.log.class', + type: 'keyword', + }, + 'kafka.log.thread': { + category: 'kafka', + description: 'Thread name the log is coming from. ', + name: 'kafka.log.thread', + type: 'keyword', + }, + 'kafka.log.trace.class': { + category: 'kafka', + description: 'Java class the trace is coming from. ', + name: 'kafka.log.trace.class', + type: 'keyword', + }, + 'kafka.log.trace.message': { + category: 'kafka', + description: 'Message part of the trace. ', + name: 'kafka.log.trace.message', + type: 'text', + }, + 'kibana.log.tags': { + category: 'kibana', + description: 'Kibana logging tags. ', + name: 'kibana.log.tags', + type: 'keyword', + }, + 'kibana.log.state': { + category: 'kibana', + description: 'Current state of Kibana. ', + name: 'kibana.log.state', + type: 'keyword', + }, + 'kibana.log.meta': { + category: 'kibana', + name: 'kibana.log.meta', + type: 'object', + }, + 'kibana.log.kibana.log.meta.req.headers.referer': { + category: 'kibana', + name: 'kibana.log.kibana.log.meta.req.headers.referer', + type: 'alias', + }, + 'kibana.log.kibana.log.meta.req.referer': { + category: 'kibana', + name: 'kibana.log.kibana.log.meta.req.referer', + type: 'alias', + }, + 'kibana.log.kibana.log.meta.req.headers.user-agent': { + category: 'kibana', + name: 'kibana.log.kibana.log.meta.req.headers.user-agent', + type: 'alias', + }, + 'kibana.log.kibana.log.meta.req.remoteAddress': { + category: 'kibana', + name: 'kibana.log.kibana.log.meta.req.remoteAddress', + type: 'alias', + }, + 'kibana.log.kibana.log.meta.req.url': { + category: 'kibana', + name: 'kibana.log.kibana.log.meta.req.url', + type: 'alias', + }, + 'kibana.log.kibana.log.meta.statusCode': { + category: 'kibana', + name: 'kibana.log.kibana.log.meta.statusCode', + type: 'alias', + }, + 'kibana.log.kibana.log.meta.method': { + category: 'kibana', + name: 'kibana.log.kibana.log.meta.method', + type: 'alias', + }, + 'logstash.log.module': { + category: 'logstash', + description: 'The module or class where the event originate. ', + name: 'logstash.log.module', + type: 'keyword', + }, + 'logstash.log.thread': { + category: 'logstash', + description: 'Information about the running thread where the log originate. ', + name: 'logstash.log.thread', + type: 'keyword', + }, + 'logstash.log.log_event': { + category: 'logstash', + description: 'key and value debugging information. ', + name: 'logstash.log.log_event', + type: 'object', + }, + 'logstash.log.pipeline_id': { + category: 'logstash', + description: 'The ID of the pipeline. ', + example: 'main', + name: 'logstash.log.pipeline_id', + type: 'keyword', + }, + 'logstash.log.message': { + category: 'logstash', + name: 'logstash.log.message', + type: 'alias', + }, + 'logstash.log.level': { + category: 'logstash', + name: 'logstash.log.level', + type: 'alias', + }, + 'logstash.slowlog.module': { + category: 'logstash', + description: 'The module or class where the event originate. ', + name: 'logstash.slowlog.module', + type: 'keyword', + }, + 'logstash.slowlog.thread': { + category: 'logstash', + description: 'Information about the running thread where the log originate. ', + name: 'logstash.slowlog.thread', + type: 'keyword', + }, + 'logstash.slowlog.event': { + category: 'logstash', + description: 'Raw dump of the original event ', + name: 'logstash.slowlog.event', + type: 'keyword', + }, + 'logstash.slowlog.plugin_name': { + category: 'logstash', + description: 'Name of the plugin ', + name: 'logstash.slowlog.plugin_name', + type: 'keyword', + }, + 'logstash.slowlog.plugin_type': { + category: 'logstash', + description: 'Type of the plugin: Inputs, Filters, Outputs or Codecs. ', + name: 'logstash.slowlog.plugin_type', + type: 'keyword', + }, + 'logstash.slowlog.took_in_millis': { + category: 'logstash', + description: 'Execution time for the plugin in milliseconds. ', + name: 'logstash.slowlog.took_in_millis', + type: 'long', + }, + 'logstash.slowlog.plugin_params': { + category: 'logstash', + description: 'String value of the plugin configuration ', + name: 'logstash.slowlog.plugin_params', + type: 'keyword', + }, + 'logstash.slowlog.plugin_params_object': { + category: 'logstash', + description: 'key -> value of the configuration used by the plugin. ', + name: 'logstash.slowlog.plugin_params_object', + type: 'object', + }, + 'logstash.slowlog.level': { + category: 'logstash', + name: 'logstash.slowlog.level', + type: 'alias', + }, + 'logstash.slowlog.took_in_nanos': { + category: 'logstash', + name: 'logstash.slowlog.took_in_nanos', + type: 'alias', + }, + 'mongodb.log.component': { + category: 'mongodb', + description: 'Functional categorization of message ', + example: 'COMMAND', + name: 'mongodb.log.component', + type: 'keyword', + }, + 'mongodb.log.context': { + category: 'mongodb', + description: 'Context of message ', + example: 'initandlisten', + name: 'mongodb.log.context', + type: 'keyword', + }, + 'mongodb.log.severity': { + category: 'mongodb', + name: 'mongodb.log.severity', + type: 'alias', + }, + 'mongodb.log.message': { + category: 'mongodb', + name: 'mongodb.log.message', + type: 'alias', + }, + 'mysql.thread_id': { + category: 'mysql', + description: 'The connection or thread ID for the query. ', + name: 'mysql.thread_id', + type: 'long', + }, + 'mysql.error.thread_id': { + category: 'mysql', + name: 'mysql.error.thread_id', + type: 'alias', + }, + 'mysql.error.level': { + category: 'mysql', + name: 'mysql.error.level', + type: 'alias', + }, + 'mysql.error.message': { + category: 'mysql', + name: 'mysql.error.message', + type: 'alias', + }, + 'mysql.slowlog.lock_time.sec': { + category: 'mysql', + description: + 'The amount of time the query waited for the lock to be available. The value is in seconds, as a floating point number. ', + name: 'mysql.slowlog.lock_time.sec', + type: 'float', + }, + 'mysql.slowlog.rows_sent': { + category: 'mysql', + description: 'The number of rows returned by the query. ', + name: 'mysql.slowlog.rows_sent', + type: 'long', + }, + 'mysql.slowlog.rows_examined': { + category: 'mysql', + description: 'The number of rows scanned by the query. ', + name: 'mysql.slowlog.rows_examined', + type: 'long', + }, + 'mysql.slowlog.rows_affected': { + category: 'mysql', + description: 'The number of rows modified by the query. ', + name: 'mysql.slowlog.rows_affected', + type: 'long', + }, + 'mysql.slowlog.bytes_sent': { + category: 'mysql', + description: 'The number of bytes sent to client. ', + name: 'mysql.slowlog.bytes_sent', + type: 'long', + format: 'bytes', + }, + 'mysql.slowlog.bytes_received': { + category: 'mysql', + description: 'The number of bytes received from client. ', + name: 'mysql.slowlog.bytes_received', + type: 'long', + format: 'bytes', + }, + 'mysql.slowlog.query': { + category: 'mysql', + description: 'The slow query. ', + name: 'mysql.slowlog.query', + }, + 'mysql.slowlog.id': { + category: 'mysql', + name: 'mysql.slowlog.id', + type: 'alias', + }, + 'mysql.slowlog.schema': { + category: 'mysql', + description: 'The schema where the slow query was executed. ', + name: 'mysql.slowlog.schema', + type: 'keyword', + }, + 'mysql.slowlog.current_user': { + category: 'mysql', + description: + 'Current authenticated user, used to determine access privileges. Can differ from the value for user. ', + name: 'mysql.slowlog.current_user', + type: 'keyword', + }, + 'mysql.slowlog.last_errno': { + category: 'mysql', + description: 'Last SQL error seen. ', + name: 'mysql.slowlog.last_errno', + type: 'keyword', + }, + 'mysql.slowlog.killed': { + category: 'mysql', + description: 'Code of the reason if the query was killed. ', + name: 'mysql.slowlog.killed', + type: 'keyword', + }, + 'mysql.slowlog.query_cache_hit': { + category: 'mysql', + description: 'Whether the query cache was hit. ', + name: 'mysql.slowlog.query_cache_hit', + type: 'boolean', + }, + 'mysql.slowlog.tmp_table': { + category: 'mysql', + description: 'Whether a temporary table was used to resolve the query. ', + name: 'mysql.slowlog.tmp_table', + type: 'boolean', + }, + 'mysql.slowlog.tmp_table_on_disk': { + category: 'mysql', + description: 'Whether the query needed temporary tables on disk. ', + name: 'mysql.slowlog.tmp_table_on_disk', + type: 'boolean', + }, + 'mysql.slowlog.tmp_tables': { + category: 'mysql', + description: 'Number of temporary tables created for this query ', + name: 'mysql.slowlog.tmp_tables', + type: 'long', + }, + 'mysql.slowlog.tmp_disk_tables': { + category: 'mysql', + description: 'Number of temporary tables created on disk for this query. ', + name: 'mysql.slowlog.tmp_disk_tables', + type: 'long', + }, + 'mysql.slowlog.tmp_table_sizes': { + category: 'mysql', + description: 'Size of temporary tables created for this query.', + name: 'mysql.slowlog.tmp_table_sizes', + type: 'long', + format: 'bytes', + }, + 'mysql.slowlog.filesort': { + category: 'mysql', + description: 'Whether filesort optimization was used. ', + name: 'mysql.slowlog.filesort', + type: 'boolean', + }, + 'mysql.slowlog.filesort_on_disk': { + category: 'mysql', + description: 'Whether filesort optimization was used and it needed temporary tables on disk. ', + name: 'mysql.slowlog.filesort_on_disk', + type: 'boolean', + }, + 'mysql.slowlog.priority_queue': { + category: 'mysql', + description: 'Whether a priority queue was used for filesort. ', + name: 'mysql.slowlog.priority_queue', + type: 'boolean', + }, + 'mysql.slowlog.full_scan': { + category: 'mysql', + description: 'Whether a full table scan was needed for the slow query. ', + name: 'mysql.slowlog.full_scan', + type: 'boolean', + }, + 'mysql.slowlog.full_join': { + category: 'mysql', + description: + 'Whether a full join was needed for the slow query (no indexes were used for joins). ', + name: 'mysql.slowlog.full_join', + type: 'boolean', + }, + 'mysql.slowlog.merge_passes': { + category: 'mysql', + description: 'Number of merge passes executed for the query. ', + name: 'mysql.slowlog.merge_passes', + type: 'long', + }, + 'mysql.slowlog.sort_merge_passes': { + category: 'mysql', + description: 'Number of merge passes that the sort algorithm has had to do. ', + name: 'mysql.slowlog.sort_merge_passes', + type: 'long', + }, + 'mysql.slowlog.sort_range_count': { + category: 'mysql', + description: 'Number of sorts that were done using ranges. ', + name: 'mysql.slowlog.sort_range_count', + type: 'long', + }, + 'mysql.slowlog.sort_rows': { + category: 'mysql', + description: 'Number of sorted rows. ', + name: 'mysql.slowlog.sort_rows', + type: 'long', + }, + 'mysql.slowlog.sort_scan_count': { + category: 'mysql', + description: 'Number of sorts that were done by scanning the table. ', + name: 'mysql.slowlog.sort_scan_count', + type: 'long', + }, + 'mysql.slowlog.log_slow_rate_type': { + category: 'mysql', + description: + 'Type of slow log rate limit, it can be `session` if the rate limit is applied per session, or `query` if it applies per query. ', + name: 'mysql.slowlog.log_slow_rate_type', + type: 'keyword', + }, + 'mysql.slowlog.log_slow_rate_limit': { + category: 'mysql', + description: + 'Slow log rate limit, a value of 100 means that one in a hundred queries or sessions are being logged. ', + name: 'mysql.slowlog.log_slow_rate_limit', + type: 'keyword', + }, + 'mysql.slowlog.read_first': { + category: 'mysql', + description: 'The number of times the first entry in an index was read. ', + name: 'mysql.slowlog.read_first', + type: 'long', + }, + 'mysql.slowlog.read_last': { + category: 'mysql', + description: 'The number of times the last key in an index was read. ', + name: 'mysql.slowlog.read_last', + type: 'long', + }, + 'mysql.slowlog.read_key': { + category: 'mysql', + description: 'The number of requests to read a row based on a key. ', + name: 'mysql.slowlog.read_key', + type: 'long', + }, + 'mysql.slowlog.read_next': { + category: 'mysql', + description: 'The number of requests to read the next row in key order. ', + name: 'mysql.slowlog.read_next', + type: 'long', + }, + 'mysql.slowlog.read_prev': { + category: 'mysql', + description: 'The number of requests to read the previous row in key order. ', + name: 'mysql.slowlog.read_prev', + type: 'long', + }, + 'mysql.slowlog.read_rnd': { + category: 'mysql', + description: 'The number of requests to read a row based on a fixed position. ', + name: 'mysql.slowlog.read_rnd', + type: 'long', + }, + 'mysql.slowlog.read_rnd_next': { + category: 'mysql', + description: 'The number of requests to read the next row in the data file. ', + name: 'mysql.slowlog.read_rnd_next', + type: 'long', + }, + 'mysql.slowlog.innodb.trx_id': { + category: 'mysql', + description: 'Transaction ID ', + name: 'mysql.slowlog.innodb.trx_id', + type: 'keyword', + }, + 'mysql.slowlog.innodb.io_r_ops': { + category: 'mysql', + description: 'Number of page read operations. ', + name: 'mysql.slowlog.innodb.io_r_ops', + type: 'long', + }, + 'mysql.slowlog.innodb.io_r_bytes': { + category: 'mysql', + description: 'Bytes read during page read operations. ', + name: 'mysql.slowlog.innodb.io_r_bytes', + type: 'long', + format: 'bytes', + }, + 'mysql.slowlog.innodb.io_r_wait.sec': { + category: 'mysql', + description: 'How long it took to read all needed data from storage. ', + name: 'mysql.slowlog.innodb.io_r_wait.sec', + type: 'long', + }, + 'mysql.slowlog.innodb.rec_lock_wait.sec': { + category: 'mysql', + description: 'How long the query waited for locks. ', + name: 'mysql.slowlog.innodb.rec_lock_wait.sec', + type: 'long', + }, + 'mysql.slowlog.innodb.queue_wait.sec': { + category: 'mysql', + description: + 'How long the query waited to enter the InnoDB queue and to be executed once in the queue. ', + name: 'mysql.slowlog.innodb.queue_wait.sec', + type: 'long', + }, + 'mysql.slowlog.innodb.pages_distinct': { + category: 'mysql', + description: 'Approximated count of pages accessed to execute the query. ', + name: 'mysql.slowlog.innodb.pages_distinct', + type: 'long', + }, + 'mysql.slowlog.user': { + category: 'mysql', + name: 'mysql.slowlog.user', + type: 'alias', + }, + 'mysql.slowlog.host': { + category: 'mysql', + name: 'mysql.slowlog.host', + type: 'alias', + }, + 'mysql.slowlog.ip': { + category: 'mysql', + name: 'mysql.slowlog.ip', + type: 'alias', + }, + 'nats.log.client.id': { + category: 'nats', + description: 'The id of the client ', + name: 'nats.log.client.id', + type: 'integer', + }, + 'nats.log.msg.bytes': { + category: 'nats', + description: 'Size of the payload in bytes ', + name: 'nats.log.msg.bytes', + type: 'long', + format: 'bytes', + }, + 'nats.log.msg.type': { + category: 'nats', + description: 'The protocol message type ', + name: 'nats.log.msg.type', + type: 'keyword', + }, + 'nats.log.msg.subject': { + category: 'nats', + description: 'Subject name this message was received on ', + name: 'nats.log.msg.subject', + type: 'keyword', + }, + 'nats.log.msg.sid': { + category: 'nats', + description: 'The unique alphanumeric subscription ID of the subject ', + name: 'nats.log.msg.sid', + type: 'integer', + }, + 'nats.log.msg.reply_to': { + category: 'nats', + description: 'The inbox subject on which the publisher is listening for responses ', + name: 'nats.log.msg.reply_to', + type: 'keyword', + }, + 'nats.log.msg.max_messages': { + category: 'nats', + description: 'An optional number of messages to wait for before automatically unsubscribing ', + name: 'nats.log.msg.max_messages', + type: 'integer', + }, + 'nats.log.msg.error.message': { + category: 'nats', + description: 'Details about the error occurred ', + name: 'nats.log.msg.error.message', + type: 'text', + }, + 'nats.log.msg.queue_group': { + category: 'nats', + description: 'The queue group which subscriber will join ', + name: 'nats.log.msg.queue_group', + type: 'text', + }, + 'nginx.access.remote_ip_list': { + category: 'nginx', + description: + 'An array of remote IP addresses. It is a list because it is common to include, besides the client IP address, IP addresses from headers like `X-Forwarded-For`. Real source IP is restored to `source.ip`. ', + name: 'nginx.access.remote_ip_list', + type: 'array', + }, + 'nginx.access.body_sent.bytes': { + category: 'nginx', + name: 'nginx.access.body_sent.bytes', + type: 'alias', + }, + 'nginx.access.user_name': { + category: 'nginx', + name: 'nginx.access.user_name', + type: 'alias', + }, + 'nginx.access.method': { + category: 'nginx', + name: 'nginx.access.method', + type: 'alias', + }, + 'nginx.access.url': { + category: 'nginx', + name: 'nginx.access.url', + type: 'alias', + }, + 'nginx.access.http_version': { + category: 'nginx', + name: 'nginx.access.http_version', + type: 'alias', + }, + 'nginx.access.response_code': { + category: 'nginx', + name: 'nginx.access.response_code', + type: 'alias', + }, + 'nginx.access.referrer': { + category: 'nginx', + name: 'nginx.access.referrer', + type: 'alias', + }, + 'nginx.access.agent': { + category: 'nginx', + name: 'nginx.access.agent', + type: 'alias', + }, + 'nginx.access.user_agent.device': { + category: 'nginx', + name: 'nginx.access.user_agent.device', + type: 'alias', + }, + 'nginx.access.user_agent.name': { + category: 'nginx', + name: 'nginx.access.user_agent.name', + type: 'alias', + }, + 'nginx.access.user_agent.os': { + category: 'nginx', + name: 'nginx.access.user_agent.os', + type: 'alias', + }, + 'nginx.access.user_agent.os_name': { + category: 'nginx', + name: 'nginx.access.user_agent.os_name', + type: 'alias', + }, + 'nginx.access.user_agent.original': { + category: 'nginx', + name: 'nginx.access.user_agent.original', + type: 'alias', + }, + 'nginx.access.geoip.continent_name': { + category: 'nginx', + name: 'nginx.access.geoip.continent_name', + type: 'alias', + }, + 'nginx.access.geoip.country_iso_code': { + category: 'nginx', + name: 'nginx.access.geoip.country_iso_code', + type: 'alias', + }, + 'nginx.access.geoip.location': { + category: 'nginx', + name: 'nginx.access.geoip.location', + type: 'alias', + }, + 'nginx.access.geoip.region_name': { + category: 'nginx', + name: 'nginx.access.geoip.region_name', + type: 'alias', + }, + 'nginx.access.geoip.city_name': { + category: 'nginx', + name: 'nginx.access.geoip.city_name', + type: 'alias', + }, + 'nginx.access.geoip.region_iso_code': { + category: 'nginx', + name: 'nginx.access.geoip.region_iso_code', + type: 'alias', + }, + 'nginx.error.connection_id': { + category: 'nginx', + description: 'Connection identifier. ', + name: 'nginx.error.connection_id', + type: 'long', + }, + 'nginx.error.level': { + category: 'nginx', + name: 'nginx.error.level', + type: 'alias', + }, + 'nginx.error.pid': { + category: 'nginx', + name: 'nginx.error.pid', + type: 'alias', + }, + 'nginx.error.tid': { + category: 'nginx', + name: 'nginx.error.tid', + type: 'alias', + }, + 'nginx.error.message': { + category: 'nginx', + name: 'nginx.error.message', + type: 'alias', + }, + 'nginx.ingress_controller.remote_ip_list': { + category: 'nginx', + description: + 'An array of remote IP addresses. It is a list because it is common to include, besides the client IP address, IP addresses from headers like `X-Forwarded-For`. Real source IP is restored to `source.ip`. ', + name: 'nginx.ingress_controller.remote_ip_list', + type: 'array', + }, + 'nginx.ingress_controller.http.request.length': { + category: 'nginx', + description: 'The request length (including request line, header, and request body) ', + name: 'nginx.ingress_controller.http.request.length', + type: 'long', + format: 'bytes', + }, + 'nginx.ingress_controller.http.request.time': { + category: 'nginx', + description: 'Time elapsed since the first bytes were read from the client ', + name: 'nginx.ingress_controller.http.request.time', + type: 'double', + format: 'duration', + }, + 'nginx.ingress_controller.upstream.name': { + category: 'nginx', + description: 'The name of the upstream. ', + name: 'nginx.ingress_controller.upstream.name', + type: 'keyword', + }, + 'nginx.ingress_controller.upstream.alternative_name': { + category: 'nginx', + description: 'The name of the alternative upstream. ', + name: 'nginx.ingress_controller.upstream.alternative_name', + type: 'keyword', + }, + 'nginx.ingress_controller.upstream.response.length': { + category: 'nginx', + description: 'The length of the response obtained from the upstream server ', + name: 'nginx.ingress_controller.upstream.response.length', + type: 'long', + format: 'bytes', + }, + 'nginx.ingress_controller.upstream.response.time': { + category: 'nginx', + description: + 'The time spent on receiving the response from the upstream server as seconds with millisecond resolution ', + name: 'nginx.ingress_controller.upstream.response.time', + type: 'double', + format: 'duration', + }, + 'nginx.ingress_controller.upstream.response.status_code': { + category: 'nginx', + description: 'The status code of the response obtained from the upstream server ', + name: 'nginx.ingress_controller.upstream.response.status_code', + type: 'long', + }, + 'nginx.ingress_controller.http.request.id': { + category: 'nginx', + description: 'The randomly generated ID of the request ', + name: 'nginx.ingress_controller.http.request.id', + type: 'keyword', + }, + 'nginx.ingress_controller.upstream.ip': { + category: 'nginx', + description: + 'The IP address of the upstream server. If several servers were contacted during request processing, their addresses are separated by commas. ', + name: 'nginx.ingress_controller.upstream.ip', + type: 'ip', + }, + 'nginx.ingress_controller.upstream.port': { + category: 'nginx', + description: 'The port of the upstream server. ', + name: 'nginx.ingress_controller.upstream.port', + type: 'long', + }, + 'nginx.ingress_controller.body_sent.bytes': { + category: 'nginx', + name: 'nginx.ingress_controller.body_sent.bytes', + type: 'alias', + }, + 'nginx.ingress_controller.user_name': { + category: 'nginx', + name: 'nginx.ingress_controller.user_name', + type: 'alias', + }, + 'nginx.ingress_controller.method': { + category: 'nginx', + name: 'nginx.ingress_controller.method', + type: 'alias', + }, + 'nginx.ingress_controller.url': { + category: 'nginx', + name: 'nginx.ingress_controller.url', + type: 'alias', + }, + 'nginx.ingress_controller.http_version': { + category: 'nginx', + name: 'nginx.ingress_controller.http_version', + type: 'alias', + }, + 'nginx.ingress_controller.response_code': { + category: 'nginx', + name: 'nginx.ingress_controller.response_code', + type: 'alias', + }, + 'nginx.ingress_controller.referrer': { + category: 'nginx', + name: 'nginx.ingress_controller.referrer', + type: 'alias', + }, + 'nginx.ingress_controller.agent': { + category: 'nginx', + name: 'nginx.ingress_controller.agent', + type: 'alias', + }, + 'nginx.ingress_controller.user_agent.device': { + category: 'nginx', + name: 'nginx.ingress_controller.user_agent.device', + type: 'alias', + }, + 'nginx.ingress_controller.user_agent.name': { + category: 'nginx', + name: 'nginx.ingress_controller.user_agent.name', + type: 'alias', + }, + 'nginx.ingress_controller.user_agent.os': { + category: 'nginx', + name: 'nginx.ingress_controller.user_agent.os', + type: 'alias', + }, + 'nginx.ingress_controller.user_agent.os_name': { + category: 'nginx', + name: 'nginx.ingress_controller.user_agent.os_name', + type: 'alias', + }, + 'nginx.ingress_controller.user_agent.original': { + category: 'nginx', + name: 'nginx.ingress_controller.user_agent.original', + type: 'alias', + }, + 'nginx.ingress_controller.geoip.continent_name': { + category: 'nginx', + name: 'nginx.ingress_controller.geoip.continent_name', + type: 'alias', + }, + 'nginx.ingress_controller.geoip.country_iso_code': { + category: 'nginx', + name: 'nginx.ingress_controller.geoip.country_iso_code', + type: 'alias', + }, + 'nginx.ingress_controller.geoip.location': { + category: 'nginx', + name: 'nginx.ingress_controller.geoip.location', + type: 'alias', + }, + 'nginx.ingress_controller.geoip.region_name': { + category: 'nginx', + name: 'nginx.ingress_controller.geoip.region_name', + type: 'alias', + }, + 'nginx.ingress_controller.geoip.city_name': { + category: 'nginx', + name: 'nginx.ingress_controller.geoip.city_name', + type: 'alias', + }, + 'nginx.ingress_controller.geoip.region_iso_code': { + category: 'nginx', + name: 'nginx.ingress_controller.geoip.region_iso_code', + type: 'alias', + }, + 'osquery.result.name': { + category: 'osquery', + description: 'The name of the query that generated this event. ', + name: 'osquery.result.name', + type: 'keyword', + }, + 'osquery.result.action': { + category: 'osquery', + description: + 'For incremental data, marks whether the entry was added or removed. It can be one of "added", "removed", or "snapshot". ', + name: 'osquery.result.action', + type: 'keyword', + }, + 'osquery.result.host_identifier': { + category: 'osquery', + description: + 'The identifier for the host on which the osquery agent is running. Normally the hostname. ', + name: 'osquery.result.host_identifier', + type: 'keyword', + }, + 'osquery.result.unix_time': { + category: 'osquery', + description: + 'Unix timestamp of the event, in seconds since the epoch. Used for computing the `@timestamp` column. ', + name: 'osquery.result.unix_time', + type: 'long', + }, + 'osquery.result.calendar_time': { + category: 'osquery', + description: 'String representation of the collection time, as formatted by osquery. ', + name: 'osquery.result.calendar_time', + type: 'keyword', + }, + 'postgresql.log.timestamp': { + category: 'postgresql', + description: 'The timestamp from the log line. ', + name: 'postgresql.log.timestamp', + }, + 'postgresql.log.core_id': { + category: 'postgresql', + description: 'Core id ', + name: 'postgresql.log.core_id', + type: 'long', + }, + 'postgresql.log.database': { + category: 'postgresql', + description: 'Name of database ', + example: 'mydb', + name: 'postgresql.log.database', + }, + 'postgresql.log.query': { + category: 'postgresql', + description: 'Query statement. ', + example: 'SELECT * FROM users;', + name: 'postgresql.log.query', + }, + 'postgresql.log.query_step': { + category: 'postgresql', + description: + 'Statement step when using extended query protocol (one of statement, parse, bind or execute) ', + example: 'parse', + name: 'postgresql.log.query_step', + }, + 'postgresql.log.query_name': { + category: 'postgresql', + description: + 'Name given to a query when using extended query protocol. If it is "<unnamed>", or not present, this field is ignored. ', + example: 'pdo_stmt_00000001', + name: 'postgresql.log.query_name', + }, + 'postgresql.log.error.code': { + category: 'postgresql', + description: 'Error code returned by Postgres (if any)', + name: 'postgresql.log.error.code', + type: 'long', + }, + 'postgresql.log.timezone': { + category: 'postgresql', + name: 'postgresql.log.timezone', + type: 'alias', + }, + 'postgresql.log.thread_id': { + category: 'postgresql', + name: 'postgresql.log.thread_id', + type: 'alias', + }, + 'postgresql.log.user': { + category: 'postgresql', + name: 'postgresql.log.user', + type: 'alias', + }, + 'postgresql.log.level': { + category: 'postgresql', + name: 'postgresql.log.level', + type: 'alias', + }, + 'postgresql.log.message': { + category: 'postgresql', + name: 'postgresql.log.message', + type: 'alias', + }, + 'redis.log.role': { + category: 'redis', + description: + 'The role of the Redis instance. Can be one of `master`, `slave`, `child` (for RDF/AOF writing child), or `sentinel`. ', + name: 'redis.log.role', + type: 'keyword', + }, + 'redis.log.pid': { + category: 'redis', + name: 'redis.log.pid', + type: 'alias', + }, + 'redis.log.level': { + category: 'redis', + name: 'redis.log.level', + type: 'alias', + }, + 'redis.log.message': { + category: 'redis', + name: 'redis.log.message', + type: 'alias', + }, + 'redis.slowlog.cmd': { + category: 'redis', + description: 'The command executed. ', + name: 'redis.slowlog.cmd', + type: 'keyword', + }, + 'redis.slowlog.duration.us': { + category: 'redis', + description: 'How long it took to execute the command in microseconds. ', + name: 'redis.slowlog.duration.us', + type: 'long', + }, + 'redis.slowlog.id': { + category: 'redis', + description: 'The ID of the query. ', + name: 'redis.slowlog.id', + type: 'long', + }, + 'redis.slowlog.key': { + category: 'redis', + description: 'The key on which the command was executed. ', + name: 'redis.slowlog.key', + type: 'keyword', + }, + 'redis.slowlog.args': { + category: 'redis', + description: 'The arguments with which the command was called. ', + name: 'redis.slowlog.args', + type: 'keyword', + }, + 'santa.action': { + category: 'santa', + description: 'Action', + example: 'EXEC', + name: 'santa.action', + type: 'keyword', + }, + 'santa.decision': { + category: 'santa', + description: 'Decision that santad took.', + example: 'ALLOW', + name: 'santa.decision', + type: 'keyword', + }, + 'santa.reason': { + category: 'santa', + description: 'Reason for the decsision.', + example: 'CERT', + name: 'santa.reason', + type: 'keyword', + }, + 'santa.mode': { + category: 'santa', + description: 'Operating mode of Santa.', + example: 'M', + name: 'santa.mode', + type: 'keyword', + }, + 'santa.disk.volume': { + category: 'santa', + description: 'The volume name.', + name: 'santa.disk.volume', + }, + 'santa.disk.bus': { + category: 'santa', + description: 'The disk bus protocol.', + name: 'santa.disk.bus', + }, + 'santa.disk.serial': { + category: 'santa', + description: 'The disk serial number.', + name: 'santa.disk.serial', + }, + 'santa.disk.bsdname': { + category: 'santa', + description: 'The disk BSD name.', + example: 'disk1s3', + name: 'santa.disk.bsdname', + }, + 'santa.disk.model': { + category: 'santa', + description: 'The disk model.', + example: 'APPLE SSD SM0512L', + name: 'santa.disk.model', + }, + 'santa.disk.fs': { + category: 'santa', + description: 'The disk volume kind (filesystem type).', + example: 'apfs', + name: 'santa.disk.fs', + }, + 'santa.disk.mount': { + category: 'santa', + description: 'The disk volume path.', + name: 'santa.disk.mount', + }, + 'santa.certificate.common_name': { + category: 'santa', + description: 'Common name from code signing certificate.', + name: 'santa.certificate.common_name', + type: 'keyword', + }, + 'santa.certificate.sha256': { + category: 'santa', + description: 'SHA256 hash of code signing certificate.', + name: 'santa.certificate.sha256', + type: 'keyword', + }, + 'system.auth.timestamp': { + category: 'system', + name: 'system.auth.timestamp', + type: 'alias', + }, + 'system.auth.hostname': { + category: 'system', + name: 'system.auth.hostname', + type: 'alias', + }, + 'system.auth.program': { + category: 'system', + name: 'system.auth.program', + type: 'alias', + }, + 'system.auth.pid': { + category: 'system', + name: 'system.auth.pid', + type: 'alias', + }, + 'system.auth.message': { + category: 'system', + name: 'system.auth.message', + type: 'alias', + }, + 'system.auth.user': { + category: 'system', + name: 'system.auth.user', + type: 'alias', + }, + 'system.auth.ssh.method': { + category: 'system', + description: 'The SSH authentication method. Can be one of "password" or "publickey". ', + name: 'system.auth.ssh.method', + }, + 'system.auth.ssh.signature': { + category: 'system', + description: 'The signature of the client public key. ', + name: 'system.auth.ssh.signature', + }, + 'system.auth.ssh.dropped_ip': { + category: 'system', + description: 'The client IP from SSH connections that are open and immediately dropped. ', + name: 'system.auth.ssh.dropped_ip', + type: 'ip', + }, + 'system.auth.ssh.event': { + category: 'system', + description: 'The SSH event as found in the logs (Accepted, Invalid, Failed, etc.) ', + example: 'Accepted', + name: 'system.auth.ssh.event', + }, + 'system.auth.ssh.ip': { + category: 'system', + name: 'system.auth.ssh.ip', + type: 'alias', + }, + 'system.auth.ssh.port': { + category: 'system', + name: 'system.auth.ssh.port', + type: 'alias', + }, + 'system.auth.ssh.geoip.continent_name': { + category: 'system', + name: 'system.auth.ssh.geoip.continent_name', + type: 'alias', + }, + 'system.auth.ssh.geoip.country_iso_code': { + category: 'system', + name: 'system.auth.ssh.geoip.country_iso_code', + type: 'alias', + }, + 'system.auth.ssh.geoip.location': { + category: 'system', + name: 'system.auth.ssh.geoip.location', + type: 'alias', + }, + 'system.auth.ssh.geoip.region_name': { + category: 'system', + name: 'system.auth.ssh.geoip.region_name', + type: 'alias', + }, + 'system.auth.ssh.geoip.city_name': { + category: 'system', + name: 'system.auth.ssh.geoip.city_name', + type: 'alias', + }, + 'system.auth.ssh.geoip.region_iso_code': { + category: 'system', + name: 'system.auth.ssh.geoip.region_iso_code', + type: 'alias', + }, + 'system.auth.sudo.error': { + category: 'system', + description: 'The error message in case the sudo command failed. ', + example: 'user NOT in sudoers', + name: 'system.auth.sudo.error', + }, + 'system.auth.sudo.tty': { + category: 'system', + description: 'The TTY where the sudo command is executed. ', + name: 'system.auth.sudo.tty', + }, + 'system.auth.sudo.pwd': { + category: 'system', + description: 'The current directory where the sudo command is executed. ', + name: 'system.auth.sudo.pwd', + }, + 'system.auth.sudo.user': { + category: 'system', + description: 'The target user to which the sudo command is switching. ', + example: 'root', + name: 'system.auth.sudo.user', + }, + 'system.auth.sudo.command': { + category: 'system', + description: 'The command executed via sudo. ', + name: 'system.auth.sudo.command', + }, + 'system.auth.useradd.home': { + category: 'system', + description: 'The home folder for the new user.', + name: 'system.auth.useradd.home', + }, + 'system.auth.useradd.shell': { + category: 'system', + description: 'The default shell for the new user.', + name: 'system.auth.useradd.shell', + }, + 'system.auth.useradd.name': { + category: 'system', + name: 'system.auth.useradd.name', + type: 'alias', + }, + 'system.auth.useradd.uid': { + category: 'system', + name: 'system.auth.useradd.uid', + type: 'alias', + }, + 'system.auth.useradd.gid': { + category: 'system', + name: 'system.auth.useradd.gid', + type: 'alias', + }, + 'system.auth.groupadd.name': { + category: 'system', + name: 'system.auth.groupadd.name', + type: 'alias', + }, + 'system.auth.groupadd.gid': { + category: 'system', + name: 'system.auth.groupadd.gid', + type: 'alias', + }, + 'system.syslog.timestamp': { + category: 'system', + name: 'system.syslog.timestamp', + type: 'alias', + }, + 'system.syslog.hostname': { + category: 'system', + name: 'system.syslog.hostname', + type: 'alias', + }, + 'system.syslog.program': { + category: 'system', + name: 'system.syslog.program', + type: 'alias', + }, + 'system.syslog.pid': { + category: 'system', + name: 'system.syslog.pid', + type: 'alias', + }, + 'system.syslog.message': { + category: 'system', + name: 'system.syslog.message', + type: 'alias', + }, + 'traefik.access.user_identifier': { + category: 'traefik', + description: 'Is the RFC 1413 identity of the client ', + name: 'traefik.access.user_identifier', + type: 'keyword', + }, + 'traefik.access.request_count': { + category: 'traefik', + description: 'The number of requests ', + name: 'traefik.access.request_count', + type: 'long', + }, + 'traefik.access.frontend_name': { + category: 'traefik', + description: 'The name of the frontend used ', + name: 'traefik.access.frontend_name', + type: 'keyword', + }, + 'traefik.access.backend_url': { + category: 'traefik', + description: 'The url of the backend where request is forwarded', + name: 'traefik.access.backend_url', + type: 'keyword', + }, + 'traefik.access.body_sent.bytes': { + category: 'traefik', + name: 'traefik.access.body_sent.bytes', + type: 'alias', + }, + 'traefik.access.remote_ip': { + category: 'traefik', + name: 'traefik.access.remote_ip', + type: 'alias', + }, + 'traefik.access.user_name': { + category: 'traefik', + name: 'traefik.access.user_name', + type: 'alias', + }, + 'traefik.access.method': { + category: 'traefik', + name: 'traefik.access.method', + type: 'alias', + }, + 'traefik.access.url': { + category: 'traefik', + name: 'traefik.access.url', + type: 'alias', + }, + 'traefik.access.http_version': { + category: 'traefik', + name: 'traefik.access.http_version', + type: 'alias', + }, + 'traefik.access.response_code': { + category: 'traefik', + name: 'traefik.access.response_code', + type: 'alias', + }, + 'traefik.access.referrer': { + category: 'traefik', + name: 'traefik.access.referrer', + type: 'alias', + }, + 'traefik.access.agent': { + category: 'traefik', + name: 'traefik.access.agent', + type: 'alias', + }, + 'traefik.access.user_agent.device': { + category: 'traefik', + name: 'traefik.access.user_agent.device', + type: 'alias', + }, + 'traefik.access.user_agent.name': { + category: 'traefik', + name: 'traefik.access.user_agent.name', + type: 'alias', + }, + 'traefik.access.user_agent.os': { + category: 'traefik', + name: 'traefik.access.user_agent.os', + type: 'alias', + }, + 'traefik.access.user_agent.os_name': { + category: 'traefik', + name: 'traefik.access.user_agent.os_name', + type: 'alias', + }, + 'traefik.access.user_agent.original': { + category: 'traefik', + name: 'traefik.access.user_agent.original', + type: 'alias', + }, + 'traefik.access.geoip.continent_name': { + category: 'traefik', + name: 'traefik.access.geoip.continent_name', + type: 'alias', + }, + 'traefik.access.geoip.country_iso_code': { + category: 'traefik', + name: 'traefik.access.geoip.country_iso_code', + type: 'alias', + }, + 'traefik.access.geoip.location': { + category: 'traefik', + name: 'traefik.access.geoip.location', + type: 'alias', + }, + 'traefik.access.geoip.region_name': { + category: 'traefik', + name: 'traefik.access.geoip.region_name', + type: 'alias', + }, + 'traefik.access.geoip.city_name': { + category: 'traefik', + name: 'traefik.access.geoip.city_name', + type: 'alias', + }, + 'traefik.access.geoip.region_iso_code': { + category: 'traefik', + name: 'traefik.access.geoip.region_iso_code', + type: 'alias', + }, + 'activemq.caller': { + category: 'activemq', + description: 'Name of the caller issuing the logging request (class or resource). ', + name: 'activemq.caller', + type: 'keyword', + }, + 'activemq.thread': { + category: 'activemq', + description: 'Thread that generated the logging event. ', + name: 'activemq.thread', + type: 'keyword', + }, + 'activemq.user': { + category: 'activemq', + description: 'User that generated the logging event. ', + name: 'activemq.user', + type: 'keyword', + }, + 'activemq.audit': { + category: 'activemq', + description: 'Fields from ActiveMQ audit logs. ', + name: 'activemq.audit', + type: 'group', + }, + 'activemq.log.stack_trace': { + category: 'activemq', + name: 'activemq.log.stack_trace', + type: 'keyword', + }, + 'aws.cloudtrail.event_version': { + category: 'aws', + description: 'The CloudTrail version of the log event format. ', + name: 'aws.cloudtrail.event_version', + type: 'keyword', + }, + 'aws.cloudtrail.user_identity.type': { + category: 'aws', + description: 'The type of the identity ', + name: 'aws.cloudtrail.user_identity.type', + type: 'keyword', + }, + 'aws.cloudtrail.user_identity.arn': { + category: 'aws', + description: 'The Amazon Resource Name (ARN) of the principal that made the call.', + name: 'aws.cloudtrail.user_identity.arn', + type: 'keyword', + }, + 'aws.cloudtrail.user_identity.access_key_id': { + category: 'aws', + description: 'The access key ID that was used to sign the request.', + name: 'aws.cloudtrail.user_identity.access_key_id', + type: 'keyword', + }, + 'aws.cloudtrail.user_identity.session_context.mfa_authenticated': { + category: 'aws', + description: + 'The value is true if the root user or IAM user whose credentials were used for the request also was authenticated with an MFA device; otherwise, false.', + name: 'aws.cloudtrail.user_identity.session_context.mfa_authenticated', + type: 'keyword', + }, + 'aws.cloudtrail.user_identity.session_context.creation_date': { + category: 'aws', + description: 'The date and time when the temporary security credentials were issued.', + name: 'aws.cloudtrail.user_identity.session_context.creation_date', + type: 'date', + }, + 'aws.cloudtrail.user_identity.session_context.session_issuer.type': { + category: 'aws', + description: + 'The source of the temporary security credentials, such as Root, IAMUser, or Role.', + name: 'aws.cloudtrail.user_identity.session_context.session_issuer.type', + type: 'keyword', + }, + 'aws.cloudtrail.user_identity.session_context.session_issuer.principal_id': { + category: 'aws', + description: 'The internal ID of the entity that was used to get credentials.', + name: 'aws.cloudtrail.user_identity.session_context.session_issuer.principal_id', + type: 'keyword', + }, + 'aws.cloudtrail.user_identity.session_context.session_issuer.arn': { + category: 'aws', + description: + 'The ARN of the source (account, IAM user, or role) that was used to get temporary security credentials.', + name: 'aws.cloudtrail.user_identity.session_context.session_issuer.arn', + type: 'keyword', + }, + 'aws.cloudtrail.user_identity.session_context.session_issuer.account_id': { + category: 'aws', + description: 'The account that owns the entity that was used to get credentials.', + name: 'aws.cloudtrail.user_identity.session_context.session_issuer.account_id', + type: 'keyword', + }, + 'aws.cloudtrail.user_identity.invoked_by': { + category: 'aws', + description: + 'The name of the AWS service that made the request, such as Amazon EC2 Auto Scaling or AWS Elastic Beanstalk.', + name: 'aws.cloudtrail.user_identity.invoked_by', + type: 'keyword', + }, + 'aws.cloudtrail.error_code': { + category: 'aws', + description: 'The AWS service error if the request returns an error.', + name: 'aws.cloudtrail.error_code', + type: 'keyword', + }, + 'aws.cloudtrail.error_message': { + category: 'aws', + description: 'If the request returns an error, the description of the error.', + name: 'aws.cloudtrail.error_message', + type: 'keyword', + }, + 'aws.cloudtrail.request_parameters': { + category: 'aws', + description: 'The parameters, if any, that were sent with the request.', + name: 'aws.cloudtrail.request_parameters', + type: 'keyword', + }, + 'aws.cloudtrail.response_elements': { + category: 'aws', + description: + 'The response element for actions that make changes (create, update, or delete actions).', + name: 'aws.cloudtrail.response_elements', + type: 'keyword', + }, + 'aws.cloudtrail.additional_eventdata': { + category: 'aws', + description: 'Additional data about the event that was not part of the request or response.', + name: 'aws.cloudtrail.additional_eventdata', + type: 'keyword', + }, + 'aws.cloudtrail.request_id': { + category: 'aws', + description: + 'The value that identifies the request. The service being called generates this value.', + name: 'aws.cloudtrail.request_id', + type: 'keyword', + }, + 'aws.cloudtrail.event_type': { + category: 'aws', + description: 'Identifies the type of event that generated the event record.', + name: 'aws.cloudtrail.event_type', + type: 'keyword', + }, + 'aws.cloudtrail.api_version': { + category: 'aws', + description: 'Identifies the API version associated with the AwsApiCall eventType value.', + name: 'aws.cloudtrail.api_version', + type: 'keyword', + }, + 'aws.cloudtrail.management_event': { + category: 'aws', + description: 'A Boolean value that identifies whether the event is a management event.', + name: 'aws.cloudtrail.management_event', + type: 'keyword', + }, + 'aws.cloudtrail.read_only': { + category: 'aws', + description: 'Identifies whether this operation is a read-only operation.', + name: 'aws.cloudtrail.read_only', + type: 'keyword', + }, + 'aws.cloudtrail.resources.arn': { + category: 'aws', + description: 'Resource ARNs', + name: 'aws.cloudtrail.resources.arn', + type: 'keyword', + }, + 'aws.cloudtrail.resources.account_id': { + category: 'aws', + description: 'Account ID of the resource owner', + name: 'aws.cloudtrail.resources.account_id', + type: 'keyword', + }, + 'aws.cloudtrail.resources.type': { + category: 'aws', + description: 'Resource type identifier in the format: AWS::aws-service-name::data-type-name', + name: 'aws.cloudtrail.resources.type', + type: 'keyword', + }, + 'aws.cloudtrail.recipient_account_id': { + category: 'aws', + description: 'Represents the account ID that received this event.', + name: 'aws.cloudtrail.recipient_account_id', + type: 'keyword', + }, + 'aws.cloudtrail.service_event_details': { + category: 'aws', + description: 'Identifies the service event, including what triggered the event and the result.', + name: 'aws.cloudtrail.service_event_details', + type: 'keyword', + }, + 'aws.cloudtrail.shared_event_id': { + category: 'aws', + description: + 'GUID generated by CloudTrail to uniquely identify CloudTrail events from the same AWS action that is sent to different AWS accounts.', + name: 'aws.cloudtrail.shared_event_id', + type: 'keyword', + }, + 'aws.cloudtrail.vpc_endpoint_id': { + category: 'aws', + description: + 'Identifies the VPC endpoint in which requests were made from a VPC to another AWS service, such as Amazon S3.', + name: 'aws.cloudtrail.vpc_endpoint_id', + type: 'keyword', + }, + 'aws.cloudtrail.console_login.additional_eventdata.mobile_version': { + category: 'aws', + description: 'Identifies whether ConsoleLogin was from mobile version', + name: 'aws.cloudtrail.console_login.additional_eventdata.mobile_version', + type: 'boolean', + }, + 'aws.cloudtrail.console_login.additional_eventdata.login_to': { + category: 'aws', + description: 'URL for ConsoleLogin', + name: 'aws.cloudtrail.console_login.additional_eventdata.login_to', + type: 'keyword', + }, + 'aws.cloudtrail.console_login.additional_eventdata.mfa_used': { + category: 'aws', + description: 'Identifies whether multi factor authentication was used during ConsoleLogin', + name: 'aws.cloudtrail.console_login.additional_eventdata.mfa_used', + type: 'boolean', + }, + 'aws.cloudtrail.flattened.additional_eventdata': { + category: 'aws', + description: 'Additional data about the event that was not part of the request or response. ', + name: 'aws.cloudtrail.flattened.additional_eventdata', + type: 'flattened', + }, + 'aws.cloudtrail.flattened.request_parameters': { + category: 'aws', + description: 'The parameters, if any, that were sent with the request.', + name: 'aws.cloudtrail.flattened.request_parameters', + type: 'flattened', + }, + 'aws.cloudtrail.flattened.response_elements': { + category: 'aws', + description: + 'The response element for actions that make changes (create, update, or delete actions).', + name: 'aws.cloudtrail.flattened.response_elements', + type: 'flattened', + }, + 'aws.cloudtrail.flattened.service_event_details': { + category: 'aws', + description: 'Identifies the service event, including what triggered the event and the result.', + name: 'aws.cloudtrail.flattened.service_event_details', + type: 'flattened', + }, + 'aws.cloudwatch.message': { + category: 'aws', + description: 'CloudWatch log message. ', + name: 'aws.cloudwatch.message', + type: 'text', + }, + 'aws.ec2.ip_address': { + category: 'aws', + description: 'The internet address of the requester. ', + name: 'aws.ec2.ip_address', + type: 'keyword', + }, + 'aws.elb.name': { + category: 'aws', + description: 'The name of the load balancer. ', + name: 'aws.elb.name', + type: 'keyword', + }, + 'aws.elb.type': { + category: 'aws', + description: 'The type of the load balancer for v2 Load Balancers. ', + name: 'aws.elb.type', + type: 'keyword', + }, + 'aws.elb.target_group.arn': { + category: 'aws', + description: 'The ARN of the target group handling the request. ', + name: 'aws.elb.target_group.arn', + type: 'keyword', + }, + 'aws.elb.listener': { + category: 'aws', + description: 'The ELB listener that received the connection. ', + name: 'aws.elb.listener', + type: 'keyword', + }, + 'aws.elb.protocol': { + category: 'aws', + description: 'The protocol of the load balancer (http or tcp). ', + name: 'aws.elb.protocol', + type: 'keyword', + }, + 'aws.elb.request_processing_time.sec': { + category: 'aws', + description: + 'The total time in seconds since the connection or request is received until it is sent to a registered backend. ', + name: 'aws.elb.request_processing_time.sec', + type: 'float', + }, + 'aws.elb.backend_processing_time.sec': { + category: 'aws', + description: + 'The total time in seconds since the connection is sent to the backend till the backend starts responding. ', + name: 'aws.elb.backend_processing_time.sec', + type: 'float', + }, + 'aws.elb.response_processing_time.sec': { + category: 'aws', + description: + 'The total time in seconds since the response is received from the backend till it is sent to the client. ', + name: 'aws.elb.response_processing_time.sec', + type: 'float', + }, + 'aws.elb.connection_time.ms': { + category: 'aws', + description: + 'The total time of the connection in milliseconds, since it is opened till it is closed. ', + name: 'aws.elb.connection_time.ms', + type: 'long', + }, + 'aws.elb.tls_handshake_time.ms': { + category: 'aws', + description: + 'The total time for the TLS handshake to complete in milliseconds once the connection has been established. ', + name: 'aws.elb.tls_handshake_time.ms', + type: 'long', + }, + 'aws.elb.backend.ip': { + category: 'aws', + description: 'The IP address of the backend processing this connection. ', + name: 'aws.elb.backend.ip', + type: 'keyword', + }, + 'aws.elb.backend.port': { + category: 'aws', + description: 'The port in the backend processing this connection. ', + name: 'aws.elb.backend.port', + type: 'keyword', + }, + 'aws.elb.backend.http.response.status_code': { + category: 'aws', + description: + 'The status code from the backend (status code sent to the client from ELB is stored in `http.response.status_code` ', + name: 'aws.elb.backend.http.response.status_code', + type: 'keyword', + }, + 'aws.elb.ssl_cipher': { + category: 'aws', + description: 'The SSL cipher used in TLS/SSL connections. ', + name: 'aws.elb.ssl_cipher', + type: 'keyword', + }, + 'aws.elb.ssl_protocol': { + category: 'aws', + description: 'The SSL protocol used in TLS/SSL connections. ', + name: 'aws.elb.ssl_protocol', + type: 'keyword', + }, + 'aws.elb.chosen_cert.arn': { + category: 'aws', + description: + 'The ARN of the chosen certificate presented to the client in TLS/SSL connections. ', + name: 'aws.elb.chosen_cert.arn', + type: 'keyword', + }, + 'aws.elb.chosen_cert.serial': { + category: 'aws', + description: + 'The serial number of the chosen certificate presented to the client in TLS/SSL connections. ', + name: 'aws.elb.chosen_cert.serial', + type: 'keyword', + }, + 'aws.elb.incoming_tls_alert': { + category: 'aws', + description: + 'The integer value of TLS alerts received by the load balancer from the client, if present. ', + name: 'aws.elb.incoming_tls_alert', + type: 'keyword', + }, + 'aws.elb.tls_named_group': { + category: 'aws', + description: 'The TLS named group. ', + name: 'aws.elb.tls_named_group', + type: 'keyword', + }, + 'aws.elb.trace_id': { + category: 'aws', + description: 'The contents of the `X-Amzn-Trace-Id` header. ', + name: 'aws.elb.trace_id', + type: 'keyword', + }, + 'aws.elb.matched_rule_priority': { + category: 'aws', + description: 'The priority value of the rule that matched the request, if a rule matched. ', + name: 'aws.elb.matched_rule_priority', + type: 'keyword', + }, + 'aws.elb.action_executed': { + category: 'aws', + description: + 'The action executed when processing the request (forward, fixed-response, authenticate...). It can contain several values. ', + name: 'aws.elb.action_executed', + type: 'keyword', + }, + 'aws.elb.redirect_url': { + category: 'aws', + description: 'The URL used if a redirection action was executed. ', + name: 'aws.elb.redirect_url', + type: 'keyword', + }, + 'aws.elb.error.reason': { + category: 'aws', + description: 'The error reason if the executed action failed. ', + name: 'aws.elb.error.reason', + type: 'keyword', + }, + 'aws.s3access.bucket_owner': { + category: 'aws', + description: 'The canonical user ID of the owner of the source bucket. ', + name: 'aws.s3access.bucket_owner', + type: 'keyword', + }, + 'aws.s3access.bucket': { + category: 'aws', + description: 'The name of the bucket that the request was processed against. ', + name: 'aws.s3access.bucket', + type: 'keyword', + }, + 'aws.s3access.remote_ip': { + category: 'aws', + description: 'The apparent internet address of the requester. ', + name: 'aws.s3access.remote_ip', + type: 'ip', + }, + 'aws.s3access.requester': { + category: 'aws', + description: 'The canonical user ID of the requester, or a - for unauthenticated requests. ', + name: 'aws.s3access.requester', + type: 'keyword', + }, + 'aws.s3access.request_id': { + category: 'aws', + description: 'A string generated by Amazon S3 to uniquely identify each request. ', + name: 'aws.s3access.request_id', + type: 'keyword', + }, + 'aws.s3access.operation': { + category: 'aws', + description: + 'The operation listed here is declared as SOAP.operation, REST.HTTP_method.resource_type, WEBSITE.HTTP_method.resource_type, or BATCH.DELETE.OBJECT. ', + name: 'aws.s3access.operation', + type: 'keyword', + }, + 'aws.s3access.key': { + category: 'aws', + description: + 'The "key" part of the request, URL encoded, or "-" if the operation does not take a key parameter. ', + name: 'aws.s3access.key', + type: 'keyword', + }, + 'aws.s3access.request_uri': { + category: 'aws', + description: 'The Request-URI part of the HTTP request message. ', + name: 'aws.s3access.request_uri', + type: 'keyword', + }, + 'aws.s3access.http_status': { + category: 'aws', + description: 'The numeric HTTP status code of the response. ', + name: 'aws.s3access.http_status', + type: 'long', + }, + 'aws.s3access.error_code': { + category: 'aws', + description: 'The Amazon S3 Error Code, or "-" if no error occurred. ', + name: 'aws.s3access.error_code', + type: 'keyword', + }, + 'aws.s3access.bytes_sent': { + category: 'aws', + description: + 'The number of response bytes sent, excluding HTTP protocol overhead, or "-" if zero. ', + name: 'aws.s3access.bytes_sent', + type: 'long', + }, + 'aws.s3access.object_size': { + category: 'aws', + description: 'The total size of the object in question. ', + name: 'aws.s3access.object_size', + type: 'long', + }, + 'aws.s3access.total_time': { + category: 'aws', + description: + "The number of milliseconds the request was in flight from the server's perspective. ", + name: 'aws.s3access.total_time', + type: 'long', + }, + 'aws.s3access.turn_around_time': { + category: 'aws', + description: 'The number of milliseconds that Amazon S3 spent processing your request. ', + name: 'aws.s3access.turn_around_time', + type: 'long', + }, + 'aws.s3access.referrer': { + category: 'aws', + description: 'The value of the HTTP Referrer header, if present. ', + name: 'aws.s3access.referrer', + type: 'keyword', + }, + 'aws.s3access.user_agent': { + category: 'aws', + description: 'The value of the HTTP User-Agent header. ', + name: 'aws.s3access.user_agent', + type: 'keyword', + }, + 'aws.s3access.version_id': { + category: 'aws', + description: + 'The version ID in the request, or "-" if the operation does not take a versionId parameter. ', + name: 'aws.s3access.version_id', + type: 'keyword', + }, + 'aws.s3access.host_id': { + category: 'aws', + description: 'The x-amz-id-2 or Amazon S3 extended request ID. ', + name: 'aws.s3access.host_id', + type: 'keyword', + }, + 'aws.s3access.signature_version': { + category: 'aws', + description: + 'The signature version, SigV2 or SigV4, that was used to authenticate the request or a - for unauthenticated requests. ', + name: 'aws.s3access.signature_version', + type: 'keyword', + }, + 'aws.s3access.cipher_suite': { + category: 'aws', + description: + 'The Secure Sockets Layer (SSL) cipher that was negotiated for HTTPS request or a - for HTTP. ', + name: 'aws.s3access.cipher_suite', + type: 'keyword', + }, + 'aws.s3access.authentication_type': { + category: 'aws', + description: + 'The type of request authentication used, AuthHeader for authentication headers, QueryString for query string (pre-signed URL) or a - for unauthenticated requests. ', + name: 'aws.s3access.authentication_type', + type: 'keyword', + }, + 'aws.s3access.host_header': { + category: 'aws', + description: 'The endpoint used to connect to Amazon S3. ', + name: 'aws.s3access.host_header', + type: 'keyword', + }, + 'aws.s3access.tls_version': { + category: 'aws', + description: 'The Transport Layer Security (TLS) version negotiated by the client. ', + name: 'aws.s3access.tls_version', + type: 'keyword', + }, + 'aws.vpcflow.version': { + category: 'aws', + description: + 'The VPC Flow Logs version. If you use the default format, the version is 2. If you specify a custom format, the version is 3. ', + name: 'aws.vpcflow.version', + type: 'keyword', + }, + 'aws.vpcflow.account_id': { + category: 'aws', + description: 'The AWS account ID for the flow log. ', + name: 'aws.vpcflow.account_id', + type: 'keyword', + }, + 'aws.vpcflow.interface_id': { + category: 'aws', + description: 'The ID of the network interface for which the traffic is recorded. ', + name: 'aws.vpcflow.interface_id', + type: 'keyword', + }, + 'aws.vpcflow.action': { + category: 'aws', + description: 'The action that is associated with the traffic, ACCEPT or REJECT. ', + name: 'aws.vpcflow.action', + type: 'keyword', + }, + 'aws.vpcflow.log_status': { + category: 'aws', + description: 'The logging status of the flow log, OK, NODATA or SKIPDATA. ', + name: 'aws.vpcflow.log_status', + type: 'keyword', + }, + 'aws.vpcflow.instance_id': { + category: 'aws', + description: + "The ID of the instance that's associated with network interface for which the traffic is recorded, if the instance is owned by you. ", + name: 'aws.vpcflow.instance_id', + type: 'keyword', + }, + 'aws.vpcflow.pkt_srcaddr': { + category: 'aws', + description: 'The packet-level (original) source IP address of the traffic. ', + name: 'aws.vpcflow.pkt_srcaddr', + type: 'ip', + }, + 'aws.vpcflow.pkt_dstaddr': { + category: 'aws', + description: 'The packet-level (original) destination IP address for the traffic. ', + name: 'aws.vpcflow.pkt_dstaddr', + type: 'ip', + }, + 'aws.vpcflow.vpc_id': { + category: 'aws', + description: + 'The ID of the VPC that contains the network interface for which the traffic is recorded. ', + name: 'aws.vpcflow.vpc_id', + type: 'keyword', + }, + 'aws.vpcflow.subnet_id': { + category: 'aws', + description: + 'The ID of the subnet that contains the network interface for which the traffic is recorded. ', + name: 'aws.vpcflow.subnet_id', + type: 'keyword', + }, + 'aws.vpcflow.tcp_flags': { + category: 'aws', + description: 'The bitmask value for the following TCP flags: 2=SYN,18=SYN-ACK,1=FIN,4=RST ', + name: 'aws.vpcflow.tcp_flags', + type: 'keyword', + }, + 'aws.vpcflow.type': { + category: 'aws', + description: 'The type of traffic: IPv4, IPv6, or EFA. ', + name: 'aws.vpcflow.type', + type: 'keyword', + }, + 'azure.subscription_id': { + category: 'azure', + description: 'Azure subscription ID ', + name: 'azure.subscription_id', + type: 'keyword', + }, + 'azure.correlation_id': { + category: 'azure', + description: 'Correlation ID ', + name: 'azure.correlation_id', + type: 'keyword', + }, + 'azure.tenant_id': { + category: 'azure', + description: 'tenant ID ', + name: 'azure.tenant_id', + type: 'keyword', + }, + 'azure.resource.id': { + category: 'azure', + description: 'Resource ID ', + name: 'azure.resource.id', + type: 'keyword', + }, + 'azure.resource.group': { + category: 'azure', + description: 'Resource group ', + name: 'azure.resource.group', + type: 'keyword', + }, + 'azure.resource.provider': { + category: 'azure', + description: 'Resource type/namespace ', + name: 'azure.resource.provider', + type: 'keyword', + }, + 'azure.resource.namespace': { + category: 'azure', + description: 'Resource type/namespace ', + name: 'azure.resource.namespace', + type: 'keyword', + }, + 'azure.resource.name': { + category: 'azure', + description: 'Name ', + name: 'azure.resource.name', + type: 'keyword', + }, + 'azure.resource.authorization_rule': { + category: 'azure', + description: 'Authorization rule ', + name: 'azure.resource.authorization_rule', + type: 'keyword', + }, + 'azure.activitylogs.identity.claims_initiated_by_user.name': { + category: 'azure', + description: 'Name ', + name: 'azure.activitylogs.identity.claims_initiated_by_user.name', + type: 'keyword', + }, + 'azure.activitylogs.identity.claims_initiated_by_user.givenname': { + category: 'azure', + description: 'Givenname ', + name: 'azure.activitylogs.identity.claims_initiated_by_user.givenname', + type: 'keyword', + }, + 'azure.activitylogs.identity.claims_initiated_by_user.surname': { + category: 'azure', + description: 'Surname ', + name: 'azure.activitylogs.identity.claims_initiated_by_user.surname', + type: 'keyword', + }, + 'azure.activitylogs.identity.claims_initiated_by_user.fullname': { + category: 'azure', + description: 'Fullname ', + name: 'azure.activitylogs.identity.claims_initiated_by_user.fullname', + type: 'keyword', + }, + 'azure.activitylogs.identity.claims_initiated_by_user.schema': { + category: 'azure', + description: 'Schema ', + name: 'azure.activitylogs.identity.claims_initiated_by_user.schema', + type: 'keyword', + }, + 'azure.activitylogs.identity.claims.*': { + category: 'azure', + description: 'Claims ', + name: 'azure.activitylogs.identity.claims.*', + type: 'object', + }, + 'azure.activitylogs.identity.authorization.scope': { + category: 'azure', + description: 'Scope ', + name: 'azure.activitylogs.identity.authorization.scope', + type: 'keyword', + }, + 'azure.activitylogs.identity.authorization.action': { + category: 'azure', + description: 'Action ', + name: 'azure.activitylogs.identity.authorization.action', + type: 'keyword', + }, + 'azure.activitylogs.identity.authorization.evidence.role_assignment_scope': { + category: 'azure', + description: 'Role assignment scope ', + name: 'azure.activitylogs.identity.authorization.evidence.role_assignment_scope', + type: 'keyword', + }, + 'azure.activitylogs.identity.authorization.evidence.role_definition_id': { + category: 'azure', + description: 'Role definition ID ', + name: 'azure.activitylogs.identity.authorization.evidence.role_definition_id', + type: 'keyword', + }, + 'azure.activitylogs.identity.authorization.evidence.role': { + category: 'azure', + description: 'Role ', + name: 'azure.activitylogs.identity.authorization.evidence.role', + type: 'keyword', + }, + 'azure.activitylogs.identity.authorization.evidence.role_assignment_id': { + category: 'azure', + description: 'Role assignment ID ', + name: 'azure.activitylogs.identity.authorization.evidence.role_assignment_id', + type: 'keyword', + }, + 'azure.activitylogs.identity.authorization.evidence.principal_id': { + category: 'azure', + description: 'Principal ID ', + name: 'azure.activitylogs.identity.authorization.evidence.principal_id', + type: 'keyword', + }, + 'azure.activitylogs.identity.authorization.evidence.principal_type': { + category: 'azure', + description: 'Principal type ', + name: 'azure.activitylogs.identity.authorization.evidence.principal_type', + type: 'keyword', + }, + 'azure.activitylogs.operation_name': { + category: 'azure', + description: 'Operation name ', + name: 'azure.activitylogs.operation_name', + type: 'keyword', + }, + 'azure.activitylogs.result_type': { + category: 'azure', + description: 'Result type ', + name: 'azure.activitylogs.result_type', + type: 'keyword', + }, + 'azure.activitylogs.result_signature': { + category: 'azure', + description: 'Result signature ', + name: 'azure.activitylogs.result_signature', + type: 'keyword', + }, + 'azure.activitylogs.category': { + category: 'azure', + description: 'Category ', + name: 'azure.activitylogs.category', + type: 'keyword', + }, + 'azure.activitylogs.event_category': { + category: 'azure', + description: 'Event Category ', + name: 'azure.activitylogs.event_category', + type: 'keyword', + }, + 'azure.activitylogs.properties.service_request_id': { + category: 'azure', + description: 'Service Request Id ', + name: 'azure.activitylogs.properties.service_request_id', + type: 'keyword', + }, + 'azure.activitylogs.properties.status_code': { + category: 'azure', + description: 'Status code ', + name: 'azure.activitylogs.properties.status_code', + type: 'keyword', + }, + 'azure.auditlogs.category': { + category: 'azure', + description: 'The category of the operation. Currently, Audit is the only supported value. ', + name: 'azure.auditlogs.category', + type: 'keyword', + }, + 'azure.auditlogs.operation_name': { + category: 'azure', + description: 'The operation name ', + name: 'azure.auditlogs.operation_name', + type: 'keyword', + }, + 'azure.auditlogs.operation_version': { + category: 'azure', + description: 'The operation version ', + name: 'azure.auditlogs.operation_version', + type: 'keyword', + }, + 'azure.auditlogs.identity': { + category: 'azure', + description: 'Identity ', + name: 'azure.auditlogs.identity', + type: 'keyword', + }, + 'azure.auditlogs.tenant_id': { + category: 'azure', + description: 'Tenant ID ', + name: 'azure.auditlogs.tenant_id', + type: 'keyword', + }, + 'azure.auditlogs.result_signature': { + category: 'azure', + description: 'Result signature ', + name: 'azure.auditlogs.result_signature', + type: 'keyword', + }, + 'azure.auditlogs.properties.result': { + category: 'azure', + description: 'Log result ', + name: 'azure.auditlogs.properties.result', + type: 'keyword', + }, + 'azure.auditlogs.properties.activity_display_name': { + category: 'azure', + description: 'Activity display name ', + name: 'azure.auditlogs.properties.activity_display_name', + type: 'keyword', + }, + 'azure.auditlogs.properties.result_reason': { + category: 'azure', + description: 'Reason for the log result ', + name: 'azure.auditlogs.properties.result_reason', + type: 'keyword', + }, + 'azure.auditlogs.properties.correlation_id': { + category: 'azure', + description: 'Correlation ID ', + name: 'azure.auditlogs.properties.correlation_id', + type: 'keyword', + }, + 'azure.auditlogs.properties.logged_by_service': { + category: 'azure', + description: 'Logged by service ', + name: 'azure.auditlogs.properties.logged_by_service', + type: 'keyword', + }, + 'azure.auditlogs.properties.operation_type': { + category: 'azure', + description: 'Operation type ', + name: 'azure.auditlogs.properties.operation_type', + type: 'keyword', + }, + 'azure.auditlogs.properties.id': { + category: 'azure', + description: 'ID ', + name: 'azure.auditlogs.properties.id', + type: 'keyword', + }, + 'azure.auditlogs.properties.activity_datetime': { + category: 'azure', + description: 'Activity timestamp ', + name: 'azure.auditlogs.properties.activity_datetime', + type: 'date', + }, + 'azure.auditlogs.properties.category': { + category: 'azure', + description: 'category ', + name: 'azure.auditlogs.properties.category', + type: 'keyword', + }, + 'azure.auditlogs.properties.target_resources.*.display_name': { + category: 'azure', + description: 'Display name ', + name: 'azure.auditlogs.properties.target_resources.*.display_name', + type: 'keyword', + }, + 'azure.auditlogs.properties.target_resources.*.id': { + category: 'azure', + description: 'ID ', + name: 'azure.auditlogs.properties.target_resources.*.id', + type: 'keyword', + }, + 'azure.auditlogs.properties.target_resources.*.type': { + category: 'azure', + description: 'Type ', + name: 'azure.auditlogs.properties.target_resources.*.type', + type: 'keyword', + }, + 'azure.auditlogs.properties.target_resources.*.ip_address': { + category: 'azure', + description: 'ip Address ', + name: 'azure.auditlogs.properties.target_resources.*.ip_address', + type: 'keyword', + }, + 'azure.auditlogs.properties.target_resources.*.user_principal_name': { + category: 'azure', + description: 'User principal name ', + name: 'azure.auditlogs.properties.target_resources.*.user_principal_name', + type: 'keyword', + }, + 'azure.auditlogs.properties.target_resources.*.modified_properties.*.new_value': { + category: 'azure', + description: 'New value ', + name: 'azure.auditlogs.properties.target_resources.*.modified_properties.*.new_value', + type: 'keyword', + }, + 'azure.auditlogs.properties.target_resources.*.modified_properties.*.display_name': { + category: 'azure', + description: 'Display value ', + name: 'azure.auditlogs.properties.target_resources.*.modified_properties.*.display_name', + type: 'keyword', + }, + 'azure.auditlogs.properties.target_resources.*.modified_properties.*.old_value': { + category: 'azure', + description: 'Old value ', + name: 'azure.auditlogs.properties.target_resources.*.modified_properties.*.old_value', + type: 'keyword', + }, + 'azure.auditlogs.properties.initiated_by.app.servicePrincipalName': { + category: 'azure', + description: 'Service principal name ', + name: 'azure.auditlogs.properties.initiated_by.app.servicePrincipalName', + type: 'keyword', + }, + 'azure.auditlogs.properties.initiated_by.app.displayName': { + category: 'azure', + description: 'Display name ', + name: 'azure.auditlogs.properties.initiated_by.app.displayName', + type: 'keyword', + }, + 'azure.auditlogs.properties.initiated_by.app.appId': { + category: 'azure', + description: 'App ID ', + name: 'azure.auditlogs.properties.initiated_by.app.appId', + type: 'keyword', + }, + 'azure.auditlogs.properties.initiated_by.app.servicePrincipalId': { + category: 'azure', + description: 'Service principal ID ', + name: 'azure.auditlogs.properties.initiated_by.app.servicePrincipalId', + type: 'keyword', + }, + 'azure.auditlogs.properties.initiated_by.user.userPrincipalName': { + category: 'azure', + description: 'User principal name ', + name: 'azure.auditlogs.properties.initiated_by.user.userPrincipalName', + type: 'keyword', + }, + 'azure.auditlogs.properties.initiated_by.user.displayName': { + category: 'azure', + description: 'Display name ', + name: 'azure.auditlogs.properties.initiated_by.user.displayName', + type: 'keyword', + }, + 'azure.auditlogs.properties.initiated_by.user.id': { + category: 'azure', + description: 'ID ', + name: 'azure.auditlogs.properties.initiated_by.user.id', + type: 'keyword', + }, + 'azure.auditlogs.properties.initiated_by.user.ipAddress': { + category: 'azure', + description: 'ip Address ', + name: 'azure.auditlogs.properties.initiated_by.user.ipAddress', + type: 'keyword', + }, + 'azure.signinlogs.operation_name': { + category: 'azure', + description: 'The operation name ', + name: 'azure.signinlogs.operation_name', + type: 'keyword', + }, + 'azure.signinlogs.operation_version': { + category: 'azure', + description: 'The operation version ', + name: 'azure.signinlogs.operation_version', + type: 'keyword', + }, + 'azure.signinlogs.tenant_id': { + category: 'azure', + description: 'Tenant ID ', + name: 'azure.signinlogs.tenant_id', + type: 'keyword', + }, + 'azure.signinlogs.result_signature': { + category: 'azure', + description: 'Result signature ', + name: 'azure.signinlogs.result_signature', + type: 'keyword', + }, + 'azure.signinlogs.result_description': { + category: 'azure', + description: 'Result description ', + name: 'azure.signinlogs.result_description', + type: 'keyword', + }, + 'azure.signinlogs.result_type': { + category: 'azure', + description: 'Result type ', + name: 'azure.signinlogs.result_type', + type: 'keyword', + }, + 'azure.signinlogs.identity': { + category: 'azure', + description: 'Identity ', + name: 'azure.signinlogs.identity', + type: 'keyword', + }, + 'azure.signinlogs.category': { + category: 'azure', + description: 'Category ', + name: 'azure.signinlogs.category', + type: 'keyword', + }, + 'azure.signinlogs.properties.id': { + category: 'azure', + description: 'ID ', + name: 'azure.signinlogs.properties.id', + type: 'keyword', + }, + 'azure.signinlogs.properties.created_at': { + category: 'azure', + description: 'Created date time ', + name: 'azure.signinlogs.properties.created_at', + type: 'date', + }, + 'azure.signinlogs.properties.user_display_name': { + category: 'azure', + description: 'User display name ', + name: 'azure.signinlogs.properties.user_display_name', + type: 'keyword', + }, + 'azure.signinlogs.properties.correlation_id': { + category: 'azure', + description: 'Correlation ID ', + name: 'azure.signinlogs.properties.correlation_id', + type: 'keyword', + }, + 'azure.signinlogs.properties.user_principal_name': { + category: 'azure', + description: 'User principal name ', + name: 'azure.signinlogs.properties.user_principal_name', + type: 'keyword', + }, + 'azure.signinlogs.properties.user_id': { + category: 'azure', + description: 'User ID ', + name: 'azure.signinlogs.properties.user_id', + type: 'keyword', + }, + 'azure.signinlogs.properties.app_id': { + category: 'azure', + description: 'App ID ', + name: 'azure.signinlogs.properties.app_id', + type: 'keyword', + }, + 'azure.signinlogs.properties.app_display_name': { + category: 'azure', + description: 'App display name ', + name: 'azure.signinlogs.properties.app_display_name', + type: 'keyword', + }, + 'azure.signinlogs.properties.ip_address': { + category: 'azure', + description: 'Ip address ', + name: 'azure.signinlogs.properties.ip_address', + type: 'keyword', + }, + 'azure.signinlogs.properties.client_app_used': { + category: 'azure', + description: 'Client app used ', + name: 'azure.signinlogs.properties.client_app_used', + type: 'keyword', + }, + 'azure.signinlogs.properties.conditional_access_status': { + category: 'azure', + description: 'Conditional access status ', + name: 'azure.signinlogs.properties.conditional_access_status', + type: 'keyword', + }, + 'azure.signinlogs.properties.original_request_id': { + category: 'azure', + description: 'Original request ID ', + name: 'azure.signinlogs.properties.original_request_id', + type: 'keyword', + }, + 'azure.signinlogs.properties.is_interactive': { + category: 'azure', + description: 'Is interactive ', + name: 'azure.signinlogs.properties.is_interactive', + type: 'keyword', + }, + 'azure.signinlogs.properties.token_issuer_name': { + category: 'azure', + description: 'Token issuer name ', + name: 'azure.signinlogs.properties.token_issuer_name', + type: 'keyword', + }, + 'azure.signinlogs.properties.token_issuer_type': { + category: 'azure', + description: 'Token issuer type ', + name: 'azure.signinlogs.properties.token_issuer_type', + type: 'keyword', + }, + 'azure.signinlogs.properties.processing_time_ms': { + category: 'azure', + description: 'Processing time in milliseconds ', + name: 'azure.signinlogs.properties.processing_time_ms', + type: 'float', + }, + 'azure.signinlogs.properties.risk_detail': { + category: 'azure', + description: 'Risk detail ', + name: 'azure.signinlogs.properties.risk_detail', + type: 'keyword', + }, + 'azure.signinlogs.properties.risk_level_aggregated': { + category: 'azure', + description: 'Risk level aggregated ', + name: 'azure.signinlogs.properties.risk_level_aggregated', + type: 'keyword', + }, + 'azure.signinlogs.properties.risk_level_during_signin': { + category: 'azure', + description: 'Risk level during signIn ', + name: 'azure.signinlogs.properties.risk_level_during_signin', + type: 'keyword', + }, + 'azure.signinlogs.properties.risk_state': { + category: 'azure', + description: 'Risk state ', + name: 'azure.signinlogs.properties.risk_state', + type: 'keyword', + }, + 'azure.signinlogs.properties.resource_display_name': { + category: 'azure', + description: 'Resource display name ', + name: 'azure.signinlogs.properties.resource_display_name', + type: 'keyword', + }, + 'azure.signinlogs.properties.status.error_code': { + category: 'azure', + description: 'Error code ', + name: 'azure.signinlogs.properties.status.error_code', + type: 'keyword', + }, + 'azure.signinlogs.properties.device_detail.device_id': { + category: 'azure', + description: 'Device ID ', + name: 'azure.signinlogs.properties.device_detail.device_id', + type: 'keyword', + }, + 'azure.signinlogs.properties.device_detail.operating_system': { + category: 'azure', + description: 'Operating system ', + name: 'azure.signinlogs.properties.device_detail.operating_system', + type: 'keyword', + }, + 'azure.signinlogs.properties.device_detail.browser': { + category: 'azure', + description: 'Browser ', + name: 'azure.signinlogs.properties.device_detail.browser', + type: 'keyword', + }, + 'azure.signinlogs.properties.device_detail.display_name': { + category: 'azure', + description: 'Display name ', + name: 'azure.signinlogs.properties.device_detail.display_name', + type: 'keyword', + }, + 'azure.signinlogs.properties.device_detail.trust_type': { + category: 'azure', + description: 'Trust type ', + name: 'azure.signinlogs.properties.device_detail.trust_type', + type: 'keyword', + }, + 'azure.signinlogs.properties.service_principal_id': { + category: 'azure', + description: 'Status ', + name: 'azure.signinlogs.properties.service_principal_id', + type: 'keyword', + }, + 'network.interface.name': { + category: 'network', + description: 'Name of the network interface where the traffic has been observed. ', + name: 'network.interface.name', + type: 'keyword', + }, + 'rsa.internal.msg': { + category: 'rsa', + description: 'This key is used to capture the raw message that comes into the Log Decoder', + name: 'rsa.internal.msg', + type: 'keyword', + }, + 'rsa.internal.messageid': { + category: 'rsa', + name: 'rsa.internal.messageid', + type: 'keyword', + }, + 'rsa.internal.event_desc': { + category: 'rsa', + name: 'rsa.internal.event_desc', + type: 'keyword', + }, + 'rsa.internal.message': { + category: 'rsa', + description: 'This key captures the contents of instant messages', + name: 'rsa.internal.message', + type: 'keyword', + }, + 'rsa.internal.time': { + category: 'rsa', + description: + 'This is the time at which a session hits a NetWitness Decoder. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness.', + name: 'rsa.internal.time', + type: 'date', + }, + 'rsa.internal.level': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.level', + type: 'long', + }, + 'rsa.internal.msg_id': { + category: 'rsa', + description: + 'This is the Message ID1 value that identifies the exact log parser definition which parses a particular log session. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.msg_id', + type: 'keyword', + }, + 'rsa.internal.msg_vid': { + category: 'rsa', + description: + 'This is the Message ID2 value that identifies the exact log parser definition which parses a particular log session. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.msg_vid', + type: 'keyword', + }, + 'rsa.internal.data': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.data', + type: 'keyword', + }, + 'rsa.internal.obj_server': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.obj_server', + type: 'keyword', + }, + 'rsa.internal.obj_val': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.obj_val', + type: 'keyword', + }, + 'rsa.internal.resource': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.resource', + type: 'keyword', + }, + 'rsa.internal.obj_id': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.obj_id', + type: 'keyword', + }, + 'rsa.internal.statement': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.statement', + type: 'keyword', + }, + 'rsa.internal.audit_class': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.audit_class', + type: 'keyword', + }, + 'rsa.internal.entry': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.entry', + type: 'keyword', + }, + 'rsa.internal.hcode': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.hcode', + type: 'keyword', + }, + 'rsa.internal.inode': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.inode', + type: 'long', + }, + 'rsa.internal.resource_class': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.resource_class', + type: 'keyword', + }, + 'rsa.internal.dead': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.dead', + type: 'long', + }, + 'rsa.internal.feed_desc': { + category: 'rsa', + description: + 'This is used to capture the description of the feed. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.feed_desc', + type: 'keyword', + }, + 'rsa.internal.feed_name': { + category: 'rsa', + description: + 'This is used to capture the name of the feed. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.feed_name', + type: 'keyword', + }, + 'rsa.internal.cid': { + category: 'rsa', + description: + 'This is the unique identifier used to identify a NetWitness Concentrator. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.cid', + type: 'keyword', + }, + 'rsa.internal.device_class': { + category: 'rsa', + description: + 'This is the Classification of the Log Event Source under a predefined fixed set of Event Source Classifications. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.device_class', + type: 'keyword', + }, + 'rsa.internal.device_group': { + category: 'rsa', + description: + 'This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.device_group', + type: 'keyword', + }, + 'rsa.internal.device_host': { + category: 'rsa', + description: + 'This is the Hostname of the log Event Source sending the logs to NetWitness. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.device_host', + type: 'keyword', + }, + 'rsa.internal.device_ip': { + category: 'rsa', + description: + 'This is the IPv4 address of the Log Event Source sending the logs to NetWitness. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.device_ip', + type: 'ip', + }, + 'rsa.internal.device_ipv6': { + category: 'rsa', + description: + 'This is the IPv6 address of the Log Event Source sending the logs to NetWitness. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.device_ipv6', + type: 'ip', + }, + 'rsa.internal.device_type': { + category: 'rsa', + description: + 'This is the name of the log parser which parsed a given session. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.device_type', + type: 'keyword', + }, + 'rsa.internal.device_type_id': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.device_type_id', + type: 'long', + }, + 'rsa.internal.did': { + category: 'rsa', + description: + 'This is the unique identifier used to identify a NetWitness Decoder. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.did', + type: 'keyword', + }, + 'rsa.internal.entropy_req': { + category: 'rsa', + description: + 'This key is only used by the Entropy Parser, the Meta Type can be either UInt16 or Float32 based on the configuration', + name: 'rsa.internal.entropy_req', + type: 'long', + }, + 'rsa.internal.entropy_res': { + category: 'rsa', + description: + 'This key is only used by the Entropy Parser, the Meta Type can be either UInt16 or Float32 based on the configuration', + name: 'rsa.internal.entropy_res', + type: 'long', + }, + 'rsa.internal.event_name': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.event_name', + type: 'keyword', + }, + 'rsa.internal.feed_category': { + category: 'rsa', + description: + 'This is used to capture the category of the feed. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.feed_category', + type: 'keyword', + }, + 'rsa.internal.forward_ip': { + category: 'rsa', + description: + 'This key should be used to capture the IPV4 address of a relay system which forwarded the events from the original system to NetWitness.', + name: 'rsa.internal.forward_ip', + type: 'ip', + }, + 'rsa.internal.forward_ipv6': { + category: 'rsa', + description: + 'This key is used to capture the IPV6 address of a relay system which forwarded the events from the original system to NetWitness. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.forward_ipv6', + type: 'ip', + }, + 'rsa.internal.header_id': { + category: 'rsa', + description: + 'This is the Header ID value that identifies the exact log parser header definition that parses a particular log session. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.header_id', + type: 'keyword', + }, + 'rsa.internal.lc_cid': { + category: 'rsa', + description: + 'This is a unique Identifier of a Log Collector. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.lc_cid', + type: 'keyword', + }, + 'rsa.internal.lc_ctime': { + category: 'rsa', + description: + 'This is the time at which a log is collected in a NetWitness Log Collector. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.lc_ctime', + type: 'date', + }, + 'rsa.internal.mcb_req': { + category: 'rsa', + description: + 'This key is only used by the Entropy Parser, the most common byte request is simply which byte for each side (0 thru 255) was seen the most', + name: 'rsa.internal.mcb_req', + type: 'long', + }, + 'rsa.internal.mcb_res': { + category: 'rsa', + description: + 'This key is only used by the Entropy Parser, the most common byte response is simply which byte for each side (0 thru 255) was seen the most', + name: 'rsa.internal.mcb_res', + type: 'long', + }, + 'rsa.internal.mcbc_req': { + category: 'rsa', + description: + 'This key is only used by the Entropy Parser, the most common byte count is the number of times the most common byte (above) was seen in the session streams', + name: 'rsa.internal.mcbc_req', + type: 'long', + }, + 'rsa.internal.mcbc_res': { + category: 'rsa', + description: + 'This key is only used by the Entropy Parser, the most common byte count is the number of times the most common byte (above) was seen in the session streams', + name: 'rsa.internal.mcbc_res', + type: 'long', + }, + 'rsa.internal.medium': { + category: 'rsa', + description: + 'This key is used to identify if it’s a log/packet session or Layer 2 Encapsulation Type. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness. 32 = log, 33 = correlation session, < 32 is packet session', + name: 'rsa.internal.medium', + type: 'long', + }, + 'rsa.internal.node_name': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.node_name', + type: 'keyword', + }, + 'rsa.internal.nwe_callback_id': { + category: 'rsa', + description: 'This key denotes that event is endpoint related', + name: 'rsa.internal.nwe_callback_id', + type: 'keyword', + }, + 'rsa.internal.parse_error': { + category: 'rsa', + description: + 'This is a special key that stores any Meta key validation error found while parsing a log session. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.parse_error', + type: 'keyword', + }, + 'rsa.internal.payload_req': { + category: 'rsa', + description: + 'This key is only used by the Entropy Parser, the payload size metrics are the payload sizes of each session side at the time of parsing. However, in order to keep', + name: 'rsa.internal.payload_req', + type: 'long', + }, + 'rsa.internal.payload_res': { + category: 'rsa', + description: + 'This key is only used by the Entropy Parser, the payload size metrics are the payload sizes of each session side at the time of parsing. However, in order to keep', + name: 'rsa.internal.payload_res', + type: 'long', + }, + 'rsa.internal.process_vid_dst': { + category: 'rsa', + description: + 'Endpoint generates and uses a unique virtual ID to identify any similar group of process. This ID represents the target process.', + name: 'rsa.internal.process_vid_dst', + type: 'keyword', + }, + 'rsa.internal.process_vid_src': { + category: 'rsa', + description: + 'Endpoint generates and uses a unique virtual ID to identify any similar group of process. This ID represents the source process.', + name: 'rsa.internal.process_vid_src', + type: 'keyword', + }, + 'rsa.internal.rid': { + category: 'rsa', + description: + 'This is a special ID of the Remote Session created by NetWitness Decoder. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.rid', + type: 'long', + }, + 'rsa.internal.session_split': { + category: 'rsa', + description: + 'This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.session_split', + type: 'keyword', + }, + 'rsa.internal.site': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.site', + type: 'keyword', + }, + 'rsa.internal.size': { + category: 'rsa', + description: + 'This is the size of the session as seen by the NetWitness Decoder. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.size', + type: 'long', + }, + 'rsa.internal.sourcefile': { + category: 'rsa', + description: + 'This is the name of the log file or PCAPs that can be imported into NetWitness. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.sourcefile', + type: 'keyword', + }, + 'rsa.internal.ubc_req': { + category: 'rsa', + description: + 'This key is only used by the Entropy Parser, Unique byte count is the number of unique bytes seen in each stream. 256 would mean all byte values of 0 thru 255 were seen at least once', + name: 'rsa.internal.ubc_req', + type: 'long', + }, + 'rsa.internal.ubc_res': { + category: 'rsa', + description: + 'This key is only used by the Entropy Parser, Unique byte count is the number of unique bytes seen in each stream. 256 would mean all byte values of 0 thru 255 were seen at least once', + name: 'rsa.internal.ubc_res', + type: 'long', + }, + 'rsa.internal.word': { + category: 'rsa', + description: + 'This is used by the Word Parsing technology to capture the first 5 character of every word in an unparsed log', + name: 'rsa.internal.word', + type: 'keyword', + }, + 'rsa.time.event_time': { + category: 'rsa', + description: + 'This key is used to capture the time mentioned in a raw session that represents the actual time an event occured in a standard normalized form', + name: 'rsa.time.event_time', + type: 'date', + }, + 'rsa.time.duration_time': { + category: 'rsa', + description: 'This key is used to capture the normalized duration/lifetime in seconds.', + name: 'rsa.time.duration_time', + type: 'double', + }, + 'rsa.time.event_time_str': { + category: 'rsa', + description: + 'This key is used to capture the incomplete time mentioned in a session as a string', + name: 'rsa.time.event_time_str', + type: 'keyword', + }, + 'rsa.time.starttime': { + category: 'rsa', + description: + 'This key is used to capture the Start time mentioned in a session in a standard form', + name: 'rsa.time.starttime', + type: 'date', + }, + 'rsa.time.month': { + category: 'rsa', + name: 'rsa.time.month', + type: 'keyword', + }, + 'rsa.time.day': { + category: 'rsa', + name: 'rsa.time.day', + type: 'keyword', + }, + 'rsa.time.endtime': { + category: 'rsa', + description: + 'This key is used to capture the End time mentioned in a session in a standard form', + name: 'rsa.time.endtime', + type: 'date', + }, + 'rsa.time.timezone': { + category: 'rsa', + description: 'This key is used to capture the timezone of the Event Time', + name: 'rsa.time.timezone', + type: 'keyword', + }, + 'rsa.time.duration_str': { + category: 'rsa', + description: 'A text string version of the duration', + name: 'rsa.time.duration_str', + type: 'keyword', + }, + 'rsa.time.date': { + category: 'rsa', + name: 'rsa.time.date', + type: 'keyword', + }, + 'rsa.time.year': { + category: 'rsa', + name: 'rsa.time.year', + type: 'keyword', + }, + 'rsa.time.recorded_time': { + category: 'rsa', + description: + "The event time as recorded by the system the event is collected from. The usage scenario is a multi-tier application where the management layer of the system records it's own timestamp at the time of collection from its child nodes. Must be in timestamp format.", + name: 'rsa.time.recorded_time', + type: 'date', + }, + 'rsa.time.datetime': { + category: 'rsa', + name: 'rsa.time.datetime', + type: 'keyword', + }, + 'rsa.time.effective_time': { + category: 'rsa', + description: + 'This key is the effective time referenced by an individual event in a Standard Timestamp format', + name: 'rsa.time.effective_time', + type: 'date', + }, + 'rsa.time.expire_time': { + category: 'rsa', + description: 'This key is the timestamp that explicitly refers to an expiration.', + name: 'rsa.time.expire_time', + type: 'date', + }, + 'rsa.time.process_time': { + category: 'rsa', + description: 'Deprecated, use duration.time', + name: 'rsa.time.process_time', + type: 'keyword', + }, + 'rsa.time.hour': { + category: 'rsa', + name: 'rsa.time.hour', + type: 'keyword', + }, + 'rsa.time.min': { + category: 'rsa', + name: 'rsa.time.min', + type: 'keyword', + }, + 'rsa.time.timestamp': { + category: 'rsa', + name: 'rsa.time.timestamp', + type: 'keyword', + }, + 'rsa.time.event_queue_time': { + category: 'rsa', + description: 'This key is the Time that the event was queued.', + name: 'rsa.time.event_queue_time', + type: 'date', + }, + 'rsa.time.p_time1': { + category: 'rsa', + name: 'rsa.time.p_time1', + type: 'keyword', + }, + 'rsa.time.tzone': { + category: 'rsa', + name: 'rsa.time.tzone', + type: 'keyword', + }, + 'rsa.time.eventtime': { + category: 'rsa', + name: 'rsa.time.eventtime', + type: 'keyword', + }, + 'rsa.time.gmtdate': { + category: 'rsa', + name: 'rsa.time.gmtdate', + type: 'keyword', + }, + 'rsa.time.gmttime': { + category: 'rsa', + name: 'rsa.time.gmttime', + type: 'keyword', + }, + 'rsa.time.p_date': { + category: 'rsa', + name: 'rsa.time.p_date', + type: 'keyword', + }, + 'rsa.time.p_month': { + category: 'rsa', + name: 'rsa.time.p_month', + type: 'keyword', + }, + 'rsa.time.p_time': { + category: 'rsa', + name: 'rsa.time.p_time', + type: 'keyword', + }, + 'rsa.time.p_time2': { + category: 'rsa', + name: 'rsa.time.p_time2', + type: 'keyword', + }, + 'rsa.time.p_year': { + category: 'rsa', + name: 'rsa.time.p_year', + type: 'keyword', + }, + 'rsa.time.expire_time_str': { + category: 'rsa', + description: + 'This key is used to capture incomplete timestamp that explicitly refers to an expiration.', + name: 'rsa.time.expire_time_str', + type: 'keyword', + }, + 'rsa.time.stamp': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.time.stamp', + type: 'date', + }, + 'rsa.misc.action': { + category: 'rsa', + name: 'rsa.misc.action', + type: 'keyword', + }, + 'rsa.misc.result': { + category: 'rsa', + description: + 'This key is used to capture the outcome/result string value of an action in a session.', + name: 'rsa.misc.result', + type: 'keyword', + }, + 'rsa.misc.severity': { + category: 'rsa', + description: 'This key is used to capture the severity given the session', + name: 'rsa.misc.severity', + type: 'keyword', + }, + 'rsa.misc.event_type': { + category: 'rsa', + description: 'This key captures the event category type as specified by the event source.', + name: 'rsa.misc.event_type', + type: 'keyword', + }, + 'rsa.misc.reference_id': { + category: 'rsa', + description: 'This key is used to capture an event id from the session directly', + name: 'rsa.misc.reference_id', + type: 'keyword', + }, + 'rsa.misc.version': { + category: 'rsa', + description: + 'This key captures Version of the application or OS which is generating the event.', + name: 'rsa.misc.version', + type: 'keyword', + }, + 'rsa.misc.disposition': { + category: 'rsa', + description: 'This key captures the The end state of an action.', + name: 'rsa.misc.disposition', + type: 'keyword', + }, + 'rsa.misc.result_code': { + category: 'rsa', + description: + 'This key is used to capture the outcome/result numeric value of an action in a session', + name: 'rsa.misc.result_code', + type: 'keyword', + }, + 'rsa.misc.category': { + category: 'rsa', + description: + 'This key is used to capture the category of an event given by the vendor in the session', + name: 'rsa.misc.category', + type: 'keyword', + }, + 'rsa.misc.obj_name': { + category: 'rsa', + description: 'This is used to capture name of object', + name: 'rsa.misc.obj_name', + type: 'keyword', + }, + 'rsa.misc.obj_type': { + category: 'rsa', + description: 'This is used to capture type of object', + name: 'rsa.misc.obj_type', + type: 'keyword', + }, + 'rsa.misc.event_source': { + category: 'rsa', + description: 'This key captures Source of the event that’s not a hostname', + name: 'rsa.misc.event_source', + type: 'keyword', + }, + 'rsa.misc.log_session_id': { + category: 'rsa', + description: 'This key is used to capture a sessionid from the session directly', + name: 'rsa.misc.log_session_id', + type: 'keyword', + }, + 'rsa.misc.group': { + category: 'rsa', + description: 'This key captures the Group Name value', + name: 'rsa.misc.group', + type: 'keyword', + }, + 'rsa.misc.policy_name': { + category: 'rsa', + description: 'This key is used to capture the Policy Name only.', + name: 'rsa.misc.policy_name', + type: 'keyword', + }, + 'rsa.misc.rule_name': { + category: 'rsa', + description: 'This key captures the Rule Name', + name: 'rsa.misc.rule_name', + type: 'keyword', + }, + 'rsa.misc.context': { + category: 'rsa', + description: 'This key captures Information which adds additional context to the event.', + name: 'rsa.misc.context', + type: 'keyword', + }, + 'rsa.misc.change_new': { + category: 'rsa', + description: + 'This key is used to capture the new values of the attribute that’s changing in a session', + name: 'rsa.misc.change_new', + type: 'keyword', + }, + 'rsa.misc.space': { + category: 'rsa', + name: 'rsa.misc.space', + type: 'keyword', + }, + 'rsa.misc.client': { + category: 'rsa', + description: + 'This key is used to capture only the name of the client application requesting resources of the server. See the user.agent meta key for capture of the specific user agent identifier or browser identification string.', + name: 'rsa.misc.client', + type: 'keyword', + }, + 'rsa.misc.msgIdPart1': { + category: 'rsa', + name: 'rsa.misc.msgIdPart1', + type: 'keyword', + }, + 'rsa.misc.msgIdPart2': { + category: 'rsa', + name: 'rsa.misc.msgIdPart2', + type: 'keyword', + }, + 'rsa.misc.change_old': { + category: 'rsa', + description: + 'This key is used to capture the old value of the attribute that’s changing in a session', + name: 'rsa.misc.change_old', + type: 'keyword', + }, + 'rsa.misc.operation_id': { + category: 'rsa', + description: + 'An alert number or operation number. The values should be unique and non-repeating.', + name: 'rsa.misc.operation_id', + type: 'keyword', + }, + 'rsa.misc.event_state': { + category: 'rsa', + description: + 'This key captures the current state of the object/item referenced within the event. Describing an on-going event.', + name: 'rsa.misc.event_state', + type: 'keyword', + }, + 'rsa.misc.group_object': { + category: 'rsa', + description: 'This key captures a collection/grouping of entities. Specific usage', + name: 'rsa.misc.group_object', + type: 'keyword', + }, + 'rsa.misc.node': { + category: 'rsa', + description: + 'Common use case is the node name within a cluster. The cluster name is reflected by the host name.', + name: 'rsa.misc.node', + type: 'keyword', + }, + 'rsa.misc.rule': { + category: 'rsa', + description: 'This key captures the Rule number', + name: 'rsa.misc.rule', + type: 'keyword', + }, + 'rsa.misc.device_name': { + category: 'rsa', + description: + 'This is used to capture name of the Device associated with the node Like: a physical disk, printer, etc', + name: 'rsa.misc.device_name', + type: 'keyword', + }, + 'rsa.misc.param': { + category: 'rsa', + description: 'This key is the parameters passed as part of a command or application, etc.', + name: 'rsa.misc.param', + type: 'keyword', + }, + 'rsa.misc.change_attrib': { + category: 'rsa', + description: + 'This key is used to capture the name of the attribute that’s changing in a session', + name: 'rsa.misc.change_attrib', + type: 'keyword', + }, + 'rsa.misc.event_computer': { + category: 'rsa', + description: + 'This key is a windows only concept, where this key is used to capture fully qualified domain name in a windows log.', + name: 'rsa.misc.event_computer', + type: 'keyword', + }, + 'rsa.misc.reference_id1': { + category: 'rsa', + description: 'This key is for Linked ID to be used as an addition to "reference.id"', + name: 'rsa.misc.reference_id1', + type: 'keyword', + }, + 'rsa.misc.event_log': { + category: 'rsa', + description: 'This key captures the Name of the event log', + name: 'rsa.misc.event_log', + type: 'keyword', + }, + 'rsa.misc.OS': { + category: 'rsa', + description: 'This key captures the Name of the Operating System', + name: 'rsa.misc.OS', + type: 'keyword', + }, + 'rsa.misc.terminal': { + category: 'rsa', + description: 'This key captures the Terminal Names only', + name: 'rsa.misc.terminal', + type: 'keyword', + }, + 'rsa.misc.msgIdPart3': { + category: 'rsa', + name: 'rsa.misc.msgIdPart3', + type: 'keyword', + }, + 'rsa.misc.filter': { + category: 'rsa', + description: 'This key captures Filter used to reduce result set', + name: 'rsa.misc.filter', + type: 'keyword', + }, + 'rsa.misc.serial_number': { + category: 'rsa', + description: 'This key is the Serial number associated with a physical asset.', + name: 'rsa.misc.serial_number', + type: 'keyword', + }, + 'rsa.misc.checksum': { + category: 'rsa', + description: + 'This key is used to capture the checksum or hash of the entity such as a file or process. Checksum should be used over checksum.src or checksum.dst when it is unclear whether the entity is a source or target of an action.', + name: 'rsa.misc.checksum', + type: 'keyword', + }, + 'rsa.misc.event_user': { + category: 'rsa', + description: + 'This key is a windows only concept, where this key is used to capture combination of domain name and username in a windows log.', + name: 'rsa.misc.event_user', + type: 'keyword', + }, + 'rsa.misc.virusname': { + category: 'rsa', + description: 'This key captures the name of the virus', + name: 'rsa.misc.virusname', + type: 'keyword', + }, + 'rsa.misc.content_type': { + category: 'rsa', + description: 'This key is used to capture Content Type only.', + name: 'rsa.misc.content_type', + type: 'keyword', + }, + 'rsa.misc.group_id': { + category: 'rsa', + description: 'This key captures Group ID Number (related to the group name)', + name: 'rsa.misc.group_id', + type: 'keyword', + }, + 'rsa.misc.policy_id': { + category: 'rsa', + description: + 'This key is used to capture the Policy ID only, this should be a numeric value, use policy.name otherwise', + name: 'rsa.misc.policy_id', + type: 'keyword', + }, + 'rsa.misc.vsys': { + category: 'rsa', + description: 'This key captures Virtual System Name', + name: 'rsa.misc.vsys', + type: 'keyword', + }, + 'rsa.misc.connection_id': { + category: 'rsa', + description: 'This key captures the Connection ID', + name: 'rsa.misc.connection_id', + type: 'keyword', + }, + 'rsa.misc.reference_id2': { + category: 'rsa', + description: + 'This key is for the 2nd Linked ID. Can be either linked to "reference.id" or "reference.id1" value but should not be used unless the other two variables are in play.', + name: 'rsa.misc.reference_id2', + type: 'keyword', + }, + 'rsa.misc.sensor': { + category: 'rsa', + description: 'This key captures Name of the sensor. Typically used in IDS/IPS based devices', + name: 'rsa.misc.sensor', + type: 'keyword', + }, + 'rsa.misc.sig_id': { + category: 'rsa', + description: 'This key captures IDS/IPS Int Signature ID', + name: 'rsa.misc.sig_id', + type: 'long', + }, + 'rsa.misc.port_name': { + category: 'rsa', + description: + 'This key is used for Physical or logical port connection but does NOT include a network port. (Example: Printer port name).', + name: 'rsa.misc.port_name', + type: 'keyword', + }, + 'rsa.misc.rule_group': { + category: 'rsa', + description: 'This key captures the Rule group name', + name: 'rsa.misc.rule_group', + type: 'keyword', + }, + 'rsa.misc.risk_num': { + category: 'rsa', + description: 'This key captures a Numeric Risk value', + name: 'rsa.misc.risk_num', + type: 'double', + }, + 'rsa.misc.trigger_val': { + category: 'rsa', + description: 'This key captures the Value of the trigger or threshold condition.', + name: 'rsa.misc.trigger_val', + type: 'keyword', + }, + 'rsa.misc.log_session_id1': { + category: 'rsa', + description: + 'This key is used to capture a Linked (Related) Session ID from the session directly', + name: 'rsa.misc.log_session_id1', + type: 'keyword', + }, + 'rsa.misc.comp_version': { + category: 'rsa', + description: 'This key captures the Version level of a sub-component of a product.', + name: 'rsa.misc.comp_version', + type: 'keyword', + }, + 'rsa.misc.content_version': { + category: 'rsa', + description: 'This key captures Version level of a signature or database content.', + name: 'rsa.misc.content_version', + type: 'keyword', + }, + 'rsa.misc.hardware_id': { + category: 'rsa', + description: + 'This key is used to capture unique identifier for a device or system (NOT a Mac address)', + name: 'rsa.misc.hardware_id', + type: 'keyword', + }, + 'rsa.misc.risk': { + category: 'rsa', + description: 'This key captures the non-numeric risk value', + name: 'rsa.misc.risk', + type: 'keyword', + }, + 'rsa.misc.event_id': { + category: 'rsa', + name: 'rsa.misc.event_id', + type: 'keyword', + }, + 'rsa.misc.reason': { + category: 'rsa', + name: 'rsa.misc.reason', + type: 'keyword', + }, + 'rsa.misc.status': { + category: 'rsa', + name: 'rsa.misc.status', + type: 'keyword', + }, + 'rsa.misc.mail_id': { + category: 'rsa', + description: 'This key is used to capture the mailbox id/name', + name: 'rsa.misc.mail_id', + type: 'keyword', + }, + 'rsa.misc.rule_uid': { + category: 'rsa', + description: 'This key is the Unique Identifier for a rule.', + name: 'rsa.misc.rule_uid', + type: 'keyword', + }, + 'rsa.misc.trigger_desc': { + category: 'rsa', + description: 'This key captures the Description of the trigger or threshold condition.', + name: 'rsa.misc.trigger_desc', + type: 'keyword', + }, + 'rsa.misc.inout': { + category: 'rsa', + name: 'rsa.misc.inout', + type: 'keyword', + }, + 'rsa.misc.p_msgid': { + category: 'rsa', + name: 'rsa.misc.p_msgid', + type: 'keyword', + }, + 'rsa.misc.data_type': { + category: 'rsa', + name: 'rsa.misc.data_type', + type: 'keyword', + }, + 'rsa.misc.msgIdPart4': { + category: 'rsa', + name: 'rsa.misc.msgIdPart4', + type: 'keyword', + }, + 'rsa.misc.error': { + category: 'rsa', + description: 'This key captures All non successful Error codes or responses', + name: 'rsa.misc.error', + type: 'keyword', + }, + 'rsa.misc.index': { + category: 'rsa', + name: 'rsa.misc.index', + type: 'keyword', + }, + 'rsa.misc.listnum': { + category: 'rsa', + description: + 'This key is used to capture listname or listnumber, primarily for collecting access-list', + name: 'rsa.misc.listnum', + type: 'keyword', + }, + 'rsa.misc.ntype': { + category: 'rsa', + name: 'rsa.misc.ntype', + type: 'keyword', + }, + 'rsa.misc.observed_val': { + category: 'rsa', + description: + 'This key captures the Value observed (from the perspective of the device generating the log).', + name: 'rsa.misc.observed_val', + type: 'keyword', + }, + 'rsa.misc.policy_value': { + category: 'rsa', + description: + 'This key captures the contents of the policy. This contains details about the policy', + name: 'rsa.misc.policy_value', + type: 'keyword', + }, + 'rsa.misc.pool_name': { + category: 'rsa', + description: 'This key captures the name of a resource pool', + name: 'rsa.misc.pool_name', + type: 'keyword', + }, + 'rsa.misc.rule_template': { + category: 'rsa', + description: + 'A default set of parameters which are overlayed onto a rule (or rulename) which efffectively constitutes a template', + name: 'rsa.misc.rule_template', + type: 'keyword', + }, + 'rsa.misc.count': { + category: 'rsa', + name: 'rsa.misc.count', + type: 'keyword', + }, + 'rsa.misc.number': { + category: 'rsa', + name: 'rsa.misc.number', + type: 'keyword', + }, + 'rsa.misc.sigcat': { + category: 'rsa', + name: 'rsa.misc.sigcat', + type: 'keyword', + }, + 'rsa.misc.type': { + category: 'rsa', + name: 'rsa.misc.type', + type: 'keyword', + }, + 'rsa.misc.comments': { + category: 'rsa', + description: 'Comment information provided in the log message', + name: 'rsa.misc.comments', + type: 'keyword', + }, + 'rsa.misc.doc_number': { + category: 'rsa', + description: 'This key captures File Identification number', + name: 'rsa.misc.doc_number', + type: 'long', + }, + 'rsa.misc.expected_val': { + category: 'rsa', + description: + 'This key captures the Value expected (from the perspective of the device generating the log).', + name: 'rsa.misc.expected_val', + type: 'keyword', + }, + 'rsa.misc.job_num': { + category: 'rsa', + description: 'This key captures the Job Number', + name: 'rsa.misc.job_num', + type: 'keyword', + }, + 'rsa.misc.spi_dst': { + category: 'rsa', + description: 'Destination SPI Index', + name: 'rsa.misc.spi_dst', + type: 'keyword', + }, + 'rsa.misc.spi_src': { + category: 'rsa', + description: 'Source SPI Index', + name: 'rsa.misc.spi_src', + type: 'keyword', + }, + 'rsa.misc.code': { + category: 'rsa', + name: 'rsa.misc.code', + type: 'keyword', + }, + 'rsa.misc.agent_id': { + category: 'rsa', + description: 'This key is used to capture agent id', + name: 'rsa.misc.agent_id', + type: 'keyword', + }, + 'rsa.misc.message_body': { + category: 'rsa', + description: 'This key captures the The contents of the message body.', + name: 'rsa.misc.message_body', + type: 'keyword', + }, + 'rsa.misc.phone': { + category: 'rsa', + name: 'rsa.misc.phone', + type: 'keyword', + }, + 'rsa.misc.sig_id_str': { + category: 'rsa', + description: 'This key captures a string object of the sigid variable.', + name: 'rsa.misc.sig_id_str', + type: 'keyword', + }, + 'rsa.misc.cmd': { + category: 'rsa', + name: 'rsa.misc.cmd', + type: 'keyword', + }, + 'rsa.misc.misc': { + category: 'rsa', + name: 'rsa.misc.misc', + type: 'keyword', + }, + 'rsa.misc.name': { + category: 'rsa', + name: 'rsa.misc.name', + type: 'keyword', + }, + 'rsa.misc.cpu': { + category: 'rsa', + description: 'This key is the CPU time used in the execution of the event being recorded.', + name: 'rsa.misc.cpu', + type: 'long', + }, + 'rsa.misc.event_desc': { + category: 'rsa', + description: + 'This key is used to capture a description of an event available directly or inferred', + name: 'rsa.misc.event_desc', + type: 'keyword', + }, + 'rsa.misc.sig_id1': { + category: 'rsa', + description: 'This key captures IDS/IPS Int Signature ID. This must be linked to the sig.id', + name: 'rsa.misc.sig_id1', + type: 'long', + }, + 'rsa.misc.im_buddyid': { + category: 'rsa', + name: 'rsa.misc.im_buddyid', + type: 'keyword', + }, + 'rsa.misc.im_client': { + category: 'rsa', + name: 'rsa.misc.im_client', + type: 'keyword', + }, + 'rsa.misc.im_userid': { + category: 'rsa', + name: 'rsa.misc.im_userid', + type: 'keyword', + }, + 'rsa.misc.pid': { + category: 'rsa', + name: 'rsa.misc.pid', + type: 'keyword', + }, + 'rsa.misc.priority': { + category: 'rsa', + name: 'rsa.misc.priority', + type: 'keyword', + }, + 'rsa.misc.context_subject': { + category: 'rsa', + description: + 'This key is to be used in an audit context where the subject is the object being identified', + name: 'rsa.misc.context_subject', + type: 'keyword', + }, + 'rsa.misc.context_target': { + category: 'rsa', + name: 'rsa.misc.context_target', + type: 'keyword', + }, + 'rsa.misc.cve': { + category: 'rsa', + description: + 'This key captures CVE (Common Vulnerabilities and Exposures) - an identifier for known information security vulnerabilities.', + name: 'rsa.misc.cve', + type: 'keyword', + }, + 'rsa.misc.fcatnum': { + category: 'rsa', + description: 'This key captures Filter Category Number. Legacy Usage', + name: 'rsa.misc.fcatnum', + type: 'keyword', + }, + 'rsa.misc.library': { + category: 'rsa', + description: 'This key is used to capture library information in mainframe devices', + name: 'rsa.misc.library', + type: 'keyword', + }, + 'rsa.misc.parent_node': { + category: 'rsa', + description: 'This key captures the Parent Node Name. Must be related to node variable.', + name: 'rsa.misc.parent_node', + type: 'keyword', + }, + 'rsa.misc.risk_info': { + category: 'rsa', + description: 'Deprecated, use New Hunting Model (inv.*, ioc, boc, eoc, analysis.*)', + name: 'rsa.misc.risk_info', + type: 'keyword', + }, + 'rsa.misc.tcp_flags': { + category: 'rsa', + description: 'This key is captures the TCP flags set in any packet of session', + name: 'rsa.misc.tcp_flags', + type: 'long', + }, + 'rsa.misc.tos': { + category: 'rsa', + description: 'This key describes the type of service', + name: 'rsa.misc.tos', + type: 'long', + }, + 'rsa.misc.vm_target': { + category: 'rsa', + description: 'VMWare Target **VMWARE** only varaible.', + name: 'rsa.misc.vm_target', + type: 'keyword', + }, + 'rsa.misc.workspace': { + category: 'rsa', + description: 'This key captures Workspace Description', + name: 'rsa.misc.workspace', + type: 'keyword', + }, + 'rsa.misc.command': { + category: 'rsa', + name: 'rsa.misc.command', + type: 'keyword', + }, + 'rsa.misc.event_category': { + category: 'rsa', + name: 'rsa.misc.event_category', + type: 'keyword', + }, + 'rsa.misc.facilityname': { + category: 'rsa', + name: 'rsa.misc.facilityname', + type: 'keyword', + }, + 'rsa.misc.forensic_info': { + category: 'rsa', + name: 'rsa.misc.forensic_info', + type: 'keyword', + }, + 'rsa.misc.jobname': { + category: 'rsa', + name: 'rsa.misc.jobname', + type: 'keyword', + }, + 'rsa.misc.mode': { + category: 'rsa', + name: 'rsa.misc.mode', + type: 'keyword', + }, + 'rsa.misc.policy': { + category: 'rsa', + name: 'rsa.misc.policy', + type: 'keyword', + }, + 'rsa.misc.policy_waiver': { + category: 'rsa', + name: 'rsa.misc.policy_waiver', + type: 'keyword', + }, + 'rsa.misc.second': { + category: 'rsa', + name: 'rsa.misc.second', + type: 'keyword', + }, + 'rsa.misc.space1': { + category: 'rsa', + name: 'rsa.misc.space1', + type: 'keyword', + }, + 'rsa.misc.subcategory': { + category: 'rsa', + name: 'rsa.misc.subcategory', + type: 'keyword', + }, + 'rsa.misc.tbdstr2': { + category: 'rsa', + name: 'rsa.misc.tbdstr2', + type: 'keyword', + }, + 'rsa.misc.alert_id': { + category: 'rsa', + description: 'Deprecated, New Hunting Model (inv.*, ioc, boc, eoc, analysis.*)', + name: 'rsa.misc.alert_id', + type: 'keyword', + }, + 'rsa.misc.checksum_dst': { + category: 'rsa', + description: + 'This key is used to capture the checksum or hash of the the target entity such as a process or file.', + name: 'rsa.misc.checksum_dst', + type: 'keyword', + }, + 'rsa.misc.checksum_src': { + category: 'rsa', + description: + 'This key is used to capture the checksum or hash of the source entity such as a file or process.', + name: 'rsa.misc.checksum_src', + type: 'keyword', + }, + 'rsa.misc.fresult': { + category: 'rsa', + description: 'This key captures the Filter Result', + name: 'rsa.misc.fresult', + type: 'long', + }, + 'rsa.misc.payload_dst': { + category: 'rsa', + description: 'This key is used to capture destination payload', + name: 'rsa.misc.payload_dst', + type: 'keyword', + }, + 'rsa.misc.payload_src': { + category: 'rsa', + description: 'This key is used to capture source payload', + name: 'rsa.misc.payload_src', + type: 'keyword', + }, + 'rsa.misc.pool_id': { + category: 'rsa', + description: 'This key captures the identifier (typically numeric field) of a resource pool', + name: 'rsa.misc.pool_id', + type: 'keyword', + }, + 'rsa.misc.process_id_val': { + category: 'rsa', + description: 'This key is a failure key for Process ID when it is not an integer value', + name: 'rsa.misc.process_id_val', + type: 'keyword', + }, + 'rsa.misc.risk_num_comm': { + category: 'rsa', + description: 'This key captures Risk Number Community', + name: 'rsa.misc.risk_num_comm', + type: 'double', + }, + 'rsa.misc.risk_num_next': { + category: 'rsa', + description: 'This key captures Risk Number NextGen', + name: 'rsa.misc.risk_num_next', + type: 'double', + }, + 'rsa.misc.risk_num_sand': { + category: 'rsa', + description: 'This key captures Risk Number SandBox', + name: 'rsa.misc.risk_num_sand', + type: 'double', + }, + 'rsa.misc.risk_num_static': { + category: 'rsa', + description: 'This key captures Risk Number Static', + name: 'rsa.misc.risk_num_static', + type: 'double', + }, + 'rsa.misc.risk_suspicious': { + category: 'rsa', + description: 'Deprecated, use New Hunting Model (inv.*, ioc, boc, eoc, analysis.*)', + name: 'rsa.misc.risk_suspicious', + type: 'keyword', + }, + 'rsa.misc.risk_warning': { + category: 'rsa', + description: 'Deprecated, use New Hunting Model (inv.*, ioc, boc, eoc, analysis.*)', + name: 'rsa.misc.risk_warning', + type: 'keyword', + }, + 'rsa.misc.snmp_oid': { + category: 'rsa', + description: 'SNMP Object Identifier', + name: 'rsa.misc.snmp_oid', + type: 'keyword', + }, + 'rsa.misc.sql': { + category: 'rsa', + description: 'This key captures the SQL query', + name: 'rsa.misc.sql', + type: 'keyword', + }, + 'rsa.misc.vuln_ref': { + category: 'rsa', + description: 'This key captures the Vulnerability Reference details', + name: 'rsa.misc.vuln_ref', + type: 'keyword', + }, + 'rsa.misc.acl_id': { + category: 'rsa', + name: 'rsa.misc.acl_id', + type: 'keyword', + }, + 'rsa.misc.acl_op': { + category: 'rsa', + name: 'rsa.misc.acl_op', + type: 'keyword', + }, + 'rsa.misc.acl_pos': { + category: 'rsa', + name: 'rsa.misc.acl_pos', + type: 'keyword', + }, + 'rsa.misc.acl_table': { + category: 'rsa', + name: 'rsa.misc.acl_table', + type: 'keyword', + }, + 'rsa.misc.admin': { + category: 'rsa', + name: 'rsa.misc.admin', + type: 'keyword', + }, + 'rsa.misc.alarm_id': { + category: 'rsa', + name: 'rsa.misc.alarm_id', + type: 'keyword', + }, + 'rsa.misc.alarmname': { + category: 'rsa', + name: 'rsa.misc.alarmname', + type: 'keyword', + }, + 'rsa.misc.app_id': { + category: 'rsa', + name: 'rsa.misc.app_id', + type: 'keyword', + }, + 'rsa.misc.audit': { + category: 'rsa', + name: 'rsa.misc.audit', + type: 'keyword', + }, + 'rsa.misc.audit_object': { + category: 'rsa', + name: 'rsa.misc.audit_object', + type: 'keyword', + }, + 'rsa.misc.auditdata': { + category: 'rsa', + name: 'rsa.misc.auditdata', + type: 'keyword', + }, + 'rsa.misc.benchmark': { + category: 'rsa', + name: 'rsa.misc.benchmark', + type: 'keyword', + }, + 'rsa.misc.bypass': { + category: 'rsa', + name: 'rsa.misc.bypass', + type: 'keyword', + }, + 'rsa.misc.cache': { + category: 'rsa', + name: 'rsa.misc.cache', + type: 'keyword', + }, + 'rsa.misc.cache_hit': { + category: 'rsa', + name: 'rsa.misc.cache_hit', + type: 'keyword', + }, + 'rsa.misc.cefversion': { + category: 'rsa', + name: 'rsa.misc.cefversion', + type: 'keyword', + }, + 'rsa.misc.cfg_attr': { + category: 'rsa', + name: 'rsa.misc.cfg_attr', + type: 'keyword', + }, + 'rsa.misc.cfg_obj': { + category: 'rsa', + name: 'rsa.misc.cfg_obj', + type: 'keyword', + }, + 'rsa.misc.cfg_path': { + category: 'rsa', + name: 'rsa.misc.cfg_path', + type: 'keyword', + }, + 'rsa.misc.changes': { + category: 'rsa', + name: 'rsa.misc.changes', + type: 'keyword', + }, + 'rsa.misc.client_ip': { + category: 'rsa', + name: 'rsa.misc.client_ip', + type: 'keyword', + }, + 'rsa.misc.clustermembers': { + category: 'rsa', + name: 'rsa.misc.clustermembers', + type: 'keyword', + }, + 'rsa.misc.cn_acttimeout': { + category: 'rsa', + name: 'rsa.misc.cn_acttimeout', + type: 'keyword', + }, + 'rsa.misc.cn_asn_src': { + category: 'rsa', + name: 'rsa.misc.cn_asn_src', + type: 'keyword', + }, + 'rsa.misc.cn_bgpv4nxthop': { + category: 'rsa', + name: 'rsa.misc.cn_bgpv4nxthop', + type: 'keyword', + }, + 'rsa.misc.cn_ctr_dst_code': { + category: 'rsa', + name: 'rsa.misc.cn_ctr_dst_code', + type: 'keyword', + }, + 'rsa.misc.cn_dst_tos': { + category: 'rsa', + name: 'rsa.misc.cn_dst_tos', + type: 'keyword', + }, + 'rsa.misc.cn_dst_vlan': { + category: 'rsa', + name: 'rsa.misc.cn_dst_vlan', + type: 'keyword', + }, + 'rsa.misc.cn_engine_id': { + category: 'rsa', + name: 'rsa.misc.cn_engine_id', + type: 'keyword', + }, + 'rsa.misc.cn_engine_type': { + category: 'rsa', + name: 'rsa.misc.cn_engine_type', + type: 'keyword', + }, + 'rsa.misc.cn_f_switch': { + category: 'rsa', + name: 'rsa.misc.cn_f_switch', + type: 'keyword', + }, + 'rsa.misc.cn_flowsampid': { + category: 'rsa', + name: 'rsa.misc.cn_flowsampid', + type: 'keyword', + }, + 'rsa.misc.cn_flowsampintv': { + category: 'rsa', + name: 'rsa.misc.cn_flowsampintv', + type: 'keyword', + }, + 'rsa.misc.cn_flowsampmode': { + category: 'rsa', + name: 'rsa.misc.cn_flowsampmode', + type: 'keyword', + }, + 'rsa.misc.cn_inacttimeout': { + category: 'rsa', + name: 'rsa.misc.cn_inacttimeout', + type: 'keyword', + }, + 'rsa.misc.cn_inpermbyts': { + category: 'rsa', + name: 'rsa.misc.cn_inpermbyts', + type: 'keyword', + }, + 'rsa.misc.cn_inpermpckts': { + category: 'rsa', + name: 'rsa.misc.cn_inpermpckts', + type: 'keyword', + }, + 'rsa.misc.cn_invalid': { + category: 'rsa', + name: 'rsa.misc.cn_invalid', + type: 'keyword', + }, + 'rsa.misc.cn_ip_proto_ver': { + category: 'rsa', + name: 'rsa.misc.cn_ip_proto_ver', + type: 'keyword', + }, + 'rsa.misc.cn_ipv4_ident': { + category: 'rsa', + name: 'rsa.misc.cn_ipv4_ident', + type: 'keyword', + }, + 'rsa.misc.cn_l_switch': { + category: 'rsa', + name: 'rsa.misc.cn_l_switch', + type: 'keyword', + }, + 'rsa.misc.cn_log_did': { + category: 'rsa', + name: 'rsa.misc.cn_log_did', + type: 'keyword', + }, + 'rsa.misc.cn_log_rid': { + category: 'rsa', + name: 'rsa.misc.cn_log_rid', + type: 'keyword', + }, + 'rsa.misc.cn_max_ttl': { + category: 'rsa', + name: 'rsa.misc.cn_max_ttl', + type: 'keyword', + }, + 'rsa.misc.cn_maxpcktlen': { + category: 'rsa', + name: 'rsa.misc.cn_maxpcktlen', + type: 'keyword', + }, + 'rsa.misc.cn_min_ttl': { + category: 'rsa', + name: 'rsa.misc.cn_min_ttl', + type: 'keyword', + }, + 'rsa.misc.cn_minpcktlen': { + category: 'rsa', + name: 'rsa.misc.cn_minpcktlen', + type: 'keyword', + }, + 'rsa.misc.cn_mpls_lbl_1': { + category: 'rsa', + name: 'rsa.misc.cn_mpls_lbl_1', + type: 'keyword', + }, + 'rsa.misc.cn_mpls_lbl_10': { + category: 'rsa', + name: 'rsa.misc.cn_mpls_lbl_10', + type: 'keyword', + }, + 'rsa.misc.cn_mpls_lbl_2': { + category: 'rsa', + name: 'rsa.misc.cn_mpls_lbl_2', + type: 'keyword', + }, + 'rsa.misc.cn_mpls_lbl_3': { + category: 'rsa', + name: 'rsa.misc.cn_mpls_lbl_3', + type: 'keyword', + }, + 'rsa.misc.cn_mpls_lbl_4': { + category: 'rsa', + name: 'rsa.misc.cn_mpls_lbl_4', + type: 'keyword', + }, + 'rsa.misc.cn_mpls_lbl_5': { + category: 'rsa', + name: 'rsa.misc.cn_mpls_lbl_5', + type: 'keyword', + }, + 'rsa.misc.cn_mpls_lbl_6': { + category: 'rsa', + name: 'rsa.misc.cn_mpls_lbl_6', + type: 'keyword', + }, + 'rsa.misc.cn_mpls_lbl_7': { + category: 'rsa', + name: 'rsa.misc.cn_mpls_lbl_7', + type: 'keyword', + }, + 'rsa.misc.cn_mpls_lbl_8': { + category: 'rsa', + name: 'rsa.misc.cn_mpls_lbl_8', + type: 'keyword', + }, + 'rsa.misc.cn_mpls_lbl_9': { + category: 'rsa', + name: 'rsa.misc.cn_mpls_lbl_9', + type: 'keyword', + }, + 'rsa.misc.cn_mplstoplabel': { + category: 'rsa', + name: 'rsa.misc.cn_mplstoplabel', + type: 'keyword', + }, + 'rsa.misc.cn_mplstoplabip': { + category: 'rsa', + name: 'rsa.misc.cn_mplstoplabip', + type: 'keyword', + }, + 'rsa.misc.cn_mul_dst_byt': { + category: 'rsa', + name: 'rsa.misc.cn_mul_dst_byt', + type: 'keyword', + }, + 'rsa.misc.cn_mul_dst_pks': { + category: 'rsa', + name: 'rsa.misc.cn_mul_dst_pks', + type: 'keyword', + }, + 'rsa.misc.cn_muligmptype': { + category: 'rsa', + name: 'rsa.misc.cn_muligmptype', + type: 'keyword', + }, + 'rsa.misc.cn_sampalgo': { + category: 'rsa', + name: 'rsa.misc.cn_sampalgo', + type: 'keyword', + }, + 'rsa.misc.cn_sampint': { + category: 'rsa', + name: 'rsa.misc.cn_sampint', + type: 'keyword', + }, + 'rsa.misc.cn_seqctr': { + category: 'rsa', + name: 'rsa.misc.cn_seqctr', + type: 'keyword', + }, + 'rsa.misc.cn_spackets': { + category: 'rsa', + name: 'rsa.misc.cn_spackets', + type: 'keyword', + }, + 'rsa.misc.cn_src_tos': { + category: 'rsa', + name: 'rsa.misc.cn_src_tos', + type: 'keyword', + }, + 'rsa.misc.cn_src_vlan': { + category: 'rsa', + name: 'rsa.misc.cn_src_vlan', + type: 'keyword', + }, + 'rsa.misc.cn_sysuptime': { + category: 'rsa', + name: 'rsa.misc.cn_sysuptime', + type: 'keyword', + }, + 'rsa.misc.cn_template_id': { + category: 'rsa', + name: 'rsa.misc.cn_template_id', + type: 'keyword', + }, + 'rsa.misc.cn_totbytsexp': { + category: 'rsa', + name: 'rsa.misc.cn_totbytsexp', + type: 'keyword', + }, + 'rsa.misc.cn_totflowexp': { + category: 'rsa', + name: 'rsa.misc.cn_totflowexp', + type: 'keyword', + }, + 'rsa.misc.cn_totpcktsexp': { + category: 'rsa', + name: 'rsa.misc.cn_totpcktsexp', + type: 'keyword', + }, + 'rsa.misc.cn_unixnanosecs': { + category: 'rsa', + name: 'rsa.misc.cn_unixnanosecs', + type: 'keyword', + }, + 'rsa.misc.cn_v6flowlabel': { + category: 'rsa', + name: 'rsa.misc.cn_v6flowlabel', + type: 'keyword', + }, + 'rsa.misc.cn_v6optheaders': { + category: 'rsa', + name: 'rsa.misc.cn_v6optheaders', + type: 'keyword', + }, + 'rsa.misc.comp_class': { + category: 'rsa', + name: 'rsa.misc.comp_class', + type: 'keyword', + }, + 'rsa.misc.comp_name': { + category: 'rsa', + name: 'rsa.misc.comp_name', + type: 'keyword', + }, + 'rsa.misc.comp_rbytes': { + category: 'rsa', + name: 'rsa.misc.comp_rbytes', + type: 'keyword', + }, + 'rsa.misc.comp_sbytes': { + category: 'rsa', + name: 'rsa.misc.comp_sbytes', + type: 'keyword', + }, + 'rsa.misc.cpu_data': { + category: 'rsa', + name: 'rsa.misc.cpu_data', + type: 'keyword', + }, + 'rsa.misc.criticality': { + category: 'rsa', + name: 'rsa.misc.criticality', + type: 'keyword', + }, + 'rsa.misc.cs_agency_dst': { + category: 'rsa', + name: 'rsa.misc.cs_agency_dst', + type: 'keyword', + }, + 'rsa.misc.cs_analyzedby': { + category: 'rsa', + name: 'rsa.misc.cs_analyzedby', + type: 'keyword', + }, + 'rsa.misc.cs_av_other': { + category: 'rsa', + name: 'rsa.misc.cs_av_other', + type: 'keyword', + }, + 'rsa.misc.cs_av_primary': { + category: 'rsa', + name: 'rsa.misc.cs_av_primary', + type: 'keyword', + }, + 'rsa.misc.cs_av_secondary': { + category: 'rsa', + name: 'rsa.misc.cs_av_secondary', + type: 'keyword', + }, + 'rsa.misc.cs_bgpv6nxthop': { + category: 'rsa', + name: 'rsa.misc.cs_bgpv6nxthop', + type: 'keyword', + }, + 'rsa.misc.cs_bit9status': { + category: 'rsa', + name: 'rsa.misc.cs_bit9status', + type: 'keyword', + }, + 'rsa.misc.cs_context': { + category: 'rsa', + name: 'rsa.misc.cs_context', + type: 'keyword', + }, + 'rsa.misc.cs_control': { + category: 'rsa', + name: 'rsa.misc.cs_control', + type: 'keyword', + }, + 'rsa.misc.cs_data': { + category: 'rsa', + name: 'rsa.misc.cs_data', + type: 'keyword', + }, + 'rsa.misc.cs_datecret': { + category: 'rsa', + name: 'rsa.misc.cs_datecret', + type: 'keyword', + }, + 'rsa.misc.cs_dst_tld': { + category: 'rsa', + name: 'rsa.misc.cs_dst_tld', + type: 'keyword', + }, + 'rsa.misc.cs_eth_dst_ven': { + category: 'rsa', + name: 'rsa.misc.cs_eth_dst_ven', + type: 'keyword', + }, + 'rsa.misc.cs_eth_src_ven': { + category: 'rsa', + name: 'rsa.misc.cs_eth_src_ven', + type: 'keyword', + }, + 'rsa.misc.cs_event_uuid': { + category: 'rsa', + name: 'rsa.misc.cs_event_uuid', + type: 'keyword', + }, + 'rsa.misc.cs_filetype': { + category: 'rsa', + name: 'rsa.misc.cs_filetype', + type: 'keyword', + }, + 'rsa.misc.cs_fld': { + category: 'rsa', + name: 'rsa.misc.cs_fld', + type: 'keyword', + }, + 'rsa.misc.cs_if_desc': { + category: 'rsa', + name: 'rsa.misc.cs_if_desc', + type: 'keyword', + }, + 'rsa.misc.cs_if_name': { + category: 'rsa', + name: 'rsa.misc.cs_if_name', + type: 'keyword', + }, + 'rsa.misc.cs_ip_next_hop': { + category: 'rsa', + name: 'rsa.misc.cs_ip_next_hop', + type: 'keyword', + }, + 'rsa.misc.cs_ipv4dstpre': { + category: 'rsa', + name: 'rsa.misc.cs_ipv4dstpre', + type: 'keyword', + }, + 'rsa.misc.cs_ipv4srcpre': { + category: 'rsa', + name: 'rsa.misc.cs_ipv4srcpre', + type: 'keyword', + }, + 'rsa.misc.cs_lifetime': { + category: 'rsa', + name: 'rsa.misc.cs_lifetime', + type: 'keyword', + }, + 'rsa.misc.cs_log_medium': { + category: 'rsa', + name: 'rsa.misc.cs_log_medium', + type: 'keyword', + }, + 'rsa.misc.cs_loginname': { + category: 'rsa', + name: 'rsa.misc.cs_loginname', + type: 'keyword', + }, + 'rsa.misc.cs_modulescore': { + category: 'rsa', + name: 'rsa.misc.cs_modulescore', + type: 'keyword', + }, + 'rsa.misc.cs_modulesign': { + category: 'rsa', + name: 'rsa.misc.cs_modulesign', + type: 'keyword', + }, + 'rsa.misc.cs_opswatresult': { + category: 'rsa', + name: 'rsa.misc.cs_opswatresult', + type: 'keyword', + }, + 'rsa.misc.cs_payload': { + category: 'rsa', + name: 'rsa.misc.cs_payload', + type: 'keyword', + }, + 'rsa.misc.cs_registrant': { + category: 'rsa', + name: 'rsa.misc.cs_registrant', + type: 'keyword', + }, + 'rsa.misc.cs_registrar': { + category: 'rsa', + name: 'rsa.misc.cs_registrar', + type: 'keyword', + }, + 'rsa.misc.cs_represult': { + category: 'rsa', + name: 'rsa.misc.cs_represult', + type: 'keyword', + }, + 'rsa.misc.cs_rpayload': { + category: 'rsa', + name: 'rsa.misc.cs_rpayload', + type: 'keyword', + }, + 'rsa.misc.cs_sampler_name': { + category: 'rsa', + name: 'rsa.misc.cs_sampler_name', + type: 'keyword', + }, + 'rsa.misc.cs_sourcemodule': { + category: 'rsa', + name: 'rsa.misc.cs_sourcemodule', + type: 'keyword', + }, + 'rsa.misc.cs_streams': { + category: 'rsa', + name: 'rsa.misc.cs_streams', + type: 'keyword', + }, + 'rsa.misc.cs_targetmodule': { + category: 'rsa', + name: 'rsa.misc.cs_targetmodule', + type: 'keyword', + }, + 'rsa.misc.cs_v6nxthop': { + category: 'rsa', + name: 'rsa.misc.cs_v6nxthop', + type: 'keyword', + }, + 'rsa.misc.cs_whois_server': { + category: 'rsa', + name: 'rsa.misc.cs_whois_server', + type: 'keyword', + }, + 'rsa.misc.cs_yararesult': { + category: 'rsa', + name: 'rsa.misc.cs_yararesult', + type: 'keyword', + }, + 'rsa.misc.description': { + category: 'rsa', + name: 'rsa.misc.description', + type: 'keyword', + }, + 'rsa.misc.devvendor': { + category: 'rsa', + name: 'rsa.misc.devvendor', + type: 'keyword', + }, + 'rsa.misc.distance': { + category: 'rsa', + name: 'rsa.misc.distance', + type: 'keyword', + }, + 'rsa.misc.dstburb': { + category: 'rsa', + name: 'rsa.misc.dstburb', + type: 'keyword', + }, + 'rsa.misc.edomain': { + category: 'rsa', + name: 'rsa.misc.edomain', + type: 'keyword', + }, + 'rsa.misc.edomaub': { + category: 'rsa', + name: 'rsa.misc.edomaub', + type: 'keyword', + }, + 'rsa.misc.euid': { + category: 'rsa', + name: 'rsa.misc.euid', + type: 'keyword', + }, + 'rsa.misc.facility': { + category: 'rsa', + name: 'rsa.misc.facility', + type: 'keyword', + }, + 'rsa.misc.finterface': { + category: 'rsa', + name: 'rsa.misc.finterface', + type: 'keyword', + }, + 'rsa.misc.flags': { + category: 'rsa', + name: 'rsa.misc.flags', + type: 'keyword', + }, + 'rsa.misc.gaddr': { + category: 'rsa', + name: 'rsa.misc.gaddr', + type: 'keyword', + }, + 'rsa.misc.id3': { + category: 'rsa', + name: 'rsa.misc.id3', + type: 'keyword', + }, + 'rsa.misc.im_buddyname': { + category: 'rsa', + name: 'rsa.misc.im_buddyname', + type: 'keyword', + }, + 'rsa.misc.im_croomid': { + category: 'rsa', + name: 'rsa.misc.im_croomid', + type: 'keyword', + }, + 'rsa.misc.im_croomtype': { + category: 'rsa', + name: 'rsa.misc.im_croomtype', + type: 'keyword', + }, + 'rsa.misc.im_members': { + category: 'rsa', + name: 'rsa.misc.im_members', + type: 'keyword', + }, + 'rsa.misc.im_username': { + category: 'rsa', + name: 'rsa.misc.im_username', + type: 'keyword', + }, + 'rsa.misc.ipkt': { + category: 'rsa', + name: 'rsa.misc.ipkt', + type: 'keyword', + }, + 'rsa.misc.ipscat': { + category: 'rsa', + name: 'rsa.misc.ipscat', + type: 'keyword', + }, + 'rsa.misc.ipspri': { + category: 'rsa', + name: 'rsa.misc.ipspri', + type: 'keyword', + }, + 'rsa.misc.latitude': { + category: 'rsa', + name: 'rsa.misc.latitude', + type: 'keyword', + }, + 'rsa.misc.linenum': { + category: 'rsa', + name: 'rsa.misc.linenum', + type: 'keyword', + }, + 'rsa.misc.list_name': { + category: 'rsa', + name: 'rsa.misc.list_name', + type: 'keyword', + }, + 'rsa.misc.load_data': { + category: 'rsa', + name: 'rsa.misc.load_data', + type: 'keyword', + }, + 'rsa.misc.location_floor': { + category: 'rsa', + name: 'rsa.misc.location_floor', + type: 'keyword', + }, + 'rsa.misc.location_mark': { + category: 'rsa', + name: 'rsa.misc.location_mark', + type: 'keyword', + }, + 'rsa.misc.log_id': { + category: 'rsa', + name: 'rsa.misc.log_id', + type: 'keyword', + }, + 'rsa.misc.log_type': { + category: 'rsa', + name: 'rsa.misc.log_type', + type: 'keyword', + }, + 'rsa.misc.logid': { + category: 'rsa', + name: 'rsa.misc.logid', + type: 'keyword', + }, + 'rsa.misc.logip': { + category: 'rsa', + name: 'rsa.misc.logip', + type: 'keyword', + }, + 'rsa.misc.logname': { + category: 'rsa', + name: 'rsa.misc.logname', + type: 'keyword', + }, + 'rsa.misc.longitude': { + category: 'rsa', + name: 'rsa.misc.longitude', + type: 'keyword', + }, + 'rsa.misc.lport': { + category: 'rsa', + name: 'rsa.misc.lport', + type: 'keyword', + }, + 'rsa.misc.mbug_data': { + category: 'rsa', + name: 'rsa.misc.mbug_data', + type: 'keyword', + }, + 'rsa.misc.misc_name': { + category: 'rsa', + name: 'rsa.misc.misc_name', + type: 'keyword', + }, + 'rsa.misc.msg_type': { + category: 'rsa', + name: 'rsa.misc.msg_type', + type: 'keyword', + }, + 'rsa.misc.msgid': { + category: 'rsa', + name: 'rsa.misc.msgid', + type: 'keyword', + }, + 'rsa.misc.netsessid': { + category: 'rsa', + name: 'rsa.misc.netsessid', + type: 'keyword', + }, + 'rsa.misc.num': { + category: 'rsa', + name: 'rsa.misc.num', + type: 'keyword', + }, + 'rsa.misc.number1': { + category: 'rsa', + name: 'rsa.misc.number1', + type: 'keyword', + }, + 'rsa.misc.number2': { + category: 'rsa', + name: 'rsa.misc.number2', + type: 'keyword', + }, + 'rsa.misc.nwwn': { + category: 'rsa', + name: 'rsa.misc.nwwn', + type: 'keyword', + }, + 'rsa.misc.object': { + category: 'rsa', + name: 'rsa.misc.object', + type: 'keyword', + }, + 'rsa.misc.operation': { + category: 'rsa', + name: 'rsa.misc.operation', + type: 'keyword', + }, + 'rsa.misc.opkt': { + category: 'rsa', + name: 'rsa.misc.opkt', + type: 'keyword', + }, + 'rsa.misc.orig_from': { + category: 'rsa', + name: 'rsa.misc.orig_from', + type: 'keyword', + }, + 'rsa.misc.owner_id': { + category: 'rsa', + name: 'rsa.misc.owner_id', + type: 'keyword', + }, + 'rsa.misc.p_action': { + category: 'rsa', + name: 'rsa.misc.p_action', + type: 'keyword', + }, + 'rsa.misc.p_filter': { + category: 'rsa', + name: 'rsa.misc.p_filter', + type: 'keyword', + }, + 'rsa.misc.p_group_object': { + category: 'rsa', + name: 'rsa.misc.p_group_object', + type: 'keyword', + }, + 'rsa.misc.p_id': { + category: 'rsa', + name: 'rsa.misc.p_id', + type: 'keyword', + }, + 'rsa.misc.p_msgid1': { + category: 'rsa', + name: 'rsa.misc.p_msgid1', + type: 'keyword', + }, + 'rsa.misc.p_msgid2': { + category: 'rsa', + name: 'rsa.misc.p_msgid2', + type: 'keyword', + }, + 'rsa.misc.p_result1': { + category: 'rsa', + name: 'rsa.misc.p_result1', + type: 'keyword', + }, + 'rsa.misc.password_chg': { + category: 'rsa', + name: 'rsa.misc.password_chg', + type: 'keyword', + }, + 'rsa.misc.password_expire': { + category: 'rsa', + name: 'rsa.misc.password_expire', + type: 'keyword', + }, + 'rsa.misc.permgranted': { + category: 'rsa', + name: 'rsa.misc.permgranted', + type: 'keyword', + }, + 'rsa.misc.permwanted': { + category: 'rsa', + name: 'rsa.misc.permwanted', + type: 'keyword', + }, + 'rsa.misc.pgid': { + category: 'rsa', + name: 'rsa.misc.pgid', + type: 'keyword', + }, + 'rsa.misc.policyUUID': { + category: 'rsa', + name: 'rsa.misc.policyUUID', + type: 'keyword', + }, + 'rsa.misc.prog_asp_num': { + category: 'rsa', + name: 'rsa.misc.prog_asp_num', + type: 'keyword', + }, + 'rsa.misc.program': { + category: 'rsa', + name: 'rsa.misc.program', + type: 'keyword', + }, + 'rsa.misc.real_data': { + category: 'rsa', + name: 'rsa.misc.real_data', + type: 'keyword', + }, + 'rsa.misc.rec_asp_device': { + category: 'rsa', + name: 'rsa.misc.rec_asp_device', + type: 'keyword', + }, + 'rsa.misc.rec_asp_num': { + category: 'rsa', + name: 'rsa.misc.rec_asp_num', + type: 'keyword', + }, + 'rsa.misc.rec_library': { + category: 'rsa', + name: 'rsa.misc.rec_library', + type: 'keyword', + }, + 'rsa.misc.recordnum': { + category: 'rsa', + name: 'rsa.misc.recordnum', + type: 'keyword', + }, + 'rsa.misc.ruid': { + category: 'rsa', + name: 'rsa.misc.ruid', + type: 'keyword', + }, + 'rsa.misc.sburb': { + category: 'rsa', + name: 'rsa.misc.sburb', + type: 'keyword', + }, + 'rsa.misc.sdomain_fld': { + category: 'rsa', + name: 'rsa.misc.sdomain_fld', + type: 'keyword', + }, + 'rsa.misc.sec': { + category: 'rsa', + name: 'rsa.misc.sec', + type: 'keyword', + }, + 'rsa.misc.sensorname': { + category: 'rsa', + name: 'rsa.misc.sensorname', + type: 'keyword', + }, + 'rsa.misc.seqnum': { + category: 'rsa', + name: 'rsa.misc.seqnum', + type: 'keyword', + }, + 'rsa.misc.session': { + category: 'rsa', + name: 'rsa.misc.session', + type: 'keyword', + }, + 'rsa.misc.sessiontype': { + category: 'rsa', + name: 'rsa.misc.sessiontype', + type: 'keyword', + }, + 'rsa.misc.sigUUID': { + category: 'rsa', + name: 'rsa.misc.sigUUID', + type: 'keyword', + }, + 'rsa.misc.spi': { + category: 'rsa', + name: 'rsa.misc.spi', + type: 'keyword', + }, + 'rsa.misc.srcburb': { + category: 'rsa', + name: 'rsa.misc.srcburb', + type: 'keyword', + }, + 'rsa.misc.srcdom': { + category: 'rsa', + name: 'rsa.misc.srcdom', + type: 'keyword', + }, + 'rsa.misc.srcservice': { + category: 'rsa', + name: 'rsa.misc.srcservice', + type: 'keyword', + }, + 'rsa.misc.state': { + category: 'rsa', + name: 'rsa.misc.state', + type: 'keyword', + }, + 'rsa.misc.status1': { + category: 'rsa', + name: 'rsa.misc.status1', + type: 'keyword', + }, + 'rsa.misc.svcno': { + category: 'rsa', + name: 'rsa.misc.svcno', + type: 'keyword', + }, + 'rsa.misc.system': { + category: 'rsa', + name: 'rsa.misc.system', + type: 'keyword', + }, + 'rsa.misc.tbdstr1': { + category: 'rsa', + name: 'rsa.misc.tbdstr1', + type: 'keyword', + }, + 'rsa.misc.tgtdom': { + category: 'rsa', + name: 'rsa.misc.tgtdom', + type: 'keyword', + }, + 'rsa.misc.tgtdomain': { + category: 'rsa', + name: 'rsa.misc.tgtdomain', + type: 'keyword', + }, + 'rsa.misc.threshold': { + category: 'rsa', + name: 'rsa.misc.threshold', + type: 'keyword', + }, + 'rsa.misc.type1': { + category: 'rsa', + name: 'rsa.misc.type1', + type: 'keyword', + }, + 'rsa.misc.udb_class': { + category: 'rsa', + name: 'rsa.misc.udb_class', + type: 'keyword', + }, + 'rsa.misc.url_fld': { + category: 'rsa', + name: 'rsa.misc.url_fld', + type: 'keyword', + }, + 'rsa.misc.user_div': { + category: 'rsa', + name: 'rsa.misc.user_div', + type: 'keyword', + }, + 'rsa.misc.userid': { + category: 'rsa', + name: 'rsa.misc.userid', + type: 'keyword', + }, + 'rsa.misc.username_fld': { + category: 'rsa', + name: 'rsa.misc.username_fld', + type: 'keyword', + }, + 'rsa.misc.utcstamp': { + category: 'rsa', + name: 'rsa.misc.utcstamp', + type: 'keyword', + }, + 'rsa.misc.v_instafname': { + category: 'rsa', + name: 'rsa.misc.v_instafname', + type: 'keyword', + }, + 'rsa.misc.virt_data': { + category: 'rsa', + name: 'rsa.misc.virt_data', + type: 'keyword', + }, + 'rsa.misc.vpnid': { + category: 'rsa', + name: 'rsa.misc.vpnid', + type: 'keyword', + }, + 'rsa.misc.autorun_type': { + category: 'rsa', + description: 'This is used to capture Auto Run type', + name: 'rsa.misc.autorun_type', + type: 'keyword', + }, + 'rsa.misc.cc_number': { + category: 'rsa', + description: 'Valid Credit Card Numbers only', + name: 'rsa.misc.cc_number', + type: 'long', + }, + 'rsa.misc.content': { + category: 'rsa', + description: 'This key captures the content type from protocol headers', + name: 'rsa.misc.content', + type: 'keyword', + }, + 'rsa.misc.ein_number': { + category: 'rsa', + description: 'Employee Identification Numbers only', + name: 'rsa.misc.ein_number', + type: 'long', + }, + 'rsa.misc.found': { + category: 'rsa', + description: 'This is used to capture the results of regex match', + name: 'rsa.misc.found', + type: 'keyword', + }, + 'rsa.misc.language': { + category: 'rsa', + description: 'This is used to capture list of languages the client support and what it prefers', + name: 'rsa.misc.language', + type: 'keyword', + }, + 'rsa.misc.lifetime': { + category: 'rsa', + description: 'This key is used to capture the session lifetime in seconds.', + name: 'rsa.misc.lifetime', + type: 'long', + }, + 'rsa.misc.link': { + category: 'rsa', + description: + 'This key is used to link the sessions together. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.misc.link', + type: 'keyword', + }, + 'rsa.misc.match': { + category: 'rsa', + description: 'This key is for regex match name from search.ini', + name: 'rsa.misc.match', + type: 'keyword', + }, + 'rsa.misc.param_dst': { + category: 'rsa', + description: 'This key captures the command line/launch argument of the target process or file', + name: 'rsa.misc.param_dst', + type: 'keyword', + }, + 'rsa.misc.param_src': { + category: 'rsa', + description: 'This key captures source parameter', + name: 'rsa.misc.param_src', + type: 'keyword', + }, + 'rsa.misc.search_text': { + category: 'rsa', + description: 'This key captures the Search Text used', + name: 'rsa.misc.search_text', + type: 'keyword', + }, + 'rsa.misc.sig_name': { + category: 'rsa', + description: 'This key is used to capture the Signature Name only.', + name: 'rsa.misc.sig_name', + type: 'keyword', + }, + 'rsa.misc.snmp_value': { + category: 'rsa', + description: 'SNMP set request value', + name: 'rsa.misc.snmp_value', + type: 'keyword', + }, + 'rsa.misc.streams': { + category: 'rsa', + description: 'This key captures number of streams in session', + name: 'rsa.misc.streams', + type: 'long', + }, + 'rsa.db.index': { + category: 'rsa', + description: 'This key captures IndexID of the index.', + name: 'rsa.db.index', + type: 'keyword', + }, + 'rsa.db.instance': { + category: 'rsa', + description: 'This key is used to capture the database server instance name', + name: 'rsa.db.instance', + type: 'keyword', + }, + 'rsa.db.database': { + category: 'rsa', + description: + 'This key is used to capture the name of a database or an instance as seen in a session', + name: 'rsa.db.database', + type: 'keyword', + }, + 'rsa.db.transact_id': { + category: 'rsa', + description: 'This key captures the SQL transantion ID of the current session', + name: 'rsa.db.transact_id', + type: 'keyword', + }, + 'rsa.db.permissions': { + category: 'rsa', + description: 'This key captures permission or privilege level assigned to a resource.', + name: 'rsa.db.permissions', + type: 'keyword', + }, + 'rsa.db.table_name': { + category: 'rsa', + description: 'This key is used to capture the table name', + name: 'rsa.db.table_name', + type: 'keyword', + }, + 'rsa.db.db_id': { + category: 'rsa', + description: 'This key is used to capture the unique identifier for a database', + name: 'rsa.db.db_id', + type: 'keyword', + }, + 'rsa.db.db_pid': { + category: 'rsa', + description: 'This key captures the process id of a connection with database server', + name: 'rsa.db.db_pid', + type: 'long', + }, + 'rsa.db.lread': { + category: 'rsa', + description: 'This key is used for the number of logical reads', + name: 'rsa.db.lread', + type: 'long', + }, + 'rsa.db.lwrite': { + category: 'rsa', + description: 'This key is used for the number of logical writes', + name: 'rsa.db.lwrite', + type: 'long', + }, + 'rsa.db.pread': { + category: 'rsa', + description: 'This key is used for the number of physical writes', + name: 'rsa.db.pread', + type: 'long', + }, + 'rsa.network.alias_host': { + category: 'rsa', + description: + 'This key should be used when the source or destination context of a hostname is not clear.Also it captures the Device Hostname. Any Hostname that isnt ad.computer.', + name: 'rsa.network.alias_host', + type: 'keyword', + }, + 'rsa.network.domain': { + category: 'rsa', + name: 'rsa.network.domain', + type: 'keyword', + }, + 'rsa.network.host_dst': { + category: 'rsa', + description: 'This key should only be used when it’s a Destination Hostname', + name: 'rsa.network.host_dst', + type: 'keyword', + }, + 'rsa.network.network_service': { + category: 'rsa', + description: 'This is used to capture layer 7 protocols/service names', + name: 'rsa.network.network_service', + type: 'keyword', + }, + 'rsa.network.interface': { + category: 'rsa', + description: + 'This key should be used when the source or destination context of an interface is not clear', + name: 'rsa.network.interface', + type: 'keyword', + }, + 'rsa.network.network_port': { + category: 'rsa', + description: + 'Deprecated, use port. NOTE: There is a type discrepancy as currently used, TM: Int32, INDEX: UInt64 (why neither chose the correct UInt16?!)', + name: 'rsa.network.network_port', + type: 'long', + }, + 'rsa.network.eth_host': { + category: 'rsa', + description: 'Deprecated, use alias.mac', + name: 'rsa.network.eth_host', + type: 'keyword', + }, + 'rsa.network.sinterface': { + category: 'rsa', + description: 'This key should only be used when it’s a Source Interface', + name: 'rsa.network.sinterface', + type: 'keyword', + }, + 'rsa.network.dinterface': { + category: 'rsa', + description: 'This key should only be used when it’s a Destination Interface', + name: 'rsa.network.dinterface', + type: 'keyword', + }, + 'rsa.network.vlan': { + category: 'rsa', + description: 'This key should only be used to capture the ID of the Virtual LAN', + name: 'rsa.network.vlan', + type: 'long', + }, + 'rsa.network.zone_src': { + category: 'rsa', + description: 'This key should only be used when it’s a Source Zone.', + name: 'rsa.network.zone_src', + type: 'keyword', + }, + 'rsa.network.zone': { + category: 'rsa', + description: + 'This key should be used when the source or destination context of a Zone is not clear', + name: 'rsa.network.zone', + type: 'keyword', + }, + 'rsa.network.zone_dst': { + category: 'rsa', + description: 'This key should only be used when it’s a Destination Zone.', + name: 'rsa.network.zone_dst', + type: 'keyword', + }, + 'rsa.network.gateway': { + category: 'rsa', + description: 'This key is used to capture the IP Address of the gateway', + name: 'rsa.network.gateway', + type: 'keyword', + }, + 'rsa.network.icmp_type': { + category: 'rsa', + description: 'This key is used to capture the ICMP type only', + name: 'rsa.network.icmp_type', + type: 'long', + }, + 'rsa.network.mask': { + category: 'rsa', + description: 'This key is used to capture the device network IPmask.', + name: 'rsa.network.mask', + type: 'keyword', + }, + 'rsa.network.icmp_code': { + category: 'rsa', + description: 'This key is used to capture the ICMP code only', + name: 'rsa.network.icmp_code', + type: 'long', + }, + 'rsa.network.protocol_detail': { + category: 'rsa', + description: 'This key should be used to capture additional protocol information', + name: 'rsa.network.protocol_detail', + type: 'keyword', + }, + 'rsa.network.dmask': { + category: 'rsa', + description: 'This key is used for Destionation Device network mask', + name: 'rsa.network.dmask', + type: 'keyword', + }, + 'rsa.network.port': { + category: 'rsa', + description: + 'This key should only be used to capture a Network Port when the directionality is not clear', + name: 'rsa.network.port', + type: 'long', + }, + 'rsa.network.smask': { + category: 'rsa', + description: 'This key is used for capturing source Network Mask', + name: 'rsa.network.smask', + type: 'keyword', + }, + 'rsa.network.netname': { + category: 'rsa', + description: + 'This key is used to capture the network name associated with an IP range. This is configured by the end user.', + name: 'rsa.network.netname', + type: 'keyword', + }, + 'rsa.network.paddr': { + category: 'rsa', + description: 'Deprecated', + name: 'rsa.network.paddr', + type: 'ip', + }, + 'rsa.network.faddr': { + category: 'rsa', + name: 'rsa.network.faddr', + type: 'keyword', + }, + 'rsa.network.lhost': { + category: 'rsa', + name: 'rsa.network.lhost', + type: 'keyword', + }, + 'rsa.network.origin': { + category: 'rsa', + name: 'rsa.network.origin', + type: 'keyword', + }, + 'rsa.network.remote_domain_id': { + category: 'rsa', + name: 'rsa.network.remote_domain_id', + type: 'keyword', + }, + 'rsa.network.addr': { + category: 'rsa', + name: 'rsa.network.addr', + type: 'keyword', + }, + 'rsa.network.dns_a_record': { + category: 'rsa', + name: 'rsa.network.dns_a_record', + type: 'keyword', + }, + 'rsa.network.dns_ptr_record': { + category: 'rsa', + name: 'rsa.network.dns_ptr_record', + type: 'keyword', + }, + 'rsa.network.fhost': { + category: 'rsa', + name: 'rsa.network.fhost', + type: 'keyword', + }, + 'rsa.network.fport': { + category: 'rsa', + name: 'rsa.network.fport', + type: 'keyword', + }, + 'rsa.network.laddr': { + category: 'rsa', + name: 'rsa.network.laddr', + type: 'keyword', + }, + 'rsa.network.linterface': { + category: 'rsa', + name: 'rsa.network.linterface', + type: 'keyword', + }, + 'rsa.network.phost': { + category: 'rsa', + name: 'rsa.network.phost', + type: 'keyword', + }, + 'rsa.network.ad_computer_dst': { + category: 'rsa', + description: 'Deprecated, use host.dst', + name: 'rsa.network.ad_computer_dst', + type: 'keyword', + }, + 'rsa.network.eth_type': { + category: 'rsa', + description: 'This key is used to capture Ethernet Type, Used for Layer 3 Protocols Only', + name: 'rsa.network.eth_type', + type: 'long', + }, + 'rsa.network.ip_proto': { + category: 'rsa', + description: + 'This key should be used to capture the Protocol number, all the protocol nubers are converted into string in UI', + name: 'rsa.network.ip_proto', + type: 'long', + }, + 'rsa.network.dns_cname_record': { + category: 'rsa', + name: 'rsa.network.dns_cname_record', + type: 'keyword', + }, + 'rsa.network.dns_id': { + category: 'rsa', + name: 'rsa.network.dns_id', + type: 'keyword', + }, + 'rsa.network.dns_opcode': { + category: 'rsa', + name: 'rsa.network.dns_opcode', + type: 'keyword', + }, + 'rsa.network.dns_resp': { + category: 'rsa', + name: 'rsa.network.dns_resp', + type: 'keyword', + }, + 'rsa.network.dns_type': { + category: 'rsa', + name: 'rsa.network.dns_type', + type: 'keyword', + }, + 'rsa.network.domain1': { + category: 'rsa', + name: 'rsa.network.domain1', + type: 'keyword', + }, + 'rsa.network.host_type': { + category: 'rsa', + name: 'rsa.network.host_type', + type: 'keyword', + }, + 'rsa.network.packet_length': { + category: 'rsa', + name: 'rsa.network.packet_length', + type: 'keyword', + }, + 'rsa.network.host_orig': { + category: 'rsa', + description: + 'This is used to capture the original hostname in case of a Forwarding Agent or a Proxy in between.', + name: 'rsa.network.host_orig', + type: 'keyword', + }, + 'rsa.network.rpayload': { + category: 'rsa', + description: + 'This key is used to capture the total number of payload bytes seen in the retransmitted packets.', + name: 'rsa.network.rpayload', + type: 'keyword', + }, + 'rsa.network.vlan_name': { + category: 'rsa', + description: 'This key should only be used to capture the name of the Virtual LAN', + name: 'rsa.network.vlan_name', + type: 'keyword', + }, + 'rsa.investigations.ec_activity': { + category: 'rsa', + description: 'This key captures the particular event activity(Ex:Logoff)', + name: 'rsa.investigations.ec_activity', + type: 'keyword', + }, + 'rsa.investigations.ec_theme': { + category: 'rsa', + description: 'This key captures the Theme of a particular Event(Ex:Authentication)', + name: 'rsa.investigations.ec_theme', + type: 'keyword', + }, + 'rsa.investigations.ec_subject': { + category: 'rsa', + description: 'This key captures the Subject of a particular Event(Ex:User)', + name: 'rsa.investigations.ec_subject', + type: 'keyword', + }, + 'rsa.investigations.ec_outcome': { + category: 'rsa', + description: 'This key captures the outcome of a particular Event(Ex:Success)', + name: 'rsa.investigations.ec_outcome', + type: 'keyword', + }, + 'rsa.investigations.event_cat': { + category: 'rsa', + description: 'This key captures the Event category number', + name: 'rsa.investigations.event_cat', + type: 'long', + }, + 'rsa.investigations.event_cat_name': { + category: 'rsa', + description: 'This key captures the event category name corresponding to the event cat code', + name: 'rsa.investigations.event_cat_name', + type: 'keyword', + }, + 'rsa.investigations.event_vcat': { + category: 'rsa', + description: + 'This is a vendor supplied category. This should be used in situations where the vendor has adopted their own event_category taxonomy.', + name: 'rsa.investigations.event_vcat', + type: 'keyword', + }, + 'rsa.investigations.analysis_file': { + category: 'rsa', + description: + 'This is used to capture all indicators used in a File Analysis. This key should be used to capture an analysis of a file', + name: 'rsa.investigations.analysis_file', + type: 'keyword', + }, + 'rsa.investigations.analysis_service': { + category: 'rsa', + description: + 'This is used to capture all indicators used in a Service Analysis. This key should be used to capture an analysis of a service', + name: 'rsa.investigations.analysis_service', + type: 'keyword', + }, + 'rsa.investigations.analysis_session': { + category: 'rsa', + description: + 'This is used to capture all indicators used for a Session Analysis. This key should be used to capture an analysis of a session', + name: 'rsa.investigations.analysis_session', + type: 'keyword', + }, + 'rsa.investigations.boc': { + category: 'rsa', + description: 'This is used to capture behaviour of compromise', + name: 'rsa.investigations.boc', + type: 'keyword', + }, + 'rsa.investigations.eoc': { + category: 'rsa', + description: 'This is used to capture Enablers of Compromise', + name: 'rsa.investigations.eoc', + type: 'keyword', + }, + 'rsa.investigations.inv_category': { + category: 'rsa', + description: 'This used to capture investigation category', + name: 'rsa.investigations.inv_category', + type: 'keyword', + }, + 'rsa.investigations.inv_context': { + category: 'rsa', + description: 'This used to capture investigation context', + name: 'rsa.investigations.inv_context', + type: 'keyword', + }, + 'rsa.investigations.ioc': { + category: 'rsa', + description: 'This is key capture indicator of compromise', + name: 'rsa.investigations.ioc', + type: 'keyword', + }, + 'rsa.counters.dclass_c1': { + category: 'rsa', + description: + 'This is a generic counter key that should be used with the label dclass.c1.str only', + name: 'rsa.counters.dclass_c1', + type: 'long', + }, + 'rsa.counters.dclass_c2': { + category: 'rsa', + description: + 'This is a generic counter key that should be used with the label dclass.c2.str only', + name: 'rsa.counters.dclass_c2', + type: 'long', + }, + 'rsa.counters.event_counter': { + category: 'rsa', + description: 'This is used to capture the number of times an event repeated', + name: 'rsa.counters.event_counter', + type: 'long', + }, + 'rsa.counters.dclass_r1': { + category: 'rsa', + description: + 'This is a generic ratio key that should be used with the label dclass.r1.str only', + name: 'rsa.counters.dclass_r1', + type: 'keyword', + }, + 'rsa.counters.dclass_c3': { + category: 'rsa', + description: + 'This is a generic counter key that should be used with the label dclass.c3.str only', + name: 'rsa.counters.dclass_c3', + type: 'long', + }, + 'rsa.counters.dclass_c1_str': { + category: 'rsa', + description: + 'This is a generic counter string key that should be used with the label dclass.c1 only', + name: 'rsa.counters.dclass_c1_str', + type: 'keyword', + }, + 'rsa.counters.dclass_c2_str': { + category: 'rsa', + description: + 'This is a generic counter string key that should be used with the label dclass.c2 only', + name: 'rsa.counters.dclass_c2_str', + type: 'keyword', + }, + 'rsa.counters.dclass_r1_str': { + category: 'rsa', + description: + 'This is a generic ratio string key that should be used with the label dclass.r1 only', + name: 'rsa.counters.dclass_r1_str', + type: 'keyword', + }, + 'rsa.counters.dclass_r2': { + category: 'rsa', + description: + 'This is a generic ratio key that should be used with the label dclass.r2.str only', + name: 'rsa.counters.dclass_r2', + type: 'keyword', + }, + 'rsa.counters.dclass_c3_str': { + category: 'rsa', + description: + 'This is a generic counter string key that should be used with the label dclass.c3 only', + name: 'rsa.counters.dclass_c3_str', + type: 'keyword', + }, + 'rsa.counters.dclass_r3': { + category: 'rsa', + description: + 'This is a generic ratio key that should be used with the label dclass.r3.str only', + name: 'rsa.counters.dclass_r3', + type: 'keyword', + }, + 'rsa.counters.dclass_r2_str': { + category: 'rsa', + description: + 'This is a generic ratio string key that should be used with the label dclass.r2 only', + name: 'rsa.counters.dclass_r2_str', + type: 'keyword', + }, + 'rsa.counters.dclass_r3_str': { + category: 'rsa', + description: + 'This is a generic ratio string key that should be used with the label dclass.r3 only', + name: 'rsa.counters.dclass_r3_str', + type: 'keyword', + }, + 'rsa.identity.auth_method': { + category: 'rsa', + description: 'This key is used to capture authentication methods used only', + name: 'rsa.identity.auth_method', + type: 'keyword', + }, + 'rsa.identity.user_role': { + category: 'rsa', + description: 'This key is used to capture the Role of a user only', + name: 'rsa.identity.user_role', + type: 'keyword', + }, + 'rsa.identity.dn': { + category: 'rsa', + description: 'X.500 (LDAP) Distinguished Name', + name: 'rsa.identity.dn', + type: 'keyword', + }, + 'rsa.identity.logon_type': { + category: 'rsa', + description: 'This key is used to capture the type of logon method used.', + name: 'rsa.identity.logon_type', + type: 'keyword', + }, + 'rsa.identity.profile': { + category: 'rsa', + description: 'This key is used to capture the user profile', + name: 'rsa.identity.profile', + type: 'keyword', + }, + 'rsa.identity.accesses': { + category: 'rsa', + description: 'This key is used to capture actual privileges used in accessing an object', + name: 'rsa.identity.accesses', + type: 'keyword', + }, + 'rsa.identity.realm': { + category: 'rsa', + description: 'Radius realm or similar grouping of accounts', + name: 'rsa.identity.realm', + type: 'keyword', + }, + 'rsa.identity.user_sid_dst': { + category: 'rsa', + description: 'This key captures Destination User Session ID', + name: 'rsa.identity.user_sid_dst', + type: 'keyword', + }, + 'rsa.identity.dn_src': { + category: 'rsa', + description: + 'An X.500 (LDAP) Distinguished name that is used in a context that indicates a Source dn', + name: 'rsa.identity.dn_src', + type: 'keyword', + }, + 'rsa.identity.org': { + category: 'rsa', + description: 'This key captures the User organization', + name: 'rsa.identity.org', + type: 'keyword', + }, + 'rsa.identity.dn_dst': { + category: 'rsa', + description: + 'An X.500 (LDAP) Distinguished name that used in a context that indicates a Destination dn', + name: 'rsa.identity.dn_dst', + type: 'keyword', + }, + 'rsa.identity.firstname': { + category: 'rsa', + description: + 'This key is for First Names only, this is used for Healthcare predominantly to capture Patients information', + name: 'rsa.identity.firstname', + type: 'keyword', + }, + 'rsa.identity.lastname': { + category: 'rsa', + description: + 'This key is for Last Names only, this is used for Healthcare predominantly to capture Patients information', + name: 'rsa.identity.lastname', + type: 'keyword', + }, + 'rsa.identity.user_dept': { + category: 'rsa', + description: "User's Department Names only", + name: 'rsa.identity.user_dept', + type: 'keyword', + }, + 'rsa.identity.user_sid_src': { + category: 'rsa', + description: 'This key captures Source User Session ID', + name: 'rsa.identity.user_sid_src', + type: 'keyword', + }, + 'rsa.identity.federated_sp': { + category: 'rsa', + description: + 'This key is the Federated Service Provider. This is the application requesting authentication.', + name: 'rsa.identity.federated_sp', + type: 'keyword', + }, + 'rsa.identity.federated_idp': { + category: 'rsa', + description: + 'This key is the federated Identity Provider. This is the server providing the authentication.', + name: 'rsa.identity.federated_idp', + type: 'keyword', + }, + 'rsa.identity.logon_type_desc': { + category: 'rsa', + description: + "This key is used to capture the textual description of an integer logon type as stored in the meta key 'logon.type'.", + name: 'rsa.identity.logon_type_desc', + type: 'keyword', + }, + 'rsa.identity.middlename': { + category: 'rsa', + description: + 'This key is for Middle Names only, this is used for Healthcare predominantly to capture Patients information', + name: 'rsa.identity.middlename', + type: 'keyword', + }, + 'rsa.identity.password': { + category: 'rsa', + description: 'This key is for Passwords seen in any session, plain text or encrypted', + name: 'rsa.identity.password', + type: 'keyword', + }, + 'rsa.identity.host_role': { + category: 'rsa', + description: 'This key should only be used to capture the role of a Host Machine', + name: 'rsa.identity.host_role', + type: 'keyword', + }, + 'rsa.identity.ldap': { + category: 'rsa', + description: + 'This key is for Uninterpreted LDAP values. Ldap Values that don’t have a clear query or response context', + name: 'rsa.identity.ldap', + type: 'keyword', + }, + 'rsa.identity.ldap_query': { + category: 'rsa', + description: 'This key is the Search criteria from an LDAP search', + name: 'rsa.identity.ldap_query', + type: 'keyword', + }, + 'rsa.identity.ldap_response': { + category: 'rsa', + description: 'This key is to capture Results from an LDAP search', + name: 'rsa.identity.ldap_response', + type: 'keyword', + }, + 'rsa.identity.owner': { + category: 'rsa', + description: + 'This is used to capture username the process or service is running as, the author of the task', + name: 'rsa.identity.owner', + type: 'keyword', + }, + 'rsa.identity.service_account': { + category: 'rsa', + description: + 'This key is a windows specific key, used for capturing name of the account a service (referenced in the event) is running under. Legacy Usage', + name: 'rsa.identity.service_account', + type: 'keyword', + }, + 'rsa.email.email_dst': { + category: 'rsa', + description: + 'This key is used to capture the Destination email address only, when the destination context is not clear use email', + name: 'rsa.email.email_dst', + type: 'keyword', + }, + 'rsa.email.email_src': { + category: 'rsa', + description: + 'This key is used to capture the source email address only, when the source context is not clear use email', + name: 'rsa.email.email_src', + type: 'keyword', + }, + 'rsa.email.subject': { + category: 'rsa', + description: 'This key is used to capture the subject string from an Email only.', + name: 'rsa.email.subject', + type: 'keyword', + }, + 'rsa.email.email': { + category: 'rsa', + description: + 'This key is used to capture a generic email address where the source or destination context is not clear', + name: 'rsa.email.email', + type: 'keyword', + }, + 'rsa.email.trans_from': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.email.trans_from', + type: 'keyword', + }, + 'rsa.email.trans_to': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.email.trans_to', + type: 'keyword', + }, + 'rsa.file.privilege': { + category: 'rsa', + description: 'Deprecated, use permissions', + name: 'rsa.file.privilege', + type: 'keyword', + }, + 'rsa.file.attachment': { + category: 'rsa', + description: 'This key captures the attachment file name', + name: 'rsa.file.attachment', + type: 'keyword', + }, + 'rsa.file.filesystem': { + category: 'rsa', + name: 'rsa.file.filesystem', + type: 'keyword', + }, + 'rsa.file.binary': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.file.binary', + type: 'keyword', + }, + 'rsa.file.filename_dst': { + category: 'rsa', + description: 'This is used to capture name of the file targeted by the action', + name: 'rsa.file.filename_dst', + type: 'keyword', + }, + 'rsa.file.filename_src': { + category: 'rsa', + description: + 'This is used to capture name of the parent filename, the file which performed the action', + name: 'rsa.file.filename_src', + type: 'keyword', + }, + 'rsa.file.filename_tmp': { + category: 'rsa', + name: 'rsa.file.filename_tmp', + type: 'keyword', + }, + 'rsa.file.directory_dst': { + category: 'rsa', + description: + '<span>This key is used to capture the directory of the target process or file</span>', + name: 'rsa.file.directory_dst', + type: 'keyword', + }, + 'rsa.file.directory_src': { + category: 'rsa', + description: 'This key is used to capture the directory of the source process or file', + name: 'rsa.file.directory_src', + type: 'keyword', + }, + 'rsa.file.file_entropy': { + category: 'rsa', + description: 'This is used to capture entropy vale of a file', + name: 'rsa.file.file_entropy', + type: 'double', + }, + 'rsa.file.file_vendor': { + category: 'rsa', + description: 'This is used to capture Company name of file located in version_info', + name: 'rsa.file.file_vendor', + type: 'keyword', + }, + 'rsa.file.task_name': { + category: 'rsa', + description: 'This is used to capture name of the task', + name: 'rsa.file.task_name', + type: 'keyword', + }, + 'rsa.web.fqdn': { + category: 'rsa', + description: 'Fully Qualified Domain Names', + name: 'rsa.web.fqdn', + type: 'keyword', + }, + 'rsa.web.web_cookie': { + category: 'rsa', + description: 'This key is used to capture the Web cookies specifically.', + name: 'rsa.web.web_cookie', + type: 'keyword', + }, + 'rsa.web.alias_host': { + category: 'rsa', + name: 'rsa.web.alias_host', + type: 'keyword', + }, + 'rsa.web.reputation_num': { + category: 'rsa', + description: 'Reputation Number of an entity. Typically used for Web Domains', + name: 'rsa.web.reputation_num', + type: 'double', + }, + 'rsa.web.web_ref_domain': { + category: 'rsa', + description: "Web referer's domain", + name: 'rsa.web.web_ref_domain', + type: 'keyword', + }, + 'rsa.web.web_ref_query': { + category: 'rsa', + description: "This key captures Web referer's query portion of the URL", + name: 'rsa.web.web_ref_query', + type: 'keyword', + }, + 'rsa.web.remote_domain': { + category: 'rsa', + name: 'rsa.web.remote_domain', + type: 'keyword', + }, + 'rsa.web.web_ref_page': { + category: 'rsa', + description: "This key captures Web referer's page information", + name: 'rsa.web.web_ref_page', + type: 'keyword', + }, + 'rsa.web.web_ref_root': { + category: 'rsa', + description: "Web referer's root URL path", + name: 'rsa.web.web_ref_root', + type: 'keyword', + }, + 'rsa.web.cn_asn_dst': { + category: 'rsa', + name: 'rsa.web.cn_asn_dst', + type: 'keyword', + }, + 'rsa.web.cn_rpackets': { + category: 'rsa', + name: 'rsa.web.cn_rpackets', + type: 'keyword', + }, + 'rsa.web.urlpage': { + category: 'rsa', + name: 'rsa.web.urlpage', + type: 'keyword', + }, + 'rsa.web.urlroot': { + category: 'rsa', + name: 'rsa.web.urlroot', + type: 'keyword', + }, + 'rsa.web.p_url': { + category: 'rsa', + name: 'rsa.web.p_url', + type: 'keyword', + }, + 'rsa.web.p_user_agent': { + category: 'rsa', + name: 'rsa.web.p_user_agent', + type: 'keyword', + }, + 'rsa.web.p_web_cookie': { + category: 'rsa', + name: 'rsa.web.p_web_cookie', + type: 'keyword', + }, + 'rsa.web.p_web_method': { + category: 'rsa', + name: 'rsa.web.p_web_method', + type: 'keyword', + }, + 'rsa.web.p_web_referer': { + category: 'rsa', + name: 'rsa.web.p_web_referer', + type: 'keyword', + }, + 'rsa.web.web_extension_tmp': { + category: 'rsa', + name: 'rsa.web.web_extension_tmp', + type: 'keyword', + }, + 'rsa.web.web_page': { + category: 'rsa', + name: 'rsa.web.web_page', + type: 'keyword', + }, + 'rsa.threat.threat_category': { + category: 'rsa', + description: 'This key captures Threat Name/Threat Category/Categorization of alert', + name: 'rsa.threat.threat_category', + type: 'keyword', + }, + 'rsa.threat.threat_desc': { + category: 'rsa', + description: + 'This key is used to capture the threat description from the session directly or inferred', + name: 'rsa.threat.threat_desc', + type: 'keyword', + }, + 'rsa.threat.alert': { + category: 'rsa', + description: 'This key is used to capture name of the alert', + name: 'rsa.threat.alert', + type: 'keyword', + }, + 'rsa.threat.threat_source': { + category: 'rsa', + description: 'This key is used to capture source of the threat', + name: 'rsa.threat.threat_source', + type: 'keyword', + }, + 'rsa.crypto.crypto': { + category: 'rsa', + description: 'This key is used to capture the Encryption Type or Encryption Key only', + name: 'rsa.crypto.crypto', + type: 'keyword', + }, + 'rsa.crypto.cipher_src': { + category: 'rsa', + description: 'This key is for Source (Client) Cipher', + name: 'rsa.crypto.cipher_src', + type: 'keyword', + }, + 'rsa.crypto.cert_subject': { + category: 'rsa', + description: 'This key is used to capture the Certificate organization only', + name: 'rsa.crypto.cert_subject', + type: 'keyword', + }, + 'rsa.crypto.peer': { + category: 'rsa', + description: "This key is for Encryption peer's IP Address", + name: 'rsa.crypto.peer', + type: 'keyword', + }, + 'rsa.crypto.cipher_size_src': { + category: 'rsa', + description: 'This key captures Source (Client) Cipher Size', + name: 'rsa.crypto.cipher_size_src', + type: 'long', + }, + 'rsa.crypto.ike': { + category: 'rsa', + description: 'IKE negotiation phase.', + name: 'rsa.crypto.ike', + type: 'keyword', + }, + 'rsa.crypto.scheme': { + category: 'rsa', + description: 'This key captures the Encryption scheme used', + name: 'rsa.crypto.scheme', + type: 'keyword', + }, + 'rsa.crypto.peer_id': { + category: 'rsa', + description: 'This key is for Encryption peer’s identity', + name: 'rsa.crypto.peer_id', + type: 'keyword', + }, + 'rsa.crypto.sig_type': { + category: 'rsa', + description: 'This key captures the Signature Type', + name: 'rsa.crypto.sig_type', + type: 'keyword', + }, + 'rsa.crypto.cert_issuer': { + category: 'rsa', + name: 'rsa.crypto.cert_issuer', + type: 'keyword', + }, + 'rsa.crypto.cert_host_name': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.crypto.cert_host_name', + type: 'keyword', + }, + 'rsa.crypto.cert_error': { + category: 'rsa', + description: 'This key captures the Certificate Error String', + name: 'rsa.crypto.cert_error', + type: 'keyword', + }, + 'rsa.crypto.cipher_dst': { + category: 'rsa', + description: 'This key is for Destination (Server) Cipher', + name: 'rsa.crypto.cipher_dst', + type: 'keyword', + }, + 'rsa.crypto.cipher_size_dst': { + category: 'rsa', + description: 'This key captures Destination (Server) Cipher Size', + name: 'rsa.crypto.cipher_size_dst', + type: 'long', + }, + 'rsa.crypto.ssl_ver_src': { + category: 'rsa', + description: 'Deprecated, use version', + name: 'rsa.crypto.ssl_ver_src', + type: 'keyword', + }, + 'rsa.crypto.d_certauth': { + category: 'rsa', + name: 'rsa.crypto.d_certauth', + type: 'keyword', + }, + 'rsa.crypto.s_certauth': { + category: 'rsa', + name: 'rsa.crypto.s_certauth', + type: 'keyword', + }, + 'rsa.crypto.ike_cookie1': { + category: 'rsa', + description: 'ID of the negotiation — sent for ISAKMP Phase One', + name: 'rsa.crypto.ike_cookie1', + type: 'keyword', + }, + 'rsa.crypto.ike_cookie2': { + category: 'rsa', + description: 'ID of the negotiation — sent for ISAKMP Phase Two', + name: 'rsa.crypto.ike_cookie2', + type: 'keyword', + }, + 'rsa.crypto.cert_checksum': { + category: 'rsa', + name: 'rsa.crypto.cert_checksum', + type: 'keyword', + }, + 'rsa.crypto.cert_host_cat': { + category: 'rsa', + description: 'This key is used for the hostname category value of a certificate', + name: 'rsa.crypto.cert_host_cat', + type: 'keyword', + }, + 'rsa.crypto.cert_serial': { + category: 'rsa', + description: 'This key is used to capture the Certificate serial number only', + name: 'rsa.crypto.cert_serial', + type: 'keyword', + }, + 'rsa.crypto.cert_status': { + category: 'rsa', + description: 'This key captures Certificate validation status', + name: 'rsa.crypto.cert_status', + type: 'keyword', + }, + 'rsa.crypto.ssl_ver_dst': { + category: 'rsa', + description: 'Deprecated, use version', + name: 'rsa.crypto.ssl_ver_dst', + type: 'keyword', + }, + 'rsa.crypto.cert_keysize': { + category: 'rsa', + name: 'rsa.crypto.cert_keysize', + type: 'keyword', + }, + 'rsa.crypto.cert_username': { + category: 'rsa', + name: 'rsa.crypto.cert_username', + type: 'keyword', + }, + 'rsa.crypto.https_insact': { + category: 'rsa', + name: 'rsa.crypto.https_insact', + type: 'keyword', + }, + 'rsa.crypto.https_valid': { + category: 'rsa', + name: 'rsa.crypto.https_valid', + type: 'keyword', + }, + 'rsa.crypto.cert_ca': { + category: 'rsa', + description: 'This key is used to capture the Certificate signing authority only', + name: 'rsa.crypto.cert_ca', + type: 'keyword', + }, + 'rsa.crypto.cert_common': { + category: 'rsa', + description: 'This key is used to capture the Certificate common name only', + name: 'rsa.crypto.cert_common', + type: 'keyword', + }, + 'rsa.wireless.wlan_ssid': { + category: 'rsa', + description: 'This key is used to capture the ssid of a Wireless Session', + name: 'rsa.wireless.wlan_ssid', + type: 'keyword', + }, + 'rsa.wireless.access_point': { + category: 'rsa', + description: 'This key is used to capture the access point name.', + name: 'rsa.wireless.access_point', + type: 'keyword', + }, + 'rsa.wireless.wlan_channel': { + category: 'rsa', + description: 'This is used to capture the channel names', + name: 'rsa.wireless.wlan_channel', + type: 'long', + }, + 'rsa.wireless.wlan_name': { + category: 'rsa', + description: 'This key captures either WLAN number/name', + name: 'rsa.wireless.wlan_name', + type: 'keyword', + }, + 'rsa.storage.disk_volume': { + category: 'rsa', + description: 'A unique name assigned to logical units (volumes) within a physical disk', + name: 'rsa.storage.disk_volume', + type: 'keyword', + }, + 'rsa.storage.lun': { + category: 'rsa', + description: 'Logical Unit Number.This key is a very useful concept in Storage.', + name: 'rsa.storage.lun', + type: 'keyword', + }, + 'rsa.storage.pwwn': { + category: 'rsa', + description: 'This uniquely identifies a port on a HBA.', + name: 'rsa.storage.pwwn', + type: 'keyword', + }, + 'rsa.physical.org_dst': { + category: 'rsa', + description: + 'This is used to capture the destination organization based on the GEOPIP Maxmind database.', + name: 'rsa.physical.org_dst', + type: 'keyword', + }, + 'rsa.physical.org_src': { + category: 'rsa', + description: + 'This is used to capture the source organization based on the GEOPIP Maxmind database.', + name: 'rsa.physical.org_src', + type: 'keyword', + }, + 'rsa.healthcare.patient_fname': { + category: 'rsa', + description: + 'This key is for First Names only, this is used for Healthcare predominantly to capture Patients information', + name: 'rsa.healthcare.patient_fname', + type: 'keyword', + }, + 'rsa.healthcare.patient_id': { + category: 'rsa', + description: 'This key captures the unique ID for a patient', + name: 'rsa.healthcare.patient_id', + type: 'keyword', + }, + 'rsa.healthcare.patient_lname': { + category: 'rsa', + description: + 'This key is for Last Names only, this is used for Healthcare predominantly to capture Patients information', + name: 'rsa.healthcare.patient_lname', + type: 'keyword', + }, + 'rsa.healthcare.patient_mname': { + category: 'rsa', + description: + 'This key is for Middle Names only, this is used for Healthcare predominantly to capture Patients information', + name: 'rsa.healthcare.patient_mname', + type: 'keyword', + }, + 'rsa.endpoint.host_state': { + category: 'rsa', + description: + 'This key is used to capture the current state of the machine, such as <strong>blacklisted</strong>, <strong>infected</strong>, <strong>firewall disabled</strong> and so on', + name: 'rsa.endpoint.host_state', + type: 'keyword', + }, + 'rsa.endpoint.registry_key': { + category: 'rsa', + description: 'This key captures the path to the registry key', + name: 'rsa.endpoint.registry_key', + type: 'keyword', + }, + 'rsa.endpoint.registry_value': { + category: 'rsa', + description: 'This key captures values or decorators used within a registry entry', + name: 'rsa.endpoint.registry_value', + type: 'keyword', + }, + 'forcepoint.virus_id': { + category: 'forcepoint', + description: 'Virus ID ', + name: 'forcepoint.virus_id', + type: 'keyword', + }, + 'checkpoint.app_risk': { + category: 'checkpoint', + description: 'Application risk.', + name: 'checkpoint.app_risk', + type: 'keyword', + }, + 'checkpoint.app_severity': { + category: 'checkpoint', + description: 'Application threat severity.', + name: 'checkpoint.app_severity', + type: 'keyword', + }, + 'checkpoint.app_sig_id': { + category: 'checkpoint', + description: 'The signature ID which the application was detected by.', + name: 'checkpoint.app_sig_id', + type: 'keyword', + }, + 'checkpoint.auth_method': { + category: 'checkpoint', + description: 'Password authentication protocol used.', + name: 'checkpoint.auth_method', + type: 'keyword', + }, + 'checkpoint.category': { + category: 'checkpoint', + description: 'Category.', + name: 'checkpoint.category', + type: 'keyword', + }, + 'checkpoint.confidence_level': { + category: 'checkpoint', + description: 'Confidence level determined.', + name: 'checkpoint.confidence_level', + type: 'integer', + }, + 'checkpoint.connectivity_state': { + category: 'checkpoint', + description: 'Connectivity state.', + name: 'checkpoint.connectivity_state', + type: 'keyword', + }, + 'checkpoint.cookie': { + category: 'checkpoint', + description: 'IKE cookie.', + name: 'checkpoint.cookie', + type: 'keyword', + }, + 'checkpoint.dst_phone_number': { + category: 'checkpoint', + description: 'Destination IP-Phone.', + name: 'checkpoint.dst_phone_number', + type: 'keyword', + }, + 'checkpoint.email_control': { + category: 'checkpoint', + description: 'Engine name.', + name: 'checkpoint.email_control', + type: 'keyword', + }, + 'checkpoint.email_id': { + category: 'checkpoint', + description: 'Internal email ID.', + name: 'checkpoint.email_id', + type: 'keyword', + }, + 'checkpoint.email_recipients_num': { + category: 'checkpoint', + description: 'Number of recipients.', + name: 'checkpoint.email_recipients_num', + type: 'long', + }, + 'checkpoint.email_session_id': { + category: 'checkpoint', + description: 'Internal email session ID.', + name: 'checkpoint.email_session_id', + type: 'keyword', + }, + 'checkpoint.email_spool_id': { + category: 'checkpoint', + description: 'Internal email spool ID.', + name: 'checkpoint.email_spool_id', + type: 'keyword', + }, + 'checkpoint.email_subject': { + category: 'checkpoint', + description: 'Email subject.', + name: 'checkpoint.email_subject', + type: 'keyword', + }, + 'checkpoint.event_count': { + category: 'checkpoint', + description: 'Number of events associated with the log.', + name: 'checkpoint.event_count', + type: 'long', + }, + 'checkpoint.frequency': { + category: 'checkpoint', + description: 'Scan frequency.', + name: 'checkpoint.frequency', + type: 'keyword', + }, + 'checkpoint.icmp_type': { + category: 'checkpoint', + description: 'ICMP type.', + name: 'checkpoint.icmp_type', + type: 'long', + }, + 'checkpoint.icmp_code': { + category: 'checkpoint', + description: 'ICMP code.', + name: 'checkpoint.icmp_code', + type: 'long', + }, + 'checkpoint.identity_type': { + category: 'checkpoint', + description: 'Identity type.', + name: 'checkpoint.identity_type', + type: 'keyword', + }, + 'checkpoint.incident_extension': { + category: 'checkpoint', + description: 'Format of original data.', + name: 'checkpoint.incident_extension', + type: 'keyword', + }, + 'checkpoint.integrity_av_invoke_type': { + category: 'checkpoint', + description: 'Scan invoke type.', + name: 'checkpoint.integrity_av_invoke_type', + type: 'keyword', + }, + 'checkpoint.malware_family': { + category: 'checkpoint', + description: 'Malware family.', + name: 'checkpoint.malware_family', + type: 'keyword', + }, + 'checkpoint.peer_gateway': { + category: 'checkpoint', + description: 'Main IP of the peer Security Gateway.', + name: 'checkpoint.peer_gateway', + type: 'ip', + }, + 'checkpoint.performance_impact': { + category: 'checkpoint', + description: 'Protection performance impact.', + name: 'checkpoint.performance_impact', + type: 'integer', + }, + 'checkpoint.protection_id': { + category: 'checkpoint', + description: 'Protection malware ID.', + name: 'checkpoint.protection_id', + type: 'keyword', + }, + 'checkpoint.protection_name': { + category: 'checkpoint', + description: 'Specific signature name of the attack.', + name: 'checkpoint.protection_name', + type: 'keyword', + }, + 'checkpoint.protection_type': { + category: 'checkpoint', + description: 'Type of protection used to detect the attack.', + name: 'checkpoint.protection_type', + type: 'keyword', + }, + 'checkpoint.scan_result': { + category: 'checkpoint', + description: 'Scan result.', + name: 'checkpoint.scan_result', + type: 'keyword', + }, + 'checkpoint.sensor_mode': { + category: 'checkpoint', + description: 'Sensor mode.', + name: 'checkpoint.sensor_mode', + type: 'keyword', + }, + 'checkpoint.severity': { + category: 'checkpoint', + description: 'Threat severity.', + name: 'checkpoint.severity', + type: 'keyword', + }, + 'checkpoint.spyware_name': { + category: 'checkpoint', + description: 'Spyware name.', + name: 'checkpoint.spyware_name', + type: 'keyword', + }, + 'checkpoint.spyware_status': { + category: 'checkpoint', + description: 'Spyware status.', + name: 'checkpoint.spyware_status', + type: 'keyword', + }, + 'checkpoint.subs_exp': { + category: 'checkpoint', + description: 'The expiration date of the subscription.', + name: 'checkpoint.subs_exp', + type: 'date', + }, + 'checkpoint.tcp_flags': { + category: 'checkpoint', + description: 'TCP packet flags.', + name: 'checkpoint.tcp_flags', + type: 'keyword', + }, + 'checkpoint.termination_reason': { + category: 'checkpoint', + description: 'Termination reason.', + name: 'checkpoint.termination_reason', + type: 'keyword', + }, + 'checkpoint.update_status': { + category: 'checkpoint', + description: 'Update status.', + name: 'checkpoint.update_status', + type: 'keyword', + }, + 'checkpoint.user_status': { + category: 'checkpoint', + description: 'User response.', + name: 'checkpoint.user_status', + type: 'keyword', + }, + 'checkpoint.uuid': { + category: 'checkpoint', + description: 'External ID.', + name: 'checkpoint.uuid', + type: 'keyword', + }, + 'checkpoint.virus_name': { + category: 'checkpoint', + description: 'Virus name.', + name: 'checkpoint.virus_name', + type: 'keyword', + }, + 'checkpoint.voip_log_type': { + category: 'checkpoint', + description: 'VoIP log types.', + name: 'checkpoint.voip_log_type', + type: 'keyword', + }, + 'cef.extensions.cp_app_risk': { + category: 'cef', + name: 'cef.extensions.cp_app_risk', + type: 'keyword', + }, + 'cef.extensions.cp_severity': { + category: 'cef', + name: 'cef.extensions.cp_severity', + type: 'keyword', + }, + 'cef.extensions.ifname': { + category: 'cef', + name: 'cef.extensions.ifname', + type: 'keyword', + }, + 'cef.extensions.inzone': { + category: 'cef', + name: 'cef.extensions.inzone', + type: 'keyword', + }, + 'cef.extensions.layer_uuid': { + category: 'cef', + name: 'cef.extensions.layer_uuid', + type: 'keyword', + }, + 'cef.extensions.layer_name': { + category: 'cef', + name: 'cef.extensions.layer_name', + type: 'keyword', + }, + 'cef.extensions.logid': { + category: 'cef', + name: 'cef.extensions.logid', + type: 'keyword', + }, + 'cef.extensions.loguid': { + category: 'cef', + name: 'cef.extensions.loguid', + type: 'keyword', + }, + 'cef.extensions.match_id': { + category: 'cef', + name: 'cef.extensions.match_id', + type: 'keyword', + }, + 'cef.extensions.nat_addtnl_rulenum': { + category: 'cef', + name: 'cef.extensions.nat_addtnl_rulenum', + type: 'keyword', + }, + 'cef.extensions.nat_rulenum': { + category: 'cef', + name: 'cef.extensions.nat_rulenum', + type: 'keyword', + }, + 'cef.extensions.origin': { + category: 'cef', + name: 'cef.extensions.origin', + type: 'keyword', + }, + 'cef.extensions.originsicname': { + category: 'cef', + name: 'cef.extensions.originsicname', + type: 'keyword', + }, + 'cef.extensions.outzone': { + category: 'cef', + name: 'cef.extensions.outzone', + type: 'keyword', + }, + 'cef.extensions.parent_rule': { + category: 'cef', + name: 'cef.extensions.parent_rule', + type: 'keyword', + }, + 'cef.extensions.product': { + category: 'cef', + name: 'cef.extensions.product', + type: 'keyword', + }, + 'cef.extensions.rule_action': { + category: 'cef', + name: 'cef.extensions.rule_action', + type: 'keyword', + }, + 'cef.extensions.rule_uid': { + category: 'cef', + name: 'cef.extensions.rule_uid', + type: 'keyword', + }, + 'cef.extensions.sequencenum': { + category: 'cef', + name: 'cef.extensions.sequencenum', + type: 'keyword', + }, + 'cef.extensions.service_id': { + category: 'cef', + name: 'cef.extensions.service_id', + type: 'keyword', + }, + 'cef.extensions.version': { + category: 'cef', + name: 'cef.extensions.version', + type: 'keyword', + }, + 'checkpoint.calc_desc': { + category: 'checkpoint', + description: 'Log description. ', + name: 'checkpoint.calc_desc', + type: 'keyword', + }, + 'checkpoint.dst_country': { + category: 'checkpoint', + description: 'Destination country. ', + name: 'checkpoint.dst_country', + type: 'keyword', + }, + 'checkpoint.dst_user_name': { + category: 'checkpoint', + description: 'Connected user name on the destination IP. ', + name: 'checkpoint.dst_user_name', + type: 'keyword', + }, + 'checkpoint.sys_message': { + category: 'checkpoint', + description: 'System messages ', + name: 'checkpoint.sys_message', + type: 'keyword', + }, + 'checkpoint.logid': { + category: 'checkpoint', + description: 'System messages ', + name: 'checkpoint.logid', + type: 'keyword', + }, + 'checkpoint.failure_impact': { + category: 'checkpoint', + description: 'The impact of update service failure. ', + name: 'checkpoint.failure_impact', + type: 'keyword', + }, + 'checkpoint.id': { + category: 'checkpoint', + description: 'Override application ID. ', + name: 'checkpoint.id', + type: 'integer', + }, + 'checkpoint.information': { + category: 'checkpoint', + description: 'Policy installation status for a specific blade. ', + name: 'checkpoint.information', + type: 'keyword', + }, + 'checkpoint.layer_name': { + category: 'checkpoint', + description: 'Layer name. ', + name: 'checkpoint.layer_name', + type: 'keyword', + }, + 'checkpoint.layer_uuid': { + category: 'checkpoint', + description: 'Layer UUID. ', + name: 'checkpoint.layer_uuid', + type: 'keyword', + }, + 'checkpoint.log_id': { + category: 'checkpoint', + description: 'Unique identity for logs. ', + name: 'checkpoint.log_id', + type: 'integer', + }, + 'checkpoint.origin_sic_name': { + category: 'checkpoint', + description: 'Machine SIC. ', + name: 'checkpoint.origin_sic_name', + type: 'keyword', + }, + 'checkpoint.policy_mgmt': { + category: 'checkpoint', + description: 'Name of the Management Server that manages this Security Gateway. ', + name: 'checkpoint.policy_mgmt', + type: 'keyword', + }, + 'checkpoint.policy_name': { + category: 'checkpoint', + description: 'Name of the last policy that this Security Gateway fetched. ', + name: 'checkpoint.policy_name', + type: 'keyword', + }, + 'checkpoint.protocol': { + category: 'checkpoint', + description: 'Protocol detected on the connection. ', + name: 'checkpoint.protocol', + type: 'keyword', + }, + 'checkpoint.proxy_src_ip': { + category: 'checkpoint', + description: 'Sender source IP (even when using proxy). ', + name: 'checkpoint.proxy_src_ip', + type: 'ip', + }, + 'checkpoint.rule': { + category: 'checkpoint', + description: 'Matched rule number. ', + name: 'checkpoint.rule', + type: 'integer', + }, + 'checkpoint.rule_action': { + category: 'checkpoint', + description: 'Action of the matched rule in the access policy. ', + name: 'checkpoint.rule_action', + type: 'keyword', + }, + 'checkpoint.scan_direction': { + category: 'checkpoint', + description: 'Scan direction. ', + name: 'checkpoint.scan_direction', + type: 'keyword', + }, + 'checkpoint.session_id': { + category: 'checkpoint', + description: 'Log uuid. ', + name: 'checkpoint.session_id', + type: 'keyword', + }, + 'checkpoint.source_os': { + category: 'checkpoint', + description: 'OS which generated the attack. ', + name: 'checkpoint.source_os', + type: 'keyword', + }, + 'checkpoint.src_country': { + category: 'checkpoint', + description: 'Country name, derived from connection source IP address. ', + name: 'checkpoint.src_country', + type: 'keyword', + }, + 'checkpoint.src_user_name': { + category: 'checkpoint', + description: 'User name connected to source IP ', + name: 'checkpoint.src_user_name', + type: 'keyword', + }, + 'checkpoint.ticket_id': { + category: 'checkpoint', + description: 'Unique ID per file. ', + name: 'checkpoint.ticket_id', + type: 'keyword', + }, + 'checkpoint.tls_server_host_name': { + category: 'checkpoint', + description: 'SNI/CN from encrypted TLS connection used by URLF for categorization. ', + name: 'checkpoint.tls_server_host_name', + type: 'keyword', + }, + 'checkpoint.verdict': { + category: 'checkpoint', + description: 'TE engine verdict Possible values: Malicious/Benign/Error. ', + name: 'checkpoint.verdict', + type: 'keyword', + }, + 'checkpoint.user': { + category: 'checkpoint', + description: 'Source user name. ', + name: 'checkpoint.user', + type: 'keyword', + }, + 'checkpoint.vendor_list': { + category: 'checkpoint', + description: 'The vendor name that provided the verdict for a malicious URL. ', + name: 'checkpoint.vendor_list', + type: 'keyword', + }, + 'checkpoint.web_server_type': { + category: 'checkpoint', + description: 'Web server detected in the HTTP response. ', + name: 'checkpoint.web_server_type', + type: 'keyword', + }, + 'checkpoint.client_name': { + category: 'checkpoint', + description: 'Client Application or Software Blade that detected the event. ', + name: 'checkpoint.client_name', + type: 'keyword', + }, + 'checkpoint.client_version': { + category: 'checkpoint', + description: 'Build version of SandBlast Agent client installed on the computer. ', + name: 'checkpoint.client_version', + type: 'keyword', + }, + 'checkpoint.extension_version': { + category: 'checkpoint', + description: 'Build version of the SandBlast Agent browser extension. ', + name: 'checkpoint.extension_version', + type: 'keyword', + }, + 'checkpoint.host_time': { + category: 'checkpoint', + description: 'Local time on the endpoint computer. ', + name: 'checkpoint.host_time', + type: 'keyword', + }, + 'checkpoint.installed_products': { + category: 'checkpoint', + description: 'List of installed Endpoint Software Blades. ', + name: 'checkpoint.installed_products', + type: 'keyword', + }, + 'checkpoint.cc': { + category: 'checkpoint', + description: 'The Carbon Copy address of the email. ', + name: 'checkpoint.cc', + type: 'keyword', + }, + 'checkpoint.parent_process_username': { + category: 'checkpoint', + description: 'Owner username of the parent process of the process that triggered the attack. ', + name: 'checkpoint.parent_process_username', + type: 'keyword', + }, + 'checkpoint.process_username': { + category: 'checkpoint', + description: 'Owner username of the process that triggered the attack. ', + name: 'checkpoint.process_username', + type: 'keyword', + }, + 'checkpoint.audit_status': { + category: 'checkpoint', + description: 'Audit Status. Can be Success or Failure. ', + name: 'checkpoint.audit_status', + type: 'keyword', + }, + 'checkpoint.objecttable': { + category: 'checkpoint', + description: 'Table of affected objects. ', + name: 'checkpoint.objecttable', + type: 'keyword', + }, + 'checkpoint.objecttype': { + category: 'checkpoint', + description: 'The type of the affected object. ', + name: 'checkpoint.objecttype', + type: 'keyword', + }, + 'checkpoint.operation_number': { + category: 'checkpoint', + description: 'The operation nuber. ', + name: 'checkpoint.operation_number', + type: 'keyword', + }, + 'checkpoint.suppressed_logs': { + category: 'checkpoint', + description: + 'Aggregated connections for five minutes on the same source, destination and port. ', + name: 'checkpoint.suppressed_logs', + type: 'integer', + }, + 'checkpoint.blade_name': { + category: 'checkpoint', + description: 'Blade name. ', + name: 'checkpoint.blade_name', + type: 'keyword', + }, + 'checkpoint.status': { + category: 'checkpoint', + description: 'Ok/Warning/Error. ', + name: 'checkpoint.status', + type: 'keyword', + }, + 'checkpoint.short_desc': { + category: 'checkpoint', + description: 'Short description of the process that was executed. ', + name: 'checkpoint.short_desc', + type: 'keyword', + }, + 'checkpoint.long_desc': { + category: 'checkpoint', + description: 'More information on the process (usually describing error reason in failure). ', + name: 'checkpoint.long_desc', + type: 'keyword', + }, + 'checkpoint.scan_hosts_hour': { + category: 'checkpoint', + description: 'Number of unique hosts during the last hour. ', + name: 'checkpoint.scan_hosts_hour', + type: 'integer', + }, + 'checkpoint.scan_hosts_day': { + category: 'checkpoint', + description: 'Number of unique hosts during the last day. ', + name: 'checkpoint.scan_hosts_day', + type: 'integer', + }, + 'checkpoint.scan_hosts_week': { + category: 'checkpoint', + description: 'Number of unique hosts during the last week. ', + name: 'checkpoint.scan_hosts_week', + type: 'integer', + }, + 'checkpoint.unique_detected_hour': { + category: 'checkpoint', + description: 'Detected virus for a specific host during the last hour. ', + name: 'checkpoint.unique_detected_hour', + type: 'integer', + }, + 'checkpoint.unique_detected_day': { + category: 'checkpoint', + description: 'Detected virus for a specific host during the last day. ', + name: 'checkpoint.unique_detected_day', + type: 'integer', + }, + 'checkpoint.unique_detected_week': { + category: 'checkpoint', + description: 'Detected virus for a specific host during the last week. ', + name: 'checkpoint.unique_detected_week', + type: 'integer', + }, + 'checkpoint.scan_mail': { + category: 'checkpoint', + description: 'Number of emails that were scanned by "AB malicious activity" engine. ', + name: 'checkpoint.scan_mail', + type: 'integer', + }, + 'checkpoint.additional_ip': { + category: 'checkpoint', + description: 'DNS host name. ', + name: 'checkpoint.additional_ip', + type: 'keyword', + }, + 'checkpoint.description': { + category: 'checkpoint', + description: 'Additional explanation how the security gateway enforced the connection. ', + name: 'checkpoint.description', + type: 'keyword', + }, + 'checkpoint.email_spam_category': { + category: 'checkpoint', + description: 'Email categories. Possible values: spam/not spam/phishing. ', + name: 'checkpoint.email_spam_category', + type: 'keyword', + }, + 'checkpoint.email_control_analysis': { + category: 'checkpoint', + description: 'Message classification, received from spam vendor engine. ', + name: 'checkpoint.email_control_analysis', + type: 'keyword', + }, + 'checkpoint.scan_results': { + category: 'checkpoint', + description: '"Infected"/description of a failure. ', + name: 'checkpoint.scan_results', + type: 'keyword', + }, + 'checkpoint.original_queue_id': { + category: 'checkpoint', + description: 'Original postfix email queue id. ', + name: 'checkpoint.original_queue_id', + type: 'keyword', + }, + 'checkpoint.risk': { + category: 'checkpoint', + description: 'Risk level we got from the engine. ', + name: 'checkpoint.risk', + type: 'keyword', + }, + 'checkpoint.observable_name': { + category: 'checkpoint', + description: 'IOC observable signature name. ', + name: 'checkpoint.observable_name', + type: 'keyword', + }, + 'checkpoint.observable_id': { + category: 'checkpoint', + description: 'IOC observable signature id. ', + name: 'checkpoint.observable_id', + type: 'keyword', + }, + 'checkpoint.observable_comment': { + category: 'checkpoint', + description: 'IOC observable signature description. ', + name: 'checkpoint.observable_comment', + type: 'keyword', + }, + 'checkpoint.indicator_name': { + category: 'checkpoint', + description: 'IOC indicator name. ', + name: 'checkpoint.indicator_name', + type: 'keyword', + }, + 'checkpoint.indicator_description': { + category: 'checkpoint', + description: 'IOC indicator description. ', + name: 'checkpoint.indicator_description', + type: 'keyword', + }, + 'checkpoint.indicator_reference': { + category: 'checkpoint', + description: 'IOC indicator reference. ', + name: 'checkpoint.indicator_reference', + type: 'keyword', + }, + 'checkpoint.indicator_uuid': { + category: 'checkpoint', + description: 'IOC indicator uuid. ', + name: 'checkpoint.indicator_uuid', + type: 'keyword', + }, + 'checkpoint.app_desc': { + category: 'checkpoint', + description: 'Application description. ', + name: 'checkpoint.app_desc', + type: 'keyword', + }, + 'checkpoint.app_id': { + category: 'checkpoint', + description: 'Application ID. ', + name: 'checkpoint.app_id', + type: 'integer', + }, + 'checkpoint.certificate_resource': { + category: 'checkpoint', + description: 'HTTPS resource Possible values: SNI or domain name (DN). ', + name: 'checkpoint.certificate_resource', + type: 'keyword', + }, + 'checkpoint.certificate_validation': { + category: 'checkpoint', + description: + 'Precise error, describing HTTPS certificate failure under "HTTPS categorize websites" feature. ', + name: 'checkpoint.certificate_validation', + type: 'keyword', + }, + 'checkpoint.browse_time': { + category: 'checkpoint', + description: 'Application session browse time. ', + name: 'checkpoint.browse_time', + type: 'keyword', + }, + 'checkpoint.limit_requested': { + category: 'checkpoint', + description: 'Indicates whether data limit was requested for the session. ', + name: 'checkpoint.limit_requested', + type: 'integer', + }, + 'checkpoint.limit_applied': { + category: 'checkpoint', + description: 'Indicates whether the session was actually date limited. ', + name: 'checkpoint.limit_applied', + type: 'integer', + }, + 'checkpoint.dropped_total': { + category: 'checkpoint', + description: 'Amount of dropped packets (both incoming and outgoing). ', + name: 'checkpoint.dropped_total', + type: 'integer', + }, + 'checkpoint.client_type_os': { + category: 'checkpoint', + description: 'Client OS detected in the HTTP request. ', + name: 'checkpoint.client_type_os', + type: 'keyword', + }, + 'checkpoint.name': { + category: 'checkpoint', + description: 'Application name. ', + name: 'checkpoint.name', + type: 'keyword', + }, + 'checkpoint.properties': { + category: 'checkpoint', + description: 'Application categories. ', + name: 'checkpoint.properties', + type: 'keyword', + }, + 'checkpoint.sig_id': { + category: 'checkpoint', + description: "Application's signature ID which how it was detected by. ", + name: 'checkpoint.sig_id', + type: 'keyword', + }, + 'checkpoint.desc': { + category: 'checkpoint', + description: 'Override application description. ', + name: 'checkpoint.desc', + type: 'keyword', + }, + 'checkpoint.referrer_self_uid': { + category: 'checkpoint', + description: 'UUID of the current log. ', + name: 'checkpoint.referrer_self_uid', + type: 'keyword', + }, + 'checkpoint.referrer_parent_uid': { + category: 'checkpoint', + description: 'Log UUID of the referring application. ', + name: 'checkpoint.referrer_parent_uid', + type: 'keyword', + }, + 'checkpoint.needs_browse_time': { + category: 'checkpoint', + description: 'Browse time required for the connection. ', + name: 'checkpoint.needs_browse_time', + type: 'integer', + }, + 'checkpoint.cluster_info': { + category: 'checkpoint', + description: + 'Cluster information. Possible options: Failover reason/cluster state changes/CP cluster or 3rd party. ', + name: 'checkpoint.cluster_info', + type: 'keyword', + }, + 'checkpoint.sync': { + category: 'checkpoint', + description: 'Sync status and the reason (stable, at risk). ', + name: 'checkpoint.sync', + type: 'keyword', + }, + 'checkpoint.file_direction': { + category: 'checkpoint', + description: 'File direction. Possible options: upload/download. ', + name: 'checkpoint.file_direction', + type: 'keyword', + }, + 'checkpoint.invalid_file_size': { + category: 'checkpoint', + description: 'File_size field is valid only if this field is set to 0. ', + name: 'checkpoint.invalid_file_size', + type: 'integer', + }, + 'checkpoint.top_archive_file_name': { + category: 'checkpoint', + description: 'In case of archive file: the file that was sent/received. ', + name: 'checkpoint.top_archive_file_name', + type: 'keyword', + }, + 'checkpoint.data_type_name': { + category: 'checkpoint', + description: 'Data type in rulebase that was matched. ', + name: 'checkpoint.data_type_name', + type: 'keyword', + }, + 'checkpoint.specific_data_type_name': { + category: 'checkpoint', + description: 'Compound/Group scenario, data type that was matched. ', + name: 'checkpoint.specific_data_type_name', + type: 'keyword', + }, + 'checkpoint.word_list': { + category: 'checkpoint', + description: 'Words matched by data type. ', + name: 'checkpoint.word_list', + type: 'keyword', + }, + 'checkpoint.info': { + category: 'checkpoint', + description: 'Special log message. ', + name: 'checkpoint.info', + type: 'keyword', + }, + 'checkpoint.outgoing_url': { + category: 'checkpoint', + description: 'URL related to this log (for HTTP). ', + name: 'checkpoint.outgoing_url', + type: 'keyword', + }, + 'checkpoint.dlp_rule_name': { + category: 'checkpoint', + description: 'Matched rule name. ', + name: 'checkpoint.dlp_rule_name', + type: 'keyword', + }, + 'checkpoint.dlp_recipients': { + category: 'checkpoint', + description: 'Mail recipients. ', + name: 'checkpoint.dlp_recipients', + type: 'keyword', + }, + 'checkpoint.dlp_subject': { + category: 'checkpoint', + description: 'Mail subject. ', + name: 'checkpoint.dlp_subject', + type: 'keyword', + }, + 'checkpoint.dlp_word_list': { + category: 'checkpoint', + description: 'Phrases matched by data type. ', + name: 'checkpoint.dlp_word_list', + type: 'keyword', + }, + 'checkpoint.dlp_template_score': { + category: 'checkpoint', + description: 'Template data type match score. ', + name: 'checkpoint.dlp_template_score', + type: 'keyword', + }, + 'checkpoint.message_size': { + category: 'checkpoint', + description: 'Mail/post size. ', + name: 'checkpoint.message_size', + type: 'integer', + }, + 'checkpoint.dlp_incident_uid': { + category: 'checkpoint', + description: 'Unique ID of the matched rule. ', + name: 'checkpoint.dlp_incident_uid', + type: 'keyword', + }, + 'checkpoint.dlp_related_incident_uid': { + category: 'checkpoint', + description: 'Other ID related to this one. ', + name: 'checkpoint.dlp_related_incident_uid', + type: 'keyword', + }, + 'checkpoint.dlp_data_type_name': { + category: 'checkpoint', + description: 'Matched data type. ', + name: 'checkpoint.dlp_data_type_name', + type: 'keyword', + }, + 'checkpoint.dlp_data_type_uid': { + category: 'checkpoint', + description: 'Unique ID of the matched data type. ', + name: 'checkpoint.dlp_data_type_uid', + type: 'keyword', + }, + 'checkpoint.dlp_violation_description': { + category: 'checkpoint', + description: 'Violation descriptions described in the rulebase. ', + name: 'checkpoint.dlp_violation_description', + type: 'keyword', + }, + 'checkpoint.dlp_relevant_data_types': { + category: 'checkpoint', + description: 'In case of Compound/Group: the inner data types that were matched. ', + name: 'checkpoint.dlp_relevant_data_types', + type: 'keyword', + }, + 'checkpoint.dlp_action_reason': { + category: 'checkpoint', + description: 'Action chosen reason. ', + name: 'checkpoint.dlp_action_reason', + type: 'keyword', + }, + 'checkpoint.dlp_categories': { + category: 'checkpoint', + description: 'Data type category. ', + name: 'checkpoint.dlp_categories', + type: 'keyword', + }, + 'checkpoint.dlp_transint': { + category: 'checkpoint', + description: 'HTTP/SMTP/FTP. ', + name: 'checkpoint.dlp_transint', + type: 'keyword', + }, + 'checkpoint.duplicate': { + category: 'checkpoint', + description: + 'Log marked as duplicated, when mail is split and the Security Gateway sees it twice. ', + name: 'checkpoint.duplicate', + type: 'keyword', + }, + 'checkpoint.matched_file': { + category: 'checkpoint', + description: 'Unique ID of the matched data type. ', + name: 'checkpoint.matched_file', + type: 'keyword', + }, + 'checkpoint.matched_file_text_segments': { + category: 'checkpoint', + description: 'Fingerprint: number of text segments matched by this traffic. ', + name: 'checkpoint.matched_file_text_segments', + type: 'integer', + }, + 'checkpoint.matched_file_percentage': { + category: 'checkpoint', + description: 'Fingerprint: match percentage of the traffic. ', + name: 'checkpoint.matched_file_percentage', + type: 'integer', + }, + 'checkpoint.dlp_additional_action': { + category: 'checkpoint', + description: 'Watermark/None. ', + name: 'checkpoint.dlp_additional_action', + type: 'keyword', + }, + 'checkpoint.dlp_watermark_profile': { + category: 'checkpoint', + description: 'Watermark which was applied. ', + name: 'checkpoint.dlp_watermark_profile', + type: 'keyword', + }, + 'checkpoint.dlp_repository_id': { + category: 'checkpoint', + description: 'ID of scanned repository. ', + name: 'checkpoint.dlp_repository_id', + type: 'keyword', + }, + 'checkpoint.dlp_repository_root_path': { + category: 'checkpoint', + description: 'Repository path. ', + name: 'checkpoint.dlp_repository_root_path', + type: 'keyword', + }, + 'checkpoint.scan_id': { + category: 'checkpoint', + description: 'Sequential number of scan. ', + name: 'checkpoint.scan_id', + type: 'keyword', + }, + 'checkpoint.special_properties': { + category: 'checkpoint', + description: + "If this field is set to '1' the log will not be shown (in use for monitoring scan progress). ", + name: 'checkpoint.special_properties', + type: 'integer', + }, + 'checkpoint.dlp_repository_total_size': { + category: 'checkpoint', + description: 'Repository size. ', + name: 'checkpoint.dlp_repository_total_size', + type: 'integer', + }, + 'checkpoint.dlp_repository_files_number': { + category: 'checkpoint', + description: 'Number of files in repository. ', + name: 'checkpoint.dlp_repository_files_number', + type: 'integer', + }, + 'checkpoint.dlp_repository_scanned_files_number': { + category: 'checkpoint', + description: 'Number of scanned files in repository. ', + name: 'checkpoint.dlp_repository_scanned_files_number', + type: 'integer', + }, + 'checkpoint.duration': { + category: 'checkpoint', + description: 'Scan duration. ', + name: 'checkpoint.duration', + type: 'keyword', + }, + 'checkpoint.dlp_fingerprint_long_status': { + category: 'checkpoint', + description: 'Scan status - long format. ', + name: 'checkpoint.dlp_fingerprint_long_status', + type: 'keyword', + }, + 'checkpoint.dlp_fingerprint_short_status': { + category: 'checkpoint', + description: 'Scan status - short format. ', + name: 'checkpoint.dlp_fingerprint_short_status', + type: 'keyword', + }, + 'checkpoint.dlp_repository_directories_number': { + category: 'checkpoint', + description: 'Number of directories in repository. ', + name: 'checkpoint.dlp_repository_directories_number', + type: 'integer', + }, + 'checkpoint.dlp_repository_unreachable_directories_number': { + category: 'checkpoint', + description: 'Number of directories the Security Gateway was unable to read. ', + name: 'checkpoint.dlp_repository_unreachable_directories_number', + type: 'integer', + }, + 'checkpoint.dlp_fingerprint_files_number': { + category: 'checkpoint', + description: 'Number of successfully scanned files in repository. ', + name: 'checkpoint.dlp_fingerprint_files_number', + type: 'integer', + }, + 'checkpoint.dlp_repository_skipped_files_number': { + category: 'checkpoint', + description: 'Skipped number of files because of configuration. ', + name: 'checkpoint.dlp_repository_skipped_files_number', + type: 'integer', + }, + 'checkpoint.dlp_repository_scanned_directories_number': { + category: 'checkpoint', + description: 'Amount of directories scanned. ', + name: 'checkpoint.dlp_repository_scanned_directories_number', + type: 'integer', + }, + 'checkpoint.number_of_errors': { + category: 'checkpoint', + description: 'Number of files that were not scanned due to an error. ', + name: 'checkpoint.number_of_errors', + type: 'integer', + }, + 'checkpoint.next_scheduled_scan_date': { + category: 'checkpoint', + description: 'Next scan scheduled time according to time object. ', + name: 'checkpoint.next_scheduled_scan_date', + type: 'keyword', + }, + 'checkpoint.dlp_repository_scanned_total_size': { + category: 'checkpoint', + description: 'Size scanned. ', + name: 'checkpoint.dlp_repository_scanned_total_size', + type: 'integer', + }, + 'checkpoint.dlp_repository_reached_directories_number': { + category: 'checkpoint', + description: 'Number of scanned directories in repository. ', + name: 'checkpoint.dlp_repository_reached_directories_number', + type: 'integer', + }, + 'checkpoint.dlp_repository_not_scanned_directories_percentage': { + category: 'checkpoint', + description: 'Percentage of directories the Security Gateway was unable to read. ', + name: 'checkpoint.dlp_repository_not_scanned_directories_percentage', + type: 'integer', + }, + 'checkpoint.speed': { + category: 'checkpoint', + description: 'Current scan speed. ', + name: 'checkpoint.speed', + type: 'integer', + }, + 'checkpoint.dlp_repository_scan_progress': { + category: 'checkpoint', + description: 'Scan percentage. ', + name: 'checkpoint.dlp_repository_scan_progress', + type: 'integer', + }, + 'checkpoint.sub_policy_name': { + category: 'checkpoint', + description: 'Layer name. ', + name: 'checkpoint.sub_policy_name', + type: 'keyword', + }, + 'checkpoint.sub_policy_uid': { + category: 'checkpoint', + description: 'Layer uid. ', + name: 'checkpoint.sub_policy_uid', + type: 'keyword', + }, + 'checkpoint.fw_message': { + category: 'checkpoint', + description: 'Used for various firewall errors. ', + name: 'checkpoint.fw_message', + type: 'keyword', + }, + 'checkpoint.message': { + category: 'checkpoint', + description: 'ISP link has failed. ', + name: 'checkpoint.message', + type: 'keyword', + }, + 'checkpoint.isp_link': { + category: 'checkpoint', + description: 'Name of ISP link. ', + name: 'checkpoint.isp_link', + type: 'keyword', + }, + 'checkpoint.fw_subproduct': { + category: 'checkpoint', + description: 'Can be vpn/non vpn. ', + name: 'checkpoint.fw_subproduct', + type: 'keyword', + }, + 'checkpoint.sctp_error': { + category: 'checkpoint', + description: 'Error information, what caused sctp to fail on out_of_state. ', + name: 'checkpoint.sctp_error', + type: 'keyword', + }, + 'checkpoint.chunk_type': { + category: 'checkpoint', + description: 'Chunck of the sctp stream. ', + name: 'checkpoint.chunk_type', + type: 'keyword', + }, + 'checkpoint.sctp_association_state': { + category: 'checkpoint', + description: 'The bad state you were trying to update to. ', + name: 'checkpoint.sctp_association_state', + type: 'keyword', + }, + 'checkpoint.tcp_packet_out_of_state': { + category: 'checkpoint', + description: 'State violation. ', + name: 'checkpoint.tcp_packet_out_of_state', + type: 'keyword', + }, + 'checkpoint.connectivity_level': { + category: 'checkpoint', + description: 'Log for a new connection in wire mode. ', + name: 'checkpoint.connectivity_level', + type: 'keyword', + }, + 'checkpoint.ip_option': { + category: 'checkpoint', + description: 'IP option that was dropped. ', + name: 'checkpoint.ip_option', + type: 'integer', + }, + 'checkpoint.tcp_state': { + category: 'checkpoint', + description: 'Log reinting a tcp state change. ', + name: 'checkpoint.tcp_state', + type: 'keyword', + }, + 'checkpoint.expire_time': { + category: 'checkpoint', + description: 'Connection closing time. ', + name: 'checkpoint.expire_time', + type: 'keyword', + }, + 'checkpoint.rpc_prog': { + category: 'checkpoint', + description: 'Log for new RPC state - prog values. ', + name: 'checkpoint.rpc_prog', + type: 'integer', + }, + 'checkpoint.dce-rpc_interface_uuid': { + category: 'checkpoint', + description: 'Log for new RPC state - UUID values ', + name: 'checkpoint.dce-rpc_interface_uuid', + type: 'keyword', + }, + 'checkpoint.elapsed': { + category: 'checkpoint', + description: 'Time passed since start time. ', + name: 'checkpoint.elapsed', + type: 'keyword', + }, + 'checkpoint.icmp': { + category: 'checkpoint', + description: 'Number of packets, received by the client. ', + name: 'checkpoint.icmp', + type: 'keyword', + }, + 'checkpoint.capture_uuid': { + category: 'checkpoint', + description: 'UUID generated for the capture. Used when enabling the capture when logging. ', + name: 'checkpoint.capture_uuid', + type: 'keyword', + }, + 'checkpoint.diameter_app_ID': { + category: 'checkpoint', + description: 'The ID of diameter application. ', + name: 'checkpoint.diameter_app_ID', + type: 'integer', + }, + 'checkpoint.diameter_cmd_code': { + category: 'checkpoint', + description: 'Diameter not allowed application command id. ', + name: 'checkpoint.diameter_cmd_code', + type: 'integer', + }, + 'checkpoint.diameter_msg_type': { + category: 'checkpoint', + description: 'Diameter message type. ', + name: 'checkpoint.diameter_msg_type', + type: 'keyword', + }, + 'checkpoint.cp_message': { + category: 'checkpoint', + description: 'Used to log a general message. ', + name: 'checkpoint.cp_message', + type: 'integer', + }, + 'checkpoint.log_delay': { + category: 'checkpoint', + description: 'Time left before deleting template. ', + name: 'checkpoint.log_delay', + type: 'integer', + }, + 'checkpoint.attack_status': { + category: 'checkpoint', + description: 'In case of a malicious event on an endpoint computer, the status of the attack. ', + name: 'checkpoint.attack_status', + type: 'keyword', + }, + 'checkpoint.impacted_files': { + category: 'checkpoint', + description: + 'In case of an infection on an endpoint computer, the list of files that the malware impacted. ', + name: 'checkpoint.impacted_files', + type: 'keyword', + }, + 'checkpoint.remediated_files': { + category: 'checkpoint', + description: + 'In case of an infection and a successful cleaning of that infection, this is a list of remediated files on the computer. ', + name: 'checkpoint.remediated_files', + type: 'keyword', + }, + 'checkpoint.triggered_by': { + category: 'checkpoint', + description: + 'The name of the mechanism that triggered the Software Blade to enforce a protection. ', + name: 'checkpoint.triggered_by', + type: 'keyword', + }, + 'checkpoint.https_inspection_rule_id': { + category: 'checkpoint', + description: 'ID of the matched rule. ', + name: 'checkpoint.https_inspection_rule_id', + type: 'keyword', + }, + 'checkpoint.https_inspection_rule_name': { + category: 'checkpoint', + description: 'Name of the matched rule. ', + name: 'checkpoint.https_inspection_rule_name', + type: 'keyword', + }, + 'checkpoint.app_properties': { + category: 'checkpoint', + description: 'List of all found categories. ', + name: 'checkpoint.app_properties', + type: 'keyword', + }, + 'checkpoint.https_validation': { + category: 'checkpoint', + description: 'Precise error, describing HTTPS inspection failure. ', + name: 'checkpoint.https_validation', + type: 'keyword', + }, + 'checkpoint.https_inspection_action': { + category: 'checkpoint', + description: 'HTTPS inspection action (Inspect/Bypass/Error). ', + name: 'checkpoint.https_inspection_action', + type: 'keyword', + }, + 'checkpoint.icap_service_id': { + category: 'checkpoint', + description: 'Service ID, can work with multiple servers, treated as services. ', + name: 'checkpoint.icap_service_id', + type: 'integer', + }, + 'checkpoint.icap_server_name': { + category: 'checkpoint', + description: 'Server name. ', + name: 'checkpoint.icap_server_name', + type: 'keyword', + }, + 'checkpoint.internal_error': { + category: 'checkpoint', + description: 'Internal error, for troubleshooting ', + name: 'checkpoint.internal_error', + type: 'keyword', + }, + 'checkpoint.icap_more_info': { + category: 'checkpoint', + description: 'Free text for verdict. ', + name: 'checkpoint.icap_more_info', + type: 'integer', + }, + 'checkpoint.reply_status': { + category: 'checkpoint', + description: 'ICAP reply status code, e.g. 200 or 204. ', + name: 'checkpoint.reply_status', + type: 'integer', + }, + 'checkpoint.icap_server_service': { + category: 'checkpoint', + description: 'Service name, as given in the ICAP URI ', + name: 'checkpoint.icap_server_service', + type: 'keyword', + }, + 'checkpoint.mirror_and_decrypt_type': { + category: 'checkpoint', + description: + 'Information about decrypt and forward. Possible values: Mirror only, Decrypt and mirror, Partial mirroring (HTTPS inspection Bypass). ', + name: 'checkpoint.mirror_and_decrypt_type', + type: 'keyword', + }, + 'checkpoint.interface_name': { + category: 'checkpoint', + description: 'Designated interface for mirror And decrypt. ', + name: 'checkpoint.interface_name', + type: 'keyword', + }, + 'checkpoint.session_uid': { + category: 'checkpoint', + description: 'HTTP session-id. ', + name: 'checkpoint.session_uid', + type: 'keyword', + }, + 'checkpoint.broker_publisher': { + category: 'checkpoint', + description: 'IP address of the broker publisher who shared the session information. ', + name: 'checkpoint.broker_publisher', + type: 'ip', + }, + 'checkpoint.src_user_dn': { + category: 'checkpoint', + description: 'User distinguished name connected to source IP. ', + name: 'checkpoint.src_user_dn', + type: 'keyword', + }, + 'checkpoint.proxy_user_name': { + category: 'checkpoint', + description: 'User name connected to proxy IP. ', + name: 'checkpoint.proxy_user_name', + type: 'keyword', + }, + 'checkpoint.proxy_machine_name': { + category: 'checkpoint', + description: 'Machine name connected to proxy IP. ', + name: 'checkpoint.proxy_machine_name', + type: 'integer', + }, + 'checkpoint.proxy_user_dn': { + category: 'checkpoint', + description: 'User distinguished name connected to proxy IP. ', + name: 'checkpoint.proxy_user_dn', + type: 'keyword', + }, + 'checkpoint.query': { + category: 'checkpoint', + description: 'DNS query. ', + name: 'checkpoint.query', + type: 'keyword', + }, + 'checkpoint.dns_query': { + category: 'checkpoint', + description: 'DNS query. ', + name: 'checkpoint.dns_query', + type: 'keyword', + }, + 'checkpoint.inspection_item': { + category: 'checkpoint', + description: 'Blade element performed inspection. ', + name: 'checkpoint.inspection_item', + type: 'keyword', + }, + 'checkpoint.inspection_category': { + category: 'checkpoint', + description: 'Inspection category: protocol anomaly, signature etc. ', + name: 'checkpoint.inspection_category', + type: 'keyword', + }, + 'checkpoint.inspection_profile': { + category: 'checkpoint', + description: 'Profile which the activated protection belongs to. ', + name: 'checkpoint.inspection_profile', + type: 'keyword', + }, + 'checkpoint.summary': { + category: 'checkpoint', + description: 'Summary message of a non-compliant DNS traffic drops or detects. ', + name: 'checkpoint.summary', + type: 'keyword', + }, + 'checkpoint.question_rdata': { + category: 'checkpoint', + description: 'List of question records domains. ', + name: 'checkpoint.question_rdata', + type: 'keyword', + }, + 'checkpoint.answer_rdata': { + category: 'checkpoint', + description: 'List of answer resource records to the questioned domains. ', + name: 'checkpoint.answer_rdata', + type: 'keyword', + }, + 'checkpoint.authority_rdata': { + category: 'checkpoint', + description: 'List of authoritative servers. ', + name: 'checkpoint.authority_rdata', + type: 'keyword', + }, + 'checkpoint.additional_rdata': { + category: 'checkpoint', + description: 'List of additional resource records. ', + name: 'checkpoint.additional_rdata', + type: 'keyword', + }, + 'checkpoint.files_names': { + category: 'checkpoint', + description: 'List of files requested by FTP. ', + name: 'checkpoint.files_names', + type: 'keyword', + }, + 'checkpoint.ftp_user': { + category: 'checkpoint', + description: 'FTP username. ', + name: 'checkpoint.ftp_user', + type: 'keyword', + }, + 'checkpoint.mime_from': { + category: 'checkpoint', + description: "Sender's address. ", + name: 'checkpoint.mime_from', + type: 'keyword', + }, + 'checkpoint.mime_to': { + category: 'checkpoint', + description: 'List of receiver address. ', + name: 'checkpoint.mime_to', + type: 'keyword', + }, + 'checkpoint.bcc': { + category: 'checkpoint', + description: 'List of BCC addresses. ', + name: 'checkpoint.bcc', + type: 'keyword', + }, + 'checkpoint.content_type': { + category: 'checkpoint', + description: + 'Mail content type. Possible values: application/msword, text/html, image/gif etc. ', + name: 'checkpoint.content_type', + type: 'keyword', + }, + 'checkpoint.user_agent': { + category: 'checkpoint', + description: 'String identifying requesting software user agent. ', + name: 'checkpoint.user_agent', + type: 'keyword', + }, + 'checkpoint.referrer': { + category: 'checkpoint', + description: 'Referrer HTTP request header, previous web page address. ', + name: 'checkpoint.referrer', + type: 'keyword', + }, + 'checkpoint.http_location': { + category: 'checkpoint', + description: 'Response header, indicates the URL to redirect a page to. ', + name: 'checkpoint.http_location', + type: 'keyword', + }, + 'checkpoint.content_disposition': { + category: 'checkpoint', + description: 'Indicates how the content is expected to be displayed inline in the browser. ', + name: 'checkpoint.content_disposition', + type: 'keyword', + }, + 'checkpoint.via': { + category: 'checkpoint', + description: + 'Via header is added by proxies for tracking purposes to avoid sending reqests in loop. ', + name: 'checkpoint.via', + type: 'keyword', + }, + 'checkpoint.http_server': { + category: 'checkpoint', + description: + 'Server HTTP header value, contains information about the software used by the origin server, which handles the request. ', + name: 'checkpoint.http_server', + type: 'keyword', + }, + 'checkpoint.content_length': { + category: 'checkpoint', + description: 'Indicates the size of the entity-body of the HTTP header. ', + name: 'checkpoint.content_length', + type: 'keyword', + }, + 'checkpoint.authorization': { + category: 'checkpoint', + description: 'Authorization HTTP header value. ', + name: 'checkpoint.authorization', + type: 'keyword', + }, + 'checkpoint.http_host': { + category: 'checkpoint', + description: 'Domain name of the server that the HTTP request is sent to. ', + name: 'checkpoint.http_host', + type: 'keyword', + }, + 'checkpoint.inspection_settings_log': { + category: 'checkpoint', + description: 'Indicats that the log was released by inspection settings. ', + name: 'checkpoint.inspection_settings_log', + type: 'keyword', + }, + 'checkpoint.cvpn_resource': { + category: 'checkpoint', + description: 'Mobile Access application. ', + name: 'checkpoint.cvpn_resource', + type: 'keyword', + }, + 'checkpoint.cvpn_category': { + category: 'checkpoint', + description: 'Mobile Access application type. ', + name: 'checkpoint.cvpn_category', + type: 'keyword', + }, + 'checkpoint.url': { + category: 'checkpoint', + description: 'Translated URL. ', + name: 'checkpoint.url', + type: 'keyword', + }, + 'checkpoint.reject_id': { + category: 'checkpoint', + description: + 'A reject ID that corresponds to the one presented in the Mobile Access error page. ', + name: 'checkpoint.reject_id', + type: 'keyword', + }, + 'checkpoint.fs-proto': { + category: 'checkpoint', + description: 'The file share protocol used in mobile acess file share application. ', + name: 'checkpoint.fs-proto', + type: 'keyword', + }, + 'checkpoint.app_package': { + category: 'checkpoint', + description: 'Unique identifier of the application on the protected mobile device. ', + name: 'checkpoint.app_package', + type: 'keyword', + }, + 'checkpoint.appi_name': { + category: 'checkpoint', + description: 'Name of application downloaded on the protected mobile device. ', + name: 'checkpoint.appi_name', + type: 'keyword', + }, + 'checkpoint.app_repackaged': { + category: 'checkpoint', + description: + 'Indicates whether the original application was repackage not by the official developer. ', + name: 'checkpoint.app_repackaged', + type: 'keyword', + }, + 'checkpoint.app_sid_id': { + category: 'checkpoint', + description: 'Unique SHA identifier of a mobile application. ', + name: 'checkpoint.app_sid_id', + type: 'keyword', + }, + 'checkpoint.app_version': { + category: 'checkpoint', + description: 'Version of the application downloaded on the protected mobile device. ', + name: 'checkpoint.app_version', + type: 'keyword', + }, + 'checkpoint.developer_certificate_name': { + category: 'checkpoint', + description: + "Name of the developer's certificate that was used to sign the mobile application. ", + name: 'checkpoint.developer_certificate_name', + type: 'keyword', + }, + 'checkpoint.email_message_id': { + category: 'checkpoint', + description: 'Email session id (uniqe ID of the mail). ', + name: 'checkpoint.email_message_id', + type: 'keyword', + }, + 'checkpoint.email_queue_id': { + category: 'checkpoint', + description: 'Postfix email queue id. ', + name: 'checkpoint.email_queue_id', + type: 'keyword', + }, + 'checkpoint.email_queue_name': { + category: 'checkpoint', + description: 'Postfix email queue name. ', + name: 'checkpoint.email_queue_name', + type: 'keyword', + }, + 'checkpoint.file_name': { + category: 'checkpoint', + description: 'Malicious file name. ', + name: 'checkpoint.file_name', + type: 'keyword', + }, + 'checkpoint.failure_reason': { + category: 'checkpoint', + description: 'MTA failure description. ', + name: 'checkpoint.failure_reason', + type: 'keyword', + }, + 'checkpoint.email_headers': { + category: 'checkpoint', + description: 'String containing all the email headers. ', + name: 'checkpoint.email_headers', + type: 'keyword', + }, + 'checkpoint.arrival_time': { + category: 'checkpoint', + description: 'Email arrival timestamp. ', + name: 'checkpoint.arrival_time', + type: 'keyword', + }, + 'checkpoint.email_status': { + category: 'checkpoint', + description: + "Describes the email's state. Possible options: delivered, deferred, skipped, bounced, hold, new, scan_started, scan_ended ", + name: 'checkpoint.email_status', + type: 'keyword', + }, + 'checkpoint.status_update': { + category: 'checkpoint', + description: 'Last time log was updated. ', + name: 'checkpoint.status_update', + type: 'keyword', + }, + 'checkpoint.delivery_time': { + category: 'checkpoint', + description: 'Timestamp of when email was delivered (MTA finished handling the email. ', + name: 'checkpoint.delivery_time', + type: 'keyword', + }, + 'checkpoint.links_num': { + category: 'checkpoint', + description: 'Number of links in the mail. ', + name: 'checkpoint.links_num', + type: 'integer', + }, + 'checkpoint.attachments_num': { + category: 'checkpoint', + description: 'Number of attachments in the mail. ', + name: 'checkpoint.attachments_num', + type: 'integer', + }, + 'checkpoint.email_content': { + category: 'checkpoint', + description: + 'Mail contents. Possible options: attachments/links & attachments/links/text only. ', + name: 'checkpoint.email_content', + type: 'keyword', + }, + 'checkpoint.allocated_ports': { + category: 'checkpoint', + description: 'Amount of allocated ports. ', + name: 'checkpoint.allocated_ports', + type: 'integer', + }, + 'checkpoint.capacity': { + category: 'checkpoint', + description: 'Capacity of the ports. ', + name: 'checkpoint.capacity', + type: 'integer', + }, + 'checkpoint.ports_usage': { + category: 'checkpoint', + description: 'Percentage of allocated ports. ', + name: 'checkpoint.ports_usage', + type: 'integer', + }, + 'checkpoint.nat_exhausted_pool': { + category: 'checkpoint', + description: '4-tuple of an exhausted pool. ', + name: 'checkpoint.nat_exhausted_pool', + type: 'keyword', + }, + 'checkpoint.nat_rulenum': { + category: 'checkpoint', + description: 'NAT rulebase first matched rule. ', + name: 'checkpoint.nat_rulenum', + type: 'integer', + }, + 'checkpoint.nat_addtnl_rulenum': { + category: 'checkpoint', + description: + 'When matching 2 automatic rules , second rule match will be shown otherwise field will be 0. ', + name: 'checkpoint.nat_addtnl_rulenum', + type: 'integer', + }, + 'checkpoint.message_info': { + category: 'checkpoint', + description: 'Used for information messages, for example:NAT connection has ended. ', + name: 'checkpoint.message_info', + type: 'keyword', + }, + 'checkpoint.nat46': { + category: 'checkpoint', + description: 'NAT 46 status, in most cases "enabled". ', + name: 'checkpoint.nat46', + type: 'keyword', + }, + 'checkpoint.end_time': { + category: 'checkpoint', + description: 'TCP connection end time. ', + name: 'checkpoint.end_time', + type: 'keyword', + }, + 'checkpoint.tcp_end_reason': { + category: 'checkpoint', + description: 'Reason for TCP connection closure. ', + name: 'checkpoint.tcp_end_reason', + type: 'keyword', + }, + 'checkpoint.cgnet': { + category: 'checkpoint', + description: 'Describes NAT allocation for specific subscriber. ', + name: 'checkpoint.cgnet', + type: 'keyword', + }, + 'checkpoint.subscriber': { + category: 'checkpoint', + description: 'Source IP before CGNAT. ', + name: 'checkpoint.subscriber', + type: 'ip', + }, + 'checkpoint.hide_ip': { + category: 'checkpoint', + description: 'Source IP which will be used after CGNAT. ', + name: 'checkpoint.hide_ip', + type: 'ip', + }, + 'checkpoint.int_start': { + category: 'checkpoint', + description: 'Subscriber start int which will be used for NAT. ', + name: 'checkpoint.int_start', + type: 'integer', + }, + 'checkpoint.int_end': { + category: 'checkpoint', + description: 'Subscriber end int which will be used for NAT. ', + name: 'checkpoint.int_end', + type: 'integer', + }, + 'checkpoint.packet_amount': { + category: 'checkpoint', + description: 'Amount of packets dropped. ', + name: 'checkpoint.packet_amount', + type: 'integer', + }, + 'checkpoint.monitor_reason': { + category: 'checkpoint', + description: 'Aggregated logs of monitored packets. ', + name: 'checkpoint.monitor_reason', + type: 'keyword', + }, + 'checkpoint.drops_amount': { + category: 'checkpoint', + description: 'Amount of multicast packets dropped. ', + name: 'checkpoint.drops_amount', + type: 'integer', + }, + 'checkpoint.securexl_message': { + category: 'checkpoint', + description: + 'Two options for a SecureXL message: 1. Missed accounting records after heavy load on logging system. 2. FW log message regarding a packet drop. ', + name: 'checkpoint.securexl_message', + type: 'keyword', + }, + 'checkpoint.conns_amount': { + category: 'checkpoint', + description: 'Connections amount of aggregated log info. ', + name: 'checkpoint.conns_amount', + type: 'integer', + }, + 'checkpoint.scope': { + category: 'checkpoint', + description: 'IP related to the attack. ', + name: 'checkpoint.scope', + type: 'keyword', + }, + 'checkpoint.analyzed_on': { + category: 'checkpoint', + description: 'Check Point ThreatCloud / emulator name. ', + name: 'checkpoint.analyzed_on', + type: 'keyword', + }, + 'checkpoint.detected_on': { + category: 'checkpoint', + description: 'System and applications version the file was emulated on. ', + name: 'checkpoint.detected_on', + type: 'keyword', + }, + 'checkpoint.dropped_file_name': { + category: 'checkpoint', + description: 'List of names dropped from the original file. ', + name: 'checkpoint.dropped_file_name', + type: 'keyword', + }, + 'checkpoint.dropped_file_type': { + category: 'checkpoint', + description: 'List of file types dropped from the original file. ', + name: 'checkpoint.dropped_file_type', + type: 'keyword', + }, + 'checkpoint.dropped_file_hash': { + category: 'checkpoint', + description: 'List of file hashes dropped from the original file. ', + name: 'checkpoint.dropped_file_hash', + type: 'keyword', + }, + 'checkpoint.dropped_file_verdict': { + category: 'checkpoint', + description: 'List of file verdics dropped from the original file. ', + name: 'checkpoint.dropped_file_verdict', + type: 'keyword', + }, + 'checkpoint.emulated_on': { + category: 'checkpoint', + description: 'Images the files were emulated on. ', + name: 'checkpoint.emulated_on', + type: 'keyword', + }, + 'checkpoint.extracted_file_type': { + category: 'checkpoint', + description: 'Types of extracted files in case of an archive. ', + name: 'checkpoint.extracted_file_type', + type: 'keyword', + }, + 'checkpoint.extracted_file_names': { + category: 'checkpoint', + description: 'Names of extracted files in case of an archive. ', + name: 'checkpoint.extracted_file_names', + type: 'keyword', + }, + 'checkpoint.extracted_file_hash': { + category: 'checkpoint', + description: 'Archive hash in case of extracted files. ', + name: 'checkpoint.extracted_file_hash', + type: 'keyword', + }, + 'checkpoint.extracted_file_verdict': { + category: 'checkpoint', + description: 'Verdict of extracted files in case of an archive. ', + name: 'checkpoint.extracted_file_verdict', + type: 'keyword', + }, + 'checkpoint.extracted_file_uid': { + category: 'checkpoint', + description: 'UID of extracted files in case of an archive. ', + name: 'checkpoint.extracted_file_uid', + type: 'keyword', + }, + 'checkpoint.mitre_initial_access': { + category: 'checkpoint', + description: 'The adversary is trying to break into your network. ', + name: 'checkpoint.mitre_initial_access', + type: 'keyword', + }, + 'checkpoint.mitre_execution': { + category: 'checkpoint', + description: 'The adversary is trying to run malicious code. ', + name: 'checkpoint.mitre_execution', + type: 'keyword', + }, + 'checkpoint.mitre_persistence': { + category: 'checkpoint', + description: 'The adversary is trying to maintain his foothold. ', + name: 'checkpoint.mitre_persistence', + type: 'keyword', + }, + 'checkpoint.mitre_privilege_escalation': { + category: 'checkpoint', + description: 'The adversary is trying to gain higher-level permissions. ', + name: 'checkpoint.mitre_privilege_escalation', + type: 'keyword', + }, + 'checkpoint.mitre_defense_evasion': { + category: 'checkpoint', + description: 'The adversary is trying to avoid being detected. ', + name: 'checkpoint.mitre_defense_evasion', + type: 'keyword', + }, + 'checkpoint.mitre_credential_access': { + category: 'checkpoint', + description: 'The adversary is trying to steal account names and passwords. ', + name: 'checkpoint.mitre_credential_access', + type: 'keyword', + }, + 'checkpoint.mitre_discovery': { + category: 'checkpoint', + description: 'The adversary is trying to expose information about your environment. ', + name: 'checkpoint.mitre_discovery', + type: 'keyword', + }, + 'checkpoint.mitre_lateral_movement': { + category: 'checkpoint', + description: 'The adversary is trying to explore your environment. ', + name: 'checkpoint.mitre_lateral_movement', + type: 'keyword', + }, + 'checkpoint.mitre_collection': { + category: 'checkpoint', + description: 'The adversary is trying to collect data of interest to achieve his goal. ', + name: 'checkpoint.mitre_collection', + type: 'keyword', + }, + 'checkpoint.mitre_command_and_control': { + category: 'checkpoint', + description: + 'The adversary is trying to communicate with compromised systems in order to control them. ', + name: 'checkpoint.mitre_command_and_control', + type: 'keyword', + }, + 'checkpoint.mitre_exfiltration': { + category: 'checkpoint', + description: 'The adversary is trying to steal data. ', + name: 'checkpoint.mitre_exfiltration', + type: 'keyword', + }, + 'checkpoint.mitre_impact': { + category: 'checkpoint', + description: + 'The adversary is trying to manipulate, interrupt, or destroy your systems and data. ', + name: 'checkpoint.mitre_impact', + type: 'keyword', + }, + 'checkpoint.parent_file_hash': { + category: 'checkpoint', + description: "Archive's hash in case of extracted files. ", + name: 'checkpoint.parent_file_hash', + type: 'keyword', + }, + 'checkpoint.parent_file_name': { + category: 'checkpoint', + description: "Archive's name in case of extracted files. ", + name: 'checkpoint.parent_file_name', + type: 'keyword', + }, + 'checkpoint.parent_file_uid': { + category: 'checkpoint', + description: "Archive's UID in case of extracted files. ", + name: 'checkpoint.parent_file_uid', + type: 'keyword', + }, + 'checkpoint.similiar_iocs': { + category: 'checkpoint', + description: 'Other IoCs similar to the ones found, related to the malicious file. ', + name: 'checkpoint.similiar_iocs', + type: 'keyword', + }, + 'checkpoint.similar_hashes': { + category: 'checkpoint', + description: 'Hashes found similar to the malicious file. ', + name: 'checkpoint.similar_hashes', + type: 'keyword', + }, + 'checkpoint.similar_strings': { + category: 'checkpoint', + description: 'Strings found similar to the malicious file. ', + name: 'checkpoint.similar_strings', + type: 'keyword', + }, + 'checkpoint.similar_communication': { + category: 'checkpoint', + description: 'Network action found similar to the malicious file. ', + name: 'checkpoint.similar_communication', + type: 'keyword', + }, + 'checkpoint.te_verdict_determined_by': { + category: 'checkpoint', + description: 'Emulators determined file verdict. ', + name: 'checkpoint.te_verdict_determined_by', + type: 'keyword', + }, + 'checkpoint.packet_capture_unique_id': { + category: 'checkpoint', + description: 'Identifier of the packet capture files. ', + name: 'checkpoint.packet_capture_unique_id', + type: 'keyword', + }, + 'checkpoint.total_attachments': { + category: 'checkpoint', + description: 'The number of attachments in an email. ', + name: 'checkpoint.total_attachments', + type: 'integer', + }, + 'checkpoint.additional_info': { + category: 'checkpoint', + description: 'ID of original file/mail which are sent by admin. ', + name: 'checkpoint.additional_info', + type: 'keyword', + }, + 'checkpoint.content_risk': { + category: 'checkpoint', + description: 'File risk. ', + name: 'checkpoint.content_risk', + type: 'integer', + }, + 'checkpoint.operation': { + category: 'checkpoint', + description: 'Operation made by Threat Extraction. ', + name: 'checkpoint.operation', + type: 'keyword', + }, + 'checkpoint.scrubbed_content': { + category: 'checkpoint', + description: 'Active content that was found. ', + name: 'checkpoint.scrubbed_content', + type: 'keyword', + }, + 'checkpoint.scrub_time': { + category: 'checkpoint', + description: 'Extraction process duration. ', + name: 'checkpoint.scrub_time', + type: 'keyword', + }, + 'checkpoint.scrub_download_time': { + category: 'checkpoint', + description: 'File download time from resource. ', + name: 'checkpoint.scrub_download_time', + type: 'keyword', + }, + 'checkpoint.scrub_total_time': { + category: 'checkpoint', + description: 'Threat extraction total file handling time. ', + name: 'checkpoint.scrub_total_time', + type: 'keyword', + }, + 'checkpoint.scrub_activity': { + category: 'checkpoint', + description: 'The result of the extraction ', + name: 'checkpoint.scrub_activity', + type: 'keyword', + }, + 'checkpoint.watermark': { + category: 'checkpoint', + description: 'Reports whether watermark is added to the cleaned file. ', + name: 'checkpoint.watermark', + type: 'keyword', + }, + 'checkpoint.source_object': { + category: 'checkpoint', + description: 'Matched object name on source column. ', + name: 'checkpoint.source_object', + type: 'integer', + }, + 'checkpoint.destination_object': { + category: 'checkpoint', + description: 'Matched object name on destination column. ', + name: 'checkpoint.destination_object', + type: 'keyword', + }, + 'checkpoint.drop_reason': { + category: 'checkpoint', + description: 'Drop reason description. ', + name: 'checkpoint.drop_reason', + type: 'keyword', + }, + 'checkpoint.hit': { + category: 'checkpoint', + description: 'Number of hits on a rule. ', + name: 'checkpoint.hit', + type: 'integer', + }, + 'checkpoint.rulebase_id': { + category: 'checkpoint', + description: 'Layer number. ', + name: 'checkpoint.rulebase_id', + type: 'integer', + }, + 'checkpoint.first_hit_time': { + category: 'checkpoint', + description: 'First hit time in current interval. ', + name: 'checkpoint.first_hit_time', + type: 'integer', + }, + 'checkpoint.last_hit_time': { + category: 'checkpoint', + description: 'Last hit time in current interval. ', + name: 'checkpoint.last_hit_time', + type: 'integer', + }, + 'checkpoint.rematch_info': { + category: 'checkpoint', + description: + 'Information sent when old connections cannot be matched during policy installation. ', + name: 'checkpoint.rematch_info', + type: 'keyword', + }, + 'checkpoint.last_rematch_time': { + category: 'checkpoint', + description: 'Connection rematched time. ', + name: 'checkpoint.last_rematch_time', + type: 'keyword', + }, + 'checkpoint.action_reason': { + category: 'checkpoint', + description: 'Connection drop reason. ', + name: 'checkpoint.action_reason', + type: 'integer', + }, + 'checkpoint.c_bytes': { + category: 'checkpoint', + description: 'Boolean value indicates whether bytes sent from the client side are used. ', + name: 'checkpoint.c_bytes', + type: 'integer', + }, + 'checkpoint.context_num': { + category: 'checkpoint', + description: 'Serial number of the log for a specific connection. ', + name: 'checkpoint.context_num', + type: 'integer', + }, + 'checkpoint.match_id': { + category: 'checkpoint', + description: 'Private key of the rule ', + name: 'checkpoint.match_id', + type: 'integer', + }, + 'checkpoint.alert': { + category: 'checkpoint', + description: 'Alert level of matched rule (for connection logs). ', + name: 'checkpoint.alert', + type: 'keyword', + }, + 'checkpoint.parent_rule': { + category: 'checkpoint', + description: 'Parent rule number, in case of inline layer. ', + name: 'checkpoint.parent_rule', + type: 'integer', + }, + 'checkpoint.match_fk': { + category: 'checkpoint', + description: 'Rule number. ', + name: 'checkpoint.match_fk', + type: 'integer', + }, + 'checkpoint.dropped_outgoing': { + category: 'checkpoint', + description: 'Number of outgoing bytes dropped when using UP-limit feature. ', + name: 'checkpoint.dropped_outgoing', + type: 'integer', + }, + 'checkpoint.dropped_incoming': { + category: 'checkpoint', + description: 'Number of incoming bytes dropped when using UP-limit feature. ', + name: 'checkpoint.dropped_incoming', + type: 'integer', + }, + 'checkpoint.media_type': { + category: 'checkpoint', + description: 'Media used (audio, video, etc.) ', + name: 'checkpoint.media_type', + type: 'keyword', + }, + 'checkpoint.sip_reason': { + category: 'checkpoint', + description: "Explains why 'source_ip' isn't allowed to redirect (handover). ", + name: 'checkpoint.sip_reason', + type: 'keyword', + }, + 'checkpoint.voip_method': { + category: 'checkpoint', + description: 'Registration request. ', + name: 'checkpoint.voip_method', + type: 'keyword', + }, + 'checkpoint.registered_ip-phones': { + category: 'checkpoint', + description: 'Registered IP-Phones. ', + name: 'checkpoint.registered_ip-phones', + type: 'keyword', + }, + 'checkpoint.voip_reg_user_type': { + category: 'checkpoint', + description: 'Registered IP-Phone type. ', + name: 'checkpoint.voip_reg_user_type', + type: 'keyword', + }, + 'checkpoint.voip_call_id': { + category: 'checkpoint', + description: 'Call-ID. ', + name: 'checkpoint.voip_call_id', + type: 'keyword', + }, + 'checkpoint.voip_reg_int': { + category: 'checkpoint', + description: 'Registration port. ', + name: 'checkpoint.voip_reg_int', + type: 'integer', + }, + 'checkpoint.voip_reg_ipp': { + category: 'checkpoint', + description: 'Registration IP protocol. ', + name: 'checkpoint.voip_reg_ipp', + type: 'integer', + }, + 'checkpoint.voip_reg_period': { + category: 'checkpoint', + description: 'Registration period. ', + name: 'checkpoint.voip_reg_period', + type: 'integer', + }, + 'checkpoint.src_phone_number': { + category: 'checkpoint', + description: 'Source IP-Phone. ', + name: 'checkpoint.src_phone_number', + type: 'keyword', + }, + 'checkpoint.voip_from_user_type': { + category: 'checkpoint', + description: 'Source IP-Phone type. ', + name: 'checkpoint.voip_from_user_type', + type: 'keyword', + }, + 'checkpoint.voip_to_user_type': { + category: 'checkpoint', + description: 'Destination IP-Phone type. ', + name: 'checkpoint.voip_to_user_type', + type: 'keyword', + }, + 'checkpoint.voip_call_dir': { + category: 'checkpoint', + description: 'Call direction: in/out. ', + name: 'checkpoint.voip_call_dir', + type: 'keyword', + }, + 'checkpoint.voip_call_state': { + category: 'checkpoint', + description: 'Call state. Possible values: in/out. ', + name: 'checkpoint.voip_call_state', + type: 'keyword', + }, + 'checkpoint.voip_call_term_time': { + category: 'checkpoint', + description: 'Call termination time stamp. ', + name: 'checkpoint.voip_call_term_time', + type: 'keyword', + }, + 'checkpoint.voip_duration': { + category: 'checkpoint', + description: 'Call duration (seconds). ', + name: 'checkpoint.voip_duration', + type: 'keyword', + }, + 'checkpoint.voip_media_port': { + category: 'checkpoint', + description: 'Media int. ', + name: 'checkpoint.voip_media_port', + type: 'keyword', + }, + 'checkpoint.voip_media_ipp': { + category: 'checkpoint', + description: 'Media IP protocol. ', + name: 'checkpoint.voip_media_ipp', + type: 'keyword', + }, + 'checkpoint.voip_est_codec': { + category: 'checkpoint', + description: 'Estimated codec. ', + name: 'checkpoint.voip_est_codec', + type: 'keyword', + }, + 'checkpoint.voip_exp': { + category: 'checkpoint', + description: 'Expiration. ', + name: 'checkpoint.voip_exp', + type: 'integer', + }, + 'checkpoint.voip_attach_sz': { + category: 'checkpoint', + description: 'Attachment size. ', + name: 'checkpoint.voip_attach_sz', + type: 'integer', + }, + 'checkpoint.voip_attach_action_info': { + category: 'checkpoint', + description: 'Attachment action Info. ', + name: 'checkpoint.voip_attach_action_info', + type: 'keyword', + }, + 'checkpoint.voip_media_codec': { + category: 'checkpoint', + description: 'Estimated codec. ', + name: 'checkpoint.voip_media_codec', + type: 'keyword', + }, + 'checkpoint.voip_reject_reason': { + category: 'checkpoint', + description: 'Reject reason. ', + name: 'checkpoint.voip_reject_reason', + type: 'keyword', + }, + 'checkpoint.voip_reason_info': { + category: 'checkpoint', + description: 'Information. ', + name: 'checkpoint.voip_reason_info', + type: 'keyword', + }, + 'checkpoint.voip_config': { + category: 'checkpoint', + description: 'Configuration. ', + name: 'checkpoint.voip_config', + type: 'keyword', + }, + 'checkpoint.voip_reg_server': { + category: 'checkpoint', + description: 'Registrar server IP address. ', + name: 'checkpoint.voip_reg_server', + type: 'ip', + }, + 'checkpoint.scv_user': { + category: 'checkpoint', + description: 'Username whose packets are dropped on SCV. ', + name: 'checkpoint.scv_user', + type: 'keyword', + }, + 'checkpoint.scv_message_info': { + category: 'checkpoint', + description: 'Drop reason. ', + name: 'checkpoint.scv_message_info', + type: 'keyword', + }, + 'checkpoint.ppp': { + category: 'checkpoint', + description: 'Authentication status. ', + name: 'checkpoint.ppp', + type: 'keyword', + }, + 'checkpoint.scheme': { + category: 'checkpoint', + description: 'Describes the scheme used for the log. ', + name: 'checkpoint.scheme', + type: 'keyword', + }, + 'checkpoint.machine': { + category: 'checkpoint', + description: 'L2TP machine which triggered the log and the log refers to it. ', + name: 'checkpoint.machine', + type: 'keyword', + }, + 'checkpoint.vpn_feature_name': { + category: 'checkpoint', + description: 'L2TP /IKE / Link Selection. ', + name: 'checkpoint.vpn_feature_name', + type: 'keyword', + }, + 'checkpoint.reject_category': { + category: 'checkpoint', + description: 'Authentication failure reason. ', + name: 'checkpoint.reject_category', + type: 'keyword', + }, + 'checkpoint.peer_ip_probing_status_update': { + category: 'checkpoint', + description: 'IP address response status. ', + name: 'checkpoint.peer_ip_probing_status_update', + type: 'keyword', + }, + 'checkpoint.peer_ip': { + category: 'checkpoint', + description: 'IP address which the client connects to. ', + name: 'checkpoint.peer_ip', + type: 'keyword', + }, + 'checkpoint.link_probing_status_update': { + category: 'checkpoint', + description: 'IP address response status. ', + name: 'checkpoint.link_probing_status_update', + type: 'keyword', + }, + 'checkpoint.source_interface': { + category: 'checkpoint', + description: 'External Interface name for source interface or Null if not found. ', + name: 'checkpoint.source_interface', + type: 'keyword', + }, + 'checkpoint.next_hop_ip': { + category: 'checkpoint', + description: 'Next hop IP address. ', + name: 'checkpoint.next_hop_ip', + type: 'keyword', + }, + 'checkpoint.srckeyid': { + category: 'checkpoint', + description: 'Initiator Spi ID. ', + name: 'checkpoint.srckeyid', + type: 'keyword', + }, + 'checkpoint.dstkeyid': { + category: 'checkpoint', + description: 'Responder Spi ID. ', + name: 'checkpoint.dstkeyid', + type: 'keyword', + }, + 'checkpoint.encryption_failure': { + category: 'checkpoint', + description: 'Message indicating why the encryption failed. ', + name: 'checkpoint.encryption_failure', + type: 'keyword', + }, + 'checkpoint.ike_ids': { + category: 'checkpoint', + description: 'All QM ids. ', + name: 'checkpoint.ike_ids', + type: 'keyword', + }, + 'checkpoint.community': { + category: 'checkpoint', + description: 'Community name for the IPSec key and the use of the IKEv. ', + name: 'checkpoint.community', + type: 'keyword', + }, + 'checkpoint.ike': { + category: 'checkpoint', + description: 'IKEMode (PHASE1, PHASE2, etc..). ', + name: 'checkpoint.ike', + type: 'keyword', + }, + 'checkpoint.cookieI': { + category: 'checkpoint', + description: 'Initiator cookie. ', + name: 'checkpoint.cookieI', + type: 'keyword', + }, + 'checkpoint.cookieR': { + category: 'checkpoint', + description: 'Responder cookie. ', + name: 'checkpoint.cookieR', + type: 'keyword', + }, + 'checkpoint.msgid': { + category: 'checkpoint', + description: 'Message ID. ', + name: 'checkpoint.msgid', + type: 'keyword', + }, + 'checkpoint.methods': { + category: 'checkpoint', + description: 'IPSEc methods. ', + name: 'checkpoint.methods', + type: 'keyword', + }, + 'checkpoint.connection_uid': { + category: 'checkpoint', + description: 'Calculation of md5 of the IP and user name as UID. ', + name: 'checkpoint.connection_uid', + type: 'keyword', + }, + 'checkpoint.site_name': { + category: 'checkpoint', + description: 'Site name. ', + name: 'checkpoint.site_name', + type: 'keyword', + }, + 'checkpoint.esod_rule_name': { + category: 'checkpoint', + description: 'Unknown rule name. ', + name: 'checkpoint.esod_rule_name', + type: 'keyword', + }, + 'checkpoint.esod_rule_action': { + category: 'checkpoint', + description: 'Unknown rule action. ', + name: 'checkpoint.esod_rule_action', + type: 'keyword', + }, + 'checkpoint.esod_rule_type': { + category: 'checkpoint', + description: 'Unknown rule type. ', + name: 'checkpoint.esod_rule_type', + type: 'keyword', + }, + 'checkpoint.esod_noncompliance_reason': { + category: 'checkpoint', + description: 'Non-compliance reason. ', + name: 'checkpoint.esod_noncompliance_reason', + type: 'keyword', + }, + 'checkpoint.esod_associated_policies': { + category: 'checkpoint', + description: 'Associated policies. ', + name: 'checkpoint.esod_associated_policies', + type: 'keyword', + }, + 'checkpoint.spyware_type': { + category: 'checkpoint', + description: 'Spyware type. ', + name: 'checkpoint.spyware_type', + type: 'keyword', + }, + 'checkpoint.anti_virus_type': { + category: 'checkpoint', + description: 'Anti virus type. ', + name: 'checkpoint.anti_virus_type', + type: 'keyword', + }, + 'checkpoint.end_user_firewall_type': { + category: 'checkpoint', + description: 'End user firewall type. ', + name: 'checkpoint.end_user_firewall_type', + type: 'keyword', + }, + 'checkpoint.esod_scan_status': { + category: 'checkpoint', + description: 'Scan failed. ', + name: 'checkpoint.esod_scan_status', + type: 'keyword', + }, + 'checkpoint.esod_access_status': { + category: 'checkpoint', + description: 'Access denied. ', + name: 'checkpoint.esod_access_status', + type: 'keyword', + }, + 'checkpoint.client_type': { + category: 'checkpoint', + description: 'Endpoint Connect. ', + name: 'checkpoint.client_type', + type: 'keyword', + }, + 'checkpoint.precise_error': { + category: 'checkpoint', + description: 'HTTP parser error. ', + name: 'checkpoint.precise_error', + type: 'keyword', + }, + 'checkpoint.method': { + category: 'checkpoint', + description: 'HTTP method. ', + name: 'checkpoint.method', + type: 'keyword', + }, + 'checkpoint.trusted_domain': { + category: 'checkpoint', + description: 'In case of phishing event, the domain, which the attacker was impersonating. ', + name: 'checkpoint.trusted_domain', + type: 'keyword', + }, + 'cisco.asa.message_id': { + category: 'cisco', + description: 'The Cisco ASA message identifier. ', + name: 'cisco.asa.message_id', + type: 'keyword', + }, + 'cisco.asa.suffix': { + category: 'cisco', + description: 'Optional suffix after %ASA identifier. ', + example: 'session', + name: 'cisco.asa.suffix', + type: 'keyword', + }, + 'cisco.asa.source_interface': { + category: 'cisco', + description: 'Source interface for the flow or event. ', + name: 'cisco.asa.source_interface', + type: 'keyword', + }, + 'cisco.asa.destination_interface': { + category: 'cisco', + description: 'Destination interface for the flow or event. ', + name: 'cisco.asa.destination_interface', + type: 'keyword', + }, + 'cisco.asa.rule_name': { + category: 'cisco', + description: 'Name of the Access Control List rule that matched this event. ', + name: 'cisco.asa.rule_name', + type: 'keyword', + }, + 'cisco.asa.source_username': { + category: 'cisco', + description: 'Name of the user that is the source for this event. ', + name: 'cisco.asa.source_username', + type: 'keyword', + }, + 'cisco.asa.destination_username': { + category: 'cisco', + description: 'Name of the user that is the destination for this event. ', + name: 'cisco.asa.destination_username', + type: 'keyword', + }, + 'cisco.asa.mapped_source_ip': { + category: 'cisco', + description: 'The translated source IP address. ', + name: 'cisco.asa.mapped_source_ip', + type: 'ip', + }, + 'cisco.asa.mapped_source_host': { + category: 'cisco', + description: 'The translated source host. ', + name: 'cisco.asa.mapped_source_host', + type: 'keyword', + }, + 'cisco.asa.mapped_source_port': { + category: 'cisco', + description: 'The translated source port. ', + name: 'cisco.asa.mapped_source_port', + type: 'long', + }, + 'cisco.asa.mapped_destination_ip': { + category: 'cisco', + description: 'The translated destination IP address. ', + name: 'cisco.asa.mapped_destination_ip', + type: 'ip', + }, + 'cisco.asa.mapped_destination_host': { + category: 'cisco', + description: 'The translated destination host. ', + name: 'cisco.asa.mapped_destination_host', + type: 'keyword', + }, + 'cisco.asa.mapped_destination_port': { + category: 'cisco', + description: 'The translated destination port. ', + name: 'cisco.asa.mapped_destination_port', + type: 'long', + }, + 'cisco.asa.threat_level': { + category: 'cisco', + description: + 'Threat level for malware / botnet traffic. One of very-low, low, moderate, high or very-high. ', + name: 'cisco.asa.threat_level', + type: 'keyword', + }, + 'cisco.asa.threat_category': { + category: 'cisco', + description: + 'Category for the malware / botnet traffic. For example: virus, botnet, trojan, etc. ', + name: 'cisco.asa.threat_category', + type: 'keyword', + }, + 'cisco.asa.connection_id': { + category: 'cisco', + description: 'Unique identifier for a flow. ', + name: 'cisco.asa.connection_id', + type: 'keyword', + }, + 'cisco.asa.icmp_type': { + category: 'cisco', + description: 'ICMP type. ', + name: 'cisco.asa.icmp_type', + type: 'short', + }, + 'cisco.asa.icmp_code': { + category: 'cisco', + description: 'ICMP code. ', + name: 'cisco.asa.icmp_code', + type: 'short', + }, + 'cisco.asa.connection_type': { + category: 'cisco', + description: 'The VPN connection type ', + name: 'cisco.asa.connection_type', + type: 'keyword', + }, + 'cisco.asa.dap_records': { + category: 'cisco', + description: 'The assigned DAP records ', + name: 'cisco.asa.dap_records', + type: 'keyword', + }, + 'cisco.ftd.message_id': { + category: 'cisco', + description: 'The Cisco FTD message identifier. ', + name: 'cisco.ftd.message_id', + type: 'keyword', + }, + 'cisco.ftd.suffix': { + category: 'cisco', + description: 'Optional suffix after %FTD identifier. ', + example: 'session', + name: 'cisco.ftd.suffix', + type: 'keyword', + }, + 'cisco.ftd.source_interface': { + category: 'cisco', + description: 'Source interface for the flow or event. ', + name: 'cisco.ftd.source_interface', + type: 'keyword', + }, + 'cisco.ftd.destination_interface': { + category: 'cisco', + description: 'Destination interface for the flow or event. ', + name: 'cisco.ftd.destination_interface', + type: 'keyword', + }, + 'cisco.ftd.rule_name': { + category: 'cisco', + description: 'Name of the Access Control List rule that matched this event. ', + name: 'cisco.ftd.rule_name', + type: 'keyword', + }, + 'cisco.ftd.source_username': { + category: 'cisco', + description: 'Name of the user that is the source for this event. ', + name: 'cisco.ftd.source_username', + type: 'keyword', + }, + 'cisco.ftd.destination_username': { + category: 'cisco', + description: 'Name of the user that is the destination for this event. ', + name: 'cisco.ftd.destination_username', + type: 'keyword', + }, + 'cisco.ftd.mapped_source_ip': { + category: 'cisco', + description: 'The translated source IP address. Use ECS source.nat.ip. ', + name: 'cisco.ftd.mapped_source_ip', + type: 'ip', + }, + 'cisco.ftd.mapped_source_host': { + category: 'cisco', + description: 'The translated source host. ', + name: 'cisco.ftd.mapped_source_host', + type: 'keyword', + }, + 'cisco.ftd.mapped_source_port': { + category: 'cisco', + description: 'The translated source port. Use ECS source.nat.port. ', + name: 'cisco.ftd.mapped_source_port', + type: 'long', + }, + 'cisco.ftd.mapped_destination_ip': { + category: 'cisco', + description: 'The translated destination IP address. Use ECS destination.nat.ip. ', + name: 'cisco.ftd.mapped_destination_ip', + type: 'ip', + }, + 'cisco.ftd.mapped_destination_host': { + category: 'cisco', + description: 'The translated destination host. ', + name: 'cisco.ftd.mapped_destination_host', + type: 'keyword', + }, + 'cisco.ftd.mapped_destination_port': { + category: 'cisco', + description: 'The translated destination port. Use ECS destination.nat.port. ', + name: 'cisco.ftd.mapped_destination_port', + type: 'long', + }, + 'cisco.ftd.threat_level': { + category: 'cisco', + description: + 'Threat level for malware / botnet traffic. One of very-low, low, moderate, high or very-high. ', + name: 'cisco.ftd.threat_level', + type: 'keyword', + }, + 'cisco.ftd.threat_category': { + category: 'cisco', + description: + 'Category for the malware / botnet traffic. For example: virus, botnet, trojan, etc. ', + name: 'cisco.ftd.threat_category', + type: 'keyword', + }, + 'cisco.ftd.connection_id': { + category: 'cisco', + description: 'Unique identifier for a flow. ', + name: 'cisco.ftd.connection_id', + type: 'keyword', + }, + 'cisco.ftd.icmp_type': { + category: 'cisco', + description: 'ICMP type. ', + name: 'cisco.ftd.icmp_type', + type: 'short', + }, + 'cisco.ftd.icmp_code': { + category: 'cisco', + description: 'ICMP code. ', + name: 'cisco.ftd.icmp_code', + type: 'short', + }, + 'cisco.ftd.security': { + category: 'cisco', + description: 'Raw fields for Security Events.', + name: 'cisco.ftd.security', + type: 'object', + }, + 'cisco.ftd.connection_type': { + category: 'cisco', + description: 'The VPN connection type ', + name: 'cisco.ftd.connection_type', + type: 'keyword', + }, + 'cisco.ftd.dap_records': { + category: 'cisco', + description: 'The assigned DAP records ', + name: 'cisco.ftd.dap_records', + type: 'keyword', + }, + 'cisco.ios.access_list': { + category: 'cisco', + description: 'Name of the IP access list. ', + name: 'cisco.ios.access_list', + type: 'keyword', + }, + 'cisco.ios.facility': { + category: 'cisco', + description: + 'The facility to which the message refers (for example, SNMP, SYS, and so forth). A facility can be a hardware device, a protocol, or a module of the system software. It denotes the source or the cause of the system message. ', + example: 'SEC', + name: 'cisco.ios.facility', + type: 'keyword', + }, + 'coredns.id': { + category: 'coredns', + description: 'id of the DNS transaction ', + name: 'coredns.id', + type: 'keyword', + }, + 'coredns.query.size': { + category: 'coredns', + description: 'size of the DNS query ', + name: 'coredns.query.size', + type: 'integer', + format: 'bytes', + }, + 'coredns.query.class': { + category: 'coredns', + description: 'DNS query class ', + name: 'coredns.query.class', + type: 'keyword', + }, + 'coredns.query.name': { + category: 'coredns', + description: 'DNS query name ', + name: 'coredns.query.name', + type: 'keyword', + }, + 'coredns.query.type': { + category: 'coredns', + description: 'DNS query type ', + name: 'coredns.query.type', + type: 'keyword', + }, + 'coredns.response.code': { + category: 'coredns', + description: 'DNS response code ', + name: 'coredns.response.code', + type: 'keyword', + }, + 'coredns.response.flags': { + category: 'coredns', + description: 'DNS response flags ', + name: 'coredns.response.flags', + type: 'keyword', + }, + 'coredns.response.size': { + category: 'coredns', + description: 'size of the DNS response ', + name: 'coredns.response.size', + type: 'integer', + format: 'bytes', + }, + 'coredns.dnssec_ok': { + category: 'coredns', + description: 'dnssec flag ', + name: 'coredns.dnssec_ok', + type: 'boolean', + }, + 'crowdstrike.metadata.eventType': { + category: 'crowdstrike', + description: + 'DetectionSummaryEvent, FirewallMatchEvent, IncidentSummaryEvent, RemoteResponseSessionStartEvent, RemoteResponseSessionEndEvent, AuthActivityAuditEvent, or UserActivityAuditEvent ', + name: 'crowdstrike.metadata.eventType', + type: 'keyword', + }, + 'crowdstrike.metadata.eventCreationTime': { + category: 'crowdstrike', + description: 'The time this event occurred on the endpoint in UTC UNIX_MS format. ', + name: 'crowdstrike.metadata.eventCreationTime', + type: 'date', + }, + 'crowdstrike.metadata.offset': { + category: 'crowdstrike', + description: + 'Offset number that tracks the location of the event in stream. This is used to identify unique detection events. ', + name: 'crowdstrike.metadata.offset', + type: 'integer', + }, + 'crowdstrike.metadata.customerIDString': { + category: 'crowdstrike', + description: 'Customer identifier ', + name: 'crowdstrike.metadata.customerIDString', + type: 'keyword', + }, + 'crowdstrike.metadata.version': { + category: 'crowdstrike', + description: 'Schema version ', + name: 'crowdstrike.metadata.version', + type: 'keyword', + }, + 'crowdstrike.event.ProcessStartTime': { + category: 'crowdstrike', + description: 'The process start time in UTC UNIX_MS format. ', + name: 'crowdstrike.event.ProcessStartTime', + type: 'date', + }, + 'crowdstrike.event.ProcessEndTime': { + category: 'crowdstrike', + description: 'The process termination time in UTC UNIX_MS format. ', + name: 'crowdstrike.event.ProcessEndTime', + type: 'date', + }, + 'crowdstrike.event.ProcessId': { + category: 'crowdstrike', + description: 'Process ID related to the detection. ', + name: 'crowdstrike.event.ProcessId', + type: 'integer', + }, + 'crowdstrike.event.ParentProcessId': { + category: 'crowdstrike', + description: 'Parent process ID related to the detection. ', + name: 'crowdstrike.event.ParentProcessId', + type: 'integer', + }, + 'crowdstrike.event.ComputerName': { + category: 'crowdstrike', + description: 'Name of the computer where the detection occurred. ', + name: 'crowdstrike.event.ComputerName', + type: 'keyword', + }, + 'crowdstrike.event.UserName': { + category: 'crowdstrike', + description: 'User name associated with the detection. ', + name: 'crowdstrike.event.UserName', + type: 'keyword', + }, + 'crowdstrike.event.DetectName': { + category: 'crowdstrike', + description: 'Name of the detection. ', + name: 'crowdstrike.event.DetectName', + type: 'keyword', + }, + 'crowdstrike.event.DetectDescription': { + category: 'crowdstrike', + description: 'Description of the detection. ', + name: 'crowdstrike.event.DetectDescription', + type: 'keyword', + }, + 'crowdstrike.event.Severity': { + category: 'crowdstrike', + description: 'Severity score of the detection. ', + name: 'crowdstrike.event.Severity', + type: 'integer', + }, + 'crowdstrike.event.SeverityName': { + category: 'crowdstrike', + description: 'Severity score text. ', + name: 'crowdstrike.event.SeverityName', + type: 'keyword', + }, + 'crowdstrike.event.FileName': { + category: 'crowdstrike', + description: 'File name of the associated process for the detection. ', + name: 'crowdstrike.event.FileName', + type: 'keyword', + }, + 'crowdstrike.event.FilePath': { + category: 'crowdstrike', + description: 'Path of the executable associated with the detection. ', + name: 'crowdstrike.event.FilePath', + type: 'keyword', + }, + 'crowdstrike.event.CommandLine': { + category: 'crowdstrike', + description: 'Executable path with command line arguments. ', + name: 'crowdstrike.event.CommandLine', + type: 'keyword', + }, + 'crowdstrike.event.SHA1String': { + category: 'crowdstrike', + description: 'SHA1 sum of the executable associated with the detection. ', + name: 'crowdstrike.event.SHA1String', + type: 'keyword', + }, + 'crowdstrike.event.SHA256String': { + category: 'crowdstrike', + description: 'SHA256 sum of the executable associated with the detection. ', + name: 'crowdstrike.event.SHA256String', + type: 'keyword', + }, + 'crowdstrike.event.MD5String': { + category: 'crowdstrike', + description: 'MD5 sum of the executable associated with the detection. ', + name: 'crowdstrike.event.MD5String', + type: 'keyword', + }, + 'crowdstrike.event.MachineDomain': { + category: 'crowdstrike', + description: 'Domain for the machine associated with the detection. ', + name: 'crowdstrike.event.MachineDomain', + type: 'keyword', + }, + 'crowdstrike.event.FalconHostLink': { + category: 'crowdstrike', + description: 'URL to view the detection in Falcon. ', + name: 'crowdstrike.event.FalconHostLink', + type: 'keyword', + }, + 'crowdstrike.event.SensorId': { + category: 'crowdstrike', + description: 'Unique ID associated with the Falcon sensor. ', + name: 'crowdstrike.event.SensorId', + type: 'keyword', + }, + 'crowdstrike.event.DetectId': { + category: 'crowdstrike', + description: 'Unique ID associated with the detection. ', + name: 'crowdstrike.event.DetectId', + type: 'keyword', + }, + 'crowdstrike.event.LocalIP': { + category: 'crowdstrike', + description: 'IP address of the host associated with the detection. ', + name: 'crowdstrike.event.LocalIP', + type: 'keyword', + }, + 'crowdstrike.event.MACAddress': { + category: 'crowdstrike', + description: 'MAC address of the host associated with the detection. ', + name: 'crowdstrike.event.MACAddress', + type: 'keyword', + }, + 'crowdstrike.event.Tactic': { + category: 'crowdstrike', + description: 'MITRE tactic category of the detection. ', + name: 'crowdstrike.event.Tactic', + type: 'keyword', + }, + 'crowdstrike.event.Technique': { + category: 'crowdstrike', + description: 'MITRE technique category of the detection. ', + name: 'crowdstrike.event.Technique', + type: 'keyword', + }, + 'crowdstrike.event.Objective': { + category: 'crowdstrike', + description: 'Method of detection. ', + name: 'crowdstrike.event.Objective', + type: 'keyword', + }, + 'crowdstrike.event.PatternDispositionDescription': { + category: 'crowdstrike', + description: 'Action taken by Falcon. ', + name: 'crowdstrike.event.PatternDispositionDescription', + type: 'keyword', + }, + 'crowdstrike.event.PatternDispositionValue': { + category: 'crowdstrike', + description: 'Unique ID associated with action taken. ', + name: 'crowdstrike.event.PatternDispositionValue', + type: 'integer', + }, + 'crowdstrike.event.PatternDispositionFlags': { + category: 'crowdstrike', + description: 'Flags indicating actions taken. ', + name: 'crowdstrike.event.PatternDispositionFlags', + type: 'object', + }, + 'crowdstrike.event.State': { + category: 'crowdstrike', + description: 'Whether the incident summary is open and ongoing or closed. ', + name: 'crowdstrike.event.State', + type: 'keyword', + }, + 'crowdstrike.event.IncidentStartTime': { + category: 'crowdstrike', + description: 'Start time for the incident in UTC UNIX format. ', + name: 'crowdstrike.event.IncidentStartTime', + type: 'date', + }, + 'crowdstrike.event.IncidentEndTime': { + category: 'crowdstrike', + description: 'End time for the incident in UTC UNIX format. ', + name: 'crowdstrike.event.IncidentEndTime', + type: 'date', + }, + 'crowdstrike.event.FineScore': { + category: 'crowdstrike', + description: 'Score for incident. ', + name: 'crowdstrike.event.FineScore', + type: 'float', + }, + 'crowdstrike.event.UserId': { + category: 'crowdstrike', + description: 'Email address or user ID associated with the event. ', + name: 'crowdstrike.event.UserId', + type: 'keyword', + }, + 'crowdstrike.event.UserIp': { + category: 'crowdstrike', + description: 'IP address associated with the user. ', + name: 'crowdstrike.event.UserIp', + type: 'keyword', + }, + 'crowdstrike.event.OperationName': { + category: 'crowdstrike', + description: 'Event subtype. ', + name: 'crowdstrike.event.OperationName', + type: 'keyword', + }, + 'crowdstrike.event.ServiceName': { + category: 'crowdstrike', + description: 'Service associated with this event. ', + name: 'crowdstrike.event.ServiceName', + type: 'keyword', + }, + 'crowdstrike.event.Success': { + category: 'crowdstrike', + description: 'Indicator of whether or not this event was successful. ', + name: 'crowdstrike.event.Success', + type: 'boolean', + }, + 'crowdstrike.event.UTCTimestamp': { + category: 'crowdstrike', + description: 'Timestamp associated with this event in UTC UNIX format. ', + name: 'crowdstrike.event.UTCTimestamp', + type: 'date', + }, + 'crowdstrike.event.AuditKeyValues': { + category: 'crowdstrike', + description: 'Fields that were changed in this event. ', + name: 'crowdstrike.event.AuditKeyValues', + type: 'nested', + }, + 'crowdstrike.event.ExecutablesWritten': { + category: 'crowdstrike', + description: 'Detected executables written to disk by a process. ', + name: 'crowdstrike.event.ExecutablesWritten', + type: 'nested', + }, + 'crowdstrike.event.SessionId': { + category: 'crowdstrike', + description: 'Session ID of the remote response session. ', + name: 'crowdstrike.event.SessionId', + type: 'keyword', + }, + 'crowdstrike.event.HostnameField': { + category: 'crowdstrike', + description: 'Host name of the machine for the remote session. ', + name: 'crowdstrike.event.HostnameField', + type: 'keyword', + }, + 'crowdstrike.event.StartTimestamp': { + category: 'crowdstrike', + description: 'Start time for the remote session in UTC UNIX format. ', + name: 'crowdstrike.event.StartTimestamp', + type: 'date', + }, + 'crowdstrike.event.EndTimestamp': { + category: 'crowdstrike', + description: 'End time for the remote session in UTC UNIX format. ', + name: 'crowdstrike.event.EndTimestamp', + type: 'date', + }, + 'crowdstrike.event.LateralMovement': { + category: 'crowdstrike', + description: 'Lateral movement field for incident. ', + name: 'crowdstrike.event.LateralMovement', + type: 'long', + }, + 'crowdstrike.event.ParentImageFileName': { + category: 'crowdstrike', + description: 'Path to the parent process. ', + name: 'crowdstrike.event.ParentImageFileName', + type: 'keyword', + }, + 'crowdstrike.event.ParentCommandLine': { + category: 'crowdstrike', + description: 'Parent process command line arguments. ', + name: 'crowdstrike.event.ParentCommandLine', + type: 'keyword', + }, + 'crowdstrike.event.GrandparentImageFileName': { + category: 'crowdstrike', + description: 'Path to the grandparent process. ', + name: 'crowdstrike.event.GrandparentImageFileName', + type: 'keyword', + }, + 'crowdstrike.event.GrandparentCommandLine': { + category: 'crowdstrike', + description: 'Grandparent process command line arguments. ', + name: 'crowdstrike.event.GrandparentCommandLine', + type: 'keyword', + }, + 'crowdstrike.event.IOCType': { + category: 'crowdstrike', + description: 'CrowdStrike type for indicator of compromise. ', + name: 'crowdstrike.event.IOCType', + type: 'keyword', + }, + 'crowdstrike.event.IOCValue': { + category: 'crowdstrike', + description: 'CrowdStrike value for indicator of compromise. ', + name: 'crowdstrike.event.IOCValue', + type: 'keyword', + }, + 'crowdstrike.event.CustomerId': { + category: 'crowdstrike', + description: 'Customer identifier. ', + name: 'crowdstrike.event.CustomerId', + type: 'keyword', + }, + 'crowdstrike.event.DeviceId': { + category: 'crowdstrike', + description: 'Device on which the event occurred. ', + name: 'crowdstrike.event.DeviceId', + type: 'keyword', + }, + 'crowdstrike.event.Ipv': { + category: 'crowdstrike', + description: 'Protocol for network request. ', + name: 'crowdstrike.event.Ipv', + type: 'keyword', + }, + 'crowdstrike.event.ConnectionDirection': { + category: 'crowdstrike', + description: 'Direction for network connection. ', + name: 'crowdstrike.event.ConnectionDirection', + type: 'keyword', + }, + 'crowdstrike.event.EventType': { + category: 'crowdstrike', + description: 'CrowdStrike provided event type. ', + name: 'crowdstrike.event.EventType', + type: 'keyword', + }, + 'crowdstrike.event.HostName': { + category: 'crowdstrike', + description: 'Host name of the local machine. ', + name: 'crowdstrike.event.HostName', + type: 'keyword', + }, + 'crowdstrike.event.ICMPCode': { + category: 'crowdstrike', + description: 'RFC2780 ICMP Code field. ', + name: 'crowdstrike.event.ICMPCode', + type: 'keyword', + }, + 'crowdstrike.event.ICMPType': { + category: 'crowdstrike', + description: 'RFC2780 ICMP Type field. ', + name: 'crowdstrike.event.ICMPType', + type: 'keyword', + }, + 'crowdstrike.event.ImageFileName': { + category: 'crowdstrike', + description: 'File name of the associated process for the detection. ', + name: 'crowdstrike.event.ImageFileName', + type: 'keyword', + }, + 'crowdstrike.event.PID': { + category: 'crowdstrike', + description: 'Associated process id for the detection. ', + name: 'crowdstrike.event.PID', + type: 'long', + }, + 'crowdstrike.event.LocalAddress': { + category: 'crowdstrike', + description: 'IP address of local machine. ', + name: 'crowdstrike.event.LocalAddress', + type: 'ip', + }, + 'crowdstrike.event.LocalPort': { + category: 'crowdstrike', + description: 'Port of local machine. ', + name: 'crowdstrike.event.LocalPort', + type: 'long', + }, + 'crowdstrike.event.RemoteAddress': { + category: 'crowdstrike', + description: 'IP address of remote machine. ', + name: 'crowdstrike.event.RemoteAddress', + type: 'ip', + }, + 'crowdstrike.event.RemotePort': { + category: 'crowdstrike', + description: 'Port of remote machine. ', + name: 'crowdstrike.event.RemotePort', + type: 'long', + }, + 'crowdstrike.event.RuleAction': { + category: 'crowdstrike', + description: 'Firewall rule action. ', + name: 'crowdstrike.event.RuleAction', + type: 'keyword', + }, + 'crowdstrike.event.RuleDescription': { + category: 'crowdstrike', + description: 'Firewall rule description. ', + name: 'crowdstrike.event.RuleDescription', + type: 'keyword', + }, + 'crowdstrike.event.RuleFamilyID': { + category: 'crowdstrike', + description: 'Firewall rule family id. ', + name: 'crowdstrike.event.RuleFamilyID', + type: 'keyword', + }, + 'crowdstrike.event.RuleGroupName': { + category: 'crowdstrike', + description: 'Firewall rule group name. ', + name: 'crowdstrike.event.RuleGroupName', + type: 'keyword', + }, + 'crowdstrike.event.RuleName': { + category: 'crowdstrike', + description: 'Firewall rule name. ', + name: 'crowdstrike.event.RuleName', + type: 'keyword', + }, + 'crowdstrike.event.RuleId': { + category: 'crowdstrike', + description: 'Firewall rule id. ', + name: 'crowdstrike.event.RuleId', + type: 'keyword', + }, + 'crowdstrike.event.MatchCount': { + category: 'crowdstrike', + description: 'Number of firewall rule matches. ', + name: 'crowdstrike.event.MatchCount', + type: 'long', + }, + 'crowdstrike.event.MatchCountSinceLastReport': { + category: 'crowdstrike', + description: 'Number of firewall rule matches since the last report. ', + name: 'crowdstrike.event.MatchCountSinceLastReport', + type: 'long', + }, + 'crowdstrike.event.Timestamp': { + category: 'crowdstrike', + description: 'Firewall rule triggered timestamp. ', + name: 'crowdstrike.event.Timestamp', + type: 'date', + }, + 'crowdstrike.event.Flags.Audit': { + category: 'crowdstrike', + description: 'CrowdStrike audit flag. ', + name: 'crowdstrike.event.Flags.Audit', + type: 'boolean', + }, + 'crowdstrike.event.Flags.Log': { + category: 'crowdstrike', + description: 'CrowdStrike log flag. ', + name: 'crowdstrike.event.Flags.Log', + type: 'boolean', + }, + 'crowdstrike.event.Flags.Monitor': { + category: 'crowdstrike', + description: 'CrowdStrike monitor flag. ', + name: 'crowdstrike.event.Flags.Monitor', + type: 'boolean', + }, + 'crowdstrike.event.Protocol': { + category: 'crowdstrike', + description: 'CrowdStrike provided protocol. ', + name: 'crowdstrike.event.Protocol', + type: 'keyword', + }, + 'crowdstrike.event.NetworkProfile': { + category: 'crowdstrike', + description: 'CrowdStrike network profile. ', + name: 'crowdstrike.event.NetworkProfile', + type: 'keyword', + }, + 'crowdstrike.event.PolicyName': { + category: 'crowdstrike', + description: 'CrowdStrike policy name. ', + name: 'crowdstrike.event.PolicyName', + type: 'keyword', + }, + 'crowdstrike.event.PolicyID': { + category: 'crowdstrike', + description: 'CrowdStrike policy id. ', + name: 'crowdstrike.event.PolicyID', + type: 'keyword', + }, + 'crowdstrike.event.Status': { + category: 'crowdstrike', + description: 'CrowdStrike status. ', + name: 'crowdstrike.event.Status', + type: 'keyword', + }, + 'crowdstrike.event.TreeID': { + category: 'crowdstrike', + description: 'CrowdStrike tree id. ', + name: 'crowdstrike.event.TreeID', + type: 'keyword', + }, + 'crowdstrike.event.Commands': { + category: 'crowdstrike', + description: 'Commands run in a remote session. ', + name: 'crowdstrike.event.Commands', + type: 'keyword', + }, + 'envoyproxy.log_type': { + category: 'envoyproxy', + description: 'Envoy log type, normally ACCESS ', + name: 'envoyproxy.log_type', + type: 'keyword', + }, + 'envoyproxy.response_flags': { + category: 'envoyproxy', + description: 'Response flags ', + name: 'envoyproxy.response_flags', + type: 'keyword', + }, + 'envoyproxy.upstream_service_time': { + category: 'envoyproxy', + description: 'Upstream service time in nanoseconds ', + name: 'envoyproxy.upstream_service_time', + type: 'long', + format: 'duration', + }, + 'envoyproxy.request_id': { + category: 'envoyproxy', + description: 'ID of the request ', + name: 'envoyproxy.request_id', + type: 'keyword', + }, + 'envoyproxy.authority': { + category: 'envoyproxy', + description: 'Envoy proxy authority field ', + name: 'envoyproxy.authority', + type: 'keyword', + }, + 'envoyproxy.proxy_type': { + category: 'envoyproxy', + description: 'Envoy proxy type, tcp or http ', + name: 'envoyproxy.proxy_type', + type: 'keyword', + }, + 'fortinet.file.hash.crc32': { + category: 'fortinet', + description: 'CRC32 Hash of file ', + name: 'fortinet.file.hash.crc32', + type: 'keyword', + }, + 'fortinet.firewall.acct_stat': { + category: 'fortinet', + description: 'Accounting state (RADIUS) ', + name: 'fortinet.firewall.acct_stat', + type: 'keyword', + }, + 'fortinet.firewall.acktime': { + category: 'fortinet', + description: 'Alarm Acknowledge Time ', + name: 'fortinet.firewall.acktime', + type: 'keyword', + }, + 'fortinet.firewall.act': { + category: 'fortinet', + description: 'Action ', + name: 'fortinet.firewall.act', + type: 'keyword', + }, + 'fortinet.firewall.action': { + category: 'fortinet', + description: 'Status of the session ', + name: 'fortinet.firewall.action', + type: 'keyword', + }, + 'fortinet.firewall.activity': { + category: 'fortinet', + description: 'HA activity message ', + name: 'fortinet.firewall.activity', + type: 'keyword', + }, + 'fortinet.firewall.addr': { + category: 'fortinet', + description: 'IP Address ', + name: 'fortinet.firewall.addr', + type: 'ip', + }, + 'fortinet.firewall.addr_type': { + category: 'fortinet', + description: 'Address Type ', + name: 'fortinet.firewall.addr_type', + type: 'keyword', + }, + 'fortinet.firewall.addrgrp': { + category: 'fortinet', + description: 'Address Group ', + name: 'fortinet.firewall.addrgrp', + type: 'keyword', + }, + 'fortinet.firewall.adgroup': { + category: 'fortinet', + description: 'AD Group Name ', + name: 'fortinet.firewall.adgroup', + type: 'keyword', + }, + 'fortinet.firewall.admin': { + category: 'fortinet', + description: 'Admin User ', + name: 'fortinet.firewall.admin', + type: 'keyword', + }, + 'fortinet.firewall.age': { + category: 'fortinet', + description: 'Time in seconds - time passed since last seen ', + name: 'fortinet.firewall.age', + type: 'integer', + }, + 'fortinet.firewall.agent': { + category: 'fortinet', + description: 'User agent - eg. agent="Mozilla/5.0" ', + name: 'fortinet.firewall.agent', + type: 'keyword', + }, + 'fortinet.firewall.alarmid': { + category: 'fortinet', + description: 'Alarm ID ', + name: 'fortinet.firewall.alarmid', + type: 'integer', + }, + 'fortinet.firewall.alert': { + category: 'fortinet', + description: 'Alert ', + name: 'fortinet.firewall.alert', + type: 'keyword', + }, + 'fortinet.firewall.analyticscksum': { + category: 'fortinet', + description: 'The checksum of the file submitted for analytics ', + name: 'fortinet.firewall.analyticscksum', + type: 'keyword', + }, + 'fortinet.firewall.analyticssubmit': { + category: 'fortinet', + description: 'The flag for analytics submission ', + name: 'fortinet.firewall.analyticssubmit', + type: 'keyword', + }, + 'fortinet.firewall.ap': { + category: 'fortinet', + description: 'Access Point ', + name: 'fortinet.firewall.ap', + type: 'keyword', + }, + 'fortinet.firewall.app-type': { + category: 'fortinet', + description: 'Address Type ', + name: 'fortinet.firewall.app-type', + type: 'keyword', + }, + 'fortinet.firewall.appact': { + category: 'fortinet', + description: 'The security action from app control ', + name: 'fortinet.firewall.appact', + type: 'keyword', + }, + 'fortinet.firewall.appid': { + category: 'fortinet', + description: 'Application ID ', + name: 'fortinet.firewall.appid', + type: 'integer', + }, + 'fortinet.firewall.applist': { + category: 'fortinet', + description: 'Application Control profile ', + name: 'fortinet.firewall.applist', + type: 'keyword', + }, + 'fortinet.firewall.apprisk': { + category: 'fortinet', + description: 'Application Risk Level ', + name: 'fortinet.firewall.apprisk', + type: 'keyword', + }, + 'fortinet.firewall.apscan': { + category: 'fortinet', + description: 'The name of the AP, which scanned and detected the rogue AP ', + name: 'fortinet.firewall.apscan', + type: 'keyword', + }, + 'fortinet.firewall.apsn': { + category: 'fortinet', + description: 'Access Point ', + name: 'fortinet.firewall.apsn', + type: 'keyword', + }, + 'fortinet.firewall.apstatus': { + category: 'fortinet', + description: 'Access Point status ', + name: 'fortinet.firewall.apstatus', + type: 'keyword', + }, + 'fortinet.firewall.aptype': { + category: 'fortinet', + description: 'Access Point type ', + name: 'fortinet.firewall.aptype', + type: 'keyword', + }, + 'fortinet.firewall.assigned': { + category: 'fortinet', + description: 'Assigned IP Address ', + name: 'fortinet.firewall.assigned', + type: 'ip', + }, + 'fortinet.firewall.assignip': { + category: 'fortinet', + description: 'Assigned IP Address ', + name: 'fortinet.firewall.assignip', + type: 'ip', + }, + 'fortinet.firewall.attachment': { + category: 'fortinet', + description: 'The flag for email attachement ', + name: 'fortinet.firewall.attachment', + type: 'keyword', + }, + 'fortinet.firewall.attack': { + category: 'fortinet', + description: 'Attack Name ', + name: 'fortinet.firewall.attack', + type: 'keyword', + }, + 'fortinet.firewall.attackcontext': { + category: 'fortinet', + description: 'The trigger patterns and the packetdata with base64 encoding ', + name: 'fortinet.firewall.attackcontext', + type: 'keyword', + }, + 'fortinet.firewall.attackcontextid': { + category: 'fortinet', + description: 'Attack context id / total ', + name: 'fortinet.firewall.attackcontextid', + type: 'keyword', + }, + 'fortinet.firewall.attackid': { + category: 'fortinet', + description: 'Attack ID ', + name: 'fortinet.firewall.attackid', + type: 'integer', + }, + 'fortinet.firewall.auditid': { + category: 'fortinet', + description: 'Audit ID ', + name: 'fortinet.firewall.auditid', + type: 'long', + }, + 'fortinet.firewall.auditscore': { + category: 'fortinet', + description: 'The Audit Score ', + name: 'fortinet.firewall.auditscore', + type: 'keyword', + }, + 'fortinet.firewall.audittime': { + category: 'fortinet', + description: 'The time of the audit ', + name: 'fortinet.firewall.audittime', + type: 'long', + }, + 'fortinet.firewall.authgrp': { + category: 'fortinet', + description: 'Authorization Group ', + name: 'fortinet.firewall.authgrp', + type: 'keyword', + }, + 'fortinet.firewall.authid': { + category: 'fortinet', + description: 'Authentication ID ', + name: 'fortinet.firewall.authid', + type: 'keyword', + }, + 'fortinet.firewall.authproto': { + category: 'fortinet', + description: 'The protocol that initiated the authentication ', + name: 'fortinet.firewall.authproto', + type: 'keyword', + }, + 'fortinet.firewall.authserver': { + category: 'fortinet', + description: 'Authentication server ', + name: 'fortinet.firewall.authserver', + type: 'keyword', + }, + 'fortinet.firewall.bandwidth': { + category: 'fortinet', + description: 'Bandwidth ', + name: 'fortinet.firewall.bandwidth', + type: 'keyword', + }, + 'fortinet.firewall.banned_rule': { + category: 'fortinet', + description: 'NAC quarantine Banned Rule Name ', + name: 'fortinet.firewall.banned_rule', + type: 'keyword', + }, + 'fortinet.firewall.banned_src': { + category: 'fortinet', + description: 'NAC quarantine Banned Source IP ', + name: 'fortinet.firewall.banned_src', + type: 'keyword', + }, + 'fortinet.firewall.banword': { + category: 'fortinet', + description: 'Banned word ', + name: 'fortinet.firewall.banword', + type: 'keyword', + }, + 'fortinet.firewall.botnetdomain': { + category: 'fortinet', + description: 'Botnet Domain Name ', + name: 'fortinet.firewall.botnetdomain', + type: 'keyword', + }, + 'fortinet.firewall.botnetip': { + category: 'fortinet', + description: 'Botnet IP Address ', + name: 'fortinet.firewall.botnetip', + type: 'ip', + }, + 'fortinet.firewall.bssid': { + category: 'fortinet', + description: 'Service Set ID ', + name: 'fortinet.firewall.bssid', + type: 'keyword', + }, + 'fortinet.firewall.call_id': { + category: 'fortinet', + description: 'Caller ID ', + name: 'fortinet.firewall.call_id', + type: 'keyword', + }, + 'fortinet.firewall.carrier_ep': { + category: 'fortinet', + description: 'The FortiOS Carrier end-point identification ', + name: 'fortinet.firewall.carrier_ep', + type: 'keyword', + }, + 'fortinet.firewall.cat': { + category: 'fortinet', + description: 'DNS category ID ', + name: 'fortinet.firewall.cat', + type: 'integer', + }, + 'fortinet.firewall.category': { + category: 'fortinet', + description: 'Authentication category ', + name: 'fortinet.firewall.category', + type: 'keyword', + }, + 'fortinet.firewall.cc': { + category: 'fortinet', + description: 'CC Email Address ', + name: 'fortinet.firewall.cc', + type: 'keyword', + }, + 'fortinet.firewall.cdrcontent': { + category: 'fortinet', + description: 'Cdrcontent ', + name: 'fortinet.firewall.cdrcontent', + type: 'keyword', + }, + 'fortinet.firewall.centralnatid': { + category: 'fortinet', + description: 'Central NAT ID ', + name: 'fortinet.firewall.centralnatid', + type: 'integer', + }, + 'fortinet.firewall.cert': { + category: 'fortinet', + description: 'Certificate ', + name: 'fortinet.firewall.cert', + type: 'keyword', + }, + 'fortinet.firewall.cert-type': { + category: 'fortinet', + description: 'Certificate type ', + name: 'fortinet.firewall.cert-type', + type: 'keyword', + }, + 'fortinet.firewall.certhash': { + category: 'fortinet', + description: 'Certificate hash ', + name: 'fortinet.firewall.certhash', + type: 'keyword', + }, + 'fortinet.firewall.cfgattr': { + category: 'fortinet', + description: 'Configuration attribute ', + name: 'fortinet.firewall.cfgattr', + type: 'keyword', + }, + 'fortinet.firewall.cfgobj': { + category: 'fortinet', + description: 'Configuration object ', + name: 'fortinet.firewall.cfgobj', + type: 'keyword', + }, + 'fortinet.firewall.cfgpath': { + category: 'fortinet', + description: 'Configuration path ', + name: 'fortinet.firewall.cfgpath', + type: 'keyword', + }, + 'fortinet.firewall.cfgtid': { + category: 'fortinet', + description: 'Configuration transaction ID ', + name: 'fortinet.firewall.cfgtid', + type: 'keyword', + }, + 'fortinet.firewall.cfgtxpower': { + category: 'fortinet', + description: 'Configuration TX power ', + name: 'fortinet.firewall.cfgtxpower', + type: 'integer', + }, + 'fortinet.firewall.channel': { + category: 'fortinet', + description: 'Wireless Channel ', + name: 'fortinet.firewall.channel', + type: 'integer', + }, + 'fortinet.firewall.channeltype': { + category: 'fortinet', + description: 'SSH channel type ', + name: 'fortinet.firewall.channeltype', + type: 'keyword', + }, + 'fortinet.firewall.chassisid': { + category: 'fortinet', + description: 'Chassis ID ', + name: 'fortinet.firewall.chassisid', + type: 'integer', + }, + 'fortinet.firewall.checksum': { + category: 'fortinet', + description: 'The checksum of the scanned file ', + name: 'fortinet.firewall.checksum', + type: 'keyword', + }, + 'fortinet.firewall.chgheaders': { + category: 'fortinet', + description: 'HTTP Headers ', + name: 'fortinet.firewall.chgheaders', + type: 'keyword', + }, + 'fortinet.firewall.cldobjid': { + category: 'fortinet', + description: 'Connector object ID ', + name: 'fortinet.firewall.cldobjid', + type: 'keyword', + }, + 'fortinet.firewall.client_addr': { + category: 'fortinet', + description: 'Wifi client address ', + name: 'fortinet.firewall.client_addr', + type: 'keyword', + }, + 'fortinet.firewall.cloudaction': { + category: 'fortinet', + description: 'Cloud Action ', + name: 'fortinet.firewall.cloudaction', + type: 'keyword', + }, + 'fortinet.firewall.clouduser': { + category: 'fortinet', + description: 'Cloud User ', + name: 'fortinet.firewall.clouduser', + type: 'keyword', + }, + 'fortinet.firewall.column': { + category: 'fortinet', + description: 'VOIP Column ', + name: 'fortinet.firewall.column', + type: 'integer', + }, + 'fortinet.firewall.command': { + category: 'fortinet', + description: 'CLI Command ', + name: 'fortinet.firewall.command', + type: 'keyword', + }, + 'fortinet.firewall.community': { + category: 'fortinet', + description: 'SNMP Community ', + name: 'fortinet.firewall.community', + type: 'keyword', + }, + 'fortinet.firewall.configcountry': { + category: 'fortinet', + description: 'Configuration country ', + name: 'fortinet.firewall.configcountry', + type: 'keyword', + }, + 'fortinet.firewall.connection_type': { + category: 'fortinet', + description: 'FortiClient Connection Type ', + name: 'fortinet.firewall.connection_type', + type: 'keyword', + }, + 'fortinet.firewall.conserve': { + category: 'fortinet', + description: 'Flag for conserve mode ', + name: 'fortinet.firewall.conserve', + type: 'keyword', + }, + 'fortinet.firewall.constraint': { + category: 'fortinet', + description: 'WAF http protocol restrictions ', + name: 'fortinet.firewall.constraint', + type: 'keyword', + }, + 'fortinet.firewall.contentdisarmed': { + category: 'fortinet', + description: 'Email scanned content ', + name: 'fortinet.firewall.contentdisarmed', + type: 'keyword', + }, + 'fortinet.firewall.contenttype': { + category: 'fortinet', + description: 'Content Type from HTTP header ', + name: 'fortinet.firewall.contenttype', + type: 'keyword', + }, + 'fortinet.firewall.cookies': { + category: 'fortinet', + description: 'VPN Cookie ', + name: 'fortinet.firewall.cookies', + type: 'keyword', + }, + 'fortinet.firewall.count': { + category: 'fortinet', + description: 'Counts of action type ', + name: 'fortinet.firewall.count', + type: 'integer', + }, + 'fortinet.firewall.countapp': { + category: 'fortinet', + description: 'Number of App Ctrl logs associated with the session ', + name: 'fortinet.firewall.countapp', + type: 'integer', + }, + 'fortinet.firewall.countav': { + category: 'fortinet', + description: 'Number of AV logs associated with the session ', + name: 'fortinet.firewall.countav', + type: 'integer', + }, + 'fortinet.firewall.countcifs': { + category: 'fortinet', + description: 'Number of CIFS logs associated with the session ', + name: 'fortinet.firewall.countcifs', + type: 'integer', + }, + 'fortinet.firewall.countdlp': { + category: 'fortinet', + description: 'Number of DLP logs associated with the session ', + name: 'fortinet.firewall.countdlp', + type: 'integer', + }, + 'fortinet.firewall.countdns': { + category: 'fortinet', + description: 'Number of DNS logs associated with the session ', + name: 'fortinet.firewall.countdns', + type: 'integer', + }, + 'fortinet.firewall.countemail': { + category: 'fortinet', + description: 'Number of email logs associated with the session ', + name: 'fortinet.firewall.countemail', + type: 'integer', + }, + 'fortinet.firewall.countff': { + category: 'fortinet', + description: 'Number of ff logs associated with the session ', + name: 'fortinet.firewall.countff', + type: 'integer', + }, + 'fortinet.firewall.countips': { + category: 'fortinet', + description: 'Number of IPS logs associated with the session ', + name: 'fortinet.firewall.countips', + type: 'integer', + }, + 'fortinet.firewall.countssh': { + category: 'fortinet', + description: 'Number of SSH logs associated with the session ', + name: 'fortinet.firewall.countssh', + type: 'integer', + }, + 'fortinet.firewall.countssl': { + category: 'fortinet', + description: 'Number of SSL logs associated with the session ', + name: 'fortinet.firewall.countssl', + type: 'integer', + }, + 'fortinet.firewall.countwaf': { + category: 'fortinet', + description: 'Number of WAF logs associated with the session ', + name: 'fortinet.firewall.countwaf', + type: 'integer', + }, + 'fortinet.firewall.countweb': { + category: 'fortinet', + description: 'Number of Web filter logs associated with the session ', + name: 'fortinet.firewall.countweb', + type: 'integer', + }, + 'fortinet.firewall.cpu': { + category: 'fortinet', + description: 'CPU Usage ', + name: 'fortinet.firewall.cpu', + type: 'integer', + }, + 'fortinet.firewall.craction': { + category: 'fortinet', + description: 'Client Reputation Action ', + name: 'fortinet.firewall.craction', + type: 'integer', + }, + 'fortinet.firewall.criticalcount': { + category: 'fortinet', + description: 'Number of critical ratings ', + name: 'fortinet.firewall.criticalcount', + type: 'integer', + }, + 'fortinet.firewall.crl': { + category: 'fortinet', + description: 'Client Reputation Level ', + name: 'fortinet.firewall.crl', + type: 'keyword', + }, + 'fortinet.firewall.crlevel': { + category: 'fortinet', + description: 'Client Reputation Level ', + name: 'fortinet.firewall.crlevel', + type: 'keyword', + }, + 'fortinet.firewall.crscore': { + category: 'fortinet', + description: 'Some description ', + name: 'fortinet.firewall.crscore', + type: 'integer', + }, + 'fortinet.firewall.cveid': { + category: 'fortinet', + description: 'CVE ID ', + name: 'fortinet.firewall.cveid', + type: 'keyword', + }, + 'fortinet.firewall.daemon': { + category: 'fortinet', + description: 'Daemon name ', + name: 'fortinet.firewall.daemon', + type: 'keyword', + }, + 'fortinet.firewall.datarange': { + category: 'fortinet', + description: 'Data range for reports ', + name: 'fortinet.firewall.datarange', + type: 'keyword', + }, + 'fortinet.firewall.date': { + category: 'fortinet', + description: 'Date ', + name: 'fortinet.firewall.date', + type: 'keyword', + }, + 'fortinet.firewall.ddnsserver': { + category: 'fortinet', + description: 'DDNS server ', + name: 'fortinet.firewall.ddnsserver', + type: 'ip', + }, + 'fortinet.firewall.desc': { + category: 'fortinet', + description: 'Description ', + name: 'fortinet.firewall.desc', + type: 'keyword', + }, + 'fortinet.firewall.detectionmethod': { + category: 'fortinet', + description: 'Detection method ', + name: 'fortinet.firewall.detectionmethod', + type: 'keyword', + }, + 'fortinet.firewall.devcategory': { + category: 'fortinet', + description: 'Device category ', + name: 'fortinet.firewall.devcategory', + type: 'keyword', + }, + 'fortinet.firewall.devintfname': { + category: 'fortinet', + description: 'HA device Interface Name ', + name: 'fortinet.firewall.devintfname', + type: 'keyword', + }, + 'fortinet.firewall.devtype': { + category: 'fortinet', + description: 'Device type ', + name: 'fortinet.firewall.devtype', + type: 'keyword', + }, + 'fortinet.firewall.dhcp_msg': { + category: 'fortinet', + description: 'DHCP Message ', + name: 'fortinet.firewall.dhcp_msg', + type: 'keyword', + }, + 'fortinet.firewall.dintf': { + category: 'fortinet', + description: 'Destination interface ', + name: 'fortinet.firewall.dintf', + type: 'keyword', + }, + 'fortinet.firewall.disk': { + category: 'fortinet', + description: 'Assosciated disk ', + name: 'fortinet.firewall.disk', + type: 'keyword', + }, + 'fortinet.firewall.disklograte': { + category: 'fortinet', + description: 'Disk logging rate ', + name: 'fortinet.firewall.disklograte', + type: 'long', + }, + 'fortinet.firewall.dlpextra': { + category: 'fortinet', + description: 'DLP extra information ', + name: 'fortinet.firewall.dlpextra', + type: 'keyword', + }, + 'fortinet.firewall.docsource': { + category: 'fortinet', + description: 'DLP fingerprint document source ', + name: 'fortinet.firewall.docsource', + type: 'keyword', + }, + 'fortinet.firewall.domainctrlauthstate': { + category: 'fortinet', + description: 'CIFS domain auth state ', + name: 'fortinet.firewall.domainctrlauthstate', + type: 'integer', + }, + 'fortinet.firewall.domainctrlauthtype': { + category: 'fortinet', + description: 'CIFS domain auth type ', + name: 'fortinet.firewall.domainctrlauthtype', + type: 'integer', + }, + 'fortinet.firewall.domainctrldomain': { + category: 'fortinet', + description: 'CIFS domain auth domain ', + name: 'fortinet.firewall.domainctrldomain', + type: 'keyword', + }, + 'fortinet.firewall.domainctrlip': { + category: 'fortinet', + description: 'CIFS Domain IP ', + name: 'fortinet.firewall.domainctrlip', + type: 'ip', + }, + 'fortinet.firewall.domainctrlname': { + category: 'fortinet', + description: 'CIFS Domain name ', + name: 'fortinet.firewall.domainctrlname', + type: 'keyword', + }, + 'fortinet.firewall.domainctrlprotocoltype': { + category: 'fortinet', + description: 'CIFS Domain connection protocol ', + name: 'fortinet.firewall.domainctrlprotocoltype', + type: 'integer', + }, + 'fortinet.firewall.domainctrlusername': { + category: 'fortinet', + description: 'CIFS Domain username ', + name: 'fortinet.firewall.domainctrlusername', + type: 'keyword', + }, + 'fortinet.firewall.domainfilteridx': { + category: 'fortinet', + description: 'Domain filter ID ', + name: 'fortinet.firewall.domainfilteridx', + type: 'integer', + }, + 'fortinet.firewall.domainfilterlist': { + category: 'fortinet', + description: 'Domain filter name ', + name: 'fortinet.firewall.domainfilterlist', + type: 'keyword', + }, + 'fortinet.firewall.ds': { + category: 'fortinet', + description: 'Direction with distribution system ', + name: 'fortinet.firewall.ds', + type: 'keyword', + }, + 'fortinet.firewall.dst_int': { + category: 'fortinet', + description: 'Destination interface ', + name: 'fortinet.firewall.dst_int', + type: 'keyword', + }, + 'fortinet.firewall.dstintfrole': { + category: 'fortinet', + description: 'Destination interface role ', + name: 'fortinet.firewall.dstintfrole', + type: 'keyword', + }, + 'fortinet.firewall.dstcountry': { + category: 'fortinet', + description: 'Destination country ', + name: 'fortinet.firewall.dstcountry', + type: 'keyword', + }, + 'fortinet.firewall.dstdevcategory': { + category: 'fortinet', + description: 'Destination device category ', + name: 'fortinet.firewall.dstdevcategory', + type: 'keyword', + }, + 'fortinet.firewall.dstdevtype': { + category: 'fortinet', + description: 'Destination device type ', + name: 'fortinet.firewall.dstdevtype', + type: 'keyword', + }, + 'fortinet.firewall.dstfamily': { + category: 'fortinet', + description: 'Destination OS family ', + name: 'fortinet.firewall.dstfamily', + type: 'keyword', + }, + 'fortinet.firewall.dsthwvendor': { + category: 'fortinet', + description: 'Destination HW vendor ', + name: 'fortinet.firewall.dsthwvendor', + type: 'keyword', + }, + 'fortinet.firewall.dsthwversion': { + category: 'fortinet', + description: 'Destination HW version ', + name: 'fortinet.firewall.dsthwversion', + type: 'keyword', + }, + 'fortinet.firewall.dstinetsvc': { + category: 'fortinet', + description: 'Destination interface service ', + name: 'fortinet.firewall.dstinetsvc', + type: 'keyword', + }, + 'fortinet.firewall.dstosname': { + category: 'fortinet', + description: 'Destination OS name ', + name: 'fortinet.firewall.dstosname', + type: 'keyword', + }, + 'fortinet.firewall.dstosversion': { + category: 'fortinet', + description: 'Destination OS version ', + name: 'fortinet.firewall.dstosversion', + type: 'keyword', + }, + 'fortinet.firewall.dstserver': { + category: 'fortinet', + description: 'Destination server ', + name: 'fortinet.firewall.dstserver', + type: 'integer', + }, + 'fortinet.firewall.dstssid': { + category: 'fortinet', + description: 'Destination SSID ', + name: 'fortinet.firewall.dstssid', + type: 'keyword', + }, + 'fortinet.firewall.dstswversion': { + category: 'fortinet', + description: 'Destination software version ', + name: 'fortinet.firewall.dstswversion', + type: 'keyword', + }, + 'fortinet.firewall.dstunauthusersource': { + category: 'fortinet', + description: 'Destination unauthenticated source ', + name: 'fortinet.firewall.dstunauthusersource', + type: 'keyword', + }, + 'fortinet.firewall.dstuuid': { + category: 'fortinet', + description: 'UUID of the Destination IP address ', + name: 'fortinet.firewall.dstuuid', + type: 'keyword', + }, + 'fortinet.firewall.duid': { + category: 'fortinet', + description: 'DHCP UID ', + name: 'fortinet.firewall.duid', + type: 'keyword', + }, + 'fortinet.firewall.eapolcnt': { + category: 'fortinet', + description: 'EAPOL packet count ', + name: 'fortinet.firewall.eapolcnt', + type: 'integer', + }, + 'fortinet.firewall.eapoltype': { + category: 'fortinet', + description: 'EAPOL packet type ', + name: 'fortinet.firewall.eapoltype', + type: 'keyword', + }, + 'fortinet.firewall.encrypt': { + category: 'fortinet', + description: 'Whether the packet is encrypted or not ', + name: 'fortinet.firewall.encrypt', + type: 'integer', + }, + 'fortinet.firewall.encryption': { + category: 'fortinet', + description: 'Encryption method ', + name: 'fortinet.firewall.encryption', + type: 'keyword', + }, + 'fortinet.firewall.epoch': { + category: 'fortinet', + description: 'Epoch used for locating file ', + name: 'fortinet.firewall.epoch', + type: 'integer', + }, + 'fortinet.firewall.espauth': { + category: 'fortinet', + description: 'ESP Authentication ', + name: 'fortinet.firewall.espauth', + type: 'keyword', + }, + 'fortinet.firewall.esptransform': { + category: 'fortinet', + description: 'ESP Transform ', + name: 'fortinet.firewall.esptransform', + type: 'keyword', + }, + 'fortinet.firewall.exch': { + category: 'fortinet', + description: 'Mail Exchanges from DNS response answer section ', + name: 'fortinet.firewall.exch', + type: 'keyword', + }, + 'fortinet.firewall.exchange': { + category: 'fortinet', + description: 'Mail Exchanges from DNS response answer section ', + name: 'fortinet.firewall.exchange', + type: 'keyword', + }, + 'fortinet.firewall.expectedsignature': { + category: 'fortinet', + description: 'Expected SSL signature ', + name: 'fortinet.firewall.expectedsignature', + type: 'keyword', + }, + 'fortinet.firewall.expiry': { + category: 'fortinet', + description: 'FortiGuard override expiry timestamp ', + name: 'fortinet.firewall.expiry', + type: 'keyword', + }, + 'fortinet.firewall.fams_pause': { + category: 'fortinet', + description: 'Fortinet Analysis and Management Service Pause ', + name: 'fortinet.firewall.fams_pause', + type: 'integer', + }, + 'fortinet.firewall.fazlograte': { + category: 'fortinet', + description: 'FortiAnalyzer Logging Rate ', + name: 'fortinet.firewall.fazlograte', + type: 'long', + }, + 'fortinet.firewall.fctemssn': { + category: 'fortinet', + description: 'FortiClient Endpoint SSN ', + name: 'fortinet.firewall.fctemssn', + type: 'keyword', + }, + 'fortinet.firewall.fctuid': { + category: 'fortinet', + description: 'FortiClient UID ', + name: 'fortinet.firewall.fctuid', + type: 'keyword', + }, + 'fortinet.firewall.field': { + category: 'fortinet', + description: 'NTP status field ', + name: 'fortinet.firewall.field', + type: 'keyword', + }, + 'fortinet.firewall.filefilter': { + category: 'fortinet', + description: 'The filter used to identify the affected file ', + name: 'fortinet.firewall.filefilter', + type: 'keyword', + }, + 'fortinet.firewall.filehashsrc': { + category: 'fortinet', + description: 'Filehash source ', + name: 'fortinet.firewall.filehashsrc', + type: 'keyword', + }, + 'fortinet.firewall.filtercat': { + category: 'fortinet', + description: 'DLP filter category ', + name: 'fortinet.firewall.filtercat', + type: 'keyword', + }, + 'fortinet.firewall.filteridx': { + category: 'fortinet', + description: 'DLP filter ID ', + name: 'fortinet.firewall.filteridx', + type: 'integer', + }, + 'fortinet.firewall.filtername': { + category: 'fortinet', + description: 'DLP rule name ', + name: 'fortinet.firewall.filtername', + type: 'keyword', + }, + 'fortinet.firewall.filtertype': { + category: 'fortinet', + description: 'DLP filter type ', + name: 'fortinet.firewall.filtertype', + type: 'keyword', + }, + 'fortinet.firewall.fortiguardresp': { + category: 'fortinet', + description: 'Antispam ESP value ', + name: 'fortinet.firewall.fortiguardresp', + type: 'keyword', + }, + 'fortinet.firewall.forwardedfor': { + category: 'fortinet', + description: 'Email address forwarded ', + name: 'fortinet.firewall.forwardedfor', + type: 'keyword', + }, + 'fortinet.firewall.fqdn': { + category: 'fortinet', + description: 'FQDN ', + name: 'fortinet.firewall.fqdn', + type: 'keyword', + }, + 'fortinet.firewall.frametype': { + category: 'fortinet', + description: 'Wireless frametype ', + name: 'fortinet.firewall.frametype', + type: 'keyword', + }, + 'fortinet.firewall.freediskstorage': { + category: 'fortinet', + description: 'Free disk integer ', + name: 'fortinet.firewall.freediskstorage', + type: 'integer', + }, + 'fortinet.firewall.from': { + category: 'fortinet', + description: 'From email address ', + name: 'fortinet.firewall.from', + type: 'keyword', + }, + 'fortinet.firewall.from_vcluster': { + category: 'fortinet', + description: 'Source virtual cluster number ', + name: 'fortinet.firewall.from_vcluster', + type: 'integer', + }, + 'fortinet.firewall.fsaverdict': { + category: 'fortinet', + description: 'FSA verdict ', + name: 'fortinet.firewall.fsaverdict', + type: 'keyword', + }, + 'fortinet.firewall.fwserver_name': { + category: 'fortinet', + description: 'Web proxy server name ', + name: 'fortinet.firewall.fwserver_name', + type: 'keyword', + }, + 'fortinet.firewall.gateway': { + category: 'fortinet', + description: 'Gateway ip address for PPPoE status report ', + name: 'fortinet.firewall.gateway', + type: 'ip', + }, + 'fortinet.firewall.green': { + category: 'fortinet', + description: 'Memory status ', + name: 'fortinet.firewall.green', + type: 'keyword', + }, + 'fortinet.firewall.groupid': { + category: 'fortinet', + description: 'User Group ID ', + name: 'fortinet.firewall.groupid', + type: 'integer', + }, + 'fortinet.firewall.ha-prio': { + category: 'fortinet', + description: 'HA Priority ', + name: 'fortinet.firewall.ha-prio', + type: 'integer', + }, + 'fortinet.firewall.ha_group': { + category: 'fortinet', + description: 'HA Group ', + name: 'fortinet.firewall.ha_group', + type: 'keyword', + }, + 'fortinet.firewall.ha_role': { + category: 'fortinet', + description: 'HA Role ', + name: 'fortinet.firewall.ha_role', + type: 'keyword', + }, + 'fortinet.firewall.handshake': { + category: 'fortinet', + description: 'SSL Handshake ', + name: 'fortinet.firewall.handshake', + type: 'keyword', + }, + 'fortinet.firewall.hash': { + category: 'fortinet', + description: 'Hash value of downloaded file ', + name: 'fortinet.firewall.hash', + type: 'keyword', + }, + 'fortinet.firewall.hbdn_reason': { + category: 'fortinet', + description: 'Heartbeat down reason ', + name: 'fortinet.firewall.hbdn_reason', + type: 'keyword', + }, + 'fortinet.firewall.highcount': { + category: 'fortinet', + description: 'Highcount fabric summary ', + name: 'fortinet.firewall.highcount', + type: 'integer', + }, + 'fortinet.firewall.host': { + category: 'fortinet', + description: 'Hostname ', + name: 'fortinet.firewall.host', + type: 'keyword', + }, + 'fortinet.firewall.iaid': { + category: 'fortinet', + description: 'DHCPv6 id ', + name: 'fortinet.firewall.iaid', + type: 'keyword', + }, + 'fortinet.firewall.icmpcode': { + category: 'fortinet', + description: 'Destination Port of the ICMP message ', + name: 'fortinet.firewall.icmpcode', + type: 'keyword', + }, + 'fortinet.firewall.icmpid': { + category: 'fortinet', + description: 'Source port of the ICMP message ', + name: 'fortinet.firewall.icmpid', + type: 'keyword', + }, + 'fortinet.firewall.icmptype': { + category: 'fortinet', + description: 'The type of ICMP message ', + name: 'fortinet.firewall.icmptype', + type: 'keyword', + }, + 'fortinet.firewall.identifier': { + category: 'fortinet', + description: 'Network traffic identifier ', + name: 'fortinet.firewall.identifier', + type: 'integer', + }, + 'fortinet.firewall.in_spi': { + category: 'fortinet', + description: 'IPSEC inbound SPI ', + name: 'fortinet.firewall.in_spi', + type: 'keyword', + }, + 'fortinet.firewall.incidentserialno': { + category: 'fortinet', + description: 'Incident serial number ', + name: 'fortinet.firewall.incidentserialno', + type: 'integer', + }, + 'fortinet.firewall.infected': { + category: 'fortinet', + description: 'Infected MMS ', + name: 'fortinet.firewall.infected', + type: 'integer', + }, + 'fortinet.firewall.infectedfilelevel': { + category: 'fortinet', + description: 'DLP infected file level ', + name: 'fortinet.firewall.infectedfilelevel', + type: 'integer', + }, + 'fortinet.firewall.informationsource': { + category: 'fortinet', + description: 'Information source ', + name: 'fortinet.firewall.informationsource', + type: 'keyword', + }, + 'fortinet.firewall.init': { + category: 'fortinet', + description: 'IPSEC init stage ', + name: 'fortinet.firewall.init', + type: 'keyword', + }, + 'fortinet.firewall.initiator': { + category: 'fortinet', + description: 'Original login user name for Fortiguard override ', + name: 'fortinet.firewall.initiator', + type: 'keyword', + }, + 'fortinet.firewall.interface': { + category: 'fortinet', + description: 'Related interface ', + name: 'fortinet.firewall.interface', + type: 'keyword', + }, + 'fortinet.firewall.intf': { + category: 'fortinet', + description: 'Related interface ', + name: 'fortinet.firewall.intf', + type: 'keyword', + }, + 'fortinet.firewall.invalidmac': { + category: 'fortinet', + description: 'The MAC address with invalid OUI ', + name: 'fortinet.firewall.invalidmac', + type: 'keyword', + }, + 'fortinet.firewall.ip': { + category: 'fortinet', + description: 'Related IP ', + name: 'fortinet.firewall.ip', + type: 'ip', + }, + 'fortinet.firewall.iptype': { + category: 'fortinet', + description: 'Related IP type ', + name: 'fortinet.firewall.iptype', + type: 'keyword', + }, + 'fortinet.firewall.keyword': { + category: 'fortinet', + description: 'Keyword used for search ', + name: 'fortinet.firewall.keyword', + type: 'keyword', + }, + 'fortinet.firewall.kind': { + category: 'fortinet', + description: 'VOIP kind ', + name: 'fortinet.firewall.kind', + type: 'keyword', + }, + 'fortinet.firewall.lanin': { + category: 'fortinet', + description: 'LAN incoming traffic in bytes ', + name: 'fortinet.firewall.lanin', + type: 'long', + }, + 'fortinet.firewall.lanout': { + category: 'fortinet', + description: 'LAN outbound traffic in bytes ', + name: 'fortinet.firewall.lanout', + type: 'long', + }, + 'fortinet.firewall.lease': { + category: 'fortinet', + description: 'DHCP lease ', + name: 'fortinet.firewall.lease', + type: 'integer', + }, + 'fortinet.firewall.license_limit': { + category: 'fortinet', + description: 'Maximum Number of FortiClients for the License ', + name: 'fortinet.firewall.license_limit', + type: 'keyword', + }, + 'fortinet.firewall.limit': { + category: 'fortinet', + description: 'Virtual Domain Resource Limit ', + name: 'fortinet.firewall.limit', + type: 'integer', + }, + 'fortinet.firewall.line': { + category: 'fortinet', + description: 'VOIP line ', + name: 'fortinet.firewall.line', + type: 'keyword', + }, + 'fortinet.firewall.live': { + category: 'fortinet', + description: 'Time in seconds ', + name: 'fortinet.firewall.live', + type: 'integer', + }, + 'fortinet.firewall.local': { + category: 'fortinet', + description: 'Local IP for a PPPD Connection ', + name: 'fortinet.firewall.local', + type: 'ip', + }, + 'fortinet.firewall.log': { + category: 'fortinet', + description: 'Log message ', + name: 'fortinet.firewall.log', + type: 'keyword', + }, + 'fortinet.firewall.login': { + category: 'fortinet', + description: 'SSH login ', + name: 'fortinet.firewall.login', + type: 'keyword', + }, + 'fortinet.firewall.lowcount': { + category: 'fortinet', + description: 'Fabric lowcount ', + name: 'fortinet.firewall.lowcount', + type: 'integer', + }, + 'fortinet.firewall.mac': { + category: 'fortinet', + description: 'DHCP mac address ', + name: 'fortinet.firewall.mac', + type: 'keyword', + }, + 'fortinet.firewall.malform_data': { + category: 'fortinet', + description: 'VOIP malformed data ', + name: 'fortinet.firewall.malform_data', + type: 'integer', + }, + 'fortinet.firewall.malform_desc': { + category: 'fortinet', + description: 'VOIP malformed data description ', + name: 'fortinet.firewall.malform_desc', + type: 'keyword', + }, + 'fortinet.firewall.manuf': { + category: 'fortinet', + description: 'Manufacturer name ', + name: 'fortinet.firewall.manuf', + type: 'keyword', + }, + 'fortinet.firewall.masterdstmac': { + category: 'fortinet', + description: 'Master mac address for a host with multiple network interfaces ', + name: 'fortinet.firewall.masterdstmac', + type: 'keyword', + }, + 'fortinet.firewall.mastersrcmac': { + category: 'fortinet', + description: 'The master MAC address for a host that has multiple network interfaces ', + name: 'fortinet.firewall.mastersrcmac', + type: 'keyword', + }, + 'fortinet.firewall.mediumcount': { + category: 'fortinet', + description: 'Fabric medium count ', + name: 'fortinet.firewall.mediumcount', + type: 'integer', + }, + 'fortinet.firewall.mem': { + category: 'fortinet', + description: 'Memory usage system statistics ', + name: 'fortinet.firewall.mem', + type: 'keyword', + }, + 'fortinet.firewall.meshmode': { + category: 'fortinet', + description: 'Wireless mesh mode ', + name: 'fortinet.firewall.meshmode', + type: 'keyword', + }, + 'fortinet.firewall.message_type': { + category: 'fortinet', + description: 'VOIP message type ', + name: 'fortinet.firewall.message_type', + type: 'keyword', + }, + 'fortinet.firewall.method': { + category: 'fortinet', + description: 'HTTP method ', + name: 'fortinet.firewall.method', + type: 'keyword', + }, + 'fortinet.firewall.mgmtcnt': { + category: 'fortinet', + description: 'The number of unauthorized client flooding managemet frames ', + name: 'fortinet.firewall.mgmtcnt', + type: 'integer', + }, + 'fortinet.firewall.mode': { + category: 'fortinet', + description: 'IPSEC mode ', + name: 'fortinet.firewall.mode', + type: 'keyword', + }, + 'fortinet.firewall.module': { + category: 'fortinet', + description: 'PCI-DSS module ', + name: 'fortinet.firewall.module', + type: 'keyword', + }, + 'fortinet.firewall.monitor-name': { + category: 'fortinet', + description: 'Health Monitor Name ', + name: 'fortinet.firewall.monitor-name', + type: 'keyword', + }, + 'fortinet.firewall.monitor-type': { + category: 'fortinet', + description: 'Health Monitor Type ', + name: 'fortinet.firewall.monitor-type', + type: 'keyword', + }, + 'fortinet.firewall.mpsk': { + category: 'fortinet', + description: 'Wireless MPSK ', + name: 'fortinet.firewall.mpsk', + type: 'keyword', + }, + 'fortinet.firewall.msgproto': { + category: 'fortinet', + description: 'Message Protocol Number ', + name: 'fortinet.firewall.msgproto', + type: 'keyword', + }, + 'fortinet.firewall.mtu': { + category: 'fortinet', + description: 'Max Transmission Unit Value ', + name: 'fortinet.firewall.mtu', + type: 'integer', + }, + 'fortinet.firewall.name': { + category: 'fortinet', + description: 'Name ', + name: 'fortinet.firewall.name', + type: 'keyword', + }, + 'fortinet.firewall.nat': { + category: 'fortinet', + description: 'NAT IP Address ', + name: 'fortinet.firewall.nat', + type: 'keyword', + }, + 'fortinet.firewall.netid': { + category: 'fortinet', + description: 'Connector NetID ', + name: 'fortinet.firewall.netid', + type: 'keyword', + }, + 'fortinet.firewall.new_status': { + category: 'fortinet', + description: 'New status on user change ', + name: 'fortinet.firewall.new_status', + type: 'keyword', + }, + 'fortinet.firewall.new_value': { + category: 'fortinet', + description: 'New Virtual Domain Name ', + name: 'fortinet.firewall.new_value', + type: 'keyword', + }, + 'fortinet.firewall.newchannel': { + category: 'fortinet', + description: 'New Channel Number ', + name: 'fortinet.firewall.newchannel', + type: 'integer', + }, + 'fortinet.firewall.newchassisid': { + category: 'fortinet', + description: 'New Chassis ID ', + name: 'fortinet.firewall.newchassisid', + type: 'integer', + }, + 'fortinet.firewall.newslot': { + category: 'fortinet', + description: 'New Slot Number ', + name: 'fortinet.firewall.newslot', + type: 'integer', + }, + 'fortinet.firewall.nextstat': { + category: 'fortinet', + description: 'Time interval in seconds for the next statistics. ', + name: 'fortinet.firewall.nextstat', + type: 'integer', + }, + 'fortinet.firewall.nf_type': { + category: 'fortinet', + description: 'Notification Type ', + name: 'fortinet.firewall.nf_type', + type: 'keyword', + }, + 'fortinet.firewall.noise': { + category: 'fortinet', + description: 'Wifi Noise ', + name: 'fortinet.firewall.noise', + type: 'integer', + }, + 'fortinet.firewall.old_status': { + category: 'fortinet', + description: 'Original Status ', + name: 'fortinet.firewall.old_status', + type: 'keyword', + }, + 'fortinet.firewall.old_value': { + category: 'fortinet', + description: 'Original Virtual Domain name ', + name: 'fortinet.firewall.old_value', + type: 'keyword', + }, + 'fortinet.firewall.oldchannel': { + category: 'fortinet', + description: 'Original channel ', + name: 'fortinet.firewall.oldchannel', + type: 'integer', + }, + 'fortinet.firewall.oldchassisid': { + category: 'fortinet', + description: 'Original Chassis Number ', + name: 'fortinet.firewall.oldchassisid', + type: 'integer', + }, + 'fortinet.firewall.oldslot': { + category: 'fortinet', + description: 'Original Slot Number ', + name: 'fortinet.firewall.oldslot', + type: 'integer', + }, + 'fortinet.firewall.oldsn': { + category: 'fortinet', + description: 'Old Serial number ', + name: 'fortinet.firewall.oldsn', + type: 'keyword', + }, + 'fortinet.firewall.oldwprof': { + category: 'fortinet', + description: 'Old Web Filter Profile ', + name: 'fortinet.firewall.oldwprof', + type: 'keyword', + }, + 'fortinet.firewall.onwire': { + category: 'fortinet', + description: 'A flag to indicate if the AP is onwire or not ', + name: 'fortinet.firewall.onwire', + type: 'keyword', + }, + 'fortinet.firewall.opercountry': { + category: 'fortinet', + description: 'Operating Country ', + name: 'fortinet.firewall.opercountry', + type: 'keyword', + }, + 'fortinet.firewall.opertxpower': { + category: 'fortinet', + description: 'Operating TX power ', + name: 'fortinet.firewall.opertxpower', + type: 'integer', + }, + 'fortinet.firewall.osname': { + category: 'fortinet', + description: 'Operating System name ', + name: 'fortinet.firewall.osname', + type: 'keyword', + }, + 'fortinet.firewall.osversion': { + category: 'fortinet', + description: 'Operating System version ', + name: 'fortinet.firewall.osversion', + type: 'keyword', + }, + 'fortinet.firewall.out_spi': { + category: 'fortinet', + description: 'Out SPI ', + name: 'fortinet.firewall.out_spi', + type: 'keyword', + }, + 'fortinet.firewall.outintf': { + category: 'fortinet', + description: 'Out interface ', + name: 'fortinet.firewall.outintf', + type: 'keyword', + }, + 'fortinet.firewall.passedcount': { + category: 'fortinet', + description: 'Fabric passed count ', + name: 'fortinet.firewall.passedcount', + type: 'integer', + }, + 'fortinet.firewall.passwd': { + category: 'fortinet', + description: 'Changed user password information ', + name: 'fortinet.firewall.passwd', + type: 'keyword', + }, + 'fortinet.firewall.path': { + category: 'fortinet', + description: 'Path of looped configuration for security fabric ', + name: 'fortinet.firewall.path', + type: 'keyword', + }, + 'fortinet.firewall.peer': { + category: 'fortinet', + description: 'WAN optimization peer ', + name: 'fortinet.firewall.peer', + type: 'keyword', + }, + 'fortinet.firewall.peer_notif': { + category: 'fortinet', + description: 'VPN peer notification ', + name: 'fortinet.firewall.peer_notif', + type: 'keyword', + }, + 'fortinet.firewall.phase2_name': { + category: 'fortinet', + description: 'VPN phase2 name ', + name: 'fortinet.firewall.phase2_name', + type: 'keyword', + }, + 'fortinet.firewall.phone': { + category: 'fortinet', + description: 'VOIP Phone ', + name: 'fortinet.firewall.phone', + type: 'keyword', + }, + 'fortinet.firewall.pid': { + category: 'fortinet', + description: 'Process ID ', + name: 'fortinet.firewall.pid', + type: 'integer', + }, + 'fortinet.firewall.policytype': { + category: 'fortinet', + description: 'Policy Type ', + name: 'fortinet.firewall.policytype', + type: 'keyword', + }, + 'fortinet.firewall.poolname': { + category: 'fortinet', + description: 'IP Pool name ', + name: 'fortinet.firewall.poolname', + type: 'keyword', + }, + 'fortinet.firewall.port': { + category: 'fortinet', + description: 'Log upload error port ', + name: 'fortinet.firewall.port', + type: 'integer', + }, + 'fortinet.firewall.portbegin': { + category: 'fortinet', + description: 'IP Pool port number to begin ', + name: 'fortinet.firewall.portbegin', + type: 'integer', + }, + 'fortinet.firewall.portend': { + category: 'fortinet', + description: 'IP Pool port number to end ', + name: 'fortinet.firewall.portend', + type: 'integer', + }, + 'fortinet.firewall.probeproto': { + category: 'fortinet', + description: 'Link Monitor Probe Protocol ', + name: 'fortinet.firewall.probeproto', + type: 'keyword', + }, + 'fortinet.firewall.process': { + category: 'fortinet', + description: 'URL Filter process ', + name: 'fortinet.firewall.process', + type: 'keyword', + }, + 'fortinet.firewall.processtime': { + category: 'fortinet', + description: 'Process time for reports ', + name: 'fortinet.firewall.processtime', + type: 'integer', + }, + 'fortinet.firewall.profile': { + category: 'fortinet', + description: 'Profile Name ', + name: 'fortinet.firewall.profile', + type: 'keyword', + }, + 'fortinet.firewall.profile_vd': { + category: 'fortinet', + description: 'Virtual Domain Name ', + name: 'fortinet.firewall.profile_vd', + type: 'keyword', + }, + 'fortinet.firewall.profilegroup': { + category: 'fortinet', + description: 'Profile Group Name ', + name: 'fortinet.firewall.profilegroup', + type: 'keyword', + }, + 'fortinet.firewall.profiletype': { + category: 'fortinet', + description: 'Profile Type ', + name: 'fortinet.firewall.profiletype', + type: 'keyword', + }, + 'fortinet.firewall.qtypeval': { + category: 'fortinet', + description: 'DNS question type value ', + name: 'fortinet.firewall.qtypeval', + type: 'integer', + }, + 'fortinet.firewall.quarskip': { + category: 'fortinet', + description: 'Quarantine skip explanation ', + name: 'fortinet.firewall.quarskip', + type: 'keyword', + }, + 'fortinet.firewall.quotaexceeded': { + category: 'fortinet', + description: 'If quota has been exceeded ', + name: 'fortinet.firewall.quotaexceeded', + type: 'keyword', + }, + 'fortinet.firewall.quotamax': { + category: 'fortinet', + description: 'Maximum quota allowed - in seconds if time-based - in bytes if traffic-based ', + name: 'fortinet.firewall.quotamax', + type: 'long', + }, + 'fortinet.firewall.quotatype': { + category: 'fortinet', + description: 'Quota type ', + name: 'fortinet.firewall.quotatype', + type: 'keyword', + }, + 'fortinet.firewall.quotaused': { + category: 'fortinet', + description: 'Quota used - in seconds if time-based - in bytes if trafficbased) ', + name: 'fortinet.firewall.quotaused', + type: 'long', + }, + 'fortinet.firewall.radioband': { + category: 'fortinet', + description: 'Radio band ', + name: 'fortinet.firewall.radioband', + type: 'keyword', + }, + 'fortinet.firewall.radioid': { + category: 'fortinet', + description: 'Radio ID ', + name: 'fortinet.firewall.radioid', + type: 'integer', + }, + 'fortinet.firewall.radioidclosest': { + category: 'fortinet', + description: 'Radio ID on the AP closest the rogue AP ', + name: 'fortinet.firewall.radioidclosest', + type: 'integer', + }, + 'fortinet.firewall.radioiddetected': { + category: 'fortinet', + description: 'Radio ID on the AP which detected the rogue AP ', + name: 'fortinet.firewall.radioiddetected', + type: 'integer', + }, + 'fortinet.firewall.rate': { + category: 'fortinet', + description: 'Wireless rogue rate value ', + name: 'fortinet.firewall.rate', + type: 'keyword', + }, + 'fortinet.firewall.rawdata': { + category: 'fortinet', + description: 'Raw data value ', + name: 'fortinet.firewall.rawdata', + type: 'keyword', + }, + 'fortinet.firewall.rawdataid': { + category: 'fortinet', + description: 'Raw data ID ', + name: 'fortinet.firewall.rawdataid', + type: 'keyword', + }, + 'fortinet.firewall.rcvddelta': { + category: 'fortinet', + description: 'Received bytes delta ', + name: 'fortinet.firewall.rcvddelta', + type: 'keyword', + }, + 'fortinet.firewall.reason': { + category: 'fortinet', + description: 'Alert reason ', + name: 'fortinet.firewall.reason', + type: 'keyword', + }, + 'fortinet.firewall.received': { + category: 'fortinet', + description: 'Server key exchange received ', + name: 'fortinet.firewall.received', + type: 'integer', + }, + 'fortinet.firewall.receivedsignature': { + category: 'fortinet', + description: 'Server key exchange received signature ', + name: 'fortinet.firewall.receivedsignature', + type: 'keyword', + }, + 'fortinet.firewall.red': { + category: 'fortinet', + description: 'Memory information in red ', + name: 'fortinet.firewall.red', + type: 'keyword', + }, + 'fortinet.firewall.referralurl': { + category: 'fortinet', + description: 'Web filter referralurl ', + name: 'fortinet.firewall.referralurl', + type: 'keyword', + }, + 'fortinet.firewall.remote': { + category: 'fortinet', + description: 'Remote PPP IP address ', + name: 'fortinet.firewall.remote', + type: 'ip', + }, + 'fortinet.firewall.remotewtptime': { + category: 'fortinet', + description: 'Remote Wifi Radius authentication time ', + name: 'fortinet.firewall.remotewtptime', + type: 'keyword', + }, + 'fortinet.firewall.reporttype': { + category: 'fortinet', + description: 'Report type ', + name: 'fortinet.firewall.reporttype', + type: 'keyword', + }, + 'fortinet.firewall.reqtype': { + category: 'fortinet', + description: 'Request type ', + name: 'fortinet.firewall.reqtype', + type: 'keyword', + }, + 'fortinet.firewall.request_name': { + category: 'fortinet', + description: 'VOIP request name ', + name: 'fortinet.firewall.request_name', + type: 'keyword', + }, + 'fortinet.firewall.result': { + category: 'fortinet', + description: 'VPN phase result ', + name: 'fortinet.firewall.result', + type: 'keyword', + }, + 'fortinet.firewall.role': { + category: 'fortinet', + description: 'VPN Phase 2 role ', + name: 'fortinet.firewall.role', + type: 'keyword', + }, + 'fortinet.firewall.rssi': { + category: 'fortinet', + description: 'Received signal strength indicator ', + name: 'fortinet.firewall.rssi', + type: 'integer', + }, + 'fortinet.firewall.rsso_key': { + category: 'fortinet', + description: 'RADIUS SSO attribute value ', + name: 'fortinet.firewall.rsso_key', + type: 'keyword', + }, + 'fortinet.firewall.ruledata': { + category: 'fortinet', + description: 'Rule data ', + name: 'fortinet.firewall.ruledata', + type: 'keyword', + }, + 'fortinet.firewall.ruletype': { + category: 'fortinet', + description: 'Rule type ', + name: 'fortinet.firewall.ruletype', + type: 'keyword', + }, + 'fortinet.firewall.scanned': { + category: 'fortinet', + description: 'Number of Scanned MMSs ', + name: 'fortinet.firewall.scanned', + type: 'integer', + }, + 'fortinet.firewall.scantime': { + category: 'fortinet', + description: 'Scanned time ', + name: 'fortinet.firewall.scantime', + type: 'long', + }, + 'fortinet.firewall.scope': { + category: 'fortinet', + description: 'FortiGuard Override Scope ', + name: 'fortinet.firewall.scope', + type: 'keyword', + }, + 'fortinet.firewall.security': { + category: 'fortinet', + description: 'Wireless rogue security ', + name: 'fortinet.firewall.security', + type: 'keyword', + }, + 'fortinet.firewall.sensitivity': { + category: 'fortinet', + description: 'Sensitivity for document fingerprint ', + name: 'fortinet.firewall.sensitivity', + type: 'keyword', + }, + 'fortinet.firewall.sensor': { + category: 'fortinet', + description: 'NAC Sensor Name ', + name: 'fortinet.firewall.sensor', + type: 'keyword', + }, + 'fortinet.firewall.sentdelta': { + category: 'fortinet', + description: 'Sent bytes delta ', + name: 'fortinet.firewall.sentdelta', + type: 'keyword', + }, + 'fortinet.firewall.seq': { + category: 'fortinet', + description: 'Sequence number ', + name: 'fortinet.firewall.seq', + type: 'keyword', + }, + 'fortinet.firewall.serial': { + category: 'fortinet', + description: 'WAN optimisation serial ', + name: 'fortinet.firewall.serial', + type: 'keyword', + }, + 'fortinet.firewall.serialno': { + category: 'fortinet', + description: 'Serial number ', + name: 'fortinet.firewall.serialno', + type: 'keyword', + }, + 'fortinet.firewall.server': { + category: 'fortinet', + description: 'AD server FQDN or IP ', + name: 'fortinet.firewall.server', + type: 'keyword', + }, + 'fortinet.firewall.session_id': { + category: 'fortinet', + description: 'Session ID ', + name: 'fortinet.firewall.session_id', + type: 'keyword', + }, + 'fortinet.firewall.sessionid': { + category: 'fortinet', + description: 'WAD Session ID ', + name: 'fortinet.firewall.sessionid', + type: 'integer', + }, + 'fortinet.firewall.setuprate': { + category: 'fortinet', + description: 'Session Setup Rate ', + name: 'fortinet.firewall.setuprate', + type: 'long', + }, + 'fortinet.firewall.severity': { + category: 'fortinet', + description: 'Severity ', + name: 'fortinet.firewall.severity', + type: 'keyword', + }, + 'fortinet.firewall.shaperdroprcvdbyte': { + category: 'fortinet', + description: 'Received bytes dropped by shaper ', + name: 'fortinet.firewall.shaperdroprcvdbyte', + type: 'integer', + }, + 'fortinet.firewall.shaperdropsentbyte': { + category: 'fortinet', + description: 'Sent bytes dropped by shaper ', + name: 'fortinet.firewall.shaperdropsentbyte', + type: 'integer', + }, + 'fortinet.firewall.shaperperipdropbyte': { + category: 'fortinet', + description: 'Dropped bytes per IP by shaper ', + name: 'fortinet.firewall.shaperperipdropbyte', + type: 'integer', + }, + 'fortinet.firewall.shaperperipname': { + category: 'fortinet', + description: 'Traffic shaper name (per IP) ', + name: 'fortinet.firewall.shaperperipname', + type: 'keyword', + }, + 'fortinet.firewall.shaperrcvdname': { + category: 'fortinet', + description: 'Traffic shaper name for received traffic ', + name: 'fortinet.firewall.shaperrcvdname', + type: 'keyword', + }, + 'fortinet.firewall.shapersentname': { + category: 'fortinet', + description: 'Traffic shaper name for sent traffic ', + name: 'fortinet.firewall.shapersentname', + type: 'keyword', + }, + 'fortinet.firewall.shapingpolicyid': { + category: 'fortinet', + description: 'Traffic shaper policy ID ', + name: 'fortinet.firewall.shapingpolicyid', + type: 'integer', + }, + 'fortinet.firewall.signal': { + category: 'fortinet', + description: 'Wireless rogue API signal ', + name: 'fortinet.firewall.signal', + type: 'integer', + }, + 'fortinet.firewall.size': { + category: 'fortinet', + description: 'Email size in bytes ', + name: 'fortinet.firewall.size', + type: 'long', + }, + 'fortinet.firewall.slot': { + category: 'fortinet', + description: 'Slot number ', + name: 'fortinet.firewall.slot', + type: 'integer', + }, + 'fortinet.firewall.sn': { + category: 'fortinet', + description: 'Security fabric serial number ', + name: 'fortinet.firewall.sn', + type: 'keyword', + }, + 'fortinet.firewall.snclosest': { + category: 'fortinet', + description: 'SN of the AP closest to the rogue AP ', + name: 'fortinet.firewall.snclosest', + type: 'keyword', + }, + 'fortinet.firewall.sndetected': { + category: 'fortinet', + description: 'SN of the AP which detected the rogue AP ', + name: 'fortinet.firewall.sndetected', + type: 'keyword', + }, + 'fortinet.firewall.snmeshparent': { + category: 'fortinet', + description: 'SN of the mesh parent ', + name: 'fortinet.firewall.snmeshparent', + type: 'keyword', + }, + 'fortinet.firewall.spi': { + category: 'fortinet', + description: 'IPSEC SPI ', + name: 'fortinet.firewall.spi', + type: 'keyword', + }, + 'fortinet.firewall.src_int': { + category: 'fortinet', + description: 'Source interface ', + name: 'fortinet.firewall.src_int', + type: 'keyword', + }, + 'fortinet.firewall.srcintfrole': { + category: 'fortinet', + description: 'Source interface role ', + name: 'fortinet.firewall.srcintfrole', + type: 'keyword', + }, + 'fortinet.firewall.srccountry': { + category: 'fortinet', + description: 'Source country ', + name: 'fortinet.firewall.srccountry', + type: 'keyword', + }, + 'fortinet.firewall.srcfamily': { + category: 'fortinet', + description: 'Source family ', + name: 'fortinet.firewall.srcfamily', + type: 'keyword', + }, + 'fortinet.firewall.srchwvendor': { + category: 'fortinet', + description: 'Source hardware vendor ', + name: 'fortinet.firewall.srchwvendor', + type: 'keyword', + }, + 'fortinet.firewall.srchwversion': { + category: 'fortinet', + description: 'Source hardware version ', + name: 'fortinet.firewall.srchwversion', + type: 'keyword', + }, + 'fortinet.firewall.srcinetsvc': { + category: 'fortinet', + description: 'Source interface service ', + name: 'fortinet.firewall.srcinetsvc', + type: 'keyword', + }, + 'fortinet.firewall.srcname': { + category: 'fortinet', + description: 'Source name ', + name: 'fortinet.firewall.srcname', + type: 'keyword', + }, + 'fortinet.firewall.srcserver': { + category: 'fortinet', + description: 'Source server ', + name: 'fortinet.firewall.srcserver', + type: 'integer', + }, + 'fortinet.firewall.srcssid': { + category: 'fortinet', + description: 'Source SSID ', + name: 'fortinet.firewall.srcssid', + type: 'keyword', + }, + 'fortinet.firewall.srcswversion': { + category: 'fortinet', + description: 'Source software version ', + name: 'fortinet.firewall.srcswversion', + type: 'keyword', + }, + 'fortinet.firewall.srcuuid': { + category: 'fortinet', + description: 'Source UUID ', + name: 'fortinet.firewall.srcuuid', + type: 'keyword', + }, + 'fortinet.firewall.sscname': { + category: 'fortinet', + description: 'SSC name ', + name: 'fortinet.firewall.sscname', + type: 'keyword', + }, + 'fortinet.firewall.ssid': { + category: 'fortinet', + description: 'Base Service Set ID ', + name: 'fortinet.firewall.ssid', + type: 'keyword', + }, + 'fortinet.firewall.sslaction': { + category: 'fortinet', + description: 'SSL Action ', + name: 'fortinet.firewall.sslaction', + type: 'keyword', + }, + 'fortinet.firewall.ssllocal': { + category: 'fortinet', + description: 'WAD SSL local ', + name: 'fortinet.firewall.ssllocal', + type: 'keyword', + }, + 'fortinet.firewall.sslremote': { + category: 'fortinet', + description: 'WAD SSL remote ', + name: 'fortinet.firewall.sslremote', + type: 'keyword', + }, + 'fortinet.firewall.stacount': { + category: 'fortinet', + description: 'Number of stations/clients ', + name: 'fortinet.firewall.stacount', + type: 'integer', + }, + 'fortinet.firewall.stage': { + category: 'fortinet', + description: 'IPSEC stage ', + name: 'fortinet.firewall.stage', + type: 'keyword', + }, + 'fortinet.firewall.stamac': { + category: 'fortinet', + description: '802.1x station mac ', + name: 'fortinet.firewall.stamac', + type: 'keyword', + }, + 'fortinet.firewall.state': { + category: 'fortinet', + description: 'Admin login state ', + name: 'fortinet.firewall.state', + type: 'keyword', + }, + 'fortinet.firewall.status': { + category: 'fortinet', + description: 'Status ', + name: 'fortinet.firewall.status', + type: 'keyword', + }, + 'fortinet.firewall.stitch': { + category: 'fortinet', + description: 'Automation stitch triggered ', + name: 'fortinet.firewall.stitch', + type: 'keyword', + }, + 'fortinet.firewall.subject': { + category: 'fortinet', + description: 'Email subject ', + name: 'fortinet.firewall.subject', + type: 'keyword', + }, + 'fortinet.firewall.submodule': { + category: 'fortinet', + description: 'Configuration Sub-Module Name ', + name: 'fortinet.firewall.submodule', + type: 'keyword', + }, + 'fortinet.firewall.subservice': { + category: 'fortinet', + description: 'AV subservice ', + name: 'fortinet.firewall.subservice', + type: 'keyword', + }, + 'fortinet.firewall.subtype': { + category: 'fortinet', + description: 'Log subtype ', + name: 'fortinet.firewall.subtype', + type: 'keyword', + }, + 'fortinet.firewall.suspicious': { + category: 'fortinet', + description: 'Number of Suspicious MMSs ', + name: 'fortinet.firewall.suspicious', + type: 'integer', + }, + 'fortinet.firewall.switchproto': { + category: 'fortinet', + description: 'Protocol change information ', + name: 'fortinet.firewall.switchproto', + type: 'keyword', + }, + 'fortinet.firewall.sync_status': { + category: 'fortinet', + description: 'The sync status with the master ', + name: 'fortinet.firewall.sync_status', + type: 'keyword', + }, + 'fortinet.firewall.sync_type': { + category: 'fortinet', + description: 'The sync type with the master ', + name: 'fortinet.firewall.sync_type', + type: 'keyword', + }, + 'fortinet.firewall.sysuptime': { + category: 'fortinet', + description: 'System uptime ', + name: 'fortinet.firewall.sysuptime', + type: 'keyword', + }, + 'fortinet.firewall.tamac': { + category: 'fortinet', + description: 'the MAC address of Transmitter, if none, then Receiver ', + name: 'fortinet.firewall.tamac', + type: 'keyword', + }, + 'fortinet.firewall.threattype': { + category: 'fortinet', + description: 'WIDS threat type ', + name: 'fortinet.firewall.threattype', + type: 'keyword', + }, + 'fortinet.firewall.time': { + category: 'fortinet', + description: 'Time of the event ', + name: 'fortinet.firewall.time', + type: 'keyword', + }, + 'fortinet.firewall.to': { + category: 'fortinet', + description: 'Email to field ', + name: 'fortinet.firewall.to', + type: 'keyword', + }, + 'fortinet.firewall.to_vcluster': { + category: 'fortinet', + description: 'destination virtual cluster number ', + name: 'fortinet.firewall.to_vcluster', + type: 'integer', + }, + 'fortinet.firewall.total': { + category: 'fortinet', + description: 'Total memory ', + name: 'fortinet.firewall.total', + type: 'integer', + }, + 'fortinet.firewall.totalsession': { + category: 'fortinet', + description: 'Total Number of Sessions ', + name: 'fortinet.firewall.totalsession', + type: 'integer', + }, + 'fortinet.firewall.trace_id': { + category: 'fortinet', + description: 'Session clash trace ID ', + name: 'fortinet.firewall.trace_id', + type: 'keyword', + }, + 'fortinet.firewall.trandisp': { + category: 'fortinet', + description: 'NAT translation type ', + name: 'fortinet.firewall.trandisp', + type: 'keyword', + }, + 'fortinet.firewall.transid': { + category: 'fortinet', + description: 'HTTP transaction ID ', + name: 'fortinet.firewall.transid', + type: 'integer', + }, + 'fortinet.firewall.translationid': { + category: 'fortinet', + description: 'DNS filter transaltion ID ', + name: 'fortinet.firewall.translationid', + type: 'keyword', + }, + 'fortinet.firewall.trigger': { + category: 'fortinet', + description: 'Automation stitch trigger ', + name: 'fortinet.firewall.trigger', + type: 'keyword', + }, + 'fortinet.firewall.trueclntip': { + category: 'fortinet', + description: 'File filter true client IP ', + name: 'fortinet.firewall.trueclntip', + type: 'ip', + }, + 'fortinet.firewall.tunnelid': { + category: 'fortinet', + description: 'IPSEC tunnel ID ', + name: 'fortinet.firewall.tunnelid', + type: 'integer', + }, + 'fortinet.firewall.tunnelip': { + category: 'fortinet', + description: 'IPSEC tunnel IP ', + name: 'fortinet.firewall.tunnelip', + type: 'ip', + }, + 'fortinet.firewall.tunneltype': { + category: 'fortinet', + description: 'IPSEC tunnel type ', + name: 'fortinet.firewall.tunneltype', + type: 'keyword', + }, + 'fortinet.firewall.type': { + category: 'fortinet', + description: 'Module type ', + name: 'fortinet.firewall.type', + type: 'keyword', + }, + 'fortinet.firewall.ui': { + category: 'fortinet', + description: 'Admin authentication UI type ', + name: 'fortinet.firewall.ui', + type: 'keyword', + }, + 'fortinet.firewall.unauthusersource': { + category: 'fortinet', + description: 'Unauthenticated user source ', + name: 'fortinet.firewall.unauthusersource', + type: 'keyword', + }, + 'fortinet.firewall.unit': { + category: 'fortinet', + description: 'Power supply unit ', + name: 'fortinet.firewall.unit', + type: 'integer', + }, + 'fortinet.firewall.urlfilteridx': { + category: 'fortinet', + description: 'URL filter ID ', + name: 'fortinet.firewall.urlfilteridx', + type: 'integer', + }, + 'fortinet.firewall.urlfilterlist': { + category: 'fortinet', + description: 'URL filter list ', + name: 'fortinet.firewall.urlfilterlist', + type: 'keyword', + }, + 'fortinet.firewall.urlsource': { + category: 'fortinet', + description: 'URL filter source ', + name: 'fortinet.firewall.urlsource', + type: 'keyword', + }, + 'fortinet.firewall.urltype': { + category: 'fortinet', + description: 'URL filter type ', + name: 'fortinet.firewall.urltype', + type: 'keyword', + }, + 'fortinet.firewall.used': { + category: 'fortinet', + description: 'Number of Used IPs ', + name: 'fortinet.firewall.used', + type: 'integer', + }, + 'fortinet.firewall.used_for_type': { + category: 'fortinet', + description: 'Connection for the type ', + name: 'fortinet.firewall.used_for_type', + type: 'integer', + }, + 'fortinet.firewall.utmaction': { + category: 'fortinet', + description: 'Security action performed by UTM ', + name: 'fortinet.firewall.utmaction', + type: 'keyword', + }, + 'fortinet.firewall.vap': { + category: 'fortinet', + description: 'Virtual AP ', + name: 'fortinet.firewall.vap', + type: 'keyword', + }, + 'fortinet.firewall.vapmode': { + category: 'fortinet', + description: 'Virtual AP mode ', + name: 'fortinet.firewall.vapmode', + type: 'keyword', + }, + 'fortinet.firewall.vcluster': { + category: 'fortinet', + description: 'virtual cluster id ', + name: 'fortinet.firewall.vcluster', + type: 'integer', + }, + 'fortinet.firewall.vcluster_member': { + category: 'fortinet', + description: 'Virtual cluster member ', + name: 'fortinet.firewall.vcluster_member', + type: 'integer', + }, + 'fortinet.firewall.vcluster_state': { + category: 'fortinet', + description: 'Virtual cluster state ', + name: 'fortinet.firewall.vcluster_state', + type: 'keyword', + }, + 'fortinet.firewall.vd': { + category: 'fortinet', + description: 'Virtual Domain Name ', + name: 'fortinet.firewall.vd', + type: 'keyword', + }, + 'fortinet.firewall.vdname': { + category: 'fortinet', + description: 'Virtual Domain Name ', + name: 'fortinet.firewall.vdname', + type: 'keyword', + }, + 'fortinet.firewall.vendorurl': { + category: 'fortinet', + description: 'Vulnerability scan vendor name ', + name: 'fortinet.firewall.vendorurl', + type: 'keyword', + }, + 'fortinet.firewall.version': { + category: 'fortinet', + description: 'Version ', + name: 'fortinet.firewall.version', + type: 'keyword', + }, + 'fortinet.firewall.vip': { + category: 'fortinet', + description: 'Virtual IP ', + name: 'fortinet.firewall.vip', + type: 'keyword', + }, + 'fortinet.firewall.virus': { + category: 'fortinet', + description: 'Virus name ', + name: 'fortinet.firewall.virus', + type: 'keyword', + }, + 'fortinet.firewall.virusid': { + category: 'fortinet', + description: 'Virus ID (unique virus identifier) ', + name: 'fortinet.firewall.virusid', + type: 'integer', + }, + 'fortinet.firewall.voip_proto': { + category: 'fortinet', + description: 'VOIP protocol ', + name: 'fortinet.firewall.voip_proto', + type: 'keyword', + }, + 'fortinet.firewall.vpn': { + category: 'fortinet', + description: 'VPN description ', + name: 'fortinet.firewall.vpn', + type: 'keyword', + }, + 'fortinet.firewall.vpntunnel': { + category: 'fortinet', + description: 'IPsec Vpn Tunnel Name ', + name: 'fortinet.firewall.vpntunnel', + type: 'keyword', + }, + 'fortinet.firewall.vpntype': { + category: 'fortinet', + description: 'The type of the VPN tunnel ', + name: 'fortinet.firewall.vpntype', + type: 'keyword', + }, + 'fortinet.firewall.vrf': { + category: 'fortinet', + description: 'VRF number ', + name: 'fortinet.firewall.vrf', + type: 'integer', + }, + 'fortinet.firewall.vulncat': { + category: 'fortinet', + description: 'Vulnerability Category ', + name: 'fortinet.firewall.vulncat', + type: 'keyword', + }, + 'fortinet.firewall.vulnid': { + category: 'fortinet', + description: 'Vulnerability ID ', + name: 'fortinet.firewall.vulnid', + type: 'integer', + }, + 'fortinet.firewall.vulnname': { + category: 'fortinet', + description: 'Vulnerability name ', + name: 'fortinet.firewall.vulnname', + type: 'keyword', + }, + 'fortinet.firewall.vwlid': { + category: 'fortinet', + description: 'VWL ID ', + name: 'fortinet.firewall.vwlid', + type: 'integer', + }, + 'fortinet.firewall.vwlquality': { + category: 'fortinet', + description: 'VWL quality ', + name: 'fortinet.firewall.vwlquality', + type: 'keyword', + }, + 'fortinet.firewall.vwlservice': { + category: 'fortinet', + description: 'VWL service ', + name: 'fortinet.firewall.vwlservice', + type: 'keyword', + }, + 'fortinet.firewall.vwpvlanid': { + category: 'fortinet', + description: 'VWP VLAN ID ', + name: 'fortinet.firewall.vwpvlanid', + type: 'integer', + }, + 'fortinet.firewall.wanin': { + category: 'fortinet', + description: 'WAN incoming traffic in bytes ', + name: 'fortinet.firewall.wanin', + type: 'long', + }, + 'fortinet.firewall.wanoptapptype': { + category: 'fortinet', + description: 'WAN Optimization Application type ', + name: 'fortinet.firewall.wanoptapptype', + type: 'keyword', + }, + 'fortinet.firewall.wanout': { + category: 'fortinet', + description: 'WAN outgoing traffic in bytes ', + name: 'fortinet.firewall.wanout', + type: 'long', + }, + 'fortinet.firewall.weakwepiv': { + category: 'fortinet', + description: 'Weak Wep Initiation Vector ', + name: 'fortinet.firewall.weakwepiv', + type: 'keyword', + }, + 'fortinet.firewall.xauthgroup': { + category: 'fortinet', + description: 'XAuth Group Name ', + name: 'fortinet.firewall.xauthgroup', + type: 'keyword', + }, + 'fortinet.firewall.xauthuser': { + category: 'fortinet', + description: 'XAuth User Name ', + name: 'fortinet.firewall.xauthuser', + type: 'keyword', + }, + 'fortinet.firewall.xid': { + category: 'fortinet', + description: 'Wireless X ID ', + name: 'fortinet.firewall.xid', + type: 'integer', + }, + 'googlecloud.destination.instance.project_id': { + category: 'googlecloud', + description: 'ID of the project containing the VM. ', + name: 'googlecloud.destination.instance.project_id', + type: 'keyword', + }, + 'googlecloud.destination.instance.region': { + category: 'googlecloud', + description: 'Region of the VM. ', + name: 'googlecloud.destination.instance.region', + type: 'keyword', + }, + 'googlecloud.destination.instance.zone': { + category: 'googlecloud', + description: 'Zone of the VM. ', + name: 'googlecloud.destination.instance.zone', + type: 'keyword', + }, + 'googlecloud.destination.vpc.project_id': { + category: 'googlecloud', + description: 'ID of the project containing the VM. ', + name: 'googlecloud.destination.vpc.project_id', + type: 'keyword', + }, + 'googlecloud.destination.vpc.vpc_name': { + category: 'googlecloud', + description: 'VPC on which the VM is operating. ', + name: 'googlecloud.destination.vpc.vpc_name', + type: 'keyword', + }, + 'googlecloud.destination.vpc.subnetwork_name': { + category: 'googlecloud', + description: 'Subnetwork on which the VM is operating. ', + name: 'googlecloud.destination.vpc.subnetwork_name', + type: 'keyword', + }, + 'googlecloud.source.instance.project_id': { + category: 'googlecloud', + description: 'ID of the project containing the VM. ', + name: 'googlecloud.source.instance.project_id', + type: 'keyword', + }, + 'googlecloud.source.instance.region': { + category: 'googlecloud', + description: 'Region of the VM. ', + name: 'googlecloud.source.instance.region', + type: 'keyword', + }, + 'googlecloud.source.instance.zone': { + category: 'googlecloud', + description: 'Zone of the VM. ', + name: 'googlecloud.source.instance.zone', + type: 'keyword', + }, + 'googlecloud.source.vpc.project_id': { + category: 'googlecloud', + description: 'ID of the project containing the VM. ', + name: 'googlecloud.source.vpc.project_id', + type: 'keyword', + }, + 'googlecloud.source.vpc.vpc_name': { + category: 'googlecloud', + description: 'VPC on which the VM is operating. ', + name: 'googlecloud.source.vpc.vpc_name', + type: 'keyword', + }, + 'googlecloud.source.vpc.subnetwork_name': { + category: 'googlecloud', + description: 'Subnetwork on which the VM is operating. ', + name: 'googlecloud.source.vpc.subnetwork_name', + type: 'keyword', + }, + 'googlecloud.audit.type': { + category: 'googlecloud', + description: 'Type property. ', + name: 'googlecloud.audit.type', + type: 'keyword', + }, + 'googlecloud.audit.authentication_info.principal_email': { + category: 'googlecloud', + description: 'The email address of the authenticated user making the request. ', + name: 'googlecloud.audit.authentication_info.principal_email', + type: 'keyword', + }, + 'googlecloud.audit.authentication_info.authority_selector': { + category: 'googlecloud', + description: + 'The authority selector specified by the requestor, if any. It is not guaranteed that the principal was allowed to use this authority. ', + name: 'googlecloud.audit.authentication_info.authority_selector', + type: 'keyword', + }, + 'googlecloud.audit.authorization_info.permission': { + category: 'googlecloud', + description: 'The required IAM permission. ', + name: 'googlecloud.audit.authorization_info.permission', + type: 'keyword', + }, + 'googlecloud.audit.authorization_info.granted': { + category: 'googlecloud', + description: 'Whether or not authorization for resource and permission was granted. ', + name: 'googlecloud.audit.authorization_info.granted', + type: 'boolean', + }, + 'googlecloud.audit.authorization_info.resource_attributes.service': { + category: 'googlecloud', + description: 'The name of the service. ', + name: 'googlecloud.audit.authorization_info.resource_attributes.service', + type: 'keyword', + }, + 'googlecloud.audit.authorization_info.resource_attributes.name': { + category: 'googlecloud', + description: 'The name of the resource. ', + name: 'googlecloud.audit.authorization_info.resource_attributes.name', + type: 'keyword', + }, + 'googlecloud.audit.authorization_info.resource_attributes.type': { + category: 'googlecloud', + description: 'The type of the resource. ', + name: 'googlecloud.audit.authorization_info.resource_attributes.type', + type: 'keyword', + }, + 'googlecloud.audit.method_name': { + category: 'googlecloud', + description: + "The name of the service method or operation. For API calls, this should be the name of the API method. For example, 'google.datastore.v1.Datastore.RunQuery'. ", + name: 'googlecloud.audit.method_name', + type: 'keyword', + }, + 'googlecloud.audit.num_response_items': { + category: 'googlecloud', + description: 'The number of items returned from a List or Query API method, if applicable. ', + name: 'googlecloud.audit.num_response_items', + type: 'long', + }, + 'googlecloud.audit.request.proto_name': { + category: 'googlecloud', + description: 'Type property of the request. ', + name: 'googlecloud.audit.request.proto_name', + type: 'keyword', + }, + 'googlecloud.audit.request.filter': { + category: 'googlecloud', + description: 'Filter of the request. ', + name: 'googlecloud.audit.request.filter', + type: 'keyword', + }, + 'googlecloud.audit.request.name': { + category: 'googlecloud', + description: 'Name of the request. ', + name: 'googlecloud.audit.request.name', + type: 'keyword', + }, + 'googlecloud.audit.request.resource_name': { + category: 'googlecloud', + description: 'Name of the request resource. ', + name: 'googlecloud.audit.request.resource_name', + type: 'keyword', + }, + 'googlecloud.audit.request_metadata.caller_ip': { + category: 'googlecloud', + description: 'The IP address of the caller. ', + name: 'googlecloud.audit.request_metadata.caller_ip', + type: 'ip', + }, + 'googlecloud.audit.request_metadata.caller_supplied_user_agent': { + category: 'googlecloud', + description: + 'The user agent of the caller. This information is not authenticated and should be treated accordingly. ', + name: 'googlecloud.audit.request_metadata.caller_supplied_user_agent', + type: 'keyword', + }, + 'googlecloud.audit.response.proto_name': { + category: 'googlecloud', + description: 'Type property of the response. ', + name: 'googlecloud.audit.response.proto_name', + type: 'keyword', + }, + 'googlecloud.audit.response.details.group': { + category: 'googlecloud', + description: 'The name of the group. ', + name: 'googlecloud.audit.response.details.group', + type: 'keyword', + }, + 'googlecloud.audit.response.details.kind': { + category: 'googlecloud', + description: 'The kind of the response details. ', + name: 'googlecloud.audit.response.details.kind', + type: 'keyword', + }, + 'googlecloud.audit.response.details.name': { + category: 'googlecloud', + description: 'The name of the response details. ', + name: 'googlecloud.audit.response.details.name', + type: 'keyword', + }, + 'googlecloud.audit.response.details.uid': { + category: 'googlecloud', + description: 'The uid of the response details. ', + name: 'googlecloud.audit.response.details.uid', + type: 'keyword', + }, + 'googlecloud.audit.response.status': { + category: 'googlecloud', + description: 'Status of the response. ', + name: 'googlecloud.audit.response.status', + type: 'keyword', + }, + 'googlecloud.audit.resource_name': { + category: 'googlecloud', + description: + "The resource or collection that is the target of the operation. The name is a scheme-less URI, not including the API service name. For example, 'shelves/SHELF_ID/books'. ", + name: 'googlecloud.audit.resource_name', + type: 'keyword', + }, + 'googlecloud.audit.resource_location.current_locations': { + category: 'googlecloud', + description: 'Current locations of the resource. ', + name: 'googlecloud.audit.resource_location.current_locations', + type: 'keyword', + }, + 'googlecloud.audit.service_name': { + category: 'googlecloud', + description: + 'The name of the API service performing the operation. For example, datastore.googleapis.com. ', + name: 'googlecloud.audit.service_name', + type: 'keyword', + }, + 'googlecloud.audit.status.code': { + category: 'googlecloud', + description: 'The status code, which should be an enum value of google.rpc.Code. ', + name: 'googlecloud.audit.status.code', + type: 'integer', + }, + 'googlecloud.audit.status.message': { + category: 'googlecloud', + description: + 'A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the google.rpc.Status.details field, or localized by the client. ', + name: 'googlecloud.audit.status.message', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.priority': { + category: 'googlecloud', + description: 'The priority for the firewall rule.', + name: 'googlecloud.firewall.rule_details.priority', + type: 'long', + }, + 'googlecloud.firewall.rule_details.action': { + category: 'googlecloud', + description: 'Action that the rule performs on match.', + name: 'googlecloud.firewall.rule_details.action', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.direction': { + category: 'googlecloud', + description: 'Direction of traffic that matches this rule.', + name: 'googlecloud.firewall.rule_details.direction', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.reference': { + category: 'googlecloud', + description: 'Reference to the firewall rule.', + name: 'googlecloud.firewall.rule_details.reference', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.source_range': { + category: 'googlecloud', + description: 'List of source ranges that the firewall rule applies to.', + name: 'googlecloud.firewall.rule_details.source_range', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.destination_range': { + category: 'googlecloud', + description: 'List of destination ranges that the firewall applies to.', + name: 'googlecloud.firewall.rule_details.destination_range', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.source_tag': { + category: 'googlecloud', + description: 'List of all the source tags that the firewall rule applies to. ', + name: 'googlecloud.firewall.rule_details.source_tag', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.target_tag': { + category: 'googlecloud', + description: 'List of all the target tags that the firewall rule applies to. ', + name: 'googlecloud.firewall.rule_details.target_tag', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.ip_port_info': { + category: 'googlecloud', + description: 'List of ip protocols and applicable port ranges for rules. ', + name: 'googlecloud.firewall.rule_details.ip_port_info', + type: 'array', + }, + 'googlecloud.firewall.rule_details.source_service_account': { + category: 'googlecloud', + description: 'List of all the source service accounts that the firewall rule applies to. ', + name: 'googlecloud.firewall.rule_details.source_service_account', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.target_service_account': { + category: 'googlecloud', + description: 'List of all the target service accounts that the firewall rule applies to. ', + name: 'googlecloud.firewall.rule_details.target_service_account', + type: 'keyword', + }, + 'googlecloud.vpcflow.reporter': { + category: 'googlecloud', + description: "The side which reported the flow. Can be either 'SRC' or 'DEST'. ", + name: 'googlecloud.vpcflow.reporter', + type: 'keyword', + }, + 'googlecloud.vpcflow.rtt.ms': { + category: 'googlecloud', + description: + 'Latency as measured (for TCP flows only) during the time interval. This is the time elapsed between sending a SEQ and receiving a corresponding ACK and it contains the network RTT as well as the application related delay. ', + name: 'googlecloud.vpcflow.rtt.ms', + type: 'long', + }, + 'gsuite.actor.type': { + category: 'gsuite', + description: + 'The type of actor. Values can be: *USER*: Another user in the same domain. *EXTERNAL_USER*: A user outside the domain. *KEY*: A non-human actor. ', + name: 'gsuite.actor.type', + type: 'keyword', + }, + 'gsuite.actor.key': { + category: 'gsuite', + description: + 'Only present when `actor.type` is `KEY`. Can be the `consumer_key` of the requestor for OAuth 2LO API requests or an identifier for robot accounts. ', + name: 'gsuite.actor.key', + type: 'keyword', + }, + 'gsuite.event.type': { + category: 'gsuite', + description: + 'The type of GSuite event, mapped from `items[].events[].type` in the original payload. Each fileset can have a different set of values for it, more details can be found at https://developers.google.com/admin-sdk/reports/v1/reference/activities/list ', + example: 'audit#activity', + name: 'gsuite.event.type', + type: 'keyword', + }, + 'gsuite.kind': { + category: 'gsuite', + description: + 'The type of API resource, mapped from `kind` in the original payload. More details can be found at https://developers.google.com/admin-sdk/reports/v1/reference/activities/list ', + example: 'audit#activity', + name: 'gsuite.kind', + type: 'keyword', + }, + 'gsuite.organization.domain': { + category: 'gsuite', + description: "The domain that is affected by the report's event. ", + name: 'gsuite.organization.domain', + type: 'keyword', + }, + 'gsuite.admin.application.edition': { + category: 'gsuite', + description: 'The GSuite edition.', + name: 'gsuite.admin.application.edition', + type: 'keyword', + }, + 'gsuite.admin.application.name': { + category: 'gsuite', + description: "The application's name.", + name: 'gsuite.admin.application.name', + type: 'keyword', + }, + 'gsuite.admin.application.enabled': { + category: 'gsuite', + description: 'The enabled application.', + name: 'gsuite.admin.application.enabled', + type: 'keyword', + }, + 'gsuite.admin.application.licences_order_number': { + category: 'gsuite', + description: 'Order number used to redeem licenses.', + name: 'gsuite.admin.application.licences_order_number', + type: 'keyword', + }, + 'gsuite.admin.application.licences_purchased': { + category: 'gsuite', + description: 'Number of licences purchased.', + name: 'gsuite.admin.application.licences_purchased', + type: 'keyword', + }, + 'gsuite.admin.application.id': { + category: 'gsuite', + description: 'The application ID.', + name: 'gsuite.admin.application.id', + type: 'keyword', + }, + 'gsuite.admin.application.asp_id': { + category: 'gsuite', + description: 'The application specific password ID.', + name: 'gsuite.admin.application.asp_id', + type: 'keyword', + }, + 'gsuite.admin.application.package_id': { + category: 'gsuite', + description: 'The mobile application package ID.', + name: 'gsuite.admin.application.package_id', + type: 'keyword', + }, + 'gsuite.admin.group.email': { + category: 'gsuite', + description: "The group's primary email address.", + name: 'gsuite.admin.group.email', + type: 'keyword', + }, + 'gsuite.admin.new_value': { + category: 'gsuite', + description: 'The new value for the setting.', + name: 'gsuite.admin.new_value', + type: 'keyword', + }, + 'gsuite.admin.old_value': { + category: 'gsuite', + description: 'The old value for the setting.', + name: 'gsuite.admin.old_value', + type: 'keyword', + }, + 'gsuite.admin.org_unit.name': { + category: 'gsuite', + description: 'The organizational unit name.', + name: 'gsuite.admin.org_unit.name', + type: 'keyword', + }, + 'gsuite.admin.org_unit.full': { + category: 'gsuite', + description: 'The org unit full path including the root org unit name.', + name: 'gsuite.admin.org_unit.full', + type: 'keyword', + }, + 'gsuite.admin.setting.name': { + category: 'gsuite', + description: 'The setting name.', + name: 'gsuite.admin.setting.name', + type: 'keyword', + }, + 'gsuite.admin.user_defined_setting.name': { + category: 'gsuite', + description: 'The name of the user-defined setting.', + name: 'gsuite.admin.user_defined_setting.name', + type: 'keyword', + }, + 'gsuite.admin.setting.description': { + category: 'gsuite', + description: 'The setting name.', + name: 'gsuite.admin.setting.description', + type: 'keyword', + }, + 'gsuite.admin.group.priorities': { + category: 'gsuite', + description: 'Group priorities.', + name: 'gsuite.admin.group.priorities', + type: 'keyword', + }, + 'gsuite.admin.domain.alias': { + category: 'gsuite', + description: 'The domain alias.', + name: 'gsuite.admin.domain.alias', + type: 'keyword', + }, + 'gsuite.admin.domain.name': { + category: 'gsuite', + description: 'The primary domain name.', + name: 'gsuite.admin.domain.name', + type: 'keyword', + }, + 'gsuite.admin.domain.secondary_name': { + category: 'gsuite', + description: 'The secondary domain name.', + name: 'gsuite.admin.domain.secondary_name', + type: 'keyword', + }, + 'gsuite.admin.managed_configuration': { + category: 'gsuite', + description: 'The name of the managed configuration.', + name: 'gsuite.admin.managed_configuration', + type: 'keyword', + }, + 'gsuite.admin.non_featured_services_selection': { + category: 'gsuite', + description: + 'Non-featured services selection. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-application-settings#FLASHLIGHT_EDU_NON_FEATURED_SERVICES_SELECTED ', + name: 'gsuite.admin.non_featured_services_selection', + type: 'keyword', + }, + 'gsuite.admin.field': { + category: 'gsuite', + description: 'The name of the field.', + name: 'gsuite.admin.field', + type: 'keyword', + }, + 'gsuite.admin.resource.id': { + category: 'gsuite', + description: 'The name of the resource identifier.', + name: 'gsuite.admin.resource.id', + type: 'keyword', + }, + 'gsuite.admin.user.email': { + category: 'gsuite', + description: "The user's primary email address.", + name: 'gsuite.admin.user.email', + type: 'keyword', + }, + 'gsuite.admin.user.nickname': { + category: 'gsuite', + description: "The user's nickname.", + name: 'gsuite.admin.user.nickname', + type: 'keyword', + }, + 'gsuite.admin.user.birthdate': { + category: 'gsuite', + description: "The user's birth date.", + name: 'gsuite.admin.user.birthdate', + type: 'date', + }, + 'gsuite.admin.gateway.name': { + category: 'gsuite', + description: 'Gateway name. Present on some chat settings.', + name: 'gsuite.admin.gateway.name', + type: 'keyword', + }, + 'gsuite.admin.chrome_os.session_type': { + category: 'gsuite', + description: 'Chrome OS session type.', + name: 'gsuite.admin.chrome_os.session_type', + type: 'keyword', + }, + 'gsuite.admin.device.serial_number': { + category: 'gsuite', + description: 'Device serial number.', + name: 'gsuite.admin.device.serial_number', + type: 'keyword', + }, + 'gsuite.admin.device.id': { + category: 'gsuite', + name: 'gsuite.admin.device.id', + type: 'keyword', + }, + 'gsuite.admin.device.type': { + category: 'gsuite', + description: 'Device type.', + name: 'gsuite.admin.device.type', + type: 'keyword', + }, + 'gsuite.admin.print_server.name': { + category: 'gsuite', + description: 'The name of the print server.', + name: 'gsuite.admin.print_server.name', + type: 'keyword', + }, + 'gsuite.admin.printer.name': { + category: 'gsuite', + description: 'The name of the printer.', + name: 'gsuite.admin.printer.name', + type: 'keyword', + }, + 'gsuite.admin.device.command_details': { + category: 'gsuite', + description: 'Command details.', + name: 'gsuite.admin.device.command_details', + type: 'keyword', + }, + 'gsuite.admin.role.id': { + category: 'gsuite', + description: 'Unique identifier for this role privilege.', + name: 'gsuite.admin.role.id', + type: 'keyword', + }, + 'gsuite.admin.role.name': { + category: 'gsuite', + description: + 'The role name. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-delegated-admin-settings ', + name: 'gsuite.admin.role.name', + type: 'keyword', + }, + 'gsuite.admin.privilege.name': { + category: 'gsuite', + description: 'Privilege name.', + name: 'gsuite.admin.privilege.name', + type: 'keyword', + }, + 'gsuite.admin.service.name': { + category: 'gsuite', + description: 'The service name.', + name: 'gsuite.admin.service.name', + type: 'keyword', + }, + 'gsuite.admin.url.name': { + category: 'gsuite', + description: 'The website name.', + name: 'gsuite.admin.url.name', + type: 'keyword', + }, + 'gsuite.admin.product.name': { + category: 'gsuite', + description: 'The product name.', + name: 'gsuite.admin.product.name', + type: 'keyword', + }, + 'gsuite.admin.product.sku': { + category: 'gsuite', + description: 'The product SKU.', + name: 'gsuite.admin.product.sku', + type: 'keyword', + }, + 'gsuite.admin.bulk_upload.failed': { + category: 'gsuite', + description: 'Number of failed records in bulk upload operation.', + name: 'gsuite.admin.bulk_upload.failed', + type: 'long', + }, + 'gsuite.admin.bulk_upload.total': { + category: 'gsuite', + description: 'Number of total records in bulk upload operation.', + name: 'gsuite.admin.bulk_upload.total', + type: 'long', + }, + 'gsuite.admin.group.allowed_list': { + category: 'gsuite', + description: 'Names of allow-listed groups.', + name: 'gsuite.admin.group.allowed_list', + type: 'keyword', + }, + 'gsuite.admin.email.quarantine_name': { + category: 'gsuite', + description: 'The name of the quarantine.', + name: 'gsuite.admin.email.quarantine_name', + type: 'keyword', + }, + 'gsuite.admin.email.log_search_filter.message_id': { + category: 'gsuite', + description: "The log search filter's email message ID.", + name: 'gsuite.admin.email.log_search_filter.message_id', + type: 'keyword', + }, + 'gsuite.admin.email.log_search_filter.start_date': { + category: 'gsuite', + description: "The log search filter's start date.", + name: 'gsuite.admin.email.log_search_filter.start_date', + type: 'date', + }, + 'gsuite.admin.email.log_search_filter.end_date': { + category: 'gsuite', + description: "The log search filter's ending date.", + name: 'gsuite.admin.email.log_search_filter.end_date', + type: 'date', + }, + 'gsuite.admin.email.log_search_filter.recipient.value': { + category: 'gsuite', + description: "The log search filter's email recipient.", + name: 'gsuite.admin.email.log_search_filter.recipient.value', + type: 'keyword', + }, + 'gsuite.admin.email.log_search_filter.sender.value': { + category: 'gsuite', + description: "The log search filter's email sender.", + name: 'gsuite.admin.email.log_search_filter.sender.value', + type: 'keyword', + }, + 'gsuite.admin.email.log_search_filter.recipient.ip': { + category: 'gsuite', + description: "The log search filter's email recipient's IP address.", + name: 'gsuite.admin.email.log_search_filter.recipient.ip', + type: 'ip', + }, + 'gsuite.admin.email.log_search_filter.sender.ip': { + category: 'gsuite', + description: "The log search filter's email sender's IP address.", + name: 'gsuite.admin.email.log_search_filter.sender.ip', + type: 'ip', + }, + 'gsuite.admin.chrome_licenses.enabled': { + category: 'gsuite', + description: + 'Licences enabled. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-org-settings ', + name: 'gsuite.admin.chrome_licenses.enabled', + type: 'keyword', + }, + 'gsuite.admin.chrome_licenses.allowed': { + category: 'gsuite', + description: + 'Licences enabled. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-org-settings ', + name: 'gsuite.admin.chrome_licenses.allowed', + type: 'keyword', + }, + 'gsuite.admin.oauth2.service.name': { + category: 'gsuite', + description: + 'OAuth2 service name. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-security-settings ', + name: 'gsuite.admin.oauth2.service.name', + type: 'keyword', + }, + 'gsuite.admin.oauth2.application.id': { + category: 'gsuite', + description: 'OAuth2 application ID.', + name: 'gsuite.admin.oauth2.application.id', + type: 'keyword', + }, + 'gsuite.admin.oauth2.application.name': { + category: 'gsuite', + description: 'OAuth2 application name.', + name: 'gsuite.admin.oauth2.application.name', + type: 'keyword', + }, + 'gsuite.admin.oauth2.application.type': { + category: 'gsuite', + description: + 'OAuth2 application type. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-security-settings ', + name: 'gsuite.admin.oauth2.application.type', + type: 'keyword', + }, + 'gsuite.admin.verification_method': { + category: 'gsuite', + description: + 'Related verification method. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-security-settings and https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-domain-settings ', + name: 'gsuite.admin.verification_method', + type: 'keyword', + }, + 'gsuite.admin.alert.name': { + category: 'gsuite', + description: 'The alert name.', + name: 'gsuite.admin.alert.name', + type: 'keyword', + }, + 'gsuite.admin.rule.name': { + category: 'gsuite', + description: 'The rule name.', + name: 'gsuite.admin.rule.name', + type: 'keyword', + }, + 'gsuite.admin.api.client.name': { + category: 'gsuite', + description: 'The API client name.', + name: 'gsuite.admin.api.client.name', + type: 'keyword', + }, + 'gsuite.admin.api.scopes': { + category: 'gsuite', + description: 'The API scopes.', + name: 'gsuite.admin.api.scopes', + type: 'keyword', + }, + 'gsuite.admin.mdm.token': { + category: 'gsuite', + description: 'The MDM vendor enrollment token.', + name: 'gsuite.admin.mdm.token', + type: 'keyword', + }, + 'gsuite.admin.mdm.vendor': { + category: 'gsuite', + description: "The MDM vendor's name.", + name: 'gsuite.admin.mdm.vendor', + type: 'keyword', + }, + 'gsuite.admin.info_type': { + category: 'gsuite', + description: + 'This will be used to state what kind of information was changed. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-domain-settings ', + name: 'gsuite.admin.info_type', + type: 'keyword', + }, + 'gsuite.admin.email_monitor.dest_email': { + category: 'gsuite', + description: 'The destination address of the email monitor.', + name: 'gsuite.admin.email_monitor.dest_email', + type: 'keyword', + }, + 'gsuite.admin.email_monitor.level.chat': { + category: 'gsuite', + description: 'The chat email monitor level.', + name: 'gsuite.admin.email_monitor.level.chat', + type: 'keyword', + }, + 'gsuite.admin.email_monitor.level.draft': { + category: 'gsuite', + description: 'The draft email monitor level.', + name: 'gsuite.admin.email_monitor.level.draft', + type: 'keyword', + }, + 'gsuite.admin.email_monitor.level.incoming': { + category: 'gsuite', + description: 'The incoming email monitor level.', + name: 'gsuite.admin.email_monitor.level.incoming', + type: 'keyword', + }, + 'gsuite.admin.email_monitor.level.outgoing': { + category: 'gsuite', + description: 'The outgoing email monitor level.', + name: 'gsuite.admin.email_monitor.level.outgoing', + type: 'keyword', + }, + 'gsuite.admin.email_dump.include_deleted': { + category: 'gsuite', + description: 'Indicates if deleted emails are included in the export.', + name: 'gsuite.admin.email_dump.include_deleted', + type: 'boolean', + }, + 'gsuite.admin.email_dump.package_content': { + category: 'gsuite', + description: 'The contents of the mailbox package.', + name: 'gsuite.admin.email_dump.package_content', + type: 'keyword', + }, + 'gsuite.admin.email_dump.query': { + category: 'gsuite', + description: 'The search query used for the dump.', + name: 'gsuite.admin.email_dump.query', + type: 'keyword', + }, + 'gsuite.admin.request.id': { + category: 'gsuite', + description: 'The request ID.', + name: 'gsuite.admin.request.id', + type: 'keyword', + }, + 'gsuite.admin.mobile.action.id': { + category: 'gsuite', + description: "The mobile device action's ID.", + name: 'gsuite.admin.mobile.action.id', + type: 'keyword', + }, + 'gsuite.admin.mobile.action.type': { + category: 'gsuite', + description: + "The mobile device action's type. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-mobile-settings ", + name: 'gsuite.admin.mobile.action.type', + type: 'keyword', + }, + 'gsuite.admin.mobile.certificate.name': { + category: 'gsuite', + description: 'The mobile certificate common name.', + name: 'gsuite.admin.mobile.certificate.name', + type: 'keyword', + }, + 'gsuite.admin.mobile.company_owned_devices': { + category: 'gsuite', + description: 'The number of devices a company owns.', + name: 'gsuite.admin.mobile.company_owned_devices', + type: 'long', + }, + 'gsuite.admin.distribution.entity.name': { + category: 'gsuite', + description: + 'The distribution entity value, which can be a group name or an org-unit name. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-mobile-settings ', + name: 'gsuite.admin.distribution.entity.name', + type: 'keyword', + }, + 'gsuite.admin.distribution.entity.type': { + category: 'gsuite', + description: + 'The distribution entity type, which can be a group or an org-unit. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-mobile-settings ', + name: 'gsuite.admin.distribution.entity.type', + type: 'keyword', + }, + 'gsuite.drive.billable': { + category: 'gsuite', + description: 'Whether this activity is billable.', + name: 'gsuite.drive.billable', + type: 'boolean', + }, + 'gsuite.drive.source_folder_id': { + category: 'gsuite', + name: 'gsuite.drive.source_folder_id', + type: 'keyword', + }, + 'gsuite.drive.source_folder_title': { + category: 'gsuite', + name: 'gsuite.drive.source_folder_title', + type: 'keyword', + }, + 'gsuite.drive.destination_folder_id': { + category: 'gsuite', + name: 'gsuite.drive.destination_folder_id', + type: 'keyword', + }, + 'gsuite.drive.destination_folder_title': { + category: 'gsuite', + name: 'gsuite.drive.destination_folder_title', + type: 'keyword', + }, + 'gsuite.drive.file.id': { + category: 'gsuite', + name: 'gsuite.drive.file.id', + type: 'keyword', + }, + 'gsuite.drive.file.type': { + category: 'gsuite', + description: + 'Document Drive type. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/drive ', + name: 'gsuite.drive.file.type', + type: 'keyword', + }, + 'gsuite.drive.originating_app_id': { + category: 'gsuite', + description: 'The Google Cloud Project ID of the application that performed the action. ', + name: 'gsuite.drive.originating_app_id', + type: 'keyword', + }, + 'gsuite.drive.file.owner.email': { + category: 'gsuite', + name: 'gsuite.drive.file.owner.email', + type: 'keyword', + }, + 'gsuite.drive.file.owner.is_shared_drive': { + category: 'gsuite', + description: 'Boolean flag denoting whether owner is a shared drive. ', + name: 'gsuite.drive.file.owner.is_shared_drive', + type: 'boolean', + }, + 'gsuite.drive.primary_event': { + category: 'gsuite', + description: + 'Whether this is a primary event. A single user action in Drive may generate several events. ', + name: 'gsuite.drive.primary_event', + type: 'boolean', + }, + 'gsuite.drive.shared_drive_id': { + category: 'gsuite', + description: + 'The unique identifier of the Team Drive. Only populated for for events relating to a Team Drive or item contained inside a Team Drive. ', + name: 'gsuite.drive.shared_drive_id', + type: 'keyword', + }, + 'gsuite.drive.visibility': { + category: 'gsuite', + description: + 'Visibility of target file. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/drive ', + name: 'gsuite.drive.visibility', + type: 'keyword', + }, + 'gsuite.drive.new_value': { + category: 'gsuite', + description: + 'When a setting or property of the file changes, the new value for it will appear here. ', + name: 'gsuite.drive.new_value', + type: 'keyword', + }, + 'gsuite.drive.old_value': { + category: 'gsuite', + description: + 'When a setting or property of the file changes, the old value for it will appear here. ', + name: 'gsuite.drive.old_value', + type: 'keyword', + }, + 'gsuite.drive.sheets_import_range_recipient_doc': { + category: 'gsuite', + description: 'Doc ID of the recipient of a sheets import range.', + name: 'gsuite.drive.sheets_import_range_recipient_doc', + type: 'keyword', + }, + 'gsuite.drive.old_visibility': { + category: 'gsuite', + description: 'When visibility changes, this holds the old value. ', + name: 'gsuite.drive.old_visibility', + type: 'keyword', + }, + 'gsuite.drive.visibility_change': { + category: 'gsuite', + description: 'When visibility changes, this holds the new overall visibility of the file. ', + name: 'gsuite.drive.visibility_change', + type: 'keyword', + }, + 'gsuite.drive.target_domain': { + category: 'gsuite', + description: + 'The domain for which the acccess scope was changed. This can also be the alias all to indicate the access scope was changed for all domains that have visibility for this document. ', + name: 'gsuite.drive.target_domain', + type: 'keyword', + }, + 'gsuite.drive.added_role': { + category: 'gsuite', + description: + 'Added membership role of a user/group in a Team Drive. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/drive ', + name: 'gsuite.drive.added_role', + type: 'keyword', + }, + 'gsuite.drive.membership_change_type': { + category: 'gsuite', + description: + 'Type of change in Team Drive membership of a user/group. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/drive ', + name: 'gsuite.drive.membership_change_type', + type: 'keyword', + }, + 'gsuite.drive.shared_drive_settings_change_type': { + category: 'gsuite', + description: + 'Type of change in Team Drive settings. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/drive ', + name: 'gsuite.drive.shared_drive_settings_change_type', + type: 'keyword', + }, + 'gsuite.drive.removed_role': { + category: 'gsuite', + description: + 'Removed membership role of a user/group in a Team Drive. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/drive ', + name: 'gsuite.drive.removed_role', + type: 'keyword', + }, + 'gsuite.drive.target': { + category: 'gsuite', + description: 'Target user or group.', + name: 'gsuite.drive.target', + type: 'keyword', + }, + 'gsuite.groups.acl_permission': { + category: 'gsuite', + description: + 'Group permission setting updated. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/groups ', + name: 'gsuite.groups.acl_permission', + type: 'keyword', + }, + 'gsuite.groups.email': { + category: 'gsuite', + description: 'Group email. ', + name: 'gsuite.groups.email', + type: 'keyword', + }, + 'gsuite.groups.member.email': { + category: 'gsuite', + description: 'Member email. ', + name: 'gsuite.groups.member.email', + type: 'keyword', + }, + 'gsuite.groups.member.role': { + category: 'gsuite', + description: + 'Member role. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/groups ', + name: 'gsuite.groups.member.role', + type: 'keyword', + }, + 'gsuite.groups.setting': { + category: 'gsuite', + description: + 'Group setting updated. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/groups ', + name: 'gsuite.groups.setting', + type: 'keyword', + }, + 'gsuite.groups.new_value': { + category: 'gsuite', + description: + 'New value(s) of the group setting. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/groups ', + name: 'gsuite.groups.new_value', + type: 'keyword', + }, + 'gsuite.groups.old_value': { + category: 'gsuite', + description: + 'Old value(s) of the group setting. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/groups', + name: 'gsuite.groups.old_value', + type: 'keyword', + }, + 'gsuite.groups.value': { + category: 'gsuite', + description: + 'Value of the group setting. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/groups ', + name: 'gsuite.groups.value', + type: 'keyword', + }, + 'gsuite.groups.message.id': { + category: 'gsuite', + description: 'SMTP message Id of an email message. Present for moderation events. ', + name: 'gsuite.groups.message.id', + type: 'keyword', + }, + 'gsuite.groups.message.moderation_action': { + category: 'gsuite', + description: 'Message moderation action. Possible values are `approved` and `rejected`. ', + name: 'gsuite.groups.message.moderation_action', + type: 'keyword', + }, + 'gsuite.groups.status': { + category: 'gsuite', + description: + 'A status describing the output of an operation. Possible values are `failed` and `succeeded`. ', + name: 'gsuite.groups.status', + type: 'keyword', + }, + 'gsuite.login.affected_email_address': { + category: 'gsuite', + name: 'gsuite.login.affected_email_address', + type: 'keyword', + }, + 'gsuite.login.challenge_method': { + category: 'gsuite', + description: + 'Login challenge method. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/login. ', + name: 'gsuite.login.challenge_method', + type: 'keyword', + }, + 'gsuite.login.failure_type': { + category: 'gsuite', + description: + 'Login failure type. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/login. ', + name: 'gsuite.login.failure_type', + type: 'keyword', + }, + 'gsuite.login.type': { + category: 'gsuite', + description: + 'Login credentials type. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/login. ', + name: 'gsuite.login.type', + type: 'keyword', + }, + 'gsuite.login.is_second_factor': { + category: 'gsuite', + name: 'gsuite.login.is_second_factor', + type: 'boolean', + }, + 'gsuite.login.is_suspicious': { + category: 'gsuite', + name: 'gsuite.login.is_suspicious', + type: 'boolean', + }, + 'gsuite.saml.application_name': { + category: 'gsuite', + description: 'Saml SP application name. ', + name: 'gsuite.saml.application_name', + type: 'keyword', + }, + 'gsuite.saml.failure_type': { + category: 'gsuite', + description: + 'Login failure type. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/saml. ', + name: 'gsuite.saml.failure_type', + type: 'keyword', + }, + 'gsuite.saml.initiated_by': { + category: 'gsuite', + description: 'Requester of SAML authentication. ', + name: 'gsuite.saml.initiated_by', + type: 'keyword', + }, + 'gsuite.saml.orgunit_path': { + category: 'gsuite', + description: 'User orgunit. ', + name: 'gsuite.saml.orgunit_path', + type: 'keyword', + }, + 'gsuite.saml.status_code': { + category: 'gsuite', + description: 'SAML status code. ', + name: 'gsuite.saml.status_code', + type: 'long', + }, + 'gsuite.saml.second_level_status_code': { + category: 'gsuite', + description: 'SAML second level status code. ', + name: 'gsuite.saml.second_level_status_code', + type: 'long', + }, + 'ibmmq.errorlog.installation': { + category: 'ibmmq', + description: + 'This is the installation name which can be given at installation time. Each installation of IBM MQ on UNIX, Linux, and Windows, has a unique identifier known as an installation name. The installation name is used to associate things such as queue managers and configuration files with an installation. ', + name: 'ibmmq.errorlog.installation', + type: 'keyword', + }, + 'ibmmq.errorlog.qmgr': { + category: 'ibmmq', + description: + 'Name of the queue manager. Queue managers provide queuing services to applications, and manages the queues that belong to them. ', + name: 'ibmmq.errorlog.qmgr', + type: 'keyword', + }, + 'ibmmq.errorlog.arithinsert': { + category: 'ibmmq', + description: 'Changing content based on error.id', + name: 'ibmmq.errorlog.arithinsert', + type: 'keyword', + }, + 'ibmmq.errorlog.commentinsert': { + category: 'ibmmq', + description: 'Changing content based on error.id', + name: 'ibmmq.errorlog.commentinsert', + type: 'keyword', + }, + 'ibmmq.errorlog.errordescription': { + category: 'ibmmq', + description: 'Please add description', + example: 'Please add example', + name: 'ibmmq.errorlog.errordescription', + type: 'text', + }, + 'ibmmq.errorlog.explanation': { + category: 'ibmmq', + description: 'Explaines the error in more detail', + name: 'ibmmq.errorlog.explanation', + type: 'keyword', + }, + 'ibmmq.errorlog.action': { + category: 'ibmmq', + description: 'Defines what to do when the error occurs', + name: 'ibmmq.errorlog.action', + type: 'keyword', + }, + 'ibmmq.errorlog.code': { + category: 'ibmmq', + description: 'Error code.', + name: 'ibmmq.errorlog.code', + type: 'keyword', + }, + 'iptables.ether_type': { + category: 'iptables', + description: 'Value of the ethernet type field identifying the network layer protocol. ', + name: 'iptables.ether_type', + type: 'long', + }, + 'iptables.flow_label': { + category: 'iptables', + description: 'IPv6 flow label. ', + name: 'iptables.flow_label', + type: 'integer', + }, + 'iptables.fragment_flags': { + category: 'iptables', + description: 'IP fragment flags. A combination of CE, DF and MF. ', + name: 'iptables.fragment_flags', + type: 'keyword', + }, + 'iptables.fragment_offset': { + category: 'iptables', + description: 'Offset of the current IP fragment. ', + name: 'iptables.fragment_offset', + type: 'long', + }, + 'iptables.icmp.code': { + category: 'iptables', + description: 'ICMP code. ', + name: 'iptables.icmp.code', + type: 'long', + }, + 'iptables.icmp.id': { + category: 'iptables', + description: 'ICMP ID. ', + name: 'iptables.icmp.id', + type: 'long', + }, + 'iptables.icmp.parameter': { + category: 'iptables', + description: 'ICMP parameter. ', + name: 'iptables.icmp.parameter', + type: 'long', + }, + 'iptables.icmp.redirect': { + category: 'iptables', + description: 'ICMP redirect address. ', + name: 'iptables.icmp.redirect', + type: 'ip', + }, + 'iptables.icmp.seq': { + category: 'iptables', + description: 'ICMP sequence number. ', + name: 'iptables.icmp.seq', + type: 'long', + }, + 'iptables.icmp.type': { + category: 'iptables', + description: 'ICMP type. ', + name: 'iptables.icmp.type', + type: 'long', + }, + 'iptables.id': { + category: 'iptables', + description: 'Packet identifier. ', + name: 'iptables.id', + type: 'long', + }, + 'iptables.incomplete_bytes': { + category: 'iptables', + description: 'Number of incomplete bytes. ', + name: 'iptables.incomplete_bytes', + type: 'long', + }, + 'iptables.input_device': { + category: 'iptables', + description: 'Device that received the packet. ', + name: 'iptables.input_device', + type: 'keyword', + }, + 'iptables.precedence_bits': { + category: 'iptables', + description: 'IP precedence bits. ', + name: 'iptables.precedence_bits', + type: 'short', + }, + 'iptables.tos': { + category: 'iptables', + description: 'IP Type of Service field. ', + name: 'iptables.tos', + type: 'long', + }, + 'iptables.length': { + category: 'iptables', + description: 'Packet length. ', + name: 'iptables.length', + type: 'long', + }, + 'iptables.output_device': { + category: 'iptables', + description: 'Device that output the packet. ', + name: 'iptables.output_device', + type: 'keyword', + }, + 'iptables.tcp.flags': { + category: 'iptables', + description: 'TCP flags. ', + name: 'iptables.tcp.flags', + type: 'keyword', + }, + 'iptables.tcp.reserved_bits': { + category: 'iptables', + description: 'TCP reserved bits. ', + name: 'iptables.tcp.reserved_bits', + type: 'short', + }, + 'iptables.tcp.seq': { + category: 'iptables', + description: 'TCP sequence number. ', + name: 'iptables.tcp.seq', + type: 'long', + }, + 'iptables.tcp.ack': { + category: 'iptables', + description: 'TCP Acknowledgment number. ', + name: 'iptables.tcp.ack', + type: 'long', + }, + 'iptables.tcp.window': { + category: 'iptables', + description: 'Advertised TCP window size. ', + name: 'iptables.tcp.window', + type: 'long', + }, + 'iptables.ttl': { + category: 'iptables', + description: 'Time To Live field. ', + name: 'iptables.ttl', + type: 'integer', + }, + 'iptables.udp.length': { + category: 'iptables', + description: 'Length of the UDP header and payload. ', + name: 'iptables.udp.length', + type: 'long', + }, + 'iptables.ubiquiti.input_zone': { + category: 'iptables', + description: 'Input zone. ', + name: 'iptables.ubiquiti.input_zone', + type: 'keyword', + }, + 'iptables.ubiquiti.output_zone': { + category: 'iptables', + description: 'Output zone. ', + name: 'iptables.ubiquiti.output_zone', + type: 'keyword', + }, + 'iptables.ubiquiti.rule_number': { + category: 'iptables', + description: 'The rule number within the rule set.', + name: 'iptables.ubiquiti.rule_number', + type: 'keyword', + }, + 'iptables.ubiquiti.rule_set': { + category: 'iptables', + description: 'The rule set name.', + name: 'iptables.ubiquiti.rule_set', + type: 'keyword', + }, + 'microsoft.defender_atp.lastUpdateTime': { + category: 'microsoft', + description: 'The date and time (in UTC) the alert was last updated. ', + name: 'microsoft.defender_atp.lastUpdateTime', + type: 'date', + }, + 'microsoft.defender_atp.resolvedTime': { + category: 'microsoft', + description: "The date and time in which the status of the alert was changed to 'Resolved'. ", + name: 'microsoft.defender_atp.resolvedTime', + type: 'date', + }, + 'microsoft.defender_atp.incidentId': { + category: 'microsoft', + description: 'The Incident ID of the Alert. ', + name: 'microsoft.defender_atp.incidentId', + type: 'keyword', + }, + 'microsoft.defender_atp.investigationId': { + category: 'microsoft', + description: 'The Investigation ID related to the Alert. ', + name: 'microsoft.defender_atp.investigationId', + type: 'keyword', + }, + 'microsoft.defender_atp.investigationState': { + category: 'microsoft', + description: 'The current state of the Investigation. ', + name: 'microsoft.defender_atp.investigationState', + type: 'keyword', + }, + 'microsoft.defender_atp.assignedTo': { + category: 'microsoft', + description: 'Owner of the alert. ', + name: 'microsoft.defender_atp.assignedTo', + type: 'keyword', + }, + 'microsoft.defender_atp.status': { + category: 'microsoft', + description: + "Specifies the current status of the alert. Possible values are: 'Unknown', 'New', 'InProgress' and 'Resolved'. ", + name: 'microsoft.defender_atp.status', + type: 'keyword', + }, + 'microsoft.defender_atp.classification': { + category: 'microsoft', + description: + "Specification of the alert. Possible values are: 'Unknown', 'FalsePositive', 'TruePositive'. ", + name: 'microsoft.defender_atp.classification', + type: 'keyword', + }, + 'microsoft.defender_atp.determination': { + category: 'microsoft', + description: + "Specifies the determination of the alert. Possible values are: 'NotAvailable', 'Apt', 'Malware', 'SecurityPersonnel', 'SecurityTesting', 'UnwantedSoftware', 'Other'. ", + name: 'microsoft.defender_atp.determination', + type: 'keyword', + }, + 'microsoft.defender_atp.threatFamilyName': { + category: 'microsoft', + description: 'Threat family. ', + name: 'microsoft.defender_atp.threatFamilyName', + type: 'keyword', + }, + 'microsoft.defender_atp.rbacGroupName': { + category: 'microsoft', + description: 'User group related to the alert ', + name: 'microsoft.defender_atp.rbacGroupName', + type: 'keyword', + }, + 'microsoft.defender_atp.evidence.domainName': { + category: 'microsoft', + description: 'Domain name related to the alert ', + name: 'microsoft.defender_atp.evidence.domainName', + type: 'keyword', + }, + 'microsoft.defender_atp.evidence.ipAddress': { + category: 'microsoft', + description: 'IP address involved in the alert ', + name: 'microsoft.defender_atp.evidence.ipAddress', + type: 'ip', + }, + 'microsoft.defender_atp.evidence.aadUserId': { + category: 'microsoft', + description: 'ID of the user involved in the alert ', + name: 'microsoft.defender_atp.evidence.aadUserId', + type: 'keyword', + }, + 'microsoft.defender_atp.evidence.accountName': { + category: 'microsoft', + description: 'Username of the user involved in the alert ', + name: 'microsoft.defender_atp.evidence.accountName', + type: 'keyword', + }, + 'microsoft.defender_atp.evidence.entityType': { + category: 'microsoft', + description: 'The type of evidence ', + name: 'microsoft.defender_atp.evidence.entityType', + type: 'keyword', + }, + 'microsoft.defender_atp.evidence.userPrincipalName': { + category: 'microsoft', + description: 'Principal name of the user involved in the alert ', + name: 'microsoft.defender_atp.evidence.userPrincipalName', + type: 'keyword', + }, + 'misp.attack_pattern.id': { + category: 'misp', + description: 'Identifier of the threat indicator. ', + name: 'misp.attack_pattern.id', + type: 'keyword', + }, + 'misp.attack_pattern.name': { + category: 'misp', + description: 'Name of the attack pattern. ', + name: 'misp.attack_pattern.name', + type: 'keyword', + }, + 'misp.attack_pattern.description': { + category: 'misp', + description: 'Description of the attack pattern. ', + name: 'misp.attack_pattern.description', + type: 'text', + }, + 'misp.attack_pattern.kill_chain_phases': { + category: 'misp', + description: 'The kill chain phase(s) to which this attack pattern corresponds. ', + name: 'misp.attack_pattern.kill_chain_phases', + type: 'keyword', + }, + 'misp.campaign.id': { + category: 'misp', + description: 'Identifier of the campaign. ', + name: 'misp.campaign.id', + type: 'keyword', + }, + 'misp.campaign.name': { + category: 'misp', + description: 'Name of the campaign. ', + name: 'misp.campaign.name', + type: 'keyword', + }, + 'misp.campaign.description': { + category: 'misp', + description: 'Description of the campaign. ', + name: 'misp.campaign.description', + type: 'text', + }, + 'misp.campaign.aliases': { + category: 'misp', + description: 'Alternative names used to identify this campaign. ', + name: 'misp.campaign.aliases', + type: 'text', + }, + 'misp.campaign.first_seen': { + category: 'misp', + description: 'The time that this Campaign was first seen, in RFC3339 format. ', + name: 'misp.campaign.first_seen', + type: 'date', + }, + 'misp.campaign.last_seen': { + category: 'misp', + description: 'The time that this Campaign was last seen, in RFC3339 format. ', + name: 'misp.campaign.last_seen', + type: 'date', + }, + 'misp.campaign.objective': { + category: 'misp', + description: + "This field defines the Campaign's primary goal, objective, desired outcome, or intended effect. ", + name: 'misp.campaign.objective', + type: 'keyword', + }, + 'misp.course_of_action.id': { + category: 'misp', + description: 'Identifier of the Course of Action. ', + name: 'misp.course_of_action.id', + type: 'keyword', + }, + 'misp.course_of_action.name': { + category: 'misp', + description: 'The name used to identify the Course of Action. ', + name: 'misp.course_of_action.name', + type: 'keyword', + }, + 'misp.course_of_action.description': { + category: 'misp', + description: 'Description of the Course of Action. ', + name: 'misp.course_of_action.description', + type: 'text', + }, + 'misp.identity.id': { + category: 'misp', + description: 'Identifier of the Identity. ', + name: 'misp.identity.id', + type: 'keyword', + }, + 'misp.identity.name': { + category: 'misp', + description: 'The name used to identify the Identity. ', + name: 'misp.identity.name', + type: 'keyword', + }, + 'misp.identity.description': { + category: 'misp', + description: 'Description of the Identity. ', + name: 'misp.identity.description', + type: 'text', + }, + 'misp.identity.identity_class': { + category: 'misp', + description: + 'The type of entity that this Identity describes, e.g., an individual or organization. Open Vocab - identity-class-ov ', + name: 'misp.identity.identity_class', + type: 'keyword', + }, + 'misp.identity.labels': { + category: 'misp', + description: 'The list of roles that this Identity performs. ', + example: 'CEO\n', + name: 'misp.identity.labels', + type: 'keyword', + }, + 'misp.identity.sectors': { + category: 'misp', + description: + 'The list of sectors that this Identity belongs to. Open Vocab - industry-sector-ov ', + name: 'misp.identity.sectors', + type: 'keyword', + }, + 'misp.identity.contact_information': { + category: 'misp', + description: 'The contact information (e-mail, phone number, etc.) for this Identity. ', + name: 'misp.identity.contact_information', + type: 'text', + }, + 'misp.intrusion_set.id': { + category: 'misp', + description: 'Identifier of the Intrusion Set. ', + name: 'misp.intrusion_set.id', + type: 'keyword', + }, + 'misp.intrusion_set.name': { + category: 'misp', + description: 'The name used to identify the Intrusion Set. ', + name: 'misp.intrusion_set.name', + type: 'keyword', + }, + 'misp.intrusion_set.description': { + category: 'misp', + description: 'Description of the Intrusion Set. ', + name: 'misp.intrusion_set.description', + type: 'text', + }, + 'misp.intrusion_set.aliases': { + category: 'misp', + description: 'Alternative names used to identify the Intrusion Set. ', + name: 'misp.intrusion_set.aliases', + type: 'text', + }, + 'misp.intrusion_set.first_seen': { + category: 'misp', + description: 'The time that this Intrusion Set was first seen, in RFC3339 format. ', + name: 'misp.intrusion_set.first_seen', + type: 'date', + }, + 'misp.intrusion_set.last_seen': { + category: 'misp', + description: 'The time that this Intrusion Set was last seen, in RFC3339 format. ', + name: 'misp.intrusion_set.last_seen', + type: 'date', + }, + 'misp.intrusion_set.goals': { + category: 'misp', + description: 'The high level goals of this Intrusion Set, namely, what are they trying to do. ', + name: 'misp.intrusion_set.goals', + type: 'text', + }, + 'misp.intrusion_set.resource_level': { + category: 'misp', + description: + 'This defines the organizational level at which this Intrusion Set typically works. Open Vocab - attack-resource-level-ov ', + name: 'misp.intrusion_set.resource_level', + type: 'text', + }, + 'misp.intrusion_set.primary_motivation': { + category: 'misp', + description: + 'The primary reason, motivation, or purpose behind this Intrusion Set. Open Vocab - attack-motivation-ov ', + name: 'misp.intrusion_set.primary_motivation', + type: 'text', + }, + 'misp.intrusion_set.secondary_motivations': { + category: 'misp', + description: + 'The secondary reasons, motivations, or purposes behind this Intrusion Set. Open Vocab - attack-motivation-ov ', + name: 'misp.intrusion_set.secondary_motivations', + type: 'text', + }, + 'misp.malware.id': { + category: 'misp', + description: 'Identifier of the Malware. ', + name: 'misp.malware.id', + type: 'keyword', + }, + 'misp.malware.name': { + category: 'misp', + description: 'The name used to identify the Malware. ', + name: 'misp.malware.name', + type: 'keyword', + }, + 'misp.malware.description': { + category: 'misp', + description: 'Description of the Malware. ', + name: 'misp.malware.description', + type: 'text', + }, + 'misp.malware.labels': { + category: 'misp', + description: + 'The type of malware being described. Open Vocab - malware-label-ov. adware,backdoor,bot,ddos,dropper,exploit-kit,keylogger,ransomware, remote-access-trojan,resource-exploitation,rogue-security-software,rootkit, screen-capture,spyware,trojan,virus,worm ', + name: 'misp.malware.labels', + type: 'keyword', + }, + 'misp.malware.kill_chain_phases': { + category: 'misp', + description: 'The list of kill chain phases for which this Malware instance can be used. ', + name: 'misp.malware.kill_chain_phases', + type: 'keyword', + format: 'string', + }, + 'misp.note.id': { + category: 'misp', + description: 'Identifier of the Note. ', + name: 'misp.note.id', + type: 'keyword', + }, + 'misp.note.summary': { + category: 'misp', + description: 'A brief description used as a summary of the Note. ', + name: 'misp.note.summary', + type: 'keyword', + }, + 'misp.note.description': { + category: 'misp', + description: 'The content of the Note. ', + name: 'misp.note.description', + type: 'text', + }, + 'misp.note.authors': { + category: 'misp', + description: 'The name of the author(s) of this Note. ', + name: 'misp.note.authors', + type: 'keyword', + }, + 'misp.note.object_refs': { + category: 'misp', + description: 'The STIX Objects (SDOs and SROs) that the note is being applied to. ', + name: 'misp.note.object_refs', + type: 'keyword', + }, + 'misp.threat_indicator.labels': { + category: 'misp', + description: 'list of type open-vocab that specifies the type of indicator. ', + example: 'Domain Watchlist\n', + name: 'misp.threat_indicator.labels', + type: 'keyword', + }, + 'misp.threat_indicator.id': { + category: 'misp', + description: 'Identifier of the threat indicator. ', + name: 'misp.threat_indicator.id', + type: 'keyword', + }, + 'misp.threat_indicator.version': { + category: 'misp', + description: 'Version of the threat indicator. ', + name: 'misp.threat_indicator.version', + type: 'keyword', + }, + 'misp.threat_indicator.type': { + category: 'misp', + description: 'Type of the threat indicator. ', + name: 'misp.threat_indicator.type', + type: 'keyword', + }, + 'misp.threat_indicator.description': { + category: 'misp', + description: 'Description of the threat indicator. ', + name: 'misp.threat_indicator.description', + type: 'text', + }, + 'misp.threat_indicator.feed': { + category: 'misp', + description: 'Name of the threat feed. ', + name: 'misp.threat_indicator.feed', + type: 'text', + }, + 'misp.threat_indicator.valid_from': { + category: 'misp', + description: + 'The time from which this Indicator should be considered valuable intelligence, in RFC3339 format. ', + name: 'misp.threat_indicator.valid_from', + type: 'date', + }, + 'misp.threat_indicator.valid_until': { + category: 'misp', + description: + 'The time at which this Indicator should no longer be considered valuable intelligence. If the valid_until property is omitted, then there is no constraint on the latest time for which the indicator should be used, in RFC3339 format. ', + name: 'misp.threat_indicator.valid_until', + type: 'date', + }, + 'misp.threat_indicator.severity': { + category: 'misp', + description: 'Threat severity to which this indicator corresponds. ', + example: 'high', + name: 'misp.threat_indicator.severity', + type: 'keyword', + format: 'string', + }, + 'misp.threat_indicator.confidence': { + category: 'misp', + description: 'Confidence level to which this indicator corresponds. ', + example: 'high', + name: 'misp.threat_indicator.confidence', + type: 'keyword', + }, + 'misp.threat_indicator.kill_chain_phases': { + category: 'misp', + description: 'The kill chain phase(s) to which this indicator corresponds. ', + name: 'misp.threat_indicator.kill_chain_phases', + type: 'keyword', + format: 'string', + }, + 'misp.threat_indicator.mitre_tactic': { + category: 'misp', + description: 'MITRE tactics to which this indicator corresponds. ', + example: 'Initial Access', + name: 'misp.threat_indicator.mitre_tactic', + type: 'keyword', + format: 'string', + }, + 'misp.threat_indicator.mitre_technique': { + category: 'misp', + description: 'MITRE techniques to which this indicator corresponds. ', + example: 'Drive-by Compromise', + name: 'misp.threat_indicator.mitre_technique', + type: 'keyword', + format: 'string', + }, + 'misp.threat_indicator.attack_pattern': { + category: 'misp', + description: + 'The attack_pattern for this indicator is a STIX Pattern as specified in STIX Version 2.0 Part 5 - STIX Patterning. ', + example: "[destination:ip = '91.219.29.188/32']\n", + name: 'misp.threat_indicator.attack_pattern', + type: 'keyword', + }, + 'misp.threat_indicator.attack_pattern_kql': { + category: 'misp', + description: + 'The attack_pattern for this indicator is KQL query that matches the attack_pattern specified in the STIX Pattern format. ', + example: 'destination.ip: "91.219.29.188/32"\n', + name: 'misp.threat_indicator.attack_pattern_kql', + type: 'keyword', + }, + 'misp.threat_indicator.negate': { + category: 'misp', + description: 'When set to true, it specifies the absence of the attack_pattern. ', + name: 'misp.threat_indicator.negate', + type: 'boolean', + }, + 'misp.threat_indicator.intrusion_set': { + category: 'misp', + description: 'Name of the intrusion set if known. ', + name: 'misp.threat_indicator.intrusion_set', + type: 'keyword', + }, + 'misp.threat_indicator.campaign': { + category: 'misp', + description: 'Name of the attack campaign if known. ', + name: 'misp.threat_indicator.campaign', + type: 'keyword', + }, + 'misp.threat_indicator.threat_actor': { + category: 'misp', + description: 'Name of the threat actor if known. ', + name: 'misp.threat_indicator.threat_actor', + type: 'keyword', + }, + 'misp.observed_data.id': { + category: 'misp', + description: 'Identifier of the Observed Data. ', + name: 'misp.observed_data.id', + type: 'keyword', + }, + 'misp.observed_data.first_observed': { + category: 'misp', + description: 'The beginning of the time window that the data was observed, in RFC3339 format. ', + name: 'misp.observed_data.first_observed', + type: 'date', + }, + 'misp.observed_data.last_observed': { + category: 'misp', + description: 'The end of the time window that the data was observed, in RFC3339 format. ', + name: 'misp.observed_data.last_observed', + type: 'date', + }, + 'misp.observed_data.number_observed': { + category: 'misp', + description: + 'The number of times the data represented in the objects property was observed. This MUST be an integer between 1 and 999,999,999 inclusive. ', + name: 'misp.observed_data.number_observed', + type: 'integer', + }, + 'misp.observed_data.objects': { + category: 'misp', + description: + 'A dictionary of Cyber Observable Objects that describes the single fact that was observed. ', + name: 'misp.observed_data.objects', + type: 'keyword', + }, + 'misp.report.id': { + category: 'misp', + description: 'Identifier of the Report. ', + name: 'misp.report.id', + type: 'keyword', + }, + 'misp.report.labels': { + category: 'misp', + description: + 'This field is an Open Vocabulary that specifies the primary subject of this report. Open Vocab - report-label-ov. threat-report,attack-pattern,campaign,identity,indicator,malware,observed-data,threat-actor,tool,vulnerability ', + name: 'misp.report.labels', + type: 'keyword', + }, + 'misp.report.name': { + category: 'misp', + description: 'The name used to identify the Report. ', + name: 'misp.report.name', + type: 'keyword', + }, + 'misp.report.description': { + category: 'misp', + description: 'A description that provides more details and context about Report. ', + name: 'misp.report.description', + type: 'text', + }, + 'misp.report.published': { + category: 'misp', + description: + 'The date that this report object was officially published by the creator of this report, in RFC3339 format. ', + name: 'misp.report.published', + type: 'date', + }, + 'misp.report.object_refs': { + category: 'misp', + description: 'Specifies the STIX Objects that are referred to by this Report. ', + name: 'misp.report.object_refs', + type: 'text', + }, + 'misp.threat_actor.id': { + category: 'misp', + description: 'Identifier of the Threat Actor. ', + name: 'misp.threat_actor.id', + type: 'keyword', + }, + 'misp.threat_actor.labels': { + category: 'misp', + description: + 'This field specifies the type of threat actor. Open Vocab - threat-actor-label-ov. activist,competitor,crime-syndicate,criminal,hacker,insider-accidental,insider-disgruntled,nation-state,sensationalist,spy,terrorist ', + name: 'misp.threat_actor.labels', + type: 'keyword', + }, + 'misp.threat_actor.name': { + category: 'misp', + description: 'The name used to identify this Threat Actor or Threat Actor group. ', + name: 'misp.threat_actor.name', + type: 'keyword', + }, + 'misp.threat_actor.description': { + category: 'misp', + description: 'A description that provides more details and context about the Threat Actor. ', + name: 'misp.threat_actor.description', + type: 'text', + }, + 'misp.threat_actor.aliases': { + category: 'misp', + description: 'A list of other names that this Threat Actor is believed to use. ', + name: 'misp.threat_actor.aliases', + type: 'text', + }, + 'misp.threat_actor.roles': { + category: 'misp', + description: + 'This is a list of roles the Threat Actor plays. Open Vocab - threat-actor-role-ov. agent,director,independent,sponsor,infrastructure-operator,infrastructure-architect,malware-author ', + name: 'misp.threat_actor.roles', + type: 'text', + }, + 'misp.threat_actor.goals': { + category: 'misp', + description: 'The high level goals of this Threat Actor, namely, what are they trying to do. ', + name: 'misp.threat_actor.goals', + type: 'text', + }, + 'misp.threat_actor.sophistication': { + category: 'misp', + description: + 'The skill, specific knowledge, special training, or expertise a Threat Actor must have to perform the attack. Open Vocab - threat-actor-sophistication-ov. none,minimal,intermediate,advanced,strategic,expert,innovator ', + name: 'misp.threat_actor.sophistication', + type: 'text', + }, + 'misp.threat_actor.resource_level': { + category: 'misp', + description: + 'This defines the organizational level at which this Threat Actor typically works. Open Vocab - attack-resource-level-ov. individual,club,contest,team,organization,government ', + name: 'misp.threat_actor.resource_level', + type: 'text', + }, + 'misp.threat_actor.primary_motivation': { + category: 'misp', + description: + 'The primary reason, motivation, or purpose behind this Threat Actor. Open Vocab - attack-motivation-ov. accidental,coercion,dominance,ideology,notoriety,organizational-gain,personal-gain,personal-satisfaction,revenge,unpredictable ', + name: 'misp.threat_actor.primary_motivation', + type: 'text', + }, + 'misp.threat_actor.secondary_motivations': { + category: 'misp', + description: + 'The secondary reasons, motivations, or purposes behind this Threat Actor. Open Vocab - attack-motivation-ov. accidental,coercion,dominance,ideology,notoriety,organizational-gain,personal-gain,personal-satisfaction,revenge,unpredictable ', + name: 'misp.threat_actor.secondary_motivations', + type: 'text', + }, + 'misp.threat_actor.personal_motivations': { + category: 'misp', + description: + 'The personal reasons, motivations, or purposes of the Threat Actor regardless of organizational goals. Open Vocab - attack-motivation-ov. accidental,coercion,dominance,ideology,notoriety,organizational-gain,personal-gain,personal-satisfaction,revenge,unpredictable ', + name: 'misp.threat_actor.personal_motivations', + type: 'text', + }, + 'misp.tool.id': { + category: 'misp', + description: 'Identifier of the Tool. ', + name: 'misp.tool.id', + type: 'keyword', + }, + 'misp.tool.labels': { + category: 'misp', + description: + 'The kind(s) of tool(s) being described. Open Vocab - tool-label-ov. denial-of-service,exploitation,information-gathering,network-capture,credential-exploitation,remote-access,vulnerability-scanning ', + name: 'misp.tool.labels', + type: 'keyword', + }, + 'misp.tool.name': { + category: 'misp', + description: 'The name used to identify the Tool. ', + name: 'misp.tool.name', + type: 'keyword', + }, + 'misp.tool.description': { + category: 'misp', + description: 'A description that provides more details and context about the Tool. ', + name: 'misp.tool.description', + type: 'text', + }, + 'misp.tool.tool_version': { + category: 'misp', + description: 'The version identifier associated with the Tool. ', + name: 'misp.tool.tool_version', + type: 'keyword', + }, + 'misp.tool.kill_chain_phases': { + category: 'misp', + description: 'The list of kill chain phases for which this Tool instance can be used. ', + name: 'misp.tool.kill_chain_phases', + type: 'text', + }, + 'misp.vulnerability.id': { + category: 'misp', + description: 'Identifier of the Vulnerability. ', + name: 'misp.vulnerability.id', + type: 'keyword', + }, + 'misp.vulnerability.name': { + category: 'misp', + description: 'The name used to identify the Vulnerability. ', + name: 'misp.vulnerability.name', + type: 'keyword', + }, + 'misp.vulnerability.description': { + category: 'misp', + description: 'A description that provides more details and context about the Vulnerability. ', + name: 'misp.vulnerability.description', + type: 'text', + }, + 'mssql.log.origin': { + category: 'mssql', + description: 'Origin of the message, usually the server but it can also be a recovery process', + name: 'mssql.log.origin', + type: 'keyword', + }, + 'o365.audit.Actor.ID': { + category: 'o365', + name: 'o365.audit.Actor.ID', + type: 'keyword', + }, + 'o365.audit.Actor.Type': { + category: 'o365', + name: 'o365.audit.Actor.Type', + type: 'keyword', + }, + 'o365.audit.ActorContextId': { + category: 'o365', + name: 'o365.audit.ActorContextId', + type: 'keyword', + }, + 'o365.audit.ActorIpAddress': { + category: 'o365', + name: 'o365.audit.ActorIpAddress', + type: 'keyword', + }, + 'o365.audit.ActorUserId': { + category: 'o365', + name: 'o365.audit.ActorUserId', + type: 'keyword', + }, + 'o365.audit.ActorYammerUserId': { + category: 'o365', + name: 'o365.audit.ActorYammerUserId', + type: 'keyword', + }, + 'o365.audit.AlertEntityId': { + category: 'o365', + name: 'o365.audit.AlertEntityId', + type: 'keyword', + }, + 'o365.audit.AlertId': { + category: 'o365', + name: 'o365.audit.AlertId', + type: 'keyword', + }, + 'o365.audit.AlertLinks': { + category: 'o365', + name: 'o365.audit.AlertLinks', + type: 'array', + }, + 'o365.audit.AlertType': { + category: 'o365', + name: 'o365.audit.AlertType', + type: 'keyword', + }, + 'o365.audit.AppId': { + category: 'o365', + name: 'o365.audit.AppId', + type: 'keyword', + }, + 'o365.audit.ApplicationDisplayName': { + category: 'o365', + name: 'o365.audit.ApplicationDisplayName', + type: 'keyword', + }, + 'o365.audit.ApplicationId': { + category: 'o365', + name: 'o365.audit.ApplicationId', + type: 'keyword', + }, + 'o365.audit.AzureActiveDirectoryEventType': { + category: 'o365', + name: 'o365.audit.AzureActiveDirectoryEventType', + type: 'keyword', + }, + 'o365.audit.ExchangeMetaData.*': { + category: 'o365', + name: 'o365.audit.ExchangeMetaData.*', + type: 'object', + }, + 'o365.audit.Category': { + category: 'o365', + name: 'o365.audit.Category', + type: 'keyword', + }, + 'o365.audit.ClientAppId': { + category: 'o365', + name: 'o365.audit.ClientAppId', + type: 'keyword', + }, + 'o365.audit.ClientInfoString': { + category: 'o365', + name: 'o365.audit.ClientInfoString', + type: 'keyword', + }, + 'o365.audit.ClientIP': { + category: 'o365', + name: 'o365.audit.ClientIP', + type: 'keyword', + }, + 'o365.audit.ClientIPAddress': { + category: 'o365', + name: 'o365.audit.ClientIPAddress', + type: 'keyword', + }, + 'o365.audit.Comments': { + category: 'o365', + name: 'o365.audit.Comments', + type: 'text', + }, + 'o365.audit.CorrelationId': { + category: 'o365', + name: 'o365.audit.CorrelationId', + type: 'keyword', + }, + 'o365.audit.CreationTime': { + category: 'o365', + name: 'o365.audit.CreationTime', + type: 'keyword', + }, + 'o365.audit.CustomUniqueId': { + category: 'o365', + name: 'o365.audit.CustomUniqueId', + type: 'keyword', + }, + 'o365.audit.Data': { + category: 'o365', + name: 'o365.audit.Data', + type: 'keyword', + }, + 'o365.audit.DataType': { + category: 'o365', + name: 'o365.audit.DataType', + type: 'keyword', + }, + 'o365.audit.EntityType': { + category: 'o365', + name: 'o365.audit.EntityType', + type: 'keyword', + }, + 'o365.audit.EventData': { + category: 'o365', + name: 'o365.audit.EventData', + type: 'keyword', + }, + 'o365.audit.EventSource': { + category: 'o365', + name: 'o365.audit.EventSource', + type: 'keyword', + }, + 'o365.audit.ExceptionInfo.*': { + category: 'o365', + name: 'o365.audit.ExceptionInfo.*', + type: 'object', + }, + 'o365.audit.ExtendedProperties.*': { + category: 'o365', + name: 'o365.audit.ExtendedProperties.*', + type: 'object', + }, + 'o365.audit.ExternalAccess': { + category: 'o365', + name: 'o365.audit.ExternalAccess', + type: 'keyword', + }, + 'o365.audit.GroupName': { + category: 'o365', + name: 'o365.audit.GroupName', + type: 'keyword', + }, + 'o365.audit.Id': { + category: 'o365', + name: 'o365.audit.Id', + type: 'keyword', + }, + 'o365.audit.ImplicitShare': { + category: 'o365', + name: 'o365.audit.ImplicitShare', + type: 'keyword', + }, + 'o365.audit.IncidentId': { + category: 'o365', + name: 'o365.audit.IncidentId', + type: 'keyword', + }, + 'o365.audit.InternalLogonType': { + category: 'o365', + name: 'o365.audit.InternalLogonType', + type: 'keyword', + }, + 'o365.audit.InterSystemsId': { + category: 'o365', + name: 'o365.audit.InterSystemsId', + type: 'keyword', + }, + 'o365.audit.IntraSystemId': { + category: 'o365', + name: 'o365.audit.IntraSystemId', + type: 'keyword', + }, + 'o365.audit.Item.*': { + category: 'o365', + name: 'o365.audit.Item.*', + type: 'object', + }, + 'o365.audit.Item.*.*': { + category: 'o365', + name: 'o365.audit.Item.*.*', + type: 'object', + }, + 'o365.audit.ItemName': { + category: 'o365', + name: 'o365.audit.ItemName', + type: 'keyword', + }, + 'o365.audit.ItemType': { + category: 'o365', + name: 'o365.audit.ItemType', + type: 'keyword', + }, + 'o365.audit.ListId': { + category: 'o365', + name: 'o365.audit.ListId', + type: 'keyword', + }, + 'o365.audit.ListItemUniqueId': { + category: 'o365', + name: 'o365.audit.ListItemUniqueId', + type: 'keyword', + }, + 'o365.audit.LogonError': { + category: 'o365', + name: 'o365.audit.LogonError', + type: 'keyword', + }, + 'o365.audit.LogonType': { + category: 'o365', + name: 'o365.audit.LogonType', + type: 'keyword', + }, + 'o365.audit.LogonUserSid': { + category: 'o365', + name: 'o365.audit.LogonUserSid', + type: 'keyword', + }, + 'o365.audit.MailboxGuid': { + category: 'o365', + name: 'o365.audit.MailboxGuid', + type: 'keyword', + }, + 'o365.audit.MailboxOwnerMasterAccountSid': { + category: 'o365', + name: 'o365.audit.MailboxOwnerMasterAccountSid', + type: 'keyword', + }, + 'o365.audit.MailboxOwnerSid': { + category: 'o365', + name: 'o365.audit.MailboxOwnerSid', + type: 'keyword', + }, + 'o365.audit.MailboxOwnerUPN': { + category: 'o365', + name: 'o365.audit.MailboxOwnerUPN', + type: 'keyword', + }, + 'o365.audit.Members': { + category: 'o365', + name: 'o365.audit.Members', + type: 'array', + }, + 'o365.audit.Members.*': { + category: 'o365', + name: 'o365.audit.Members.*', + type: 'object', + }, + 'o365.audit.ModifiedProperties.*.*': { + category: 'o365', + name: 'o365.audit.ModifiedProperties.*.*', + type: 'object', + }, + 'o365.audit.Name': { + category: 'o365', + name: 'o365.audit.Name', + type: 'keyword', + }, + 'o365.audit.ObjectId': { + category: 'o365', + name: 'o365.audit.ObjectId', + type: 'keyword', + }, + 'o365.audit.Operation': { + category: 'o365', + name: 'o365.audit.Operation', + type: 'keyword', + }, + 'o365.audit.OrganizationId': { + category: 'o365', + name: 'o365.audit.OrganizationId', + type: 'keyword', + }, + 'o365.audit.OrganizationName': { + category: 'o365', + name: 'o365.audit.OrganizationName', + type: 'keyword', + }, + 'o365.audit.OriginatingServer': { + category: 'o365', + name: 'o365.audit.OriginatingServer', + type: 'keyword', + }, + 'o365.audit.Parameters.*': { + category: 'o365', + name: 'o365.audit.Parameters.*', + type: 'object', + }, + 'o365.audit.PolicyDetails': { + category: 'o365', + name: 'o365.audit.PolicyDetails', + type: 'array', + }, + 'o365.audit.PolicyId': { + category: 'o365', + name: 'o365.audit.PolicyId', + type: 'keyword', + }, + 'o365.audit.RecordType': { + category: 'o365', + name: 'o365.audit.RecordType', + type: 'keyword', + }, + 'o365.audit.ResultStatus': { + category: 'o365', + name: 'o365.audit.ResultStatus', + type: 'keyword', + }, + 'o365.audit.SensitiveInfoDetectionIsIncluded': { + category: 'o365', + name: 'o365.audit.SensitiveInfoDetectionIsIncluded', + type: 'keyword', + }, + 'o365.audit.SharePointMetaData.*': { + category: 'o365', + name: 'o365.audit.SharePointMetaData.*', + type: 'object', + }, + 'o365.audit.SessionId': { + category: 'o365', + name: 'o365.audit.SessionId', + type: 'keyword', + }, + 'o365.audit.Severity': { + category: 'o365', + name: 'o365.audit.Severity', + type: 'keyword', + }, + 'o365.audit.Site': { + category: 'o365', + name: 'o365.audit.Site', + type: 'keyword', + }, + 'o365.audit.SiteUrl': { + category: 'o365', + name: 'o365.audit.SiteUrl', + type: 'keyword', + }, + 'o365.audit.Source': { + category: 'o365', + name: 'o365.audit.Source', + type: 'keyword', + }, + 'o365.audit.SourceFileExtension': { + category: 'o365', + name: 'o365.audit.SourceFileExtension', + type: 'keyword', + }, + 'o365.audit.SourceFileName': { + category: 'o365', + name: 'o365.audit.SourceFileName', + type: 'keyword', + }, + 'o365.audit.SourceRelativeUrl': { + category: 'o365', + name: 'o365.audit.SourceRelativeUrl', + type: 'keyword', + }, + 'o365.audit.Status': { + category: 'o365', + name: 'o365.audit.Status', + type: 'keyword', + }, + 'o365.audit.SupportTicketId': { + category: 'o365', + name: 'o365.audit.SupportTicketId', + type: 'keyword', + }, + 'o365.audit.Target.ID': { + category: 'o365', + name: 'o365.audit.Target.ID', + type: 'keyword', + }, + 'o365.audit.Target.Type': { + category: 'o365', + name: 'o365.audit.Target.Type', + type: 'keyword', + }, + 'o365.audit.TargetContextId': { + category: 'o365', + name: 'o365.audit.TargetContextId', + type: 'keyword', + }, + 'o365.audit.TargetUserOrGroupName': { + category: 'o365', + name: 'o365.audit.TargetUserOrGroupName', + type: 'keyword', + }, + 'o365.audit.TargetUserOrGroupType': { + category: 'o365', + name: 'o365.audit.TargetUserOrGroupType', + type: 'keyword', + }, + 'o365.audit.TeamName': { + category: 'o365', + name: 'o365.audit.TeamName', + type: 'keyword', + }, + 'o365.audit.TeamGuid': { + category: 'o365', + name: 'o365.audit.TeamGuid', + type: 'keyword', + }, + 'o365.audit.UniqueSharingId': { + category: 'o365', + name: 'o365.audit.UniqueSharingId', + type: 'keyword', + }, + 'o365.audit.UserAgent': { + category: 'o365', + name: 'o365.audit.UserAgent', + type: 'keyword', + }, + 'o365.audit.UserId': { + category: 'o365', + name: 'o365.audit.UserId', + type: 'keyword', + }, + 'o365.audit.UserKey': { + category: 'o365', + name: 'o365.audit.UserKey', + type: 'keyword', + }, + 'o365.audit.UserType': { + category: 'o365', + name: 'o365.audit.UserType', + type: 'keyword', + }, + 'o365.audit.Version': { + category: 'o365', + name: 'o365.audit.Version', + type: 'keyword', + }, + 'o365.audit.WebId': { + category: 'o365', + name: 'o365.audit.WebId', + type: 'keyword', + }, + 'o365.audit.Workload': { + category: 'o365', + name: 'o365.audit.Workload', + type: 'keyword', + }, + 'o365.audit.YammerNetworkId': { + category: 'o365', + name: 'o365.audit.YammerNetworkId', + type: 'keyword', + }, + 'okta.uuid': { + category: 'okta', + description: 'The unique identifier of the Okta LogEvent. ', + name: 'okta.uuid', + type: 'keyword', + }, + 'okta.event_type': { + category: 'okta', + description: 'The type of the LogEvent. ', + name: 'okta.event_type', + type: 'keyword', + }, + 'okta.version': { + category: 'okta', + description: 'The version of the LogEvent. ', + name: 'okta.version', + type: 'keyword', + }, + 'okta.severity': { + category: 'okta', + description: 'The severity of the LogEvent. Must be one of DEBUG, INFO, WARN, or ERROR. ', + name: 'okta.severity', + type: 'keyword', + }, + 'okta.display_message': { + category: 'okta', + description: 'The display message of the LogEvent. ', + name: 'okta.display_message', + type: 'keyword', + }, + 'okta.actor.id': { + category: 'okta', + description: 'Identifier of the actor. ', + name: 'okta.actor.id', + type: 'keyword', + }, + 'okta.actor.type': { + category: 'okta', + description: 'Type of the actor. ', + name: 'okta.actor.type', + type: 'keyword', + }, + 'okta.actor.alternate_id': { + category: 'okta', + description: 'Alternate identifier of the actor. ', + name: 'okta.actor.alternate_id', + type: 'keyword', + }, + 'okta.actor.display_name': { + category: 'okta', + description: 'Display name of the actor. ', + name: 'okta.actor.display_name', + type: 'keyword', + }, + 'okta.client.ip': { + category: 'okta', + description: 'The IP address of the client. ', + name: 'okta.client.ip', + type: 'ip', + }, + 'okta.client.user_agent.raw_user_agent': { + category: 'okta', + description: 'The raw informaton of the user agent. ', + name: 'okta.client.user_agent.raw_user_agent', + type: 'keyword', + }, + 'okta.client.user_agent.os': { + category: 'okta', + description: 'The OS informaton. ', + name: 'okta.client.user_agent.os', + type: 'keyword', + }, + 'okta.client.user_agent.browser': { + category: 'okta', + description: 'The browser informaton of the client. ', + name: 'okta.client.user_agent.browser', + type: 'keyword', + }, + 'okta.client.zone': { + category: 'okta', + description: 'The zone information of the client. ', + name: 'okta.client.zone', + type: 'keyword', + }, + 'okta.client.device': { + category: 'okta', + description: 'The information of the client device. ', + name: 'okta.client.device', + type: 'keyword', + }, + 'okta.client.id': { + category: 'okta', + description: 'The identifier of the client. ', + name: 'okta.client.id', + type: 'keyword', + }, + 'okta.outcome.reason': { + category: 'okta', + description: 'The reason of the outcome. ', + name: 'okta.outcome.reason', + type: 'keyword', + }, + 'okta.outcome.result': { + category: 'okta', + description: + 'The result of the outcome. Must be one of: SUCCESS, FAILURE, SKIPPED, ALLOW, DENY, CHALLENGE, UNKNOWN. ', + name: 'okta.outcome.result', + type: 'keyword', + }, + 'okta.target.id': { + category: 'okta', + description: 'Identifier of the actor. ', + name: 'okta.target.id', + type: 'keyword', + }, + 'okta.target.type': { + category: 'okta', + description: 'Type of the actor. ', + name: 'okta.target.type', + type: 'keyword', + }, + 'okta.target.alternate_id': { + category: 'okta', + description: 'Alternate identifier of the actor. ', + name: 'okta.target.alternate_id', + type: 'keyword', + }, + 'okta.target.display_name': { + category: 'okta', + description: 'Display name of the actor. ', + name: 'okta.target.display_name', + type: 'keyword', + }, + 'okta.transaction.id': { + category: 'okta', + description: 'Identifier of the transaction. ', + name: 'okta.transaction.id', + type: 'keyword', + }, + 'okta.transaction.type': { + category: 'okta', + description: 'The type of transaction. Must be one of "WEB", "JOB". ', + name: 'okta.transaction.type', + type: 'keyword', + }, + 'okta.debug_context.debug_data.device_fingerprint': { + category: 'okta', + description: 'The fingerprint of the device. ', + name: 'okta.debug_context.debug_data.device_fingerprint', + type: 'keyword', + }, + 'okta.debug_context.debug_data.request_id': { + category: 'okta', + description: 'The identifier of the request. ', + name: 'okta.debug_context.debug_data.request_id', + type: 'keyword', + }, + 'okta.debug_context.debug_data.request_uri': { + category: 'okta', + description: 'The request URI. ', + name: 'okta.debug_context.debug_data.request_uri', + type: 'keyword', + }, + 'okta.debug_context.debug_data.threat_suspected': { + category: 'okta', + description: 'Threat suspected. ', + name: 'okta.debug_context.debug_data.threat_suspected', + type: 'keyword', + }, + 'okta.debug_context.debug_data.url': { + category: 'okta', + description: 'The URL. ', + name: 'okta.debug_context.debug_data.url', + type: 'keyword', + }, + 'okta.authentication_context.authentication_provider': { + category: 'okta', + description: + 'The information about the authentication provider. Must be one of OKTA_AUTHENTICATION_PROVIDER, ACTIVE_DIRECTORY, LDAP, FEDERATION, SOCIAL, FACTOR_PROVIDER. ', + name: 'okta.authentication_context.authentication_provider', + type: 'keyword', + }, + 'okta.authentication_context.authentication_step': { + category: 'okta', + description: 'The authentication step. ', + name: 'okta.authentication_context.authentication_step', + type: 'integer', + }, + 'okta.authentication_context.credential_provider': { + category: 'okta', + description: + 'The information about credential provider. Must be one of OKTA_CREDENTIAL_PROVIDER, RSA, SYMANTEC, GOOGLE, DUO, YUBIKEY. ', + name: 'okta.authentication_context.credential_provider', + type: 'keyword', + }, + 'okta.authentication_context.credential_type': { + category: 'okta', + description: + 'The information about credential type. Must be one of OTP, SMS, PASSWORD, ASSERTION, IWA, EMAIL, OAUTH2, JWT, CERTIFICATE, PRE_SHARED_SYMMETRIC_KEY, OKTA_CLIENT_SESSION, DEVICE_UDID. ', + name: 'okta.authentication_context.credential_type', + type: 'keyword', + }, + 'okta.authentication_context.issuer.id': { + category: 'okta', + description: 'The identifier of the issuer. ', + name: 'okta.authentication_context.issuer.id', + type: 'keyword', + }, + 'okta.authentication_context.issuer.type': { + category: 'okta', + description: 'The type of the issuer. ', + name: 'okta.authentication_context.issuer.type', + type: 'keyword', + }, + 'okta.authentication_context.external_session_id': { + category: 'okta', + description: 'The session identifer of the external session if any. ', + name: 'okta.authentication_context.external_session_id', + type: 'keyword', + }, + 'okta.authentication_context.interface': { + category: 'okta', + description: 'The interface used. e.g., Outlook, Office365, wsTrust ', + name: 'okta.authentication_context.interface', + type: 'keyword', + }, + 'okta.security_context.as.number': { + category: 'okta', + description: 'The AS number. ', + name: 'okta.security_context.as.number', + type: 'integer', + }, + 'okta.security_context.as.organization.name': { + category: 'okta', + description: 'The organization name. ', + name: 'okta.security_context.as.organization.name', + type: 'keyword', + }, + 'okta.security_context.isp': { + category: 'okta', + description: 'The Internet Service Provider. ', + name: 'okta.security_context.isp', + type: 'keyword', + }, + 'okta.security_context.domain': { + category: 'okta', + description: 'The domain name. ', + name: 'okta.security_context.domain', + type: 'keyword', + }, + 'okta.security_context.is_proxy': { + category: 'okta', + description: 'Whether it is a proxy or not. ', + name: 'okta.security_context.is_proxy', + type: 'boolean', + }, + 'okta.request.ip_chain.ip': { + category: 'okta', + description: 'IP address. ', + name: 'okta.request.ip_chain.ip', + type: 'ip', + }, + 'okta.request.ip_chain.version': { + category: 'okta', + description: 'IP version. Must be one of V4, V6. ', + name: 'okta.request.ip_chain.version', + type: 'keyword', + }, + 'okta.request.ip_chain.source': { + category: 'okta', + description: 'Source information. ', + name: 'okta.request.ip_chain.source', + type: 'keyword', + }, + 'okta.request.ip_chain.geographical_context.city': { + category: 'okta', + description: 'The city.', + name: 'okta.request.ip_chain.geographical_context.city', + type: 'keyword', + }, + 'okta.request.ip_chain.geographical_context.state': { + category: 'okta', + description: 'The state.', + name: 'okta.request.ip_chain.geographical_context.state', + type: 'keyword', + }, + 'okta.request.ip_chain.geographical_context.postal_code': { + category: 'okta', + description: 'The postal code.', + name: 'okta.request.ip_chain.geographical_context.postal_code', + type: 'keyword', + }, + 'okta.request.ip_chain.geographical_context.country': { + category: 'okta', + description: 'The country.', + name: 'okta.request.ip_chain.geographical_context.country', + type: 'keyword', + }, + 'okta.request.ip_chain.geographical_context.geolocation': { + category: 'okta', + description: 'Geolocation information. ', + name: 'okta.request.ip_chain.geographical_context.geolocation', + type: 'geo_point', + }, + 'panw.panos.ruleset': { + category: 'panw', + description: 'Name of the rule that matched this session. ', + name: 'panw.panos.ruleset', + type: 'keyword', + }, + 'panw.panos.source.zone': { + category: 'panw', + description: 'Source zone for this session. ', + name: 'panw.panos.source.zone', + type: 'keyword', + }, + 'panw.panos.source.interface': { + category: 'panw', + description: 'Source interface for this session. ', + name: 'panw.panos.source.interface', + type: 'keyword', + }, + 'panw.panos.source.nat.ip': { + category: 'panw', + description: 'Post-NAT source IP. ', + name: 'panw.panos.source.nat.ip', + type: 'ip', + }, + 'panw.panos.source.nat.port': { + category: 'panw', + description: 'Post-NAT source port. ', + name: 'panw.panos.source.nat.port', + type: 'long', + }, + 'panw.panos.destination.zone': { + category: 'panw', + description: 'Destination zone for this session. ', + name: 'panw.panos.destination.zone', + type: 'keyword', + }, + 'panw.panos.destination.interface': { + category: 'panw', + description: 'Destination interface for this session. ', + name: 'panw.panos.destination.interface', + type: 'keyword', + }, + 'panw.panos.destination.nat.ip': { + category: 'panw', + description: 'Post-NAT destination IP. ', + name: 'panw.panos.destination.nat.ip', + type: 'ip', + }, + 'panw.panos.destination.nat.port': { + category: 'panw', + description: 'Post-NAT destination port. ', + name: 'panw.panos.destination.nat.port', + type: 'long', + }, + 'panw.panos.network.pcap_id': { + category: 'panw', + description: 'Packet capture ID for a threat. ', + name: 'panw.panos.network.pcap_id', + type: 'keyword', + }, + 'panw.panos.network.nat.community_id': { + category: 'panw', + description: 'Community ID flow-hash for the NAT 5-tuple. ', + name: 'panw.panos.network.nat.community_id', + type: 'keyword', + }, + 'panw.panos.file.hash': { + category: 'panw', + description: 'Binary hash for a threat file sent to be analyzed by the WildFire service. ', + name: 'panw.panos.file.hash', + type: 'keyword', + }, + 'panw.panos.url.category': { + category: 'panw', + description: + "For threat URLs, it's the URL category. For WildFire, the verdict on the file and is either 'malicious', 'grayware', or 'benign'. ", + name: 'panw.panos.url.category', + type: 'keyword', + }, + 'panw.panos.flow_id': { + category: 'panw', + description: 'Internal numeric identifier for each session. ', + name: 'panw.panos.flow_id', + type: 'keyword', + }, + 'panw.panos.sequence_number': { + category: 'panw', + description: + 'Log entry identifier that is incremented sequentially. Unique for each log type. ', + name: 'panw.panos.sequence_number', + type: 'long', + }, + 'panw.panos.threat.resource': { + category: 'panw', + description: 'URL or file name for a threat. ', + name: 'panw.panos.threat.resource', + type: 'keyword', + }, + 'panw.panos.threat.id': { + category: 'panw', + description: 'Palo Alto Networks identifier for the threat. ', + name: 'panw.panos.threat.id', + type: 'keyword', + }, + 'panw.panos.threat.name': { + category: 'panw', + description: 'Palo Alto Networks name for the threat. ', + name: 'panw.panos.threat.name', + type: 'keyword', + }, + 'panw.panos.action': { + category: 'panw', + description: 'Action taken for the session.', + name: 'panw.panos.action', + type: 'keyword', + }, + 'rabbitmq.log.pid': { + category: 'rabbitmq', + description: 'The Erlang process id', + example: '<0.222.0>', + name: 'rabbitmq.log.pid', + type: 'keyword', + }, + 'sophos.xg.device': { + category: 'sophos', + description: 'device ', + name: 'sophos.xg.device', + type: 'keyword', + }, + 'sophos.xg.date': { + category: 'sophos', + description: 'Date (yyyy-mm-dd) when the event occurred ', + name: 'sophos.xg.date', + type: 'date', + }, + 'sophos.xg.timezone': { + category: 'sophos', + description: 'Time (hh:mm:ss) when the event occurred ', + name: 'sophos.xg.timezone', + type: 'keyword', + }, + 'sophos.xg.device_name': { + category: 'sophos', + description: 'Model number of the device ', + name: 'sophos.xg.device_name', + type: 'keyword', + }, + 'sophos.xg.device_id': { + category: 'sophos', + description: 'Serial number of the device ', + name: 'sophos.xg.device_id', + type: 'keyword', + }, + 'sophos.xg.log_id': { + category: 'sophos', + description: 'Unique 12 characters code (0101011) ', + name: 'sophos.xg.log_id', + type: 'keyword', + }, + 'sophos.xg.log_type': { + category: 'sophos', + description: 'Type of event e.g. firewall event ', + name: 'sophos.xg.log_type', + type: 'keyword', + }, + 'sophos.xg.log_component': { + category: 'sophos', + description: 'Component responsible for logging e.g. Firewall rule ', + name: 'sophos.xg.log_component', + type: 'keyword', + }, + 'sophos.xg.log_subtype': { + category: 'sophos', + description: 'Sub type of event ', + name: 'sophos.xg.log_subtype', + type: 'keyword', + }, + 'sophos.xg.hb_health': { + category: 'sophos', + description: 'Heartbeat status ', + name: 'sophos.xg.hb_health', + type: 'keyword', + }, + 'sophos.xg.priority': { + category: 'sophos', + description: 'Severity level of traffic ', + name: 'sophos.xg.priority', + type: 'keyword', + }, + 'sophos.xg.status': { + category: 'sophos', + description: 'Ultimate status of traffic – Allowed or Denied ', + name: 'sophos.xg.status', + type: 'keyword', + }, + 'sophos.xg.duration': { + category: 'sophos', + description: 'Durability of traffic (seconds) ', + name: 'sophos.xg.duration', + type: 'long', + }, + 'sophos.xg.fw_rule_id': { + category: 'sophos', + description: 'Firewall Rule ID which is applied on the traffic ', + name: 'sophos.xg.fw_rule_id', + type: 'integer', + }, + 'sophos.xg.user_name': { + category: 'sophos', + description: 'user_name ', + name: 'sophos.xg.user_name', + type: 'keyword', + }, + 'sophos.xg.user_group': { + category: 'sophos', + description: 'Group name to which the user belongs ', + name: 'sophos.xg.user_group', + type: 'keyword', + }, + 'sophos.xg.iap': { + category: 'sophos', + description: 'Internet Access policy ID applied on the traffic ', + name: 'sophos.xg.iap', + type: 'keyword', + }, + 'sophos.xg.ips_policy_id': { + category: 'sophos', + description: 'IPS policy ID applied on the traffic ', + name: 'sophos.xg.ips_policy_id', + type: 'integer', + }, + 'sophos.xg.policy_type': { + category: 'sophos', + description: 'Policy type applied to the traffic ', + name: 'sophos.xg.policy_type', + type: 'keyword', + }, + 'sophos.xg.appfilter_policy_id': { + category: 'sophos', + description: 'Application Filter policy applied on the traffic ', + name: 'sophos.xg.appfilter_policy_id', + type: 'integer', + }, + 'sophos.xg.application_filter_policy': { + category: 'sophos', + description: 'Application Filter policy applied on the traffic ', + name: 'sophos.xg.application_filter_policy', + type: 'integer', + }, + 'sophos.xg.application': { + category: 'sophos', + description: 'Application name ', + name: 'sophos.xg.application', + type: 'keyword', + }, + 'sophos.xg.application_name': { + category: 'sophos', + description: 'Application name ', + name: 'sophos.xg.application_name', + type: 'keyword', + }, + 'sophos.xg.application_risk': { + category: 'sophos', + description: 'Risk level assigned to the application ', + name: 'sophos.xg.application_risk', + type: 'keyword', + }, + 'sophos.xg.application_technology': { + category: 'sophos', + description: 'Technology of the application ', + name: 'sophos.xg.application_technology', + type: 'keyword', + }, + 'sophos.xg.application_category': { + category: 'sophos', + description: 'Application is resolved by signature or synchronized application ', + name: 'sophos.xg.application_category', + type: 'keyword', + }, + 'sophos.xg.appresolvedby': { + category: 'sophos', + description: 'Technology of the application ', + name: 'sophos.xg.appresolvedby', + type: 'keyword', + }, + 'sophos.xg.app_is_cloud': { + category: 'sophos', + description: 'Application is Cloud ', + name: 'sophos.xg.app_is_cloud', + type: 'keyword', + }, + 'sophos.xg.in_interface': { + category: 'sophos', + description: 'Interface for incoming traffic, e.g., Port A ', + name: 'sophos.xg.in_interface', + type: 'keyword', + }, + 'sophos.xg.out_interface': { + category: 'sophos', + description: 'Interface for outgoing traffic, e.g., Port B ', + name: 'sophos.xg.out_interface', + type: 'keyword', + }, + 'sophos.xg.src_ip': { + category: 'sophos', + description: 'Original source IP address of traffic ', + name: 'sophos.xg.src_ip', + type: 'ip', + }, + 'sophos.xg.src_mac': { + category: 'sophos', + description: 'Original source MAC address of traffic ', + name: 'sophos.xg.src_mac', + type: 'keyword', + }, + 'sophos.xg.src_country_code': { + category: 'sophos', + description: 'Code of the country to which the source IP belongs ', + name: 'sophos.xg.src_country_code', + type: 'keyword', + }, + 'sophos.xg.dst_ip': { + category: 'sophos', + description: 'Original destination IP address of traffic ', + name: 'sophos.xg.dst_ip', + type: 'ip', + }, + 'sophos.xg.dst_country_code': { + category: 'sophos', + description: 'Code of the country to which the destination IP belongs ', + name: 'sophos.xg.dst_country_code', + type: 'keyword', + }, + 'sophos.xg.protocol': { + category: 'sophos', + description: 'Protocol number of traffic ', + name: 'sophos.xg.protocol', + type: 'keyword', + }, + 'sophos.xg.src_port': { + category: 'sophos', + description: 'Original source port of TCP and UDP traffic ', + name: 'sophos.xg.src_port', + type: 'integer', + }, + 'sophos.xg.dst_port': { + category: 'sophos', + description: 'Original destination port of TCP and UDP traffic ', + name: 'sophos.xg.dst_port', + type: 'integer', + }, + 'sophos.xg.icmp_type': { + category: 'sophos', + description: 'ICMP type of ICMP traffic ', + name: 'sophos.xg.icmp_type', + type: 'keyword', + }, + 'sophos.xg.icmp_code': { + category: 'sophos', + description: 'ICMP code of ICMP traffic ', + name: 'sophos.xg.icmp_code', + type: 'keyword', + }, + 'sophos.xg.sent_pkts': { + category: 'sophos', + description: 'Total number of packets sent ', + name: 'sophos.xg.sent_pkts', + type: 'long', + }, + 'sophos.xg.received_pkts': { + category: 'sophos', + description: 'Total number of packets received ', + name: 'sophos.xg.received_pkts', + type: 'long', + }, + 'sophos.xg.sent_bytes': { + category: 'sophos', + description: 'Total number of bytes sent ', + name: 'sophos.xg.sent_bytes', + type: 'long', + }, + 'sophos.xg.recv_bytes': { + category: 'sophos', + description: 'Total number of bytes received ', + name: 'sophos.xg.recv_bytes', + type: 'long', + }, + 'sophos.xg.trans_src_ ip': { + category: 'sophos', + description: 'Translated source IP address for outgoing traffic ', + name: 'sophos.xg.trans_src_ ip', + type: 'ip', + }, + 'sophos.xg.trans_src_port': { + category: 'sophos', + description: 'Translated source port for outgoing traffic ', + name: 'sophos.xg.trans_src_port', + type: 'integer', + }, + 'sophos.xg.trans_dst_ip': { + category: 'sophos', + description: 'Translated destination IP address for outgoing traffic ', + name: 'sophos.xg.trans_dst_ip', + type: 'ip', + }, + 'sophos.xg.trans_dst_port': { + category: 'sophos', + description: 'Translated destination port for outgoing traffic ', + name: 'sophos.xg.trans_dst_port', + type: 'integer', + }, + 'sophos.xg.srczonetype': { + category: 'sophos', + description: 'Type of source zone, e.g., LAN ', + name: 'sophos.xg.srczonetype', + type: 'keyword', + }, + 'sophos.xg.srczone': { + category: 'sophos', + description: 'Name of source zone ', + name: 'sophos.xg.srczone', + type: 'keyword', + }, + 'sophos.xg.dstzonetype': { + category: 'sophos', + description: 'Type of destination zone, e.g., WAN ', + name: 'sophos.xg.dstzonetype', + type: 'keyword', + }, + 'sophos.xg.dstzone': { + category: 'sophos', + description: 'Name of destination zone ', + name: 'sophos.xg.dstzone', + type: 'keyword', + }, + 'sophos.xg.dir_disp': { + category: 'sophos', + description: 'TPacket direction. Possible values:“org”, “reply”, “” ', + name: 'sophos.xg.dir_disp', + type: 'keyword', + }, + 'sophos.xg.connevent': { + category: 'sophos', + description: 'Event on which this log is generated ', + name: 'sophos.xg.connevent', + type: 'keyword', + }, + 'sophos.xg.conn_id': { + category: 'sophos', + description: 'Unique identifier of connection ', + name: 'sophos.xg.conn_id', + type: 'integer', + }, + 'sophos.xg.vconn_id': { + category: 'sophos', + description: 'Connection ID of the master connection ', + name: 'sophos.xg.vconn_id', + type: 'integer', + }, + 'sophos.xg.idp_policy_id': { + category: 'sophos', + description: 'IPS policy ID which is applied on the traffic ', + name: 'sophos.xg.idp_policy_id', + type: 'integer', + }, + 'sophos.xg.idp_policy_name': { + category: 'sophos', + description: 'IPS policy name i.e. IPS policy name which is applied on the traffic ', + name: 'sophos.xg.idp_policy_name', + type: 'keyword', + }, + 'sophos.xg.signature_id': { + category: 'sophos', + description: 'Signature ID ', + name: 'sophos.xg.signature_id', + type: 'keyword', + }, + 'sophos.xg.signature_msg': { + category: 'sophos', + description: 'Signature messsage ', + name: 'sophos.xg.signature_msg', + type: 'keyword', + }, + 'sophos.xg.classification': { + category: 'sophos', + description: 'Signature classification ', + name: 'sophos.xg.classification', + type: 'keyword', + }, + 'sophos.xg.rule_priority': { + category: 'sophos', + description: 'Priority of IPS policy ', + name: 'sophos.xg.rule_priority', + type: 'keyword', + }, + 'sophos.xg.platform': { + category: 'sophos', + description: 'Platform of the traffic. ', + name: 'sophos.xg.platform', + type: 'keyword', + }, + 'sophos.xg.category': { + category: 'sophos', + description: 'IPS signature category. ', + name: 'sophos.xg.category', + type: 'keyword', + }, + 'sophos.xg.target': { + category: 'sophos', + description: 'Platform of the traffic. ', + name: 'sophos.xg.target', + type: 'keyword', + }, + 'sophos.xg.eventid': { + category: 'sophos', + description: 'ATP Evenet ID ', + name: 'sophos.xg.eventid', + type: 'keyword', + }, + 'sophos.xg.ep_uuid': { + category: 'sophos', + description: 'Endpoint UUID ', + name: 'sophos.xg.ep_uuid', + type: 'keyword', + }, + 'sophos.xg.threatname': { + category: 'sophos', + description: 'ATP threatname ', + name: 'sophos.xg.threatname', + type: 'keyword', + }, + 'sophos.xg.sourceip': { + category: 'sophos', + description: 'Original source IP address of traffic ', + name: 'sophos.xg.sourceip', + type: 'ip', + }, + 'sophos.xg.destinationip': { + category: 'sophos', + description: 'Original destination IP address of traffic ', + name: 'sophos.xg.destinationip', + type: 'ip', + }, + 'sophos.xg.login_user': { + category: 'sophos', + description: 'ATP login user ', + name: 'sophos.xg.login_user', + type: 'keyword', + }, + 'sophos.xg.eventtype': { + category: 'sophos', + description: 'ATP event type ', + name: 'sophos.xg.eventtype', + type: 'keyword', + }, + 'sophos.xg.execution_path': { + category: 'sophos', + description: 'ATP execution path ', + name: 'sophos.xg.execution_path', + type: 'keyword', + }, + 'sophos.xg.av_policy_name': { + category: 'sophos', + description: 'Malware scanning policy name which is applied on the traffic ', + name: 'sophos.xg.av_policy_name', + type: 'keyword', + }, + 'sophos.xg.from_email_address': { + category: 'sophos', + description: 'Sender email address ', + name: 'sophos.xg.from_email_address', + type: 'keyword', + }, + 'sophos.xg.to_email_address': { + category: 'sophos', + description: 'Receipeint email address ', + name: 'sophos.xg.to_email_address', + type: 'keyword', + }, + 'sophos.xg.subject': { + category: 'sophos', + description: 'Email subject ', + name: 'sophos.xg.subject', + type: 'keyword', + }, + 'sophos.xg.mailsize': { + category: 'sophos', + description: 'mailsize ', + name: 'sophos.xg.mailsize', + type: 'integer', + }, + 'sophos.xg.virus': { + category: 'sophos', + description: 'virus name ', + name: 'sophos.xg.virus', + type: 'keyword', + }, + 'sophos.xg.FTP_url': { + category: 'sophos', + description: 'FTP URL from which virus was downloaded ', + name: 'sophos.xg.FTP_url', + type: 'keyword', + }, + 'sophos.xg.FTP_direction': { + category: 'sophos', + description: 'Direction of FTP transfer: Upload or Download ', + name: 'sophos.xg.FTP_direction', + type: 'keyword', + }, + 'sophos.xg.filesize': { + category: 'sophos', + description: 'Size of the file that contained virus ', + name: 'sophos.xg.filesize', + type: 'integer', + }, + 'sophos.xg.filepath': { + category: 'sophos', + description: 'Path of the file containing virus ', + name: 'sophos.xg.filepath', + type: 'keyword', + }, + 'sophos.xg.filename': { + category: 'sophos', + description: 'File name associated with the event ', + name: 'sophos.xg.filename', + type: 'keyword', + }, + 'sophos.xg.ftpcommand': { + category: 'sophos', + description: 'FTP command used when virus was found ', + name: 'sophos.xg.ftpcommand', + type: 'keyword', + }, + 'sophos.xg.url': { + category: 'sophos', + description: 'URL from which virus was downloaded ', + name: 'sophos.xg.url', + type: 'keyword', + }, + 'sophos.xg.domainname': { + category: 'sophos', + description: 'Domain from which virus was downloaded ', + name: 'sophos.xg.domainname', + type: 'keyword', + }, + 'sophos.xg.quarantine': { + category: 'sophos', + description: 'Path and filename of the file quarantined ', + name: 'sophos.xg.quarantine', + type: 'keyword', + }, + 'sophos.xg.src_domainname': { + category: 'sophos', + description: 'Sender domain name ', + name: 'sophos.xg.src_domainname', + type: 'keyword', + }, + 'sophos.xg.dst_domainname': { + category: 'sophos', + description: 'Receiver domain name ', + name: 'sophos.xg.dst_domainname', + type: 'keyword', + }, + 'sophos.xg.reason': { + category: 'sophos', + description: 'Reason why the record was detected as spam/malicious ', + name: 'sophos.xg.reason', + type: 'keyword', + }, + 'sophos.xg.referer': { + category: 'sophos', + description: 'Referer ', + name: 'sophos.xg.referer', + type: 'keyword', + }, + 'sophos.xg.spamaction': { + category: 'sophos', + description: 'Spam Action ', + name: 'sophos.xg.spamaction', + type: 'keyword', + }, + 'sophos.xg.mailid': { + category: 'sophos', + description: 'mailid ', + name: 'sophos.xg.mailid', + type: 'keyword', + }, + 'sophos.xg.quarantine_reason': { + category: 'sophos', + description: 'Quarantine reason ', + name: 'sophos.xg.quarantine_reason', + type: 'keyword', + }, + 'sophos.xg.status_code': { + category: 'sophos', + description: 'Status code ', + name: 'sophos.xg.status_code', + type: 'keyword', + }, + 'sophos.xg.override_token': { + category: 'sophos', + description: 'Override token ', + name: 'sophos.xg.override_token', + type: 'keyword', + }, + 'sophos.xg.con_id': { + category: 'sophos', + description: 'Unique identifier of connection ', + name: 'sophos.xg.con_id', + type: 'integer', + }, + 'sophos.xg.override_authorizer': { + category: 'sophos', + description: 'Override authorizer ', + name: 'sophos.xg.override_authorizer', + type: 'keyword', + }, + 'sophos.xg.transactionid': { + category: 'sophos', + description: 'Transaction ID of the AV scan. ', + name: 'sophos.xg.transactionid', + type: 'keyword', + }, + 'sophos.xg.upload_file_type': { + category: 'sophos', + description: 'Upload file type ', + name: 'sophos.xg.upload_file_type', + type: 'keyword', + }, + 'sophos.xg.upload_file_name': { + category: 'sophos', + description: 'Upload file name ', + name: 'sophos.xg.upload_file_name', + type: 'keyword', + }, + 'sophos.xg.httpresponsecode': { + category: 'sophos', + description: 'code of HTTP response ', + name: 'sophos.xg.httpresponsecode', + type: 'long', + }, + 'sophos.xg.user_gp': { + category: 'sophos', + description: 'Group name to which the user belongs. ', + name: 'sophos.xg.user_gp', + type: 'keyword', + }, + 'sophos.xg.category_type': { + category: 'sophos', + description: 'Type of category under which website falls ', + name: 'sophos.xg.category_type', + type: 'keyword', + }, + 'sophos.xg.download_file_type': { + category: 'sophos', + description: 'Download file type ', + name: 'sophos.xg.download_file_type', + type: 'keyword', + }, + 'sophos.xg.exceptions': { + category: 'sophos', + description: 'List of the checks excluded by web exceptions. ', + name: 'sophos.xg.exceptions', + type: 'keyword', + }, + 'sophos.xg.contenttype': { + category: 'sophos', + description: 'Type of the content ', + name: 'sophos.xg.contenttype', + type: 'keyword', + }, + 'sophos.xg.override_name': { + category: 'sophos', + description: 'Override name ', + name: 'sophos.xg.override_name', + type: 'keyword', + }, + 'sophos.xg.activityname': { + category: 'sophos', + description: 'Web policy activity that matched and caused the policy result. ', + name: 'sophos.xg.activityname', + type: 'keyword', + }, + 'sophos.xg.download_file_name': { + category: 'sophos', + description: 'Download file name ', + name: 'sophos.xg.download_file_name', + type: 'keyword', + }, + 'sophos.xg.sha1sum': { + category: 'sophos', + description: 'SHA1 checksum of the item being analyzed ', + name: 'sophos.xg.sha1sum', + type: 'keyword', + }, + 'sophos.xg.message_id': { + category: 'sophos', + description: 'Message ID ', + name: 'sophos.xg.message_id', + type: 'keyword', + }, + 'sophos.xg.connid': { + category: 'sophos', + description: 'Connection ID ', + name: 'sophos.xg.connid', + type: 'keyword', + }, + 'sophos.xg.message': { + category: 'sophos', + description: 'Message ', + name: 'sophos.xg.message', + type: 'keyword', + }, + 'sophos.xg.email_subject': { + category: 'sophos', + description: 'Email Subject ', + name: 'sophos.xg.email_subject', + type: 'keyword', + }, + 'sophos.xg.file_path': { + category: 'sophos', + description: 'File path ', + name: 'sophos.xg.file_path', + type: 'keyword', + }, + 'sophos.xg.dstdomain': { + category: 'sophos', + description: 'Destination Domain ', + name: 'sophos.xg.dstdomain', + type: 'keyword', + }, + 'sophos.xg.file_size': { + category: 'sophos', + description: 'File Size ', + name: 'sophos.xg.file_size', + type: 'integer', + }, + 'sophos.xg.transaction_id': { + category: 'sophos', + description: 'Transaction ID ', + name: 'sophos.xg.transaction_id', + type: 'keyword', + }, + 'sophos.xg.website': { + category: 'sophos', + description: 'Website ', + name: 'sophos.xg.website', + type: 'keyword', + }, + 'sophos.xg.file_name': { + category: 'sophos', + description: 'Filename ', + name: 'sophos.xg.file_name', + type: 'keyword', + }, + 'sophos.xg.context_prefix': { + category: 'sophos', + description: 'Content Prefix ', + name: 'sophos.xg.context_prefix', + type: 'keyword', + }, + 'sophos.xg.site_category': { + category: 'sophos', + description: 'Site Category ', + name: 'sophos.xg.site_category', + type: 'keyword', + }, + 'sophos.xg.context_suffix': { + category: 'sophos', + description: 'Context Suffix ', + name: 'sophos.xg.context_suffix', + type: 'keyword', + }, + 'sophos.xg.dictionary_name': { + category: 'sophos', + description: 'Dictionary Name ', + name: 'sophos.xg.dictionary_name', + type: 'keyword', + }, + 'sophos.xg.action': { + category: 'sophos', + description: 'Event Action ', + name: 'sophos.xg.action', + type: 'keyword', + }, + 'sophos.xg.user': { + category: 'sophos', + description: 'User ', + name: 'sophos.xg.user', + type: 'keyword', + }, + 'sophos.xg.context_match': { + category: 'sophos', + description: 'Context Match ', + name: 'sophos.xg.context_match', + type: 'keyword', + }, + 'sophos.xg.direction': { + category: 'sophos', + description: 'Direction ', + name: 'sophos.xg.direction', + type: 'keyword', + }, + 'sophos.xg.auth_client': { + category: 'sophos', + description: 'Auth Client ', + name: 'sophos.xg.auth_client', + type: 'keyword', + }, + 'sophos.xg.auth_mechanism': { + category: 'sophos', + description: 'Auth mechanism ', + name: 'sophos.xg.auth_mechanism', + type: 'keyword', + }, + 'sophos.xg.connectionname': { + category: 'sophos', + description: 'Connectionname ', + name: 'sophos.xg.connectionname', + type: 'keyword', + }, + 'sophos.xg.remotenetwork': { + category: 'sophos', + description: 'remotenetwork ', + name: 'sophos.xg.remotenetwork', + type: 'keyword', + }, + 'sophos.xg.localgateway': { + category: 'sophos', + description: 'Localgateway ', + name: 'sophos.xg.localgateway', + type: 'keyword', + }, + 'sophos.xg.localnetwork': { + category: 'sophos', + description: 'Localnetwork ', + name: 'sophos.xg.localnetwork', + type: 'keyword', + }, + 'sophos.xg.connectiontype': { + category: 'sophos', + description: 'Connectiontype ', + name: 'sophos.xg.connectiontype', + type: 'keyword', + }, + 'sophos.xg.oldversion': { + category: 'sophos', + description: 'Oldversion ', + name: 'sophos.xg.oldversion', + type: 'keyword', + }, + 'sophos.xg.newversion': { + category: 'sophos', + description: 'Newversion ', + name: 'sophos.xg.newversion', + type: 'keyword', + }, + 'sophos.xg.ipaddress': { + category: 'sophos', + description: 'Ipaddress ', + name: 'sophos.xg.ipaddress', + type: 'keyword', + }, + 'sophos.xg.client_physical_address': { + category: 'sophos', + description: 'Client physical address ', + name: 'sophos.xg.client_physical_address', + type: 'keyword', + }, + 'sophos.xg.client_host_name': { + category: 'sophos', + description: 'Client host name ', + name: 'sophos.xg.client_host_name', + type: 'keyword', + }, + 'sophos.xg.raw_data': { + category: 'sophos', + description: 'Raw data ', + name: 'sophos.xg.raw_data', + type: 'keyword', + }, + 'sophos.xg.Mode': { + category: 'sophos', + description: 'Mode ', + name: 'sophos.xg.Mode', + type: 'keyword', + }, + 'sophos.xg.sessionid': { + category: 'sophos', + description: 'Sessionid ', + name: 'sophos.xg.sessionid', + type: 'keyword', + }, + 'sophos.xg.starttime': { + category: 'sophos', + description: 'Starttime ', + name: 'sophos.xg.starttime', + type: 'date', + }, + 'sophos.xg.remote_ip': { + category: 'sophos', + description: 'Remote IP ', + name: 'sophos.xg.remote_ip', + type: 'ip', + }, + 'sophos.xg.timestamp': { + category: 'sophos', + description: 'timestamp ', + name: 'sophos.xg.timestamp', + type: 'date', + }, + 'sophos.xg.SysLog_SERVER_NAME': { + category: 'sophos', + description: 'SysLog SERVER NAME ', + name: 'sophos.xg.SysLog_SERVER_NAME', + type: 'keyword', + }, + 'sophos.xg.backup_mode': { + category: 'sophos', + description: 'Backup mode ', + name: 'sophos.xg.backup_mode', + type: 'keyword', + }, + 'sophos.xg.source': { + category: 'sophos', + description: 'Source ', + name: 'sophos.xg.source', + type: 'keyword', + }, + 'sophos.xg.server': { + category: 'sophos', + description: 'Server ', + name: 'sophos.xg.server', + type: 'keyword', + }, + 'sophos.xg.host': { + category: 'sophos', + description: 'Host ', + name: 'sophos.xg.host', + type: 'keyword', + }, + 'sophos.xg.responsetime': { + category: 'sophos', + description: 'Responsetime ', + name: 'sophos.xg.responsetime', + type: 'long', + }, + 'sophos.xg.cookie': { + category: 'sophos', + description: 'cookie ', + name: 'sophos.xg.cookie', + type: 'keyword', + }, + 'sophos.xg.querystring': { + category: 'sophos', + description: 'querystring ', + name: 'sophos.xg.querystring', + type: 'keyword', + }, + 'sophos.xg.extra': { + category: 'sophos', + description: 'extra ', + name: 'sophos.xg.extra', + type: 'keyword', + }, + 'sophos.xg.PHPSESSID': { + category: 'sophos', + description: 'PHPSESSID ', + name: 'sophos.xg.PHPSESSID', + type: 'keyword', + }, + 'sophos.xg.start_time': { + category: 'sophos', + description: 'Start time ', + name: 'sophos.xg.start_time', + type: 'date', + }, + 'sophos.xg.eventtime': { + category: 'sophos', + description: 'Event time ', + name: 'sophos.xg.eventtime', + type: 'date', + }, + 'sophos.xg.red_id': { + category: 'sophos', + description: 'RED ID ', + name: 'sophos.xg.red_id', + type: 'keyword', + }, + 'sophos.xg.branch_name': { + category: 'sophos', + description: 'Branch Name ', + name: 'sophos.xg.branch_name', + type: 'keyword', + }, + 'sophos.xg.updatedip': { + category: 'sophos', + description: 'updatedip ', + name: 'sophos.xg.updatedip', + type: 'ip', + }, + 'sophos.xg.idle_cpu': { + category: 'sophos', + description: 'idle ## ', + name: 'sophos.xg.idle_cpu', + type: 'float', + }, + 'sophos.xg.system_cpu': { + category: 'sophos', + description: 'system ', + name: 'sophos.xg.system_cpu', + type: 'float', + }, + 'sophos.xg.user_cpu': { + category: 'sophos', + description: 'system ', + name: 'sophos.xg.user_cpu', + type: 'float', + }, + 'sophos.xg.used': { + category: 'sophos', + description: 'used ', + name: 'sophos.xg.used', + type: 'integer', + }, + 'sophos.xg.unit': { + category: 'sophos', + description: 'unit ', + name: 'sophos.xg.unit', + type: 'keyword', + }, + 'sophos.xg.total_memory': { + category: 'sophos', + description: 'Total Memory ', + name: 'sophos.xg.total_memory', + type: 'integer', + }, + 'sophos.xg.free': { + category: 'sophos', + description: 'free ', + name: 'sophos.xg.free', + type: 'integer', + }, + 'sophos.xg.transmittederrors': { + category: 'sophos', + description: 'transmitted errors ', + name: 'sophos.xg.transmittederrors', + type: 'keyword', + }, + 'sophos.xg.receivederrors': { + category: 'sophos', + description: 'received errors ', + name: 'sophos.xg.receivederrors', + type: 'keyword', + }, + 'sophos.xg.receivedkbits': { + category: 'sophos', + description: 'received kbits ', + name: 'sophos.xg.receivedkbits', + type: 'long', + }, + 'sophos.xg.transmittedkbits': { + category: 'sophos', + description: 'transmitted kbits ', + name: 'sophos.xg.transmittedkbits', + type: 'long', + }, + 'sophos.xg.transmitteddrops': { + category: 'sophos', + description: 'transmitted drops ', + name: 'sophos.xg.transmitteddrops', + type: 'long', + }, + 'sophos.xg.receiveddrops': { + category: 'sophos', + description: 'received drops ', + name: 'sophos.xg.receiveddrops', + type: 'long', + }, + 'sophos.xg.collisions': { + category: 'sophos', + description: 'collisions ', + name: 'sophos.xg.collisions', + type: 'long', + }, + 'sophos.xg.interface': { + category: 'sophos', + description: 'interface ', + name: 'sophos.xg.interface', + type: 'keyword', + }, + 'sophos.xg.Configuration': { + category: 'sophos', + description: 'Configuration ', + name: 'sophos.xg.Configuration', + type: 'float', + }, + 'sophos.xg.Reports': { + category: 'sophos', + description: 'Reports ', + name: 'sophos.xg.Reports', + type: 'float', + }, + 'sophos.xg.Signature': { + category: 'sophos', + description: 'Signature ', + name: 'sophos.xg.Signature', + type: 'float', + }, + 'sophos.xg.Temp': { + category: 'sophos', + description: 'Temp ', + name: 'sophos.xg.Temp', + type: 'float', + }, + 'sophos.xg.users': { + category: 'sophos', + description: 'users ', + name: 'sophos.xg.users', + type: 'keyword', + }, + 'sophos.xg.ssid': { + category: 'sophos', + description: 'ssid ', + name: 'sophos.xg.ssid', + type: 'keyword', + }, + 'sophos.xg.ap': { + category: 'sophos', + description: 'ap ', + name: 'sophos.xg.ap', + type: 'keyword', + }, + 'sophos.xg.clients_conn_ssid': { + category: 'sophos', + description: 'clients connection ssid ', + name: 'sophos.xg.clients_conn_ssid', + type: 'keyword', + }, + 'suricata.eve.event_type': { + category: 'suricata', + name: 'suricata.eve.event_type', + type: 'keyword', + }, + 'suricata.eve.app_proto_orig': { + category: 'suricata', + name: 'suricata.eve.app_proto_orig', + type: 'keyword', + }, + 'suricata.eve.tcp.tcp_flags': { + category: 'suricata', + name: 'suricata.eve.tcp.tcp_flags', + type: 'keyword', + }, + 'suricata.eve.tcp.psh': { + category: 'suricata', + name: 'suricata.eve.tcp.psh', + type: 'boolean', + }, + 'suricata.eve.tcp.tcp_flags_tc': { + category: 'suricata', + name: 'suricata.eve.tcp.tcp_flags_tc', + type: 'keyword', + }, + 'suricata.eve.tcp.ack': { + category: 'suricata', + name: 'suricata.eve.tcp.ack', + type: 'boolean', + }, + 'suricata.eve.tcp.syn': { + category: 'suricata', + name: 'suricata.eve.tcp.syn', + type: 'boolean', + }, + 'suricata.eve.tcp.state': { + category: 'suricata', + name: 'suricata.eve.tcp.state', + type: 'keyword', + }, + 'suricata.eve.tcp.tcp_flags_ts': { + category: 'suricata', + name: 'suricata.eve.tcp.tcp_flags_ts', + type: 'keyword', + }, + 'suricata.eve.tcp.rst': { + category: 'suricata', + name: 'suricata.eve.tcp.rst', + type: 'boolean', + }, + 'suricata.eve.tcp.fin': { + category: 'suricata', + name: 'suricata.eve.tcp.fin', + type: 'boolean', + }, + 'suricata.eve.fileinfo.sha1': { + category: 'suricata', + name: 'suricata.eve.fileinfo.sha1', + type: 'keyword', + }, + 'suricata.eve.fileinfo.filename': { + category: 'suricata', + name: 'suricata.eve.fileinfo.filename', + type: 'alias', + }, + 'suricata.eve.fileinfo.tx_id': { + category: 'suricata', + name: 'suricata.eve.fileinfo.tx_id', + type: 'long', + }, + 'suricata.eve.fileinfo.state': { + category: 'suricata', + name: 'suricata.eve.fileinfo.state', + type: 'keyword', + }, + 'suricata.eve.fileinfo.stored': { + category: 'suricata', + name: 'suricata.eve.fileinfo.stored', + type: 'boolean', + }, + 'suricata.eve.fileinfo.gaps': { + category: 'suricata', + name: 'suricata.eve.fileinfo.gaps', + type: 'boolean', + }, + 'suricata.eve.fileinfo.sha256': { + category: 'suricata', + name: 'suricata.eve.fileinfo.sha256', + type: 'keyword', + }, + 'suricata.eve.fileinfo.md5': { + category: 'suricata', + name: 'suricata.eve.fileinfo.md5', + type: 'keyword', + }, + 'suricata.eve.fileinfo.size': { + category: 'suricata', + name: 'suricata.eve.fileinfo.size', + type: 'alias', + }, + 'suricata.eve.icmp_type': { + category: 'suricata', + name: 'suricata.eve.icmp_type', + type: 'long', + }, + 'suricata.eve.dest_port': { + category: 'suricata', + name: 'suricata.eve.dest_port', + type: 'alias', + }, + 'suricata.eve.src_port': { + category: 'suricata', + name: 'suricata.eve.src_port', + type: 'alias', + }, + 'suricata.eve.proto': { + category: 'suricata', + name: 'suricata.eve.proto', + type: 'alias', + }, + 'suricata.eve.pcap_cnt': { + category: 'suricata', + name: 'suricata.eve.pcap_cnt', + type: 'long', + }, + 'suricata.eve.src_ip': { + category: 'suricata', + name: 'suricata.eve.src_ip', + type: 'alias', + }, + 'suricata.eve.dns.type': { + category: 'suricata', + name: 'suricata.eve.dns.type', + type: 'keyword', + }, + 'suricata.eve.dns.rrtype': { + category: 'suricata', + name: 'suricata.eve.dns.rrtype', + type: 'keyword', + }, + 'suricata.eve.dns.rrname': { + category: 'suricata', + name: 'suricata.eve.dns.rrname', + type: 'keyword', + }, + 'suricata.eve.dns.rdata': { + category: 'suricata', + name: 'suricata.eve.dns.rdata', + type: 'keyword', + }, + 'suricata.eve.dns.tx_id': { + category: 'suricata', + name: 'suricata.eve.dns.tx_id', + type: 'long', + }, + 'suricata.eve.dns.ttl': { + category: 'suricata', + name: 'suricata.eve.dns.ttl', + type: 'long', + }, + 'suricata.eve.dns.rcode': { + category: 'suricata', + name: 'suricata.eve.dns.rcode', + type: 'keyword', + }, + 'suricata.eve.dns.id': { + category: 'suricata', + name: 'suricata.eve.dns.id', + type: 'long', + }, + 'suricata.eve.flow_id': { + category: 'suricata', + name: 'suricata.eve.flow_id', + type: 'keyword', + }, + 'suricata.eve.email.status': { + category: 'suricata', + name: 'suricata.eve.email.status', + type: 'keyword', + }, + 'suricata.eve.dest_ip': { + category: 'suricata', + name: 'suricata.eve.dest_ip', + type: 'alias', + }, + 'suricata.eve.icmp_code': { + category: 'suricata', + name: 'suricata.eve.icmp_code', + type: 'long', + }, + 'suricata.eve.http.status': { + category: 'suricata', + name: 'suricata.eve.http.status', + type: 'alias', + }, + 'suricata.eve.http.redirect': { + category: 'suricata', + name: 'suricata.eve.http.redirect', + type: 'keyword', + }, + 'suricata.eve.http.http_user_agent': { + category: 'suricata', + name: 'suricata.eve.http.http_user_agent', + type: 'alias', + }, + 'suricata.eve.http.protocol': { + category: 'suricata', + name: 'suricata.eve.http.protocol', + type: 'keyword', + }, + 'suricata.eve.http.http_refer': { + category: 'suricata', + name: 'suricata.eve.http.http_refer', + type: 'alias', + }, + 'suricata.eve.http.url': { + category: 'suricata', + name: 'suricata.eve.http.url', + type: 'alias', + }, + 'suricata.eve.http.hostname': { + category: 'suricata', + name: 'suricata.eve.http.hostname', + type: 'alias', + }, + 'suricata.eve.http.length': { + category: 'suricata', + name: 'suricata.eve.http.length', + type: 'alias', + }, + 'suricata.eve.http.http_method': { + category: 'suricata', + name: 'suricata.eve.http.http_method', + type: 'alias', + }, + 'suricata.eve.http.http_content_type': { + category: 'suricata', + name: 'suricata.eve.http.http_content_type', + type: 'keyword', + }, + 'suricata.eve.timestamp': { + category: 'suricata', + name: 'suricata.eve.timestamp', + type: 'alias', + }, + 'suricata.eve.in_iface': { + category: 'suricata', + name: 'suricata.eve.in_iface', + type: 'keyword', + }, + 'suricata.eve.alert.category': { + category: 'suricata', + name: 'suricata.eve.alert.category', + type: 'keyword', + }, + 'suricata.eve.alert.severity': { + category: 'suricata', + name: 'suricata.eve.alert.severity', + type: 'alias', + }, + 'suricata.eve.alert.rev': { + category: 'suricata', + name: 'suricata.eve.alert.rev', + type: 'long', + }, + 'suricata.eve.alert.gid': { + category: 'suricata', + name: 'suricata.eve.alert.gid', + type: 'long', + }, + 'suricata.eve.alert.signature': { + category: 'suricata', + name: 'suricata.eve.alert.signature', + type: 'keyword', + }, + 'suricata.eve.alert.action': { + category: 'suricata', + name: 'suricata.eve.alert.action', + type: 'alias', + }, + 'suricata.eve.alert.signature_id': { + category: 'suricata', + name: 'suricata.eve.alert.signature_id', + type: 'long', + }, + 'suricata.eve.ssh.client.proto_version': { + category: 'suricata', + name: 'suricata.eve.ssh.client.proto_version', + type: 'keyword', + }, + 'suricata.eve.ssh.client.software_version': { + category: 'suricata', + name: 'suricata.eve.ssh.client.software_version', + type: 'keyword', + }, + 'suricata.eve.ssh.server.proto_version': { + category: 'suricata', + name: 'suricata.eve.ssh.server.proto_version', + type: 'keyword', + }, + 'suricata.eve.ssh.server.software_version': { + category: 'suricata', + name: 'suricata.eve.ssh.server.software_version', + type: 'keyword', + }, + 'suricata.eve.stats.capture.kernel_packets': { + category: 'suricata', + name: 'suricata.eve.stats.capture.kernel_packets', + type: 'long', + }, + 'suricata.eve.stats.capture.kernel_drops': { + category: 'suricata', + name: 'suricata.eve.stats.capture.kernel_drops', + type: 'long', + }, + 'suricata.eve.stats.capture.kernel_ifdrops': { + category: 'suricata', + name: 'suricata.eve.stats.capture.kernel_ifdrops', + type: 'long', + }, + 'suricata.eve.stats.uptime': { + category: 'suricata', + name: 'suricata.eve.stats.uptime', + type: 'long', + }, + 'suricata.eve.stats.detect.alert': { + category: 'suricata', + name: 'suricata.eve.stats.detect.alert', + type: 'long', + }, + 'suricata.eve.stats.http.memcap': { + category: 'suricata', + name: 'suricata.eve.stats.http.memcap', + type: 'long', + }, + 'suricata.eve.stats.http.memuse': { + category: 'suricata', + name: 'suricata.eve.stats.http.memuse', + type: 'long', + }, + 'suricata.eve.stats.file_store.open_files': { + category: 'suricata', + name: 'suricata.eve.stats.file_store.open_files', + type: 'long', + }, + 'suricata.eve.stats.defrag.max_frag_hits': { + category: 'suricata', + name: 'suricata.eve.stats.defrag.max_frag_hits', + type: 'long', + }, + 'suricata.eve.stats.defrag.ipv4.timeouts': { + category: 'suricata', + name: 'suricata.eve.stats.defrag.ipv4.timeouts', + type: 'long', + }, + 'suricata.eve.stats.defrag.ipv4.fragments': { + category: 'suricata', + name: 'suricata.eve.stats.defrag.ipv4.fragments', + type: 'long', + }, + 'suricata.eve.stats.defrag.ipv4.reassembled': { + category: 'suricata', + name: 'suricata.eve.stats.defrag.ipv4.reassembled', + type: 'long', + }, + 'suricata.eve.stats.defrag.ipv6.timeouts': { + category: 'suricata', + name: 'suricata.eve.stats.defrag.ipv6.timeouts', + type: 'long', + }, + 'suricata.eve.stats.defrag.ipv6.fragments': { + category: 'suricata', + name: 'suricata.eve.stats.defrag.ipv6.fragments', + type: 'long', + }, + 'suricata.eve.stats.defrag.ipv6.reassembled': { + category: 'suricata', + name: 'suricata.eve.stats.defrag.ipv6.reassembled', + type: 'long', + }, + 'suricata.eve.stats.flow.tcp_reuse': { + category: 'suricata', + name: 'suricata.eve.stats.flow.tcp_reuse', + type: 'long', + }, + 'suricata.eve.stats.flow.udp': { + category: 'suricata', + name: 'suricata.eve.stats.flow.udp', + type: 'long', + }, + 'suricata.eve.stats.flow.memcap': { + category: 'suricata', + name: 'suricata.eve.stats.flow.memcap', + type: 'long', + }, + 'suricata.eve.stats.flow.emerg_mode_entered': { + category: 'suricata', + name: 'suricata.eve.stats.flow.emerg_mode_entered', + type: 'long', + }, + 'suricata.eve.stats.flow.emerg_mode_over': { + category: 'suricata', + name: 'suricata.eve.stats.flow.emerg_mode_over', + type: 'long', + }, + 'suricata.eve.stats.flow.tcp': { + category: 'suricata', + name: 'suricata.eve.stats.flow.tcp', + type: 'long', + }, + 'suricata.eve.stats.flow.icmpv6': { + category: 'suricata', + name: 'suricata.eve.stats.flow.icmpv6', + type: 'long', + }, + 'suricata.eve.stats.flow.icmpv4': { + category: 'suricata', + name: 'suricata.eve.stats.flow.icmpv4', + type: 'long', + }, + 'suricata.eve.stats.flow.spare': { + category: 'suricata', + name: 'suricata.eve.stats.flow.spare', + type: 'long', + }, + 'suricata.eve.stats.flow.memuse': { + category: 'suricata', + name: 'suricata.eve.stats.flow.memuse', + type: 'long', + }, + 'suricata.eve.stats.tcp.pseudo_failed': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.pseudo_failed', + type: 'long', + }, + 'suricata.eve.stats.tcp.ssn_memcap_drop': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.ssn_memcap_drop', + type: 'long', + }, + 'suricata.eve.stats.tcp.insert_data_overlap_fail': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.insert_data_overlap_fail', + type: 'long', + }, + 'suricata.eve.stats.tcp.sessions': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.sessions', + type: 'long', + }, + 'suricata.eve.stats.tcp.pseudo': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.pseudo', + type: 'long', + }, + 'suricata.eve.stats.tcp.synack': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.synack', + type: 'long', + }, + 'suricata.eve.stats.tcp.insert_data_normal_fail': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.insert_data_normal_fail', + type: 'long', + }, + 'suricata.eve.stats.tcp.syn': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.syn', + type: 'long', + }, + 'suricata.eve.stats.tcp.memuse': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.memuse', + type: 'long', + }, + 'suricata.eve.stats.tcp.invalid_checksum': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.invalid_checksum', + type: 'long', + }, + 'suricata.eve.stats.tcp.segment_memcap_drop': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.segment_memcap_drop', + type: 'long', + }, + 'suricata.eve.stats.tcp.overlap': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.overlap', + type: 'long', + }, + 'suricata.eve.stats.tcp.insert_list_fail': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.insert_list_fail', + type: 'long', + }, + 'suricata.eve.stats.tcp.rst': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.rst', + type: 'long', + }, + 'suricata.eve.stats.tcp.stream_depth_reached': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.stream_depth_reached', + type: 'long', + }, + 'suricata.eve.stats.tcp.reassembly_memuse': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.reassembly_memuse', + type: 'long', + }, + 'suricata.eve.stats.tcp.reassembly_gap': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.reassembly_gap', + type: 'long', + }, + 'suricata.eve.stats.tcp.overlap_diff_data': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.overlap_diff_data', + type: 'long', + }, + 'suricata.eve.stats.tcp.no_flow': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.no_flow', + type: 'long', + }, + 'suricata.eve.stats.decoder.avg_pkt_size': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.avg_pkt_size', + type: 'long', + }, + 'suricata.eve.stats.decoder.bytes': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.bytes', + type: 'long', + }, + 'suricata.eve.stats.decoder.tcp': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.tcp', + type: 'long', + }, + 'suricata.eve.stats.decoder.raw': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.raw', + type: 'long', + }, + 'suricata.eve.stats.decoder.ppp': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.ppp', + type: 'long', + }, + 'suricata.eve.stats.decoder.vlan_qinq': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.vlan_qinq', + type: 'long', + }, + 'suricata.eve.stats.decoder.null': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.null', + type: 'long', + }, + 'suricata.eve.stats.decoder.ltnull.unsupported_type': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.ltnull.unsupported_type', + type: 'long', + }, + 'suricata.eve.stats.decoder.ltnull.pkt_too_small': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.ltnull.pkt_too_small', + type: 'long', + }, + 'suricata.eve.stats.decoder.invalid': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.invalid', + type: 'long', + }, + 'suricata.eve.stats.decoder.gre': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.gre', + type: 'long', + }, + 'suricata.eve.stats.decoder.ipv4': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.ipv4', + type: 'long', + }, + 'suricata.eve.stats.decoder.ipv6': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.ipv6', + type: 'long', + }, + 'suricata.eve.stats.decoder.pkts': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.pkts', + type: 'long', + }, + 'suricata.eve.stats.decoder.ipv6_in_ipv6': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.ipv6_in_ipv6', + type: 'long', + }, + 'suricata.eve.stats.decoder.ipraw.invalid_ip_version': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.ipraw.invalid_ip_version', + type: 'long', + }, + 'suricata.eve.stats.decoder.pppoe': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.pppoe', + type: 'long', + }, + 'suricata.eve.stats.decoder.udp': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.udp', + type: 'long', + }, + 'suricata.eve.stats.decoder.dce.pkt_too_small': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.dce.pkt_too_small', + type: 'long', + }, + 'suricata.eve.stats.decoder.vlan': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.vlan', + type: 'long', + }, + 'suricata.eve.stats.decoder.sctp': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.sctp', + type: 'long', + }, + 'suricata.eve.stats.decoder.max_pkt_size': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.max_pkt_size', + type: 'long', + }, + 'suricata.eve.stats.decoder.teredo': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.teredo', + type: 'long', + }, + 'suricata.eve.stats.decoder.mpls': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.mpls', + type: 'long', + }, + 'suricata.eve.stats.decoder.sll': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.sll', + type: 'long', + }, + 'suricata.eve.stats.decoder.icmpv6': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.icmpv6', + type: 'long', + }, + 'suricata.eve.stats.decoder.icmpv4': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.icmpv4', + type: 'long', + }, + 'suricata.eve.stats.decoder.erspan': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.erspan', + type: 'long', + }, + 'suricata.eve.stats.decoder.ethernet': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.ethernet', + type: 'long', + }, + 'suricata.eve.stats.decoder.ipv4_in_ipv6': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.ipv4_in_ipv6', + type: 'long', + }, + 'suricata.eve.stats.decoder.ieee8021ah': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.ieee8021ah', + type: 'long', + }, + 'suricata.eve.stats.dns.memcap_global': { + category: 'suricata', + name: 'suricata.eve.stats.dns.memcap_global', + type: 'long', + }, + 'suricata.eve.stats.dns.memcap_state': { + category: 'suricata', + name: 'suricata.eve.stats.dns.memcap_state', + type: 'long', + }, + 'suricata.eve.stats.dns.memuse': { + category: 'suricata', + name: 'suricata.eve.stats.dns.memuse', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.rows_busy': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.rows_busy', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.flows_timeout': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.flows_timeout', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.flows_notimeout': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.flows_notimeout', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.rows_skipped': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.rows_skipped', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.closed_pruned': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.closed_pruned', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.new_pruned': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.new_pruned', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.flows_removed': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.flows_removed', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.bypassed_pruned': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.bypassed_pruned', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.est_pruned': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.est_pruned', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.flows_timeout_inuse': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.flows_timeout_inuse', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.flows_checked': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.flows_checked', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.rows_maxlen': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.rows_maxlen', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.rows_checked': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.rows_checked', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.rows_empty': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.rows_empty', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.tls': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.tls', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.ftp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.ftp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.http': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.http', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.failed_udp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.failed_udp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.dns_udp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.dns_udp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.dns_tcp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.dns_tcp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.smtp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.smtp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.failed_tcp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.failed_tcp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.msn': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.msn', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.ssh': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.ssh', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.imap': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.imap', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.dcerpc_udp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.dcerpc_udp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.dcerpc_tcp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.dcerpc_tcp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.smb': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.smb', + type: 'long', + }, + 'suricata.eve.stats.app_layer.tx.tls': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.tx.tls', + type: 'long', + }, + 'suricata.eve.stats.app_layer.tx.ftp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.tx.ftp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.tx.http': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.tx.http', + type: 'long', + }, + 'suricata.eve.stats.app_layer.tx.dns_udp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.tx.dns_udp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.tx.dns_tcp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.tx.dns_tcp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.tx.smtp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.tx.smtp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.tx.ssh': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.tx.ssh', + type: 'long', + }, + 'suricata.eve.stats.app_layer.tx.dcerpc_udp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.tx.dcerpc_udp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.tx.dcerpc_tcp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.tx.dcerpc_tcp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.tx.smb': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.tx.smb', + type: 'long', + }, + 'suricata.eve.tls.notbefore': { + category: 'suricata', + name: 'suricata.eve.tls.notbefore', + type: 'date', + }, + 'suricata.eve.tls.issuerdn': { + category: 'suricata', + name: 'suricata.eve.tls.issuerdn', + type: 'keyword', + }, + 'suricata.eve.tls.sni': { + category: 'suricata', + name: 'suricata.eve.tls.sni', + type: 'keyword', + }, + 'suricata.eve.tls.version': { + category: 'suricata', + name: 'suricata.eve.tls.version', + type: 'keyword', + }, + 'suricata.eve.tls.session_resumed': { + category: 'suricata', + name: 'suricata.eve.tls.session_resumed', + type: 'boolean', + }, + 'suricata.eve.tls.fingerprint': { + category: 'suricata', + name: 'suricata.eve.tls.fingerprint', + type: 'keyword', + }, + 'suricata.eve.tls.serial': { + category: 'suricata', + name: 'suricata.eve.tls.serial', + type: 'keyword', + }, + 'suricata.eve.tls.notafter': { + category: 'suricata', + name: 'suricata.eve.tls.notafter', + type: 'date', + }, + 'suricata.eve.tls.subject': { + category: 'suricata', + name: 'suricata.eve.tls.subject', + type: 'keyword', + }, + 'suricata.eve.tls.ja3s.string': { + category: 'suricata', + name: 'suricata.eve.tls.ja3s.string', + type: 'keyword', + }, + 'suricata.eve.tls.ja3s.hash': { + category: 'suricata', + name: 'suricata.eve.tls.ja3s.hash', + type: 'keyword', + }, + 'suricata.eve.tls.ja3.string': { + category: 'suricata', + name: 'suricata.eve.tls.ja3.string', + type: 'keyword', + }, + 'suricata.eve.tls.ja3.hash': { + category: 'suricata', + name: 'suricata.eve.tls.ja3.hash', + type: 'keyword', + }, + 'suricata.eve.app_proto_ts': { + category: 'suricata', + name: 'suricata.eve.app_proto_ts', + type: 'keyword', + }, + 'suricata.eve.flow.bytes_toclient': { + category: 'suricata', + name: 'suricata.eve.flow.bytes_toclient', + type: 'alias', + }, + 'suricata.eve.flow.start': { + category: 'suricata', + name: 'suricata.eve.flow.start', + type: 'alias', + }, + 'suricata.eve.flow.pkts_toclient': { + category: 'suricata', + name: 'suricata.eve.flow.pkts_toclient', + type: 'alias', + }, + 'suricata.eve.flow.age': { + category: 'suricata', + name: 'suricata.eve.flow.age', + type: 'long', + }, + 'suricata.eve.flow.state': { + category: 'suricata', + name: 'suricata.eve.flow.state', + type: 'keyword', + }, + 'suricata.eve.flow.bytes_toserver': { + category: 'suricata', + name: 'suricata.eve.flow.bytes_toserver', + type: 'alias', + }, + 'suricata.eve.flow.reason': { + category: 'suricata', + name: 'suricata.eve.flow.reason', + type: 'keyword', + }, + 'suricata.eve.flow.pkts_toserver': { + category: 'suricata', + name: 'suricata.eve.flow.pkts_toserver', + type: 'alias', + }, + 'suricata.eve.flow.end': { + category: 'suricata', + name: 'suricata.eve.flow.end', + type: 'date', + }, + 'suricata.eve.flow.alerted': { + category: 'suricata', + name: 'suricata.eve.flow.alerted', + type: 'boolean', + }, + 'suricata.eve.app_proto': { + category: 'suricata', + name: 'suricata.eve.app_proto', + type: 'alias', + }, + 'suricata.eve.tx_id': { + category: 'suricata', + name: 'suricata.eve.tx_id', + type: 'long', + }, + 'suricata.eve.app_proto_tc': { + category: 'suricata', + name: 'suricata.eve.app_proto_tc', + type: 'keyword', + }, + 'suricata.eve.smtp.rcpt_to': { + category: 'suricata', + name: 'suricata.eve.smtp.rcpt_to', + type: 'keyword', + }, + 'suricata.eve.smtp.mail_from': { + category: 'suricata', + name: 'suricata.eve.smtp.mail_from', + type: 'keyword', + }, + 'suricata.eve.smtp.helo': { + category: 'suricata', + name: 'suricata.eve.smtp.helo', + type: 'keyword', + }, + 'suricata.eve.app_proto_expected': { + category: 'suricata', + name: 'suricata.eve.app_proto_expected', + type: 'keyword', + }, + 'suricata.eve.flags': { + category: 'suricata', + name: 'suricata.eve.flags', + type: 'group', + }, + 'zeek.session_id': { + category: 'zeek', + description: 'A unique identifier of the session ', + name: 'zeek.session_id', + type: 'keyword', + }, + 'zeek.capture_loss.ts_delta': { + category: 'zeek', + description: 'The time delay between this measurement and the last. ', + name: 'zeek.capture_loss.ts_delta', + type: 'integer', + }, + 'zeek.capture_loss.peer': { + category: 'zeek', + description: + 'In the event that there are multiple Bro instances logging to the same host, this distinguishes each peer with its individual name. ', + name: 'zeek.capture_loss.peer', + type: 'keyword', + }, + 'zeek.capture_loss.gaps': { + category: 'zeek', + description: 'Number of missed ACKs from the previous measurement interval. ', + name: 'zeek.capture_loss.gaps', + type: 'integer', + }, + 'zeek.capture_loss.acks': { + category: 'zeek', + description: 'Total number of ACKs seen in the previous measurement interval. ', + name: 'zeek.capture_loss.acks', + type: 'integer', + }, + 'zeek.capture_loss.percent_lost': { + category: 'zeek', + description: "Percentage of ACKs seen where the data being ACKed wasn't seen. ", + name: 'zeek.capture_loss.percent_lost', + type: 'double', + }, + 'zeek.connection.local_orig': { + category: 'zeek', + description: 'Indicates whether the session is originated locally. ', + name: 'zeek.connection.local_orig', + type: 'boolean', + }, + 'zeek.connection.local_resp': { + category: 'zeek', + description: 'Indicates whether the session is responded locally. ', + name: 'zeek.connection.local_resp', + type: 'boolean', + }, + 'zeek.connection.missed_bytes': { + category: 'zeek', + description: 'Missed bytes for the session. ', + name: 'zeek.connection.missed_bytes', + type: 'long', + }, + 'zeek.connection.state': { + category: 'zeek', + description: 'Code indicating the state of the session. ', + name: 'zeek.connection.state', + type: 'keyword', + }, + 'zeek.connection.state_message': { + category: 'zeek', + description: 'The state of the session. ', + name: 'zeek.connection.state_message', + type: 'keyword', + }, + 'zeek.connection.icmp.type': { + category: 'zeek', + description: 'ICMP message type. ', + name: 'zeek.connection.icmp.type', + type: 'integer', + }, + 'zeek.connection.icmp.code': { + category: 'zeek', + description: 'ICMP message code. ', + name: 'zeek.connection.icmp.code', + type: 'integer', + }, + 'zeek.connection.history': { + category: 'zeek', + description: 'Flags indicating the history of the session. ', + name: 'zeek.connection.history', + type: 'keyword', + }, + 'zeek.connection.vlan': { + category: 'zeek', + description: 'VLAN identifier. ', + name: 'zeek.connection.vlan', + type: 'integer', + }, + 'zeek.connection.inner_vlan': { + category: 'zeek', + description: 'VLAN identifier. ', + name: 'zeek.connection.inner_vlan', + type: 'integer', + }, + 'zeek.dce_rpc.rtt': { + category: 'zeek', + description: + "Round trip time from the request to the response. If either the request or response wasn't seen, this will be null. ", + name: 'zeek.dce_rpc.rtt', + type: 'integer', + }, + 'zeek.dce_rpc.named_pipe': { + category: 'zeek', + description: 'Remote pipe name. ', + name: 'zeek.dce_rpc.named_pipe', + type: 'keyword', + }, + 'zeek.dce_rpc.endpoint': { + category: 'zeek', + description: 'Endpoint name looked up from the uuid. ', + name: 'zeek.dce_rpc.endpoint', + type: 'keyword', + }, + 'zeek.dce_rpc.operation': { + category: 'zeek', + description: 'Operation seen in the call. ', + name: 'zeek.dce_rpc.operation', + type: 'keyword', + }, + 'zeek.dhcp.domain': { + category: 'zeek', + description: 'Domain given by the server in option 15. ', + name: 'zeek.dhcp.domain', + type: 'keyword', + }, + 'zeek.dhcp.duration': { + category: 'zeek', + description: + 'Duration of the DHCP session representing the time from the first message to the last, in seconds. ', + name: 'zeek.dhcp.duration', + type: 'double', + }, + 'zeek.dhcp.hostname': { + category: 'zeek', + description: 'Name given by client in Hostname option 12. ', + name: 'zeek.dhcp.hostname', + type: 'keyword', + }, + 'zeek.dhcp.client_fqdn': { + category: 'zeek', + description: 'FQDN given by client in Client FQDN option 81. ', + name: 'zeek.dhcp.client_fqdn', + type: 'keyword', + }, + 'zeek.dhcp.lease_time': { + category: 'zeek', + description: 'IP address lease interval in seconds. ', + name: 'zeek.dhcp.lease_time', + type: 'integer', + }, + 'zeek.dhcp.address.assigned': { + category: 'zeek', + description: 'IP address assigned by the server. ', + name: 'zeek.dhcp.address.assigned', + type: 'ip', + }, + 'zeek.dhcp.address.client': { + category: 'zeek', + description: + 'IP address of the client. If a transaction is only a client sending INFORM messages then there is no lease information exchanged so this is helpful to know who sent the messages. Getting an address in this field does require that the client sources at least one DHCP message using a non-broadcast address. ', + name: 'zeek.dhcp.address.client', + type: 'ip', + }, + 'zeek.dhcp.address.mac': { + category: 'zeek', + description: "Client's hardware address. ", + name: 'zeek.dhcp.address.mac', + type: 'keyword', + }, + 'zeek.dhcp.address.requested': { + category: 'zeek', + description: 'IP address requested by the client. ', + name: 'zeek.dhcp.address.requested', + type: 'ip', + }, + 'zeek.dhcp.address.server': { + category: 'zeek', + description: 'IP address of the DHCP server. ', + name: 'zeek.dhcp.address.server', + type: 'ip', + }, + 'zeek.dhcp.msg.types': { + category: 'zeek', + description: 'List of DHCP message types seen in this exchange. ', + name: 'zeek.dhcp.msg.types', + type: 'keyword', + }, + 'zeek.dhcp.msg.origin': { + category: 'zeek', + description: + '(present if policy/protocols/dhcp/msg-orig.bro is loaded) The address that originated each message from the msg.types field. ', + name: 'zeek.dhcp.msg.origin', + type: 'ip', + }, + 'zeek.dhcp.msg.client': { + category: 'zeek', + description: + 'Message typically accompanied with a DHCP_DECLINE so the client can tell the server why it rejected an address. ', + name: 'zeek.dhcp.msg.client', + type: 'keyword', + }, + 'zeek.dhcp.msg.server': { + category: 'zeek', + description: + 'Message typically accompanied with a DHCP_NAK to let the client know why it rejected the request. ', + name: 'zeek.dhcp.msg.server', + type: 'keyword', + }, + 'zeek.dhcp.software.client': { + category: 'zeek', + description: + '(present if policy/protocols/dhcp/software.bro is loaded) Software reported by the client in the vendor_class option. ', + name: 'zeek.dhcp.software.client', + type: 'keyword', + }, + 'zeek.dhcp.software.server': { + category: 'zeek', + description: + '(present if policy/protocols/dhcp/software.bro is loaded) Software reported by the client in the vendor_class option. ', + name: 'zeek.dhcp.software.server', + type: 'keyword', + }, + 'zeek.dhcp.id.circuit': { + category: 'zeek', + description: + '(present if policy/protocols/dhcp/sub-opts.bro is loaded) Added by DHCP relay agents which terminate switched or permanent circuits. It encodes an agent-local identifier of the circuit from which a DHCP client-to-server packet was received. Typically it should represent a router or switch interface number. ', + name: 'zeek.dhcp.id.circuit', + type: 'keyword', + }, + 'zeek.dhcp.id.remote_agent': { + category: 'zeek', + description: + '(present if policy/protocols/dhcp/sub-opts.bro is loaded) A globally unique identifier added by relay agents to identify the remote host end of the circuit. ', + name: 'zeek.dhcp.id.remote_agent', + type: 'keyword', + }, + 'zeek.dhcp.id.subscriber': { + category: 'zeek', + description: + "(present if policy/protocols/dhcp/sub-opts.bro is loaded) The subscriber ID is a value independent of the physical network configuration so that a customer's DHCP configuration can be given to them correctly no matter where they are physically connected. ", + name: 'zeek.dhcp.id.subscriber', + type: 'keyword', + }, + 'zeek.dnp3.function.request': { + category: 'zeek', + description: 'The name of the function message in the request. ', + name: 'zeek.dnp3.function.request', + type: 'keyword', + }, + 'zeek.dnp3.function.reply': { + category: 'zeek', + description: 'The name of the function message in the reply. ', + name: 'zeek.dnp3.function.reply', + type: 'keyword', + }, + 'zeek.dnp3.id': { + category: 'zeek', + description: "The response's internal indication number. ", + name: 'zeek.dnp3.id', + type: 'integer', + }, + 'zeek.dns.trans_id': { + category: 'zeek', + description: 'DNS transaction identifier. ', + name: 'zeek.dns.trans_id', + type: 'keyword', + }, + 'zeek.dns.rtt': { + category: 'zeek', + description: 'Round trip time for the query and response. ', + name: 'zeek.dns.rtt', + type: 'double', + }, + 'zeek.dns.query': { + category: 'zeek', + description: 'The domain name that is the subject of the DNS query. ', + name: 'zeek.dns.query', + type: 'keyword', + }, + 'zeek.dns.qclass': { + category: 'zeek', + description: 'The QCLASS value specifying the class of the query. ', + name: 'zeek.dns.qclass', + type: 'long', + }, + 'zeek.dns.qclass_name': { + category: 'zeek', + description: 'A descriptive name for the class of the query. ', + name: 'zeek.dns.qclass_name', + type: 'keyword', + }, + 'zeek.dns.qtype': { + category: 'zeek', + description: 'A QTYPE value specifying the type of the query. ', + name: 'zeek.dns.qtype', + type: 'long', + }, + 'zeek.dns.qtype_name': { + category: 'zeek', + description: 'A descriptive name for the type of the query. ', + name: 'zeek.dns.qtype_name', + type: 'keyword', + }, + 'zeek.dns.rcode': { + category: 'zeek', + description: 'The response code value in DNS response messages. ', + name: 'zeek.dns.rcode', + type: 'long', + }, + 'zeek.dns.rcode_name': { + category: 'zeek', + description: 'A descriptive name for the response code value. ', + name: 'zeek.dns.rcode_name', + type: 'keyword', + }, + 'zeek.dns.AA': { + category: 'zeek', + description: + 'The Authoritative Answer bit for response messages specifies that the responding name server is an authority for the domain name in the question section. ', + name: 'zeek.dns.AA', + type: 'boolean', + }, + 'zeek.dns.TC': { + category: 'zeek', + description: 'The Truncation bit specifies that the message was truncated. ', + name: 'zeek.dns.TC', + type: 'boolean', + }, + 'zeek.dns.RD': { + category: 'zeek', + description: + 'The Recursion Desired bit in a request message indicates that the client wants recursive service for this query. ', + name: 'zeek.dns.RD', + type: 'boolean', + }, + 'zeek.dns.RA': { + category: 'zeek', + description: + 'The Recursion Available bit in a response message indicates that the name server supports recursive queries. ', + name: 'zeek.dns.RA', + type: 'boolean', + }, + 'zeek.dns.answers': { + category: 'zeek', + description: 'The set of resource descriptions in the query answer. ', + name: 'zeek.dns.answers', + type: 'keyword', + }, + 'zeek.dns.TTLs': { + category: 'zeek', + description: 'The caching intervals of the associated RRs described by the answers field. ', + name: 'zeek.dns.TTLs', + type: 'double', + }, + 'zeek.dns.rejected': { + category: 'zeek', + description: 'Indicates whether the DNS query was rejected by the server. ', + name: 'zeek.dns.rejected', + type: 'boolean', + }, + 'zeek.dns.total_answers': { + category: 'zeek', + description: 'The total number of resource records in the reply. ', + name: 'zeek.dns.total_answers', + type: 'integer', + }, + 'zeek.dns.total_replies': { + category: 'zeek', + description: 'The total number of resource records in the reply message. ', + name: 'zeek.dns.total_replies', + type: 'integer', + }, + 'zeek.dns.saw_query': { + category: 'zeek', + description: 'Whether the full DNS query has been seen. ', + name: 'zeek.dns.saw_query', + type: 'boolean', + }, + 'zeek.dns.saw_reply': { + category: 'zeek', + description: 'Whether the full DNS reply has been seen. ', + name: 'zeek.dns.saw_reply', + type: 'boolean', + }, + 'zeek.dpd.analyzer': { + category: 'zeek', + description: 'The analyzer that generated the violation. ', + name: 'zeek.dpd.analyzer', + type: 'keyword', + }, + 'zeek.dpd.failure_reason': { + category: 'zeek', + description: 'The textual reason for the analysis failure. ', + name: 'zeek.dpd.failure_reason', + type: 'keyword', + }, + 'zeek.dpd.packet_segment': { + category: 'zeek', + description: + '(present if policy/frameworks/dpd/packet-segment-logging.bro is loaded) A chunk of the payload that most likely resulted in the protocol violation. ', + name: 'zeek.dpd.packet_segment', + type: 'keyword', + }, + 'zeek.files.fuid': { + category: 'zeek', + description: 'A file unique identifier. ', + name: 'zeek.files.fuid', + type: 'keyword', + }, + 'zeek.files.tx_host': { + category: 'zeek', + description: 'The host that transferred the file. ', + name: 'zeek.files.tx_host', + type: 'ip', + }, + 'zeek.files.rx_host': { + category: 'zeek', + description: 'The host that received the file. ', + name: 'zeek.files.rx_host', + type: 'ip', + }, + 'zeek.files.session_ids': { + category: 'zeek', + description: 'The sessions that have this file. ', + name: 'zeek.files.session_ids', + type: 'keyword', + }, + 'zeek.files.source': { + category: 'zeek', + description: + 'An identification of the source of the file data. E.g. it may be a network protocol over which it was transferred, or a local file path which was read, or some other input source. ', + name: 'zeek.files.source', + type: 'keyword', + }, + 'zeek.files.depth': { + category: 'zeek', + description: + 'A value to represent the depth of this file in relation to its source. In SMTP, it is the depth of the MIME attachment on the message. In HTTP, it is the depth of the request within the TCP connection. ', + name: 'zeek.files.depth', + type: 'long', + }, + 'zeek.files.analyzers': { + category: 'zeek', + description: 'A set of analysis types done during the file analysis. ', + name: 'zeek.files.analyzers', + type: 'keyword', + }, + 'zeek.files.mime_type': { + category: 'zeek', + description: 'Mime type of the file. ', + name: 'zeek.files.mime_type', + type: 'keyword', + }, + 'zeek.files.filename': { + category: 'zeek', + description: 'Name of the file if available. ', + name: 'zeek.files.filename', + type: 'keyword', + }, + 'zeek.files.local_orig': { + category: 'zeek', + description: + 'If the source of this file is a network connection, this field indicates if the data originated from the local network or not. ', + name: 'zeek.files.local_orig', + type: 'boolean', + }, + 'zeek.files.is_orig': { + category: 'zeek', + description: + 'If the source of this file is a network connection, this field indicates if the file is being sent by the originator of the connection or the responder. ', + name: 'zeek.files.is_orig', + type: 'boolean', + }, + 'zeek.files.duration': { + category: 'zeek', + description: 'The duration the file was analyzed for. Not the duration of the session. ', + name: 'zeek.files.duration', + type: 'double', + }, + 'zeek.files.seen_bytes': { + category: 'zeek', + description: 'Number of bytes provided to the file analysis engine for the file. ', + name: 'zeek.files.seen_bytes', + type: 'long', + }, + 'zeek.files.total_bytes': { + category: 'zeek', + description: 'Total number of bytes that are supposed to comprise the full file. ', + name: 'zeek.files.total_bytes', + type: 'long', + }, + 'zeek.files.missing_bytes': { + category: 'zeek', + description: + 'The number of bytes in the file stream that were completely missed during the process of analysis. ', + name: 'zeek.files.missing_bytes', + type: 'long', + }, + 'zeek.files.overflow_bytes': { + category: 'zeek', + description: + "The number of bytes in the file stream that were not delivered to stream file analyzers. This could be overlapping bytes or bytes that couldn't be reassembled. ", + name: 'zeek.files.overflow_bytes', + type: 'long', + }, + 'zeek.files.timedout': { + category: 'zeek', + description: 'Whether the file analysis timed out at least once for the file. ', + name: 'zeek.files.timedout', + type: 'boolean', + }, + 'zeek.files.parent_fuid': { + category: 'zeek', + description: + 'Identifier associated with a container file from which this one was extracted as part of the file analysis. ', + name: 'zeek.files.parent_fuid', + type: 'keyword', + }, + 'zeek.files.md5': { + category: 'zeek', + description: 'An MD5 digest of the file contents. ', + name: 'zeek.files.md5', + type: 'keyword', + }, + 'zeek.files.sha1': { + category: 'zeek', + description: 'A SHA1 digest of the file contents. ', + name: 'zeek.files.sha1', + type: 'keyword', + }, + 'zeek.files.sha256': { + category: 'zeek', + description: 'A SHA256 digest of the file contents. ', + name: 'zeek.files.sha256', + type: 'keyword', + }, + 'zeek.files.extracted': { + category: 'zeek', + description: 'Local filename of extracted file. ', + name: 'zeek.files.extracted', + type: 'keyword', + }, + 'zeek.files.extracted_cutoff': { + category: 'zeek', + description: + 'Indicate whether the file being extracted was cut off hence not extracted completely. ', + name: 'zeek.files.extracted_cutoff', + type: 'boolean', + }, + 'zeek.files.extracted_size': { + category: 'zeek', + description: 'The number of bytes extracted to disk. ', + name: 'zeek.files.extracted_size', + type: 'long', + }, + 'zeek.files.entropy': { + category: 'zeek', + description: 'The information density of the contents of the file. ', + name: 'zeek.files.entropy', + type: 'double', + }, + 'zeek.ftp.user': { + category: 'zeek', + description: 'User name for the current FTP session. ', + name: 'zeek.ftp.user', + type: 'keyword', + }, + 'zeek.ftp.password': { + category: 'zeek', + description: 'Password for the current FTP session if captured. ', + name: 'zeek.ftp.password', + type: 'keyword', + }, + 'zeek.ftp.command': { + category: 'zeek', + description: 'Command given by the client. ', + name: 'zeek.ftp.command', + type: 'keyword', + }, + 'zeek.ftp.arg': { + category: 'zeek', + description: 'Argument for the command if one is given. ', + name: 'zeek.ftp.arg', + type: 'keyword', + }, + 'zeek.ftp.file.size': { + category: 'zeek', + description: 'Size of the file if the command indicates a file transfer. ', + name: 'zeek.ftp.file.size', + type: 'long', + }, + 'zeek.ftp.file.mime_type': { + category: 'zeek', + description: 'Sniffed mime type of file. ', + name: 'zeek.ftp.file.mime_type', + type: 'keyword', + }, + 'zeek.ftp.file.fuid': { + category: 'zeek', + description: '(present if base/protocols/ftp/files.bro is loaded) File unique ID. ', + name: 'zeek.ftp.file.fuid', + type: 'keyword', + }, + 'zeek.ftp.reply.code': { + category: 'zeek', + description: 'Reply code from the server in response to the command. ', + name: 'zeek.ftp.reply.code', + type: 'integer', + }, + 'zeek.ftp.reply.msg': { + category: 'zeek', + description: 'Reply message from the server in response to the command. ', + name: 'zeek.ftp.reply.msg', + type: 'keyword', + }, + 'zeek.ftp.data_channel.passive': { + category: 'zeek', + description: 'Whether PASV mode is toggled for control channel. ', + name: 'zeek.ftp.data_channel.passive', + type: 'boolean', + }, + 'zeek.ftp.data_channel.originating_host': { + category: 'zeek', + description: 'The host that will be initiating the data connection. ', + name: 'zeek.ftp.data_channel.originating_host', + type: 'ip', + }, + 'zeek.ftp.data_channel.response_host': { + category: 'zeek', + description: 'The host that will be accepting the data connection. ', + name: 'zeek.ftp.data_channel.response_host', + type: 'ip', + }, + 'zeek.ftp.data_channel.response_port': { + category: 'zeek', + description: 'The port at which the acceptor is listening for the data connection. ', + name: 'zeek.ftp.data_channel.response_port', + type: 'integer', + }, + 'zeek.ftp.cwd': { + category: 'zeek', + description: + "Current working directory that this session is in. By making the default value '.', we can indicate that unless something more concrete is discovered that the existing but unknown directory is ok to use. ", + name: 'zeek.ftp.cwd', + type: 'keyword', + }, + 'zeek.ftp.cmdarg.cmd': { + category: 'zeek', + description: 'Command. ', + name: 'zeek.ftp.cmdarg.cmd', + type: 'keyword', + }, + 'zeek.ftp.cmdarg.arg': { + category: 'zeek', + description: 'Argument for the command if one was given. ', + name: 'zeek.ftp.cmdarg.arg', + type: 'keyword', + }, + 'zeek.ftp.cmdarg.seq': { + category: 'zeek', + description: 'Counter to track how many commands have been executed. ', + name: 'zeek.ftp.cmdarg.seq', + type: 'integer', + }, + 'zeek.ftp.pending_commands': { + category: 'zeek', + description: + 'Queue for commands that have been sent but not yet responded to are tracked here. ', + name: 'zeek.ftp.pending_commands', + type: 'integer', + }, + 'zeek.ftp.passive': { + category: 'zeek', + description: 'Indicates if the session is in active or passive mode. ', + name: 'zeek.ftp.passive', + type: 'boolean', + }, + 'zeek.ftp.capture_password': { + category: 'zeek', + description: 'Determines if the password will be captured for this request. ', + name: 'zeek.ftp.capture_password', + type: 'boolean', + }, + 'zeek.ftp.last_auth_requested': { + category: 'zeek', + description: + 'present if base/protocols/ftp/gridftp.bro is loaded. Last authentication/security mechanism that was used. ', + name: 'zeek.ftp.last_auth_requested', + type: 'keyword', + }, + 'zeek.http.trans_depth': { + category: 'zeek', + description: + 'Represents the pipelined depth into the connection of this request/response transaction. ', + name: 'zeek.http.trans_depth', + type: 'integer', + }, + 'zeek.http.status_msg': { + category: 'zeek', + description: 'Status message returned by the server. ', + name: 'zeek.http.status_msg', + type: 'keyword', + }, + 'zeek.http.info_code': { + category: 'zeek', + description: 'Last seen 1xx informational reply code returned by the server. ', + name: 'zeek.http.info_code', + type: 'integer', + }, + 'zeek.http.info_msg': { + category: 'zeek', + description: 'Last seen 1xx informational reply message returned by the server. ', + name: 'zeek.http.info_msg', + type: 'keyword', + }, + 'zeek.http.tags': { + category: 'zeek', + description: + 'A set of indicators of various attributes discovered and related to a particular request/response pair. ', + name: 'zeek.http.tags', + type: 'keyword', + }, + 'zeek.http.password': { + category: 'zeek', + description: 'Password if basic-auth is performed for the request. ', + name: 'zeek.http.password', + type: 'keyword', + }, + 'zeek.http.captured_password': { + category: 'zeek', + description: 'Determines if the password will be captured for this request. ', + name: 'zeek.http.captured_password', + type: 'boolean', + }, + 'zeek.http.proxied': { + category: 'zeek', + description: 'All of the headers that may indicate if the HTTP request was proxied. ', + name: 'zeek.http.proxied', + type: 'keyword', + }, + 'zeek.http.range_request': { + category: 'zeek', + description: 'Indicates if this request can assume 206 partial content in response. ', + name: 'zeek.http.range_request', + type: 'boolean', + }, + 'zeek.http.client_header_names': { + category: 'zeek', + description: + 'The vector of HTTP header names sent by the client. No header values are included here, just the header names. ', + name: 'zeek.http.client_header_names', + type: 'keyword', + }, + 'zeek.http.server_header_names': { + category: 'zeek', + description: + 'The vector of HTTP header names sent by the server. No header values are included here, just the header names. ', + name: 'zeek.http.server_header_names', + type: 'keyword', + }, + 'zeek.http.orig_fuids': { + category: 'zeek', + description: 'An ordered vector of file unique IDs from the originator. ', + name: 'zeek.http.orig_fuids', + type: 'keyword', + }, + 'zeek.http.orig_mime_types': { + category: 'zeek', + description: 'An ordered vector of mime types from the originator. ', + name: 'zeek.http.orig_mime_types', + type: 'keyword', + }, + 'zeek.http.orig_filenames': { + category: 'zeek', + description: 'An ordered vector of filenames from the originator. ', + name: 'zeek.http.orig_filenames', + type: 'keyword', + }, + 'zeek.http.resp_fuids': { + category: 'zeek', + description: 'An ordered vector of file unique IDs from the responder. ', + name: 'zeek.http.resp_fuids', + type: 'keyword', + }, + 'zeek.http.resp_mime_types': { + category: 'zeek', + description: 'An ordered vector of mime types from the responder. ', + name: 'zeek.http.resp_mime_types', + type: 'keyword', + }, + 'zeek.http.resp_filenames': { + category: 'zeek', + description: 'An ordered vector of filenames from the responder. ', + name: 'zeek.http.resp_filenames', + type: 'keyword', + }, + 'zeek.http.orig_mime_depth': { + category: 'zeek', + description: 'Current number of MIME entities in the HTTP request message body. ', + name: 'zeek.http.orig_mime_depth', + type: 'integer', + }, + 'zeek.http.resp_mime_depth': { + category: 'zeek', + description: 'Current number of MIME entities in the HTTP response message body. ', + name: 'zeek.http.resp_mime_depth', + type: 'integer', + }, + 'zeek.intel.seen.indicator': { + category: 'zeek', + description: 'The intelligence indicator. ', + name: 'zeek.intel.seen.indicator', + type: 'keyword', + }, + 'zeek.intel.seen.indicator_type': { + category: 'zeek', + description: 'The type of data the indicator represents. ', + name: 'zeek.intel.seen.indicator_type', + type: 'keyword', + }, + 'zeek.intel.seen.host': { + category: 'zeek', + description: 'If the indicator type was Intel::ADDR, then this field will be present. ', + name: 'zeek.intel.seen.host', + type: 'keyword', + }, + 'zeek.intel.seen.conn': { + category: 'zeek', + description: + 'If the data was discovered within a connection, the connection record should go here to give context to the data. ', + name: 'zeek.intel.seen.conn', + type: 'keyword', + }, + 'zeek.intel.seen.where': { + category: 'zeek', + description: 'Where the data was discovered. ', + name: 'zeek.intel.seen.where', + type: 'keyword', + }, + 'zeek.intel.seen.node': { + category: 'zeek', + description: 'The name of the node where the match was discovered. ', + name: 'zeek.intel.seen.node', + type: 'keyword', + }, + 'zeek.intel.seen.uid': { + category: 'zeek', + description: + 'If the data was discovered within a connection, the connection uid should go here to give context to the data. If the conn field is provided, this will be automatically filled out. ', + name: 'zeek.intel.seen.uid', + type: 'keyword', + }, + 'zeek.intel.seen.f': { + category: 'zeek', + description: + 'If the data was discovered within a file, the file record should go here to provide context to the data. ', + name: 'zeek.intel.seen.f', + type: 'object', + }, + 'zeek.intel.seen.fuid': { + category: 'zeek', + description: + 'If the data was discovered within a file, the file uid should go here to provide context to the data. If the file record f is provided, this will be automatically filled out. ', + name: 'zeek.intel.seen.fuid', + type: 'keyword', + }, + 'zeek.intel.matched': { + category: 'zeek', + description: 'Event to represent a match in the intelligence data from data that was seen. ', + name: 'zeek.intel.matched', + type: 'keyword', + }, + 'zeek.intel.sources': { + category: 'zeek', + description: 'Sources which supplied data for this match. ', + name: 'zeek.intel.sources', + type: 'keyword', + }, + 'zeek.intel.fuid': { + category: 'zeek', + description: + 'If a file was associated with this intelligence hit, this is the uid for the file. ', + name: 'zeek.intel.fuid', + type: 'keyword', + }, + 'zeek.intel.file_mime_type': { + category: 'zeek', + description: + 'A mime type if the intelligence hit is related to a file. If the $f field is provided this will be automatically filled out. ', + name: 'zeek.intel.file_mime_type', + type: 'keyword', + }, + 'zeek.intel.file_desc': { + category: 'zeek', + description: + 'Frequently files can be described to give a bit more context. If the $f field is provided this field will be automatically filled out. ', + name: 'zeek.intel.file_desc', + type: 'keyword', + }, + 'zeek.irc.nick': { + category: 'zeek', + description: 'Nickname given for the connection. ', + name: 'zeek.irc.nick', + type: 'keyword', + }, + 'zeek.irc.user': { + category: 'zeek', + description: 'Username given for the connection. ', + name: 'zeek.irc.user', + type: 'keyword', + }, + 'zeek.irc.command': { + category: 'zeek', + description: 'Command given by the client. ', + name: 'zeek.irc.command', + type: 'keyword', + }, + 'zeek.irc.value': { + category: 'zeek', + description: 'Value for the command given by the client. ', + name: 'zeek.irc.value', + type: 'keyword', + }, + 'zeek.irc.addl': { + category: 'zeek', + description: 'Any additional data for the command. ', + name: 'zeek.irc.addl', + type: 'keyword', + }, + 'zeek.irc.dcc.file.name': { + category: 'zeek', + description: 'Present if base/protocols/irc/dcc-send.bro is loaded. DCC filename requested. ', + name: 'zeek.irc.dcc.file.name', + type: 'keyword', + }, + 'zeek.irc.dcc.file.size': { + category: 'zeek', + description: + 'Present if base/protocols/irc/dcc-send.bro is loaded. Size of the DCC transfer as indicated by the sender. ', + name: 'zeek.irc.dcc.file.size', + type: 'long', + }, + 'zeek.irc.dcc.mime_type': { + category: 'zeek', + description: + 'present if base/protocols/irc/dcc-send.bro is loaded. Sniffed mime type of the file. ', + name: 'zeek.irc.dcc.mime_type', + type: 'keyword', + }, + 'zeek.irc.fuid': { + category: 'zeek', + description: 'present if base/protocols/irc/files.bro is loaded. File unique ID. ', + name: 'zeek.irc.fuid', + type: 'keyword', + }, + 'zeek.kerberos.request_type': { + category: 'zeek', + description: 'Request type - Authentication Service (AS) or Ticket Granting Service (TGS). ', + name: 'zeek.kerberos.request_type', + type: 'keyword', + }, + 'zeek.kerberos.client': { + category: 'zeek', + description: 'Client name. ', + name: 'zeek.kerberos.client', + type: 'keyword', + }, + 'zeek.kerberos.service': { + category: 'zeek', + description: 'Service name. ', + name: 'zeek.kerberos.service', + type: 'keyword', + }, + 'zeek.kerberos.success': { + category: 'zeek', + description: 'Request result. ', + name: 'zeek.kerberos.success', + type: 'boolean', + }, + 'zeek.kerberos.error.code': { + category: 'zeek', + description: 'Error code. ', + name: 'zeek.kerberos.error.code', + type: 'integer', + }, + 'zeek.kerberos.error.msg': { + category: 'zeek', + description: 'Error message. ', + name: 'zeek.kerberos.error.msg', + type: 'keyword', + }, + 'zeek.kerberos.valid.from': { + category: 'zeek', + description: 'Ticket valid from. ', + name: 'zeek.kerberos.valid.from', + type: 'date', + }, + 'zeek.kerberos.valid.until': { + category: 'zeek', + description: 'Ticket valid until. ', + name: 'zeek.kerberos.valid.until', + type: 'date', + }, + 'zeek.kerberos.valid.days': { + category: 'zeek', + description: 'Number of days the ticket is valid for. ', + name: 'zeek.kerberos.valid.days', + type: 'integer', + }, + 'zeek.kerberos.cipher': { + category: 'zeek', + description: 'Ticket encryption type. ', + name: 'zeek.kerberos.cipher', + type: 'keyword', + }, + 'zeek.kerberos.forwardable': { + category: 'zeek', + description: 'Forwardable ticket requested. ', + name: 'zeek.kerberos.forwardable', + type: 'boolean', + }, + 'zeek.kerberos.renewable': { + category: 'zeek', + description: 'Renewable ticket requested. ', + name: 'zeek.kerberos.renewable', + type: 'boolean', + }, + 'zeek.kerberos.ticket.auth': { + category: 'zeek', + description: 'Hash of ticket used to authorize request/transaction. ', + name: 'zeek.kerberos.ticket.auth', + type: 'keyword', + }, + 'zeek.kerberos.ticket.new': { + category: 'zeek', + description: 'Hash of ticket returned by the KDC. ', + name: 'zeek.kerberos.ticket.new', + type: 'keyword', + }, + 'zeek.kerberos.cert.client.value': { + category: 'zeek', + description: 'Client certificate. ', + name: 'zeek.kerberos.cert.client.value', + type: 'keyword', + }, + 'zeek.kerberos.cert.client.fuid': { + category: 'zeek', + description: 'File unique ID of client cert. ', + name: 'zeek.kerberos.cert.client.fuid', + type: 'keyword', + }, + 'zeek.kerberos.cert.client.subject': { + category: 'zeek', + description: 'Subject of client certificate. ', + name: 'zeek.kerberos.cert.client.subject', + type: 'keyword', + }, + 'zeek.kerberos.cert.server.value': { + category: 'zeek', + description: 'Server certificate. ', + name: 'zeek.kerberos.cert.server.value', + type: 'keyword', + }, + 'zeek.kerberos.cert.server.fuid': { + category: 'zeek', + description: 'File unique ID of server certificate. ', + name: 'zeek.kerberos.cert.server.fuid', + type: 'keyword', + }, + 'zeek.kerberos.cert.server.subject': { + category: 'zeek', + description: 'Subject of server certificate. ', + name: 'zeek.kerberos.cert.server.subject', + type: 'keyword', + }, + 'zeek.modbus.function': { + category: 'zeek', + description: 'The name of the function message that was sent. ', + name: 'zeek.modbus.function', + type: 'keyword', + }, + 'zeek.modbus.exception': { + category: 'zeek', + description: 'The exception if the response was a failure. ', + name: 'zeek.modbus.exception', + type: 'keyword', + }, + 'zeek.modbus.track_address': { + category: 'zeek', + description: + 'Present if policy/protocols/modbus/track-memmap.bro is loaded. Modbus track address. ', + name: 'zeek.modbus.track_address', + type: 'integer', + }, + 'zeek.mysql.cmd': { + category: 'zeek', + description: 'The command that was issued. ', + name: 'zeek.mysql.cmd', + type: 'keyword', + }, + 'zeek.mysql.arg': { + category: 'zeek', + description: 'The argument issued to the command. ', + name: 'zeek.mysql.arg', + type: 'keyword', + }, + 'zeek.mysql.success': { + category: 'zeek', + description: 'Whether the command succeeded. ', + name: 'zeek.mysql.success', + type: 'boolean', + }, + 'zeek.mysql.rows': { + category: 'zeek', + description: 'The number of affected rows, if any. ', + name: 'zeek.mysql.rows', + type: 'integer', + }, + 'zeek.mysql.response': { + category: 'zeek', + description: 'Server message, if any. ', + name: 'zeek.mysql.response', + type: 'keyword', + }, + 'zeek.notice.connection_id': { + category: 'zeek', + description: 'Identifier of the related connection session. ', + name: 'zeek.notice.connection_id', + type: 'keyword', + }, + 'zeek.notice.icmp_id': { + category: 'zeek', + description: 'Identifier of the related ICMP session. ', + name: 'zeek.notice.icmp_id', + type: 'keyword', + }, + 'zeek.notice.file.id': { + category: 'zeek', + description: 'An identifier associated with a single file that is related to this notice. ', + name: 'zeek.notice.file.id', + type: 'keyword', + }, + 'zeek.notice.file.parent_id': { + category: 'zeek', + description: 'Identifier associated with a container file from which this one was extracted. ', + name: 'zeek.notice.file.parent_id', + type: 'keyword', + }, + 'zeek.notice.file.source': { + category: 'zeek', + description: + 'An identification of the source of the file data. E.g. it may be a network protocol over which it was transferred, or a local file path which was read, or some other input source. ', + name: 'zeek.notice.file.source', + type: 'keyword', + }, + 'zeek.notice.file.mime_type': { + category: 'zeek', + description: 'A mime type if the notice is related to a file. ', + name: 'zeek.notice.file.mime_type', + type: 'keyword', + }, + 'zeek.notice.file.is_orig': { + category: 'zeek', + description: + 'If the source of this file is a network connection, this field indicates if the file is being sent by the originator of the connection or the responder. ', + name: 'zeek.notice.file.is_orig', + type: 'boolean', + }, + 'zeek.notice.file.seen_bytes': { + category: 'zeek', + description: 'Number of bytes provided to the file analysis engine for the file. ', + name: 'zeek.notice.file.seen_bytes', + type: 'long', + }, + 'zeek.notice.ffile.total_bytes': { + category: 'zeek', + description: 'Total number of bytes that are supposed to comprise the full file. ', + name: 'zeek.notice.ffile.total_bytes', + type: 'long', + }, + 'zeek.notice.file.missing_bytes': { + category: 'zeek', + description: + 'The number of bytes in the file stream that were completely missed during the process of analysis. ', + name: 'zeek.notice.file.missing_bytes', + type: 'long', + }, + 'zeek.notice.file.overflow_bytes': { + category: 'zeek', + description: + "The number of bytes in the file stream that were not delivered to stream file analyzers. This could be overlapping bytes or bytes that couldn't be reassembled. ", + name: 'zeek.notice.file.overflow_bytes', + type: 'long', + }, + 'zeek.notice.fuid': { + category: 'zeek', + description: 'A file unique ID if this notice is related to a file. ', + name: 'zeek.notice.fuid', + type: 'keyword', + }, + 'zeek.notice.note': { + category: 'zeek', + description: 'The type of the notice. ', + name: 'zeek.notice.note', + type: 'keyword', + }, + 'zeek.notice.msg': { + category: 'zeek', + description: 'The human readable message for the notice. ', + name: 'zeek.notice.msg', + type: 'keyword', + }, + 'zeek.notice.sub': { + category: 'zeek', + description: 'The human readable sub-message. ', + name: 'zeek.notice.sub', + type: 'keyword', + }, + 'zeek.notice.n': { + category: 'zeek', + description: 'Associated count, or a status code. ', + name: 'zeek.notice.n', + type: 'long', + }, + 'zeek.notice.peer_name': { + category: 'zeek', + description: 'Name of remote peer that raised this notice. ', + name: 'zeek.notice.peer_name', + type: 'keyword', + }, + 'zeek.notice.peer_descr': { + category: 'zeek', + description: 'Textual description for the peer that raised this notice. ', + name: 'zeek.notice.peer_descr', + type: 'text', + }, + 'zeek.notice.actions': { + category: 'zeek', + description: 'The actions which have been applied to this notice. ', + name: 'zeek.notice.actions', + type: 'keyword', + }, + 'zeek.notice.email_body_sections': { + category: 'zeek', + description: + 'By adding chunks of text into this element, other scripts can expand on notices that are being emailed. ', + name: 'zeek.notice.email_body_sections', + type: 'text', + }, + 'zeek.notice.email_delay_tokens': { + category: 'zeek', + description: + 'Adding a string token to this set will cause the built-in emailing functionality to delay sending the email either the token has been removed or the email has been delayed for the specified time duration. ', + name: 'zeek.notice.email_delay_tokens', + type: 'keyword', + }, + 'zeek.notice.identifier': { + category: 'zeek', + description: + 'This field is provided when a notice is generated for the purpose of deduplicating notices. ', + name: 'zeek.notice.identifier', + type: 'keyword', + }, + 'zeek.notice.suppress_for': { + category: 'zeek', + description: + 'This field indicates the length of time that this unique notice should be suppressed. ', + name: 'zeek.notice.suppress_for', + type: 'double', + }, + 'zeek.notice.dropped': { + category: 'zeek', + description: 'Indicate if the source IP address was dropped and denied network access. ', + name: 'zeek.notice.dropped', + type: 'boolean', + }, + 'zeek.ntlm.domain': { + category: 'zeek', + description: 'Domain name given by the client. ', + name: 'zeek.ntlm.domain', + type: 'keyword', + }, + 'zeek.ntlm.hostname': { + category: 'zeek', + description: 'Hostname given by the client. ', + name: 'zeek.ntlm.hostname', + type: 'keyword', + }, + 'zeek.ntlm.success': { + category: 'zeek', + description: 'Indicate whether or not the authentication was successful. ', + name: 'zeek.ntlm.success', + type: 'boolean', + }, + 'zeek.ntlm.username': { + category: 'zeek', + description: 'Username given by the client. ', + name: 'zeek.ntlm.username', + type: 'keyword', + }, + 'zeek.ntlm.server.name.dns': { + category: 'zeek', + description: 'DNS name given by the server in a CHALLENGE. ', + name: 'zeek.ntlm.server.name.dns', + type: 'keyword', + }, + 'zeek.ntlm.server.name.netbios': { + category: 'zeek', + description: 'NetBIOS name given by the server in a CHALLENGE. ', + name: 'zeek.ntlm.server.name.netbios', + type: 'keyword', + }, + 'zeek.ntlm.server.name.tree': { + category: 'zeek', + description: 'Tree name given by the server in a CHALLENGE. ', + name: 'zeek.ntlm.server.name.tree', + type: 'keyword', + }, + 'zeek.ocsp.file_id': { + category: 'zeek', + description: 'File id of the OCSP reply. ', + name: 'zeek.ocsp.file_id', + type: 'keyword', + }, + 'zeek.ocsp.hash.algorithm': { + category: 'zeek', + description: 'Hash algorithm used to generate issuerNameHash and issuerKeyHash. ', + name: 'zeek.ocsp.hash.algorithm', + type: 'keyword', + }, + 'zeek.ocsp.hash.issuer.name': { + category: 'zeek', + description: "Hash of the issuer's distingueshed name. ", + name: 'zeek.ocsp.hash.issuer.name', + type: 'keyword', + }, + 'zeek.ocsp.hash.issuer.key': { + category: 'zeek', + description: "Hash of the issuer's public key. ", + name: 'zeek.ocsp.hash.issuer.key', + type: 'keyword', + }, + 'zeek.ocsp.serial_number': { + category: 'zeek', + description: 'Serial number of the affected certificate. ', + name: 'zeek.ocsp.serial_number', + type: 'keyword', + }, + 'zeek.ocsp.status': { + category: 'zeek', + description: 'Status of the affected certificate. ', + name: 'zeek.ocsp.status', + type: 'keyword', + }, + 'zeek.ocsp.revoke.time': { + category: 'zeek', + description: 'Time at which the certificate was revoked. ', + name: 'zeek.ocsp.revoke.time', + type: 'date', + }, + 'zeek.ocsp.revoke.reason': { + category: 'zeek', + description: 'Reason for which the certificate was revoked. ', + name: 'zeek.ocsp.revoke.reason', + type: 'keyword', + }, + 'zeek.ocsp.update.this': { + category: 'zeek', + description: 'The time at which the status being shows is known to have been correct. ', + name: 'zeek.ocsp.update.this', + type: 'date', + }, + 'zeek.ocsp.update.next': { + category: 'zeek', + description: + 'The latest time at which new information about the status of the certificate will be available. ', + name: 'zeek.ocsp.update.next', + type: 'date', + }, + 'zeek.pe.client': { + category: 'zeek', + description: "The client's version string. ", + name: 'zeek.pe.client', + type: 'keyword', + }, + 'zeek.pe.id': { + category: 'zeek', + description: 'File id of this portable executable file. ', + name: 'zeek.pe.id', + type: 'keyword', + }, + 'zeek.pe.machine': { + category: 'zeek', + description: 'The target machine that the file was compiled for. ', + name: 'zeek.pe.machine', + type: 'keyword', + }, + 'zeek.pe.compile_time': { + category: 'zeek', + description: 'The time that the file was created at. ', + name: 'zeek.pe.compile_time', + type: 'date', + }, + 'zeek.pe.os': { + category: 'zeek', + description: 'The required operating system. ', + name: 'zeek.pe.os', + type: 'keyword', + }, + 'zeek.pe.subsystem': { + category: 'zeek', + description: 'The subsystem that is required to run this file. ', + name: 'zeek.pe.subsystem', + type: 'keyword', + }, + 'zeek.pe.is_exe': { + category: 'zeek', + description: 'Is the file an executable, or just an object file? ', + name: 'zeek.pe.is_exe', + type: 'boolean', + }, + 'zeek.pe.is_64bit': { + category: 'zeek', + description: 'Is the file a 64-bit executable? ', + name: 'zeek.pe.is_64bit', + type: 'boolean', + }, + 'zeek.pe.uses_aslr': { + category: 'zeek', + description: 'Does the file support Address Space Layout Randomization? ', + name: 'zeek.pe.uses_aslr', + type: 'boolean', + }, + 'zeek.pe.uses_dep': { + category: 'zeek', + description: 'Does the file support Data Execution Prevention? ', + name: 'zeek.pe.uses_dep', + type: 'boolean', + }, + 'zeek.pe.uses_code_integrity': { + category: 'zeek', + description: 'Does the file enforce code integrity checks? ', + name: 'zeek.pe.uses_code_integrity', + type: 'boolean', + }, + 'zeek.pe.uses_seh': { + category: 'zeek', + description: 'Does the file use structured exception handing? ', + name: 'zeek.pe.uses_seh', + type: 'boolean', + }, + 'zeek.pe.has_import_table': { + category: 'zeek', + description: 'Does the file have an import table? ', + name: 'zeek.pe.has_import_table', + type: 'boolean', + }, + 'zeek.pe.has_export_table': { + category: 'zeek', + description: 'Does the file have an export table? ', + name: 'zeek.pe.has_export_table', + type: 'boolean', + }, + 'zeek.pe.has_cert_table': { + category: 'zeek', + description: 'Does the file have an attribute certificate table? ', + name: 'zeek.pe.has_cert_table', + type: 'boolean', + }, + 'zeek.pe.has_debug_data': { + category: 'zeek', + description: 'Does the file have a debug table? ', + name: 'zeek.pe.has_debug_data', + type: 'boolean', + }, + 'zeek.pe.section_names': { + category: 'zeek', + description: 'The names of the sections, in order. ', + name: 'zeek.pe.section_names', + type: 'keyword', + }, + 'zeek.radius.username': { + category: 'zeek', + description: 'The username, if present. ', + name: 'zeek.radius.username', + type: 'keyword', + }, + 'zeek.radius.mac': { + category: 'zeek', + description: 'MAC address, if present. ', + name: 'zeek.radius.mac', + type: 'keyword', + }, + 'zeek.radius.framed_addr': { + category: 'zeek', + description: + 'The address given to the network access server, if present. This is only a hint from the RADIUS server and the network access server is not required to honor the address. ', + name: 'zeek.radius.framed_addr', + type: 'ip', + }, + 'zeek.radius.remote_ip': { + category: 'zeek', + description: + 'Remote IP address, if present. This is collected from the Tunnel-Client-Endpoint attribute. ', + name: 'zeek.radius.remote_ip', + type: 'ip', + }, + 'zeek.radius.connect_info': { + category: 'zeek', + description: 'Connect info, if present. ', + name: 'zeek.radius.connect_info', + type: 'keyword', + }, + 'zeek.radius.reply_msg': { + category: 'zeek', + description: + 'Reply message from the server challenge. This is frequently shown to the user authenticating. ', + name: 'zeek.radius.reply_msg', + type: 'keyword', + }, + 'zeek.radius.result': { + category: 'zeek', + description: 'Successful or failed authentication. ', + name: 'zeek.radius.result', + type: 'keyword', + }, + 'zeek.radius.ttl': { + category: 'zeek', + description: + 'The duration between the first request and either the "Access-Accept" message or an error. If the field is empty, it means that either the request or response was not seen. ', + name: 'zeek.radius.ttl', + type: 'integer', + }, + 'zeek.radius.logged': { + category: 'zeek', + description: 'Whether this has already been logged and can be ignored. ', + name: 'zeek.radius.logged', + type: 'boolean', + }, + 'zeek.rdp.cookie': { + category: 'zeek', + description: 'Cookie value used by the client machine. This is typically a username. ', + name: 'zeek.rdp.cookie', + type: 'keyword', + }, + 'zeek.rdp.result': { + category: 'zeek', + description: + "Status result for the connection. It's a mix between RDP negotation failure messages and GCC server create response messages. ", + name: 'zeek.rdp.result', + type: 'keyword', + }, + 'zeek.rdp.security_protocol': { + category: 'zeek', + description: 'Security protocol chosen by the server. ', + name: 'zeek.rdp.security_protocol', + type: 'keyword', + }, + 'zeek.rdp.keyboard_layout': { + category: 'zeek', + description: 'Keyboard layout (language) of the client machine. ', + name: 'zeek.rdp.keyboard_layout', + type: 'keyword', + }, + 'zeek.rdp.client.build': { + category: 'zeek', + description: 'RDP client version used by the client machine. ', + name: 'zeek.rdp.client.build', + type: 'keyword', + }, + 'zeek.rdp.client.client_name': { + category: 'zeek', + description: 'Name of the client machine. ', + name: 'zeek.rdp.client.client_name', + type: 'keyword', + }, + 'zeek.rdp.client.product_id': { + category: 'zeek', + description: 'Product ID of the client machine. ', + name: 'zeek.rdp.client.product_id', + type: 'keyword', + }, + 'zeek.rdp.desktop.width': { + category: 'zeek', + description: 'Desktop width of the client machine. ', + name: 'zeek.rdp.desktop.width', + type: 'integer', + }, + 'zeek.rdp.desktop.height': { + category: 'zeek', + description: 'Desktop height of the client machine. ', + name: 'zeek.rdp.desktop.height', + type: 'integer', + }, + 'zeek.rdp.desktop.color_depth': { + category: 'zeek', + description: 'The color depth requested by the client in the high_color_depth field. ', + name: 'zeek.rdp.desktop.color_depth', + type: 'keyword', + }, + 'zeek.rdp.cert.type': { + category: 'zeek', + description: + 'If the connection is being encrypted with native RDP encryption, this is the type of cert being used. ', + name: 'zeek.rdp.cert.type', + type: 'keyword', + }, + 'zeek.rdp.cert.count': { + category: 'zeek', + description: 'The number of certs seen. X.509 can transfer an entire certificate chain. ', + name: 'zeek.rdp.cert.count', + type: 'integer', + }, + 'zeek.rdp.cert.permanent': { + category: 'zeek', + description: + 'Indicates if the provided certificate or certificate chain is permanent or temporary. ', + name: 'zeek.rdp.cert.permanent', + type: 'boolean', + }, + 'zeek.rdp.encryption.level': { + category: 'zeek', + description: 'Encryption level of the connection. ', + name: 'zeek.rdp.encryption.level', + type: 'keyword', + }, + 'zeek.rdp.encryption.method': { + category: 'zeek', + description: 'Encryption method of the connection. ', + name: 'zeek.rdp.encryption.method', + type: 'keyword', + }, + 'zeek.rdp.done': { + category: 'zeek', + description: 'Track status of logging RDP connections. ', + name: 'zeek.rdp.done', + type: 'boolean', + }, + 'zeek.rdp.ssl': { + category: 'zeek', + description: + '(present if policy/protocols/rdp/indicate_ssl.bro is loaded) Flag the connection if it was seen over SSL. ', + name: 'zeek.rdp.ssl', + type: 'boolean', + }, + 'zeek.rfb.version.client.major': { + category: 'zeek', + description: 'Major version of the client. ', + name: 'zeek.rfb.version.client.major', + type: 'keyword', + }, + 'zeek.rfb.version.client.minor': { + category: 'zeek', + description: 'Minor version of the client. ', + name: 'zeek.rfb.version.client.minor', + type: 'keyword', + }, + 'zeek.rfb.version.server.major': { + category: 'zeek', + description: 'Major version of the server. ', + name: 'zeek.rfb.version.server.major', + type: 'keyword', + }, + 'zeek.rfb.version.server.minor': { + category: 'zeek', + description: 'Minor version of the server. ', + name: 'zeek.rfb.version.server.minor', + type: 'keyword', + }, + 'zeek.rfb.auth.success': { + category: 'zeek', + description: 'Whether or not authentication was successful. ', + name: 'zeek.rfb.auth.success', + type: 'boolean', + }, + 'zeek.rfb.auth.method': { + category: 'zeek', + description: 'Identifier of authentication method used. ', + name: 'zeek.rfb.auth.method', + type: 'keyword', + }, + 'zeek.rfb.share_flag': { + category: 'zeek', + description: 'Whether the client has an exclusive or a shared session. ', + name: 'zeek.rfb.share_flag', + type: 'boolean', + }, + 'zeek.rfb.desktop_name': { + category: 'zeek', + description: 'Name of the screen that is being shared. ', + name: 'zeek.rfb.desktop_name', + type: 'keyword', + }, + 'zeek.rfb.width': { + category: 'zeek', + description: 'Width of the screen that is being shared. ', + name: 'zeek.rfb.width', + type: 'integer', + }, + 'zeek.rfb.height': { + category: 'zeek', + description: 'Height of the screen that is being shared. ', + name: 'zeek.rfb.height', + type: 'integer', + }, + 'zeek.sip.transaction_depth': { + category: 'zeek', + description: + 'Represents the pipelined depth into the connection of this request/response transaction. ', + name: 'zeek.sip.transaction_depth', + type: 'integer', + }, + 'zeek.sip.sequence.method': { + category: 'zeek', + description: 'Verb used in the SIP request (INVITE, REGISTER etc.). ', + name: 'zeek.sip.sequence.method', + type: 'keyword', + }, + 'zeek.sip.sequence.number': { + category: 'zeek', + description: 'Contents of the CSeq: header from the client. ', + name: 'zeek.sip.sequence.number', + type: 'keyword', + }, + 'zeek.sip.uri': { + category: 'zeek', + description: 'URI used in the request. ', + name: 'zeek.sip.uri', + type: 'keyword', + }, + 'zeek.sip.date': { + category: 'zeek', + description: 'Contents of the Date: header from the client. ', + name: 'zeek.sip.date', + type: 'keyword', + }, + 'zeek.sip.request.from': { + category: 'zeek', + description: + "Contents of the request From: header Note: The tag= value that's usually appended to the sender is stripped off and not logged. ", + name: 'zeek.sip.request.from', + type: 'keyword', + }, + 'zeek.sip.request.to': { + category: 'zeek', + description: 'Contents of the To: header. ', + name: 'zeek.sip.request.to', + type: 'keyword', + }, + 'zeek.sip.request.path': { + category: 'zeek', + description: 'The client message transmission path, as extracted from the headers. ', + name: 'zeek.sip.request.path', + type: 'keyword', + }, + 'zeek.sip.request.body_length': { + category: 'zeek', + description: 'Contents of the Content-Length: header from the client. ', + name: 'zeek.sip.request.body_length', + type: 'long', + }, + 'zeek.sip.response.from': { + category: 'zeek', + description: + "Contents of the response From: header Note: The tag= value that's usually appended to the sender is stripped off and not logged. ", + name: 'zeek.sip.response.from', + type: 'keyword', + }, + 'zeek.sip.response.to': { + category: 'zeek', + description: 'Contents of the response To: header. ', + name: 'zeek.sip.response.to', + type: 'keyword', + }, + 'zeek.sip.response.path': { + category: 'zeek', + description: 'The server message transmission path, as extracted from the headers. ', + name: 'zeek.sip.response.path', + type: 'keyword', + }, + 'zeek.sip.response.body_length': { + category: 'zeek', + description: 'Contents of the Content-Length: header from the server. ', + name: 'zeek.sip.response.body_length', + type: 'long', + }, + 'zeek.sip.reply_to': { + category: 'zeek', + description: 'Contents of the Reply-To: header. ', + name: 'zeek.sip.reply_to', + type: 'keyword', + }, + 'zeek.sip.call_id': { + category: 'zeek', + description: 'Contents of the Call-ID: header from the client. ', + name: 'zeek.sip.call_id', + type: 'keyword', + }, + 'zeek.sip.subject': { + category: 'zeek', + description: 'Contents of the Subject: header from the client. ', + name: 'zeek.sip.subject', + type: 'keyword', + }, + 'zeek.sip.user_agent': { + category: 'zeek', + description: 'Contents of the User-Agent: header from the client. ', + name: 'zeek.sip.user_agent', + type: 'keyword', + }, + 'zeek.sip.status.code': { + category: 'zeek', + description: 'Status code returned by the server. ', + name: 'zeek.sip.status.code', + type: 'integer', + }, + 'zeek.sip.status.msg': { + category: 'zeek', + description: 'Status message returned by the server. ', + name: 'zeek.sip.status.msg', + type: 'keyword', + }, + 'zeek.sip.warning': { + category: 'zeek', + description: 'Contents of the Warning: header. ', + name: 'zeek.sip.warning', + type: 'keyword', + }, + 'zeek.sip.content_type': { + category: 'zeek', + description: 'Contents of the Content-Type: header from the server. ', + name: 'zeek.sip.content_type', + type: 'keyword', + }, + 'zeek.smb_cmd.command': { + category: 'zeek', + description: 'The command sent by the client. ', + name: 'zeek.smb_cmd.command', + type: 'keyword', + }, + 'zeek.smb_cmd.sub_command': { + category: 'zeek', + description: 'The subcommand sent by the client, if present. ', + name: 'zeek.smb_cmd.sub_command', + type: 'keyword', + }, + 'zeek.smb_cmd.argument': { + category: 'zeek', + description: 'Command argument sent by the client, if any. ', + name: 'zeek.smb_cmd.argument', + type: 'keyword', + }, + 'zeek.smb_cmd.status': { + category: 'zeek', + description: "Server reply to the client's command. ", + name: 'zeek.smb_cmd.status', + type: 'keyword', + }, + 'zeek.smb_cmd.rtt': { + category: 'zeek', + description: 'Round trip time from the request to the response. ', + name: 'zeek.smb_cmd.rtt', + type: 'double', + }, + 'zeek.smb_cmd.version': { + category: 'zeek', + description: 'Version of SMB for the command. ', + name: 'zeek.smb_cmd.version', + type: 'keyword', + }, + 'zeek.smb_cmd.username': { + category: 'zeek', + description: 'Authenticated username, if available. ', + name: 'zeek.smb_cmd.username', + type: 'keyword', + }, + 'zeek.smb_cmd.tree': { + category: 'zeek', + description: + 'If this is related to a tree, this is the tree that was used for the current command. ', + name: 'zeek.smb_cmd.tree', + type: 'keyword', + }, + 'zeek.smb_cmd.tree_service': { + category: 'zeek', + description: 'The type of tree (disk share, printer share, named pipe, etc.). ', + name: 'zeek.smb_cmd.tree_service', + type: 'keyword', + }, + 'zeek.smb_cmd.file.name': { + category: 'zeek', + description: 'Filename if one was seen. ', + name: 'zeek.smb_cmd.file.name', + type: 'keyword', + }, + 'zeek.smb_cmd.file.action': { + category: 'zeek', + description: 'Action this log record represents. ', + name: 'zeek.smb_cmd.file.action', + type: 'keyword', + }, + 'zeek.smb_cmd.file.uid': { + category: 'zeek', + description: 'UID of the referenced file. ', + name: 'zeek.smb_cmd.file.uid', + type: 'keyword', + }, + 'zeek.smb_cmd.file.host.tx': { + category: 'zeek', + description: 'Address of the transmitting host. ', + name: 'zeek.smb_cmd.file.host.tx', + type: 'ip', + }, + 'zeek.smb_cmd.file.host.rx': { + category: 'zeek', + description: 'Address of the receiving host. ', + name: 'zeek.smb_cmd.file.host.rx', + type: 'ip', + }, + 'zeek.smb_cmd.smb1_offered_dialects': { + category: 'zeek', + description: + 'Present if base/protocols/smb/smb1-main.bro is loaded. Dialects offered by the client. ', + name: 'zeek.smb_cmd.smb1_offered_dialects', + type: 'keyword', + }, + 'zeek.smb_cmd.smb2_offered_dialects': { + category: 'zeek', + description: + 'Present if base/protocols/smb/smb2-main.bro is loaded. Dialects offered by the client. ', + name: 'zeek.smb_cmd.smb2_offered_dialects', + type: 'integer', + }, + 'zeek.smb_files.action': { + category: 'zeek', + description: 'Action this log record represents. ', + name: 'zeek.smb_files.action', + type: 'keyword', + }, + 'zeek.smb_files.fid': { + category: 'zeek', + description: 'ID referencing this file. ', + name: 'zeek.smb_files.fid', + type: 'integer', + }, + 'zeek.smb_files.name': { + category: 'zeek', + description: 'Filename if one was seen. ', + name: 'zeek.smb_files.name', + type: 'keyword', + }, + 'zeek.smb_files.path': { + category: 'zeek', + description: 'Path pulled from the tree this file was transferred to or from. ', + name: 'zeek.smb_files.path', + type: 'keyword', + }, + 'zeek.smb_files.previous_name': { + category: 'zeek', + description: "If the rename action was seen, this will be the file's previous name. ", + name: 'zeek.smb_files.previous_name', + type: 'keyword', + }, + 'zeek.smb_files.size': { + category: 'zeek', + description: 'Byte size of the file. ', + name: 'zeek.smb_files.size', + type: 'long', + }, + 'zeek.smb_files.times.accessed': { + category: 'zeek', + description: "The file's access time. ", + name: 'zeek.smb_files.times.accessed', + type: 'date', + }, + 'zeek.smb_files.times.changed': { + category: 'zeek', + description: "The file's change time. ", + name: 'zeek.smb_files.times.changed', + type: 'date', + }, + 'zeek.smb_files.times.created': { + category: 'zeek', + description: "The file's create time. ", + name: 'zeek.smb_files.times.created', + type: 'date', + }, + 'zeek.smb_files.times.modified': { + category: 'zeek', + description: "The file's modify time. ", + name: 'zeek.smb_files.times.modified', + type: 'date', + }, + 'zeek.smb_files.uuid': { + category: 'zeek', + description: 'UUID referencing this file if DCE/RPC. ', + name: 'zeek.smb_files.uuid', + type: 'keyword', + }, + 'zeek.smb_mapping.path': { + category: 'zeek', + description: 'Name of the tree path. ', + name: 'zeek.smb_mapping.path', + type: 'keyword', + }, + 'zeek.smb_mapping.service': { + category: 'zeek', + description: 'The type of resource of the tree (disk share, printer share, named pipe, etc.). ', + name: 'zeek.smb_mapping.service', + type: 'keyword', + }, + 'zeek.smb_mapping.native_file_system': { + category: 'zeek', + description: 'File system of the tree. ', + name: 'zeek.smb_mapping.native_file_system', + type: 'keyword', + }, + 'zeek.smb_mapping.share_type': { + category: 'zeek', + description: + 'If this is SMB2, a share type will be included. For SMB1, the type of share will be deduced and included as well. ', + name: 'zeek.smb_mapping.share_type', + type: 'keyword', + }, + 'zeek.smtp.transaction_depth': { + category: 'zeek', + description: + 'A count to represent the depth of this message transaction in a single connection where multiple messages were transferred. ', + name: 'zeek.smtp.transaction_depth', + type: 'integer', + }, + 'zeek.smtp.helo': { + category: 'zeek', + description: 'Contents of the Helo header. ', + name: 'zeek.smtp.helo', + type: 'keyword', + }, + 'zeek.smtp.mail_from': { + category: 'zeek', + description: 'Email addresses found in the MAIL FROM header. ', + name: 'zeek.smtp.mail_from', + type: 'keyword', + }, + 'zeek.smtp.rcpt_to': { + category: 'zeek', + description: 'Email addresses found in the RCPT TO header. ', + name: 'zeek.smtp.rcpt_to', + type: 'keyword', + }, + 'zeek.smtp.date': { + category: 'zeek', + description: 'Contents of the Date header. ', + name: 'zeek.smtp.date', + type: 'date', + }, + 'zeek.smtp.from': { + category: 'zeek', + description: 'Contents of the From header. ', + name: 'zeek.smtp.from', + type: 'keyword', + }, + 'zeek.smtp.to': { + category: 'zeek', + description: 'Contents of the To header. ', + name: 'zeek.smtp.to', + type: 'keyword', + }, + 'zeek.smtp.cc': { + category: 'zeek', + description: 'Contents of the CC header. ', + name: 'zeek.smtp.cc', + type: 'keyword', + }, + 'zeek.smtp.reply_to': { + category: 'zeek', + description: 'Contents of the ReplyTo header. ', + name: 'zeek.smtp.reply_to', + type: 'keyword', + }, + 'zeek.smtp.msg_id': { + category: 'zeek', + description: 'Contents of the MsgID header. ', + name: 'zeek.smtp.msg_id', + type: 'keyword', + }, + 'zeek.smtp.in_reply_to': { + category: 'zeek', + description: 'Contents of the In-Reply-To header. ', + name: 'zeek.smtp.in_reply_to', + type: 'keyword', + }, + 'zeek.smtp.subject': { + category: 'zeek', + description: 'Contents of the Subject header. ', + name: 'zeek.smtp.subject', + type: 'keyword', + }, + 'zeek.smtp.x_originating_ip': { + category: 'zeek', + description: 'Contents of the X-Originating-IP header. ', + name: 'zeek.smtp.x_originating_ip', + type: 'keyword', + }, + 'zeek.smtp.first_received': { + category: 'zeek', + description: 'Contents of the first Received header. ', + name: 'zeek.smtp.first_received', + type: 'keyword', + }, + 'zeek.smtp.second_received': { + category: 'zeek', + description: 'Contents of the second Received header. ', + name: 'zeek.smtp.second_received', + type: 'keyword', + }, + 'zeek.smtp.last_reply': { + category: 'zeek', + description: 'The last message that the server sent to the client. ', + name: 'zeek.smtp.last_reply', + type: 'keyword', + }, + 'zeek.smtp.path': { + category: 'zeek', + description: 'The message transmission path, as extracted from the headers. ', + name: 'zeek.smtp.path', + type: 'ip', + }, + 'zeek.smtp.user_agent': { + category: 'zeek', + description: 'Value of the User-Agent header from the client. ', + name: 'zeek.smtp.user_agent', + type: 'keyword', + }, + 'zeek.smtp.tls': { + category: 'zeek', + description: 'Indicates that the connection has switched to using TLS. ', + name: 'zeek.smtp.tls', + type: 'boolean', + }, + 'zeek.smtp.process_received_from': { + category: 'zeek', + description: 'Indicates if the "Received: from" headers should still be processed. ', + name: 'zeek.smtp.process_received_from', + type: 'boolean', + }, + 'zeek.smtp.has_client_activity': { + category: 'zeek', + description: 'Indicates if client activity has been seen, but not yet logged. ', + name: 'zeek.smtp.has_client_activity', + type: 'boolean', + }, + 'zeek.smtp.fuids': { + category: 'zeek', + description: + '(present if base/protocols/smtp/files.bro is loaded) An ordered vector of file unique IDs seen attached to the message. ', + name: 'zeek.smtp.fuids', + type: 'keyword', + }, + 'zeek.smtp.is_webmail': { + category: 'zeek', + description: 'Indicates if the message was sent through a webmail interface. ', + name: 'zeek.smtp.is_webmail', + type: 'boolean', + }, + 'zeek.snmp.duration': { + category: 'zeek', + description: + 'The amount of time between the first packet beloning to the SNMP session and the latest one seen. ', + name: 'zeek.snmp.duration', + type: 'double', + }, + 'zeek.snmp.version': { + category: 'zeek', + description: 'The version of SNMP being used. ', + name: 'zeek.snmp.version', + type: 'keyword', + }, + 'zeek.snmp.community': { + category: 'zeek', + description: + "The community string of the first SNMP packet associated with the session. This is used as part of SNMP's (v1 and v2c) administrative/security framework. See RFC 1157 or RFC 1901. ", + name: 'zeek.snmp.community', + type: 'keyword', + }, + 'zeek.snmp.get.requests': { + category: 'zeek', + description: + 'The number of variable bindings in GetRequest/GetNextRequest PDUs seen for the session. ', + name: 'zeek.snmp.get.requests', + type: 'integer', + }, + 'zeek.snmp.get.bulk_requests': { + category: 'zeek', + description: 'The number of variable bindings in GetBulkRequest PDUs seen for the session. ', + name: 'zeek.snmp.get.bulk_requests', + type: 'integer', + }, + 'zeek.snmp.get.responses': { + category: 'zeek', + description: + 'The number of variable bindings in GetResponse/Response PDUs seen for the session. ', + name: 'zeek.snmp.get.responses', + type: 'integer', + }, + 'zeek.snmp.set.requests': { + category: 'zeek', + description: 'The number of variable bindings in SetRequest PDUs seen for the session. ', + name: 'zeek.snmp.set.requests', + type: 'integer', + }, + 'zeek.snmp.display_string': { + category: 'zeek', + description: 'A system description of the SNMP responder endpoint. ', + name: 'zeek.snmp.display_string', + type: 'keyword', + }, + 'zeek.snmp.up_since': { + category: 'zeek', + description: "The time at which the SNMP responder endpoint claims it's been up since. ", + name: 'zeek.snmp.up_since', + type: 'date', + }, + 'zeek.socks.version': { + category: 'zeek', + description: 'Protocol version of SOCKS. ', + name: 'zeek.socks.version', + type: 'integer', + }, + 'zeek.socks.user': { + category: 'zeek', + description: 'Username used to request a login to the proxy. ', + name: 'zeek.socks.user', + type: 'keyword', + }, + 'zeek.socks.password': { + category: 'zeek', + description: 'Password used to request a login to the proxy. ', + name: 'zeek.socks.password', + type: 'keyword', + }, + 'zeek.socks.status': { + category: 'zeek', + description: 'Server status for the attempt at using the proxy. ', + name: 'zeek.socks.status', + type: 'keyword', + }, + 'zeek.socks.request.host': { + category: 'zeek', + description: 'Client requested SOCKS address. Could be an address, a name or both. ', + name: 'zeek.socks.request.host', + type: 'keyword', + }, + 'zeek.socks.request.port': { + category: 'zeek', + description: 'Client requested port. ', + name: 'zeek.socks.request.port', + type: 'integer', + }, + 'zeek.socks.bound.host': { + category: 'zeek', + description: 'Server bound address. Could be an address, a name or both. ', + name: 'zeek.socks.bound.host', + type: 'keyword', + }, + 'zeek.socks.bound.port': { + category: 'zeek', + description: 'Server bound port. ', + name: 'zeek.socks.bound.port', + type: 'integer', + }, + 'zeek.socks.capture_password': { + category: 'zeek', + description: 'Determines if the password will be captured for this request. ', + name: 'zeek.socks.capture_password', + type: 'boolean', + }, + 'zeek.ssh.client': { + category: 'zeek', + description: "The client's version string. ", + name: 'zeek.ssh.client', + type: 'keyword', + }, + 'zeek.ssh.direction': { + category: 'zeek', + description: + 'Direction of the connection. If the client was a local host logging into an external host, this would be OUTBOUND. INBOUND would be set for the opposite situation. ', + name: 'zeek.ssh.direction', + type: 'keyword', + }, + 'zeek.ssh.host_key': { + category: 'zeek', + description: "The server's key thumbprint. ", + name: 'zeek.ssh.host_key', + type: 'keyword', + }, + 'zeek.ssh.server': { + category: 'zeek', + description: "The server's version string. ", + name: 'zeek.ssh.server', + type: 'keyword', + }, + 'zeek.ssh.version': { + category: 'zeek', + description: 'SSH major version (1 or 2). ', + name: 'zeek.ssh.version', + type: 'integer', + }, + 'zeek.ssh.algorithm.cipher': { + category: 'zeek', + description: 'The encryption algorithm in use. ', + name: 'zeek.ssh.algorithm.cipher', + type: 'keyword', + }, + 'zeek.ssh.algorithm.compression': { + category: 'zeek', + description: 'The compression algorithm in use. ', + name: 'zeek.ssh.algorithm.compression', + type: 'keyword', + }, + 'zeek.ssh.algorithm.host_key': { + category: 'zeek', + description: "The server host key's algorithm. ", + name: 'zeek.ssh.algorithm.host_key', + type: 'keyword', + }, + 'zeek.ssh.algorithm.key_exchange': { + category: 'zeek', + description: 'The key exchange algorithm in use. ', + name: 'zeek.ssh.algorithm.key_exchange', + type: 'keyword', + }, + 'zeek.ssh.algorithm.mac': { + category: 'zeek', + description: 'The signing (MAC) algorithm in use. ', + name: 'zeek.ssh.algorithm.mac', + type: 'keyword', + }, + 'zeek.ssh.auth.attempts': { + category: 'zeek', + description: + "The number of authentication attemps we observed. There's always at least one, since some servers might support no authentication at all. It's important to note that not all of these are failures, since some servers require two-factor auth (e.g. password AND pubkey). ", + name: 'zeek.ssh.auth.attempts', + type: 'integer', + }, + 'zeek.ssh.auth.success': { + category: 'zeek', + description: 'Authentication result. ', + name: 'zeek.ssh.auth.success', + type: 'boolean', + }, + 'zeek.ssl.version': { + category: 'zeek', + description: 'SSL/TLS version that was logged. ', + name: 'zeek.ssl.version', + type: 'keyword', + }, + 'zeek.ssl.cipher': { + category: 'zeek', + description: 'SSL/TLS cipher suite that was logged. ', + name: 'zeek.ssl.cipher', + type: 'keyword', + }, + 'zeek.ssl.curve': { + category: 'zeek', + description: 'Elliptic curve that was logged when using ECDH/ECDHE. ', + name: 'zeek.ssl.curve', + type: 'keyword', + }, + 'zeek.ssl.resumed': { + category: 'zeek', + description: + 'Flag to indicate if the session was resumed reusing the key material exchanged in an earlier connection. ', + name: 'zeek.ssl.resumed', + type: 'boolean', + }, + 'zeek.ssl.next_protocol': { + category: 'zeek', + description: + 'Next protocol the server chose using the application layer next protocol extension. ', + name: 'zeek.ssl.next_protocol', + type: 'keyword', + }, + 'zeek.ssl.established': { + category: 'zeek', + description: 'Flag to indicate if this ssl session has been established successfully. ', + name: 'zeek.ssl.established', + type: 'boolean', + }, + 'zeek.ssl.validation.status': { + category: 'zeek', + description: 'Result of certificate validation for this connection. ', + name: 'zeek.ssl.validation.status', + type: 'keyword', + }, + 'zeek.ssl.validation.code': { + category: 'zeek', + description: + 'Result of certificate validation for this connection, given as OpenSSL validation code. ', + name: 'zeek.ssl.validation.code', + type: 'keyword', + }, + 'zeek.ssl.last_alert': { + category: 'zeek', + description: 'Last alert that was seen during the connection. ', + name: 'zeek.ssl.last_alert', + type: 'keyword', + }, + 'zeek.ssl.server.name': { + category: 'zeek', + description: + 'Value of the Server Name Indicator SSL/TLS extension. It indicates the server name that the client was requesting. ', + name: 'zeek.ssl.server.name', + type: 'keyword', + }, + 'zeek.ssl.server.cert_chain': { + category: 'zeek', + description: + 'Chain of certificates offered by the server to validate its complete signing chain. ', + name: 'zeek.ssl.server.cert_chain', + type: 'keyword', + }, + 'zeek.ssl.server.cert_chain_fuids': { + category: 'zeek', + description: + 'An ordered vector of certificate file identifiers for the certificates offered by the server. ', + name: 'zeek.ssl.server.cert_chain_fuids', + type: 'keyword', + }, + 'zeek.ssl.server.issuer.common_name': { + category: 'zeek', + description: 'Common name of the signer of the X.509 certificate offered by the server. ', + name: 'zeek.ssl.server.issuer.common_name', + type: 'keyword', + }, + 'zeek.ssl.server.issuer.country': { + category: 'zeek', + description: 'Country code of the signer of the X.509 certificate offered by the server. ', + name: 'zeek.ssl.server.issuer.country', + type: 'keyword', + }, + 'zeek.ssl.server.issuer.locality': { + category: 'zeek', + description: 'Locality of the signer of the X.509 certificate offered by the server. ', + name: 'zeek.ssl.server.issuer.locality', + type: 'keyword', + }, + 'zeek.ssl.server.issuer.organization': { + category: 'zeek', + description: 'Organization of the signer of the X.509 certificate offered by the server. ', + name: 'zeek.ssl.server.issuer.organization', + type: 'keyword', + }, + 'zeek.ssl.server.issuer.organizational_unit': { + category: 'zeek', + description: + 'Organizational unit of the signer of the X.509 certificate offered by the server. ', + name: 'zeek.ssl.server.issuer.organizational_unit', + type: 'keyword', + }, + 'zeek.ssl.server.issuer.state': { + category: 'zeek', + description: + 'State or province name of the signer of the X.509 certificate offered by the server. ', + name: 'zeek.ssl.server.issuer.state', + type: 'keyword', + }, + 'zeek.ssl.server.subject.common_name': { + category: 'zeek', + description: 'Common name of the X.509 certificate offered by the server. ', + name: 'zeek.ssl.server.subject.common_name', + type: 'keyword', + }, + 'zeek.ssl.server.subject.country': { + category: 'zeek', + description: 'Country code of the X.509 certificate offered by the server. ', + name: 'zeek.ssl.server.subject.country', + type: 'keyword', + }, + 'zeek.ssl.server.subject.locality': { + category: 'zeek', + description: 'Locality of the X.509 certificate offered by the server. ', + name: 'zeek.ssl.server.subject.locality', + type: 'keyword', + }, + 'zeek.ssl.server.subject.organization': { + category: 'zeek', + description: 'Organization of the X.509 certificate offered by the server. ', + name: 'zeek.ssl.server.subject.organization', + type: 'keyword', + }, + 'zeek.ssl.server.subject.organizational_unit': { + category: 'zeek', + description: 'Organizational unit of the X.509 certificate offered by the server. ', + name: 'zeek.ssl.server.subject.organizational_unit', + type: 'keyword', + }, + 'zeek.ssl.server.subject.state': { + category: 'zeek', + description: 'State or province name of the X.509 certificate offered by the server. ', + name: 'zeek.ssl.server.subject.state', + type: 'keyword', + }, + 'zeek.ssl.client.cert_chain': { + category: 'zeek', + description: + 'Chain of certificates offered by the client to validate its complete signing chain. ', + name: 'zeek.ssl.client.cert_chain', + type: 'keyword', + }, + 'zeek.ssl.client.cert_chain_fuids': { + category: 'zeek', + description: + 'An ordered vector of certificate file identifiers for the certificates offered by the client. ', + name: 'zeek.ssl.client.cert_chain_fuids', + type: 'keyword', + }, + 'zeek.ssl.client.issuer.common_name': { + category: 'zeek', + description: 'Common name of the signer of the X.509 certificate offered by the client. ', + name: 'zeek.ssl.client.issuer.common_name', + type: 'keyword', + }, + 'zeek.ssl.client.issuer.country': { + category: 'zeek', + description: 'Country code of the signer of the X.509 certificate offered by the client. ', + name: 'zeek.ssl.client.issuer.country', + type: 'keyword', + }, + 'zeek.ssl.client.issuer.locality': { + category: 'zeek', + description: 'Locality of the signer of the X.509 certificate offered by the client. ', + name: 'zeek.ssl.client.issuer.locality', + type: 'keyword', + }, + 'zeek.ssl.client.issuer.organization': { + category: 'zeek', + description: 'Organization of the signer of the X.509 certificate offered by the client. ', + name: 'zeek.ssl.client.issuer.organization', + type: 'keyword', + }, + 'zeek.ssl.client.issuer.organizational_unit': { + category: 'zeek', + description: + 'Organizational unit of the signer of the X.509 certificate offered by the client. ', + name: 'zeek.ssl.client.issuer.organizational_unit', + type: 'keyword', + }, + 'zeek.ssl.client.issuer.state': { + category: 'zeek', + description: + 'State or province name of the signer of the X.509 certificate offered by the client. ', + name: 'zeek.ssl.client.issuer.state', + type: 'keyword', + }, + 'zeek.ssl.client.subject.common_name': { + category: 'zeek', + description: 'Common name of the X.509 certificate offered by the client. ', + name: 'zeek.ssl.client.subject.common_name', + type: 'keyword', + }, + 'zeek.ssl.client.subject.country': { + category: 'zeek', + description: 'Country code of the X.509 certificate offered by the client. ', + name: 'zeek.ssl.client.subject.country', + type: 'keyword', + }, + 'zeek.ssl.client.subject.locality': { + category: 'zeek', + description: 'Locality of the X.509 certificate offered by the client. ', + name: 'zeek.ssl.client.subject.locality', + type: 'keyword', + }, + 'zeek.ssl.client.subject.organization': { + category: 'zeek', + description: 'Organization of the X.509 certificate offered by the client. ', + name: 'zeek.ssl.client.subject.organization', + type: 'keyword', + }, + 'zeek.ssl.client.subject.organizational_unit': { + category: 'zeek', + description: 'Organizational unit of the X.509 certificate offered by the client. ', + name: 'zeek.ssl.client.subject.organizational_unit', + type: 'keyword', + }, + 'zeek.ssl.client.subject.state': { + category: 'zeek', + description: 'State or province name of the X.509 certificate offered by the client. ', + name: 'zeek.ssl.client.subject.state', + type: 'keyword', + }, + 'zeek.stats.peer': { + category: 'zeek', + description: 'Peer that generated this log. Mostly for clusters. ', + name: 'zeek.stats.peer', + type: 'keyword', + }, + 'zeek.stats.memory': { + category: 'zeek', + description: 'Amount of memory currently in use in MB. ', + name: 'zeek.stats.memory', + type: 'integer', + }, + 'zeek.stats.packets.processed': { + category: 'zeek', + description: 'Number of packets processed since the last stats interval. ', + name: 'zeek.stats.packets.processed', + type: 'long', + }, + 'zeek.stats.packets.dropped': { + category: 'zeek', + description: + 'Number of packets dropped since the last stats interval if reading live traffic. ', + name: 'zeek.stats.packets.dropped', + type: 'long', + }, + 'zeek.stats.packets.received': { + category: 'zeek', + description: + 'Number of packets seen on the link since the last stats interval if reading live traffic. ', + name: 'zeek.stats.packets.received', + type: 'long', + }, + 'zeek.stats.bytes.received': { + category: 'zeek', + description: 'Number of bytes received since the last stats interval if reading live traffic. ', + name: 'zeek.stats.bytes.received', + type: 'long', + }, + 'zeek.stats.connections.tcp.active': { + category: 'zeek', + description: 'TCP connections currently in memory. ', + name: 'zeek.stats.connections.tcp.active', + type: 'integer', + }, + 'zeek.stats.connections.tcp.count': { + category: 'zeek', + description: 'TCP connections seen since last stats interval. ', + name: 'zeek.stats.connections.tcp.count', + type: 'integer', + }, + 'zeek.stats.connections.udp.active': { + category: 'zeek', + description: 'UDP connections currently in memory. ', + name: 'zeek.stats.connections.udp.active', + type: 'integer', + }, + 'zeek.stats.connections.udp.count': { + category: 'zeek', + description: 'UDP connections seen since last stats interval. ', + name: 'zeek.stats.connections.udp.count', + type: 'integer', + }, + 'zeek.stats.connections.icmp.active': { + category: 'zeek', + description: 'ICMP connections currently in memory. ', + name: 'zeek.stats.connections.icmp.active', + type: 'integer', + }, + 'zeek.stats.connections.icmp.count': { + category: 'zeek', + description: 'ICMP connections seen since last stats interval. ', + name: 'zeek.stats.connections.icmp.count', + type: 'integer', + }, + 'zeek.stats.events.processed': { + category: 'zeek', + description: 'Number of events processed since the last stats interval. ', + name: 'zeek.stats.events.processed', + type: 'integer', + }, + 'zeek.stats.events.queued': { + category: 'zeek', + description: 'Number of events that have been queued since the last stats interval. ', + name: 'zeek.stats.events.queued', + type: 'integer', + }, + 'zeek.stats.timers.count': { + category: 'zeek', + description: 'Number of timers scheduled since last stats interval. ', + name: 'zeek.stats.timers.count', + type: 'integer', + }, + 'zeek.stats.timers.active': { + category: 'zeek', + description: 'Current number of scheduled timers. ', + name: 'zeek.stats.timers.active', + type: 'integer', + }, + 'zeek.stats.files.count': { + category: 'zeek', + description: 'Number of files seen since last stats interval. ', + name: 'zeek.stats.files.count', + type: 'integer', + }, + 'zeek.stats.files.active': { + category: 'zeek', + description: 'Current number of files actively being seen. ', + name: 'zeek.stats.files.active', + type: 'integer', + }, + 'zeek.stats.dns_requests.count': { + category: 'zeek', + description: 'Number of DNS requests seen since last stats interval. ', + name: 'zeek.stats.dns_requests.count', + type: 'integer', + }, + 'zeek.stats.dns_requests.active': { + category: 'zeek', + description: 'Current number of DNS requests awaiting a reply. ', + name: 'zeek.stats.dns_requests.active', + type: 'integer', + }, + 'zeek.stats.reassembly_size.tcp': { + category: 'zeek', + description: 'Current size of TCP data in reassembly. ', + name: 'zeek.stats.reassembly_size.tcp', + type: 'integer', + }, + 'zeek.stats.reassembly_size.file': { + category: 'zeek', + description: 'Current size of File data in reassembly. ', + name: 'zeek.stats.reassembly_size.file', + type: 'integer', + }, + 'zeek.stats.reassembly_size.frag': { + category: 'zeek', + description: 'Current size of packet fragment data in reassembly. ', + name: 'zeek.stats.reassembly_size.frag', + type: 'integer', + }, + 'zeek.stats.reassembly_size.unknown': { + category: 'zeek', + description: 'Current size of unknown data in reassembly (this is only PIA buffer right now). ', + name: 'zeek.stats.reassembly_size.unknown', + type: 'integer', + }, + 'zeek.stats.timestamp_lag': { + category: 'zeek', + description: 'Lag between the wall clock and packet timestamps if reading live traffic. ', + name: 'zeek.stats.timestamp_lag', + type: 'integer', + }, + 'zeek.syslog.facility': { + category: 'zeek', + description: 'Syslog facility for the message. ', + name: 'zeek.syslog.facility', + type: 'keyword', + }, + 'zeek.syslog.severity': { + category: 'zeek', + description: 'Syslog severity for the message. ', + name: 'zeek.syslog.severity', + type: 'keyword', + }, + 'zeek.syslog.message': { + category: 'zeek', + description: 'The plain text message. ', + name: 'zeek.syslog.message', + type: 'keyword', + }, + 'zeek.tunnel.type': { + category: 'zeek', + description: 'The type of tunnel. ', + name: 'zeek.tunnel.type', + type: 'keyword', + }, + 'zeek.tunnel.action': { + category: 'zeek', + description: 'The type of activity that occurred. ', + name: 'zeek.tunnel.action', + type: 'keyword', + }, + 'zeek.weird.name': { + category: 'zeek', + description: 'The name of the weird that occurred. ', + name: 'zeek.weird.name', + type: 'keyword', + }, + 'zeek.weird.additional_info': { + category: 'zeek', + description: 'Additional information accompanying the weird if any. ', + name: 'zeek.weird.additional_info', + type: 'keyword', + }, + 'zeek.weird.notice': { + category: 'zeek', + description: 'Indicate if this weird was also turned into a notice. ', + name: 'zeek.weird.notice', + type: 'boolean', + }, + 'zeek.weird.peer': { + category: 'zeek', + description: + 'The peer that originated this weird. This is helpful in cluster deployments if a particular cluster node is having trouble to help identify which node is having trouble. ', + name: 'zeek.weird.peer', + type: 'keyword', + }, + 'zeek.weird.identifier': { + category: 'zeek', + description: + 'This field is to be provided when a weird is generated for the purpose of deduplicating weirds. The identifier string should be unique for a single instance of the weird. This field is used to define when a weird is conceptually a duplicate of a previous weird. ', + name: 'zeek.weird.identifier', + type: 'keyword', + }, + 'zeek.x509.id': { + category: 'zeek', + description: 'File id of this certificate. ', + name: 'zeek.x509.id', + type: 'keyword', + }, + 'zeek.x509.certificate.version': { + category: 'zeek', + description: 'Version number. ', + name: 'zeek.x509.certificate.version', + type: 'integer', + }, + 'zeek.x509.certificate.serial': { + category: 'zeek', + description: 'Serial number. ', + name: 'zeek.x509.certificate.serial', + type: 'keyword', + }, + 'zeek.x509.certificate.subject.country': { + category: 'zeek', + description: 'Country provided in the certificate subject. ', + name: 'zeek.x509.certificate.subject.country', + type: 'keyword', + }, + 'zeek.x509.certificate.subject.common_name': { + category: 'zeek', + description: 'Common name provided in the certificate subject. ', + name: 'zeek.x509.certificate.subject.common_name', + type: 'keyword', + }, + 'zeek.x509.certificate.subject.locality': { + category: 'zeek', + description: 'Locality provided in the certificate subject. ', + name: 'zeek.x509.certificate.subject.locality', + type: 'keyword', + }, + 'zeek.x509.certificate.subject.organization': { + category: 'zeek', + description: 'Organization provided in the certificate subject. ', + name: 'zeek.x509.certificate.subject.organization', + type: 'keyword', + }, + 'zeek.x509.certificate.subject.organizational_unit': { + category: 'zeek', + description: 'Organizational unit provided in the certificate subject. ', + name: 'zeek.x509.certificate.subject.organizational_unit', + type: 'keyword', + }, + 'zeek.x509.certificate.subject.state': { + category: 'zeek', + description: 'State or province provided in the certificate subject. ', + name: 'zeek.x509.certificate.subject.state', + type: 'keyword', + }, + 'zeek.x509.certificate.issuer.country': { + category: 'zeek', + description: 'Country provided in the certificate issuer field. ', + name: 'zeek.x509.certificate.issuer.country', + type: 'keyword', + }, + 'zeek.x509.certificate.issuer.common_name': { + category: 'zeek', + description: 'Common name provided in the certificate issuer field. ', + name: 'zeek.x509.certificate.issuer.common_name', + type: 'keyword', + }, + 'zeek.x509.certificate.issuer.locality': { + category: 'zeek', + description: 'Locality provided in the certificate issuer field. ', + name: 'zeek.x509.certificate.issuer.locality', + type: 'keyword', + }, + 'zeek.x509.certificate.issuer.organization': { + category: 'zeek', + description: 'Organization provided in the certificate issuer field. ', + name: 'zeek.x509.certificate.issuer.organization', + type: 'keyword', + }, + 'zeek.x509.certificate.issuer.organizational_unit': { + category: 'zeek', + description: 'Organizational unit provided in the certificate issuer field. ', + name: 'zeek.x509.certificate.issuer.organizational_unit', + type: 'keyword', + }, + 'zeek.x509.certificate.issuer.state': { + category: 'zeek', + description: 'State or province provided in the certificate issuer field. ', + name: 'zeek.x509.certificate.issuer.state', + type: 'keyword', + }, + 'zeek.x509.certificate.common_name': { + category: 'zeek', + description: 'Last (most specific) common name. ', + name: 'zeek.x509.certificate.common_name', + type: 'keyword', + }, + 'zeek.x509.certificate.valid.from': { + category: 'zeek', + description: 'Timestamp before when certificate is not valid. ', + name: 'zeek.x509.certificate.valid.from', + type: 'date', + }, + 'zeek.x509.certificate.valid.until': { + category: 'zeek', + description: 'Timestamp after when certificate is not valid. ', + name: 'zeek.x509.certificate.valid.until', + type: 'date', + }, + 'zeek.x509.certificate.key.algorithm': { + category: 'zeek', + description: 'Name of the key algorithm. ', + name: 'zeek.x509.certificate.key.algorithm', + type: 'keyword', + }, + 'zeek.x509.certificate.key.type': { + category: 'zeek', + description: 'Key type, if key parseable by openssl (either rsa, dsa or ec). ', + name: 'zeek.x509.certificate.key.type', + type: 'keyword', + }, + 'zeek.x509.certificate.key.length': { + category: 'zeek', + description: 'Key length in bits. ', + name: 'zeek.x509.certificate.key.length', + type: 'integer', + }, + 'zeek.x509.certificate.signature_algorithm': { + category: 'zeek', + description: 'Name of the signature algorithm. ', + name: 'zeek.x509.certificate.signature_algorithm', + type: 'keyword', + }, + 'zeek.x509.certificate.exponent': { + category: 'zeek', + description: 'Exponent, if RSA-certificate. ', + name: 'zeek.x509.certificate.exponent', + type: 'keyword', + }, + 'zeek.x509.certificate.curve': { + category: 'zeek', + description: 'Curve, if EC-certificate. ', + name: 'zeek.x509.certificate.curve', + type: 'keyword', + }, + 'zeek.x509.san.dns': { + category: 'zeek', + description: 'List of DNS entries in SAN. ', + name: 'zeek.x509.san.dns', + type: 'keyword', + }, + 'zeek.x509.san.uri': { + category: 'zeek', + description: 'List of URI entries in SAN. ', + name: 'zeek.x509.san.uri', + type: 'keyword', + }, + 'zeek.x509.san.email': { + category: 'zeek', + description: 'List of email entries in SAN. ', + name: 'zeek.x509.san.email', + type: 'keyword', + }, + 'zeek.x509.san.ip': { + category: 'zeek', + description: 'List of IP entries in SAN. ', + name: 'zeek.x509.san.ip', + type: 'ip', + }, + 'zeek.x509.san.other_fields': { + category: 'zeek', + description: 'True if the certificate contained other, not recognized or parsed name fields. ', + name: 'zeek.x509.san.other_fields', + type: 'boolean', + }, + 'zeek.x509.basic_constraints.certificate_authority': { + category: 'zeek', + description: 'CA flag set or not. ', + name: 'zeek.x509.basic_constraints.certificate_authority', + type: 'boolean', + }, + 'zeek.x509.basic_constraints.path_length': { + category: 'zeek', + description: 'Maximum path length. ', + name: 'zeek.x509.basic_constraints.path_length', + type: 'integer', + }, + 'zeek.x509.log_cert': { + category: 'zeek', + description: + 'Present if policy/protocols/ssl/log-hostcerts-only.bro is loaded Logging of certificate is suppressed if set to F. ', + name: 'zeek.x509.log_cert', + type: 'boolean', + }, + 'awscloudwatch.log_group': { + category: 'awscloudwatch', + description: 'The name of the log group to which this event belongs.', + name: 'awscloudwatch.log_group', + type: 'keyword', + }, + 'awscloudwatch.log_stream': { + category: 'awscloudwatch', + description: 'The name of the log stream to which this event belongs.', + name: 'awscloudwatch.log_stream', + type: 'keyword', + }, + 'awscloudwatch.ingestion_time': { + category: 'awscloudwatch', + description: 'The time the event was ingested in AWS CloudWatch.', + name: 'awscloudwatch.ingestion_time', + type: 'keyword', + }, + 'netflow.type': { + category: 'netflow', + description: 'The type of NetFlow record described by this event. ', + name: 'netflow.type', + type: 'keyword', + }, + 'netflow.exporter.address': { + category: 'netflow', + description: "Exporter's network address in IP:port format. ", + name: 'netflow.exporter.address', + type: 'keyword', + }, + 'netflow.exporter.source_id': { + category: 'netflow', + description: 'Observation domain ID to which this record belongs. ', + name: 'netflow.exporter.source_id', + type: 'long', + }, + 'netflow.exporter.timestamp': { + category: 'netflow', + description: 'Time and date of export. ', + name: 'netflow.exporter.timestamp', + type: 'date', + }, + 'netflow.exporter.uptime_millis': { + category: 'netflow', + description: 'How long the exporter process has been running, in milliseconds. ', + name: 'netflow.exporter.uptime_millis', + type: 'long', + }, + 'netflow.exporter.version': { + category: 'netflow', + description: 'NetFlow version used. ', + name: 'netflow.exporter.version', + type: 'integer', + }, + 'netflow.octet_delta_count': { + category: 'netflow', + name: 'netflow.octet_delta_count', + type: 'long', + }, + 'netflow.packet_delta_count': { + category: 'netflow', + name: 'netflow.packet_delta_count', + type: 'long', + }, + 'netflow.delta_flow_count': { + category: 'netflow', + name: 'netflow.delta_flow_count', + type: 'long', + }, + 'netflow.protocol_identifier': { + category: 'netflow', + name: 'netflow.protocol_identifier', + type: 'short', + }, + 'netflow.ip_class_of_service': { + category: 'netflow', + name: 'netflow.ip_class_of_service', + type: 'short', + }, + 'netflow.tcp_control_bits': { + category: 'netflow', + name: 'netflow.tcp_control_bits', + type: 'integer', + }, + 'netflow.source_transport_port': { + category: 'netflow', + name: 'netflow.source_transport_port', + type: 'integer', + }, + 'netflow.source_ipv4_address': { + category: 'netflow', + name: 'netflow.source_ipv4_address', + type: 'ip', + }, + 'netflow.source_ipv4_prefix_length': { + category: 'netflow', + name: 'netflow.source_ipv4_prefix_length', + type: 'short', + }, + 'netflow.ingress_interface': { + category: 'netflow', + name: 'netflow.ingress_interface', + type: 'long', + }, + 'netflow.destination_transport_port': { + category: 'netflow', + name: 'netflow.destination_transport_port', + type: 'integer', + }, + 'netflow.destination_ipv4_address': { + category: 'netflow', + name: 'netflow.destination_ipv4_address', + type: 'ip', + }, + 'netflow.destination_ipv4_prefix_length': { + category: 'netflow', + name: 'netflow.destination_ipv4_prefix_length', + type: 'short', + }, + 'netflow.egress_interface': { + category: 'netflow', + name: 'netflow.egress_interface', + type: 'long', + }, + 'netflow.ip_next_hop_ipv4_address': { + category: 'netflow', + name: 'netflow.ip_next_hop_ipv4_address', + type: 'ip', + }, + 'netflow.bgp_source_as_number': { + category: 'netflow', + name: 'netflow.bgp_source_as_number', + type: 'long', + }, + 'netflow.bgp_destination_as_number': { + category: 'netflow', + name: 'netflow.bgp_destination_as_number', + type: 'long', + }, + 'netflow.bgp_next_hop_ipv4_address': { + category: 'netflow', + name: 'netflow.bgp_next_hop_ipv4_address', + type: 'ip', + }, + 'netflow.post_mcast_packet_delta_count': { + category: 'netflow', + name: 'netflow.post_mcast_packet_delta_count', + type: 'long', + }, + 'netflow.post_mcast_octet_delta_count': { + category: 'netflow', + name: 'netflow.post_mcast_octet_delta_count', + type: 'long', + }, + 'netflow.flow_end_sys_up_time': { + category: 'netflow', + name: 'netflow.flow_end_sys_up_time', + type: 'long', + }, + 'netflow.flow_start_sys_up_time': { + category: 'netflow', + name: 'netflow.flow_start_sys_up_time', + type: 'long', + }, + 'netflow.post_octet_delta_count': { + category: 'netflow', + name: 'netflow.post_octet_delta_count', + type: 'long', + }, + 'netflow.post_packet_delta_count': { + category: 'netflow', + name: 'netflow.post_packet_delta_count', + type: 'long', + }, + 'netflow.minimum_ip_total_length': { + category: 'netflow', + name: 'netflow.minimum_ip_total_length', + type: 'long', + }, + 'netflow.maximum_ip_total_length': { + category: 'netflow', + name: 'netflow.maximum_ip_total_length', + type: 'long', + }, + 'netflow.source_ipv6_address': { + category: 'netflow', + name: 'netflow.source_ipv6_address', + type: 'ip', + }, + 'netflow.destination_ipv6_address': { + category: 'netflow', + name: 'netflow.destination_ipv6_address', + type: 'ip', + }, + 'netflow.source_ipv6_prefix_length': { + category: 'netflow', + name: 'netflow.source_ipv6_prefix_length', + type: 'short', + }, + 'netflow.destination_ipv6_prefix_length': { + category: 'netflow', + name: 'netflow.destination_ipv6_prefix_length', + type: 'short', + }, + 'netflow.flow_label_ipv6': { + category: 'netflow', + name: 'netflow.flow_label_ipv6', + type: 'long', + }, + 'netflow.icmp_type_code_ipv4': { + category: 'netflow', + name: 'netflow.icmp_type_code_ipv4', + type: 'integer', + }, + 'netflow.igmp_type': { + category: 'netflow', + name: 'netflow.igmp_type', + type: 'short', + }, + 'netflow.sampling_interval': { + category: 'netflow', + name: 'netflow.sampling_interval', + type: 'long', + }, + 'netflow.sampling_algorithm': { + category: 'netflow', + name: 'netflow.sampling_algorithm', + type: 'short', + }, + 'netflow.flow_active_timeout': { + category: 'netflow', + name: 'netflow.flow_active_timeout', + type: 'integer', + }, + 'netflow.flow_idle_timeout': { + category: 'netflow', + name: 'netflow.flow_idle_timeout', + type: 'integer', + }, + 'netflow.engine_type': { + category: 'netflow', + name: 'netflow.engine_type', + type: 'short', + }, + 'netflow.engine_id': { + category: 'netflow', + name: 'netflow.engine_id', + type: 'short', + }, + 'netflow.exported_octet_total_count': { + category: 'netflow', + name: 'netflow.exported_octet_total_count', + type: 'long', + }, + 'netflow.exported_message_total_count': { + category: 'netflow', + name: 'netflow.exported_message_total_count', + type: 'long', + }, + 'netflow.exported_flow_record_total_count': { + category: 'netflow', + name: 'netflow.exported_flow_record_total_count', + type: 'long', + }, + 'netflow.ipv4_router_sc': { + category: 'netflow', + name: 'netflow.ipv4_router_sc', + type: 'ip', + }, + 'netflow.source_ipv4_prefix': { + category: 'netflow', + name: 'netflow.source_ipv4_prefix', + type: 'ip', + }, + 'netflow.destination_ipv4_prefix': { + category: 'netflow', + name: 'netflow.destination_ipv4_prefix', + type: 'ip', + }, + 'netflow.mpls_top_label_type': { + category: 'netflow', + name: 'netflow.mpls_top_label_type', + type: 'short', + }, + 'netflow.mpls_top_label_ipv4_address': { + category: 'netflow', + name: 'netflow.mpls_top_label_ipv4_address', + type: 'ip', + }, + 'netflow.sampler_id': { + category: 'netflow', + name: 'netflow.sampler_id', + type: 'short', + }, + 'netflow.sampler_mode': { + category: 'netflow', + name: 'netflow.sampler_mode', + type: 'short', + }, + 'netflow.sampler_random_interval': { + category: 'netflow', + name: 'netflow.sampler_random_interval', + type: 'long', + }, + 'netflow.class_id': { + category: 'netflow', + name: 'netflow.class_id', + type: 'long', + }, + 'netflow.minimum_ttl': { + category: 'netflow', + name: 'netflow.minimum_ttl', + type: 'short', + }, + 'netflow.maximum_ttl': { + category: 'netflow', + name: 'netflow.maximum_ttl', + type: 'short', + }, + 'netflow.fragment_identification': { + category: 'netflow', + name: 'netflow.fragment_identification', + type: 'long', + }, + 'netflow.post_ip_class_of_service': { + category: 'netflow', + name: 'netflow.post_ip_class_of_service', + type: 'short', + }, + 'netflow.source_mac_address': { + category: 'netflow', + name: 'netflow.source_mac_address', + type: 'keyword', + }, + 'netflow.post_destination_mac_address': { + category: 'netflow', + name: 'netflow.post_destination_mac_address', + type: 'keyword', + }, + 'netflow.vlan_id': { + category: 'netflow', + name: 'netflow.vlan_id', + type: 'integer', + }, + 'netflow.post_vlan_id': { + category: 'netflow', + name: 'netflow.post_vlan_id', + type: 'integer', + }, + 'netflow.ip_version': { + category: 'netflow', + name: 'netflow.ip_version', + type: 'short', + }, + 'netflow.flow_direction': { + category: 'netflow', + name: 'netflow.flow_direction', + type: 'short', + }, + 'netflow.ip_next_hop_ipv6_address': { + category: 'netflow', + name: 'netflow.ip_next_hop_ipv6_address', + type: 'ip', + }, + 'netflow.bgp_next_hop_ipv6_address': { + category: 'netflow', + name: 'netflow.bgp_next_hop_ipv6_address', + type: 'ip', + }, + 'netflow.ipv6_extension_headers': { + category: 'netflow', + name: 'netflow.ipv6_extension_headers', + type: 'long', + }, + 'netflow.mpls_top_label_stack_section': { + category: 'netflow', + name: 'netflow.mpls_top_label_stack_section', + type: 'short', + }, + 'netflow.mpls_label_stack_section2': { + category: 'netflow', + name: 'netflow.mpls_label_stack_section2', + type: 'short', + }, + 'netflow.mpls_label_stack_section3': { + category: 'netflow', + name: 'netflow.mpls_label_stack_section3', + type: 'short', + }, + 'netflow.mpls_label_stack_section4': { + category: 'netflow', + name: 'netflow.mpls_label_stack_section4', + type: 'short', + }, + 'netflow.mpls_label_stack_section5': { + category: 'netflow', + name: 'netflow.mpls_label_stack_section5', + type: 'short', + }, + 'netflow.mpls_label_stack_section6': { + category: 'netflow', + name: 'netflow.mpls_label_stack_section6', + type: 'short', + }, + 'netflow.mpls_label_stack_section7': { + category: 'netflow', + name: 'netflow.mpls_label_stack_section7', + type: 'short', + }, + 'netflow.mpls_label_stack_section8': { + category: 'netflow', + name: 'netflow.mpls_label_stack_section8', + type: 'short', + }, + 'netflow.mpls_label_stack_section9': { + category: 'netflow', + name: 'netflow.mpls_label_stack_section9', + type: 'short', + }, + 'netflow.mpls_label_stack_section10': { + category: 'netflow', + name: 'netflow.mpls_label_stack_section10', + type: 'short', + }, + 'netflow.destination_mac_address': { + category: 'netflow', + name: 'netflow.destination_mac_address', + type: 'keyword', + }, + 'netflow.post_source_mac_address': { + category: 'netflow', + name: 'netflow.post_source_mac_address', + type: 'keyword', + }, + 'netflow.interface_name': { + category: 'netflow', + name: 'netflow.interface_name', + type: 'keyword', + }, + 'netflow.interface_description': { + category: 'netflow', + name: 'netflow.interface_description', + type: 'keyword', + }, + 'netflow.sampler_name': { + category: 'netflow', + name: 'netflow.sampler_name', + type: 'keyword', + }, + 'netflow.octet_total_count': { + category: 'netflow', + name: 'netflow.octet_total_count', + type: 'long', + }, + 'netflow.packet_total_count': { + category: 'netflow', + name: 'netflow.packet_total_count', + type: 'long', + }, + 'netflow.flags_and_sampler_id': { + category: 'netflow', + name: 'netflow.flags_and_sampler_id', + type: 'long', + }, + 'netflow.fragment_offset': { + category: 'netflow', + name: 'netflow.fragment_offset', + type: 'integer', + }, + 'netflow.forwarding_status': { + category: 'netflow', + name: 'netflow.forwarding_status', + type: 'short', + }, + 'netflow.mpls_vpn_route_distinguisher': { + category: 'netflow', + name: 'netflow.mpls_vpn_route_distinguisher', + type: 'short', + }, + 'netflow.mpls_top_label_prefix_length': { + category: 'netflow', + name: 'netflow.mpls_top_label_prefix_length', + type: 'short', + }, + 'netflow.src_traffic_index': { + category: 'netflow', + name: 'netflow.src_traffic_index', + type: 'long', + }, + 'netflow.dst_traffic_index': { + category: 'netflow', + name: 'netflow.dst_traffic_index', + type: 'long', + }, + 'netflow.application_description': { + category: 'netflow', + name: 'netflow.application_description', + type: 'keyword', + }, + 'netflow.application_id': { + category: 'netflow', + name: 'netflow.application_id', + type: 'short', + }, + 'netflow.application_name': { + category: 'netflow', + name: 'netflow.application_name', + type: 'keyword', + }, + 'netflow.post_ip_diff_serv_code_point': { + category: 'netflow', + name: 'netflow.post_ip_diff_serv_code_point', + type: 'short', + }, + 'netflow.multicast_replication_factor': { + category: 'netflow', + name: 'netflow.multicast_replication_factor', + type: 'long', + }, + 'netflow.class_name': { + category: 'netflow', + name: 'netflow.class_name', + type: 'keyword', + }, + 'netflow.classification_engine_id': { + category: 'netflow', + name: 'netflow.classification_engine_id', + type: 'short', + }, + 'netflow.layer2packet_section_offset': { + category: 'netflow', + name: 'netflow.layer2packet_section_offset', + type: 'integer', + }, + 'netflow.layer2packet_section_size': { + category: 'netflow', + name: 'netflow.layer2packet_section_size', + type: 'integer', + }, + 'netflow.layer2packet_section_data': { + category: 'netflow', + name: 'netflow.layer2packet_section_data', + type: 'short', + }, + 'netflow.bgp_next_adjacent_as_number': { + category: 'netflow', + name: 'netflow.bgp_next_adjacent_as_number', + type: 'long', + }, + 'netflow.bgp_prev_adjacent_as_number': { + category: 'netflow', + name: 'netflow.bgp_prev_adjacent_as_number', + type: 'long', + }, + 'netflow.exporter_ipv4_address': { + category: 'netflow', + name: 'netflow.exporter_ipv4_address', + type: 'ip', + }, + 'netflow.exporter_ipv6_address': { + category: 'netflow', + name: 'netflow.exporter_ipv6_address', + type: 'ip', + }, + 'netflow.dropped_octet_delta_count': { + category: 'netflow', + name: 'netflow.dropped_octet_delta_count', + type: 'long', + }, + 'netflow.dropped_packet_delta_count': { + category: 'netflow', + name: 'netflow.dropped_packet_delta_count', + type: 'long', + }, + 'netflow.dropped_octet_total_count': { + category: 'netflow', + name: 'netflow.dropped_octet_total_count', + type: 'long', + }, + 'netflow.dropped_packet_total_count': { + category: 'netflow', + name: 'netflow.dropped_packet_total_count', + type: 'long', + }, + 'netflow.flow_end_reason': { + category: 'netflow', + name: 'netflow.flow_end_reason', + type: 'short', + }, + 'netflow.common_properties_id': { + category: 'netflow', + name: 'netflow.common_properties_id', + type: 'long', + }, + 'netflow.observation_point_id': { + category: 'netflow', + name: 'netflow.observation_point_id', + type: 'long', + }, + 'netflow.icmp_type_code_ipv6': { + category: 'netflow', + name: 'netflow.icmp_type_code_ipv6', + type: 'integer', + }, + 'netflow.mpls_top_label_ipv6_address': { + category: 'netflow', + name: 'netflow.mpls_top_label_ipv6_address', + type: 'ip', + }, + 'netflow.line_card_id': { + category: 'netflow', + name: 'netflow.line_card_id', + type: 'long', + }, + 'netflow.port_id': { + category: 'netflow', + name: 'netflow.port_id', + type: 'long', + }, + 'netflow.metering_process_id': { + category: 'netflow', + name: 'netflow.metering_process_id', + type: 'long', + }, + 'netflow.exporting_process_id': { + category: 'netflow', + name: 'netflow.exporting_process_id', + type: 'long', + }, + 'netflow.template_id': { + category: 'netflow', + name: 'netflow.template_id', + type: 'integer', + }, + 'netflow.wlan_channel_id': { + category: 'netflow', + name: 'netflow.wlan_channel_id', + type: 'short', + }, + 'netflow.wlan_ssid': { + category: 'netflow', + name: 'netflow.wlan_ssid', + type: 'keyword', + }, + 'netflow.flow_id': { + category: 'netflow', + name: 'netflow.flow_id', + type: 'long', + }, + 'netflow.observation_domain_id': { + category: 'netflow', + name: 'netflow.observation_domain_id', + type: 'long', + }, + 'netflow.flow_start_seconds': { + category: 'netflow', + name: 'netflow.flow_start_seconds', + type: 'date', + }, + 'netflow.flow_end_seconds': { + category: 'netflow', + name: 'netflow.flow_end_seconds', + type: 'date', + }, + 'netflow.flow_start_milliseconds': { + category: 'netflow', + name: 'netflow.flow_start_milliseconds', + type: 'date', + }, + 'netflow.flow_end_milliseconds': { + category: 'netflow', + name: 'netflow.flow_end_milliseconds', + type: 'date', + }, + 'netflow.flow_start_microseconds': { + category: 'netflow', + name: 'netflow.flow_start_microseconds', + type: 'date', + }, + 'netflow.flow_end_microseconds': { + category: 'netflow', + name: 'netflow.flow_end_microseconds', + type: 'date', + }, + 'netflow.flow_start_nanoseconds': { + category: 'netflow', + name: 'netflow.flow_start_nanoseconds', + type: 'date', + }, + 'netflow.flow_end_nanoseconds': { + category: 'netflow', + name: 'netflow.flow_end_nanoseconds', + type: 'date', + }, + 'netflow.flow_start_delta_microseconds': { + category: 'netflow', + name: 'netflow.flow_start_delta_microseconds', + type: 'long', + }, + 'netflow.flow_end_delta_microseconds': { + category: 'netflow', + name: 'netflow.flow_end_delta_microseconds', + type: 'long', + }, + 'netflow.system_init_time_milliseconds': { + category: 'netflow', + name: 'netflow.system_init_time_milliseconds', + type: 'date', + }, + 'netflow.flow_duration_milliseconds': { + category: 'netflow', + name: 'netflow.flow_duration_milliseconds', + type: 'long', + }, + 'netflow.flow_duration_microseconds': { + category: 'netflow', + name: 'netflow.flow_duration_microseconds', + type: 'long', + }, + 'netflow.observed_flow_total_count': { + category: 'netflow', + name: 'netflow.observed_flow_total_count', + type: 'long', + }, + 'netflow.ignored_packet_total_count': { + category: 'netflow', + name: 'netflow.ignored_packet_total_count', + type: 'long', + }, + 'netflow.ignored_octet_total_count': { + category: 'netflow', + name: 'netflow.ignored_octet_total_count', + type: 'long', + }, + 'netflow.not_sent_flow_total_count': { + category: 'netflow', + name: 'netflow.not_sent_flow_total_count', + type: 'long', + }, + 'netflow.not_sent_packet_total_count': { + category: 'netflow', + name: 'netflow.not_sent_packet_total_count', + type: 'long', + }, + 'netflow.not_sent_octet_total_count': { + category: 'netflow', + name: 'netflow.not_sent_octet_total_count', + type: 'long', + }, + 'netflow.destination_ipv6_prefix': { + category: 'netflow', + name: 'netflow.destination_ipv6_prefix', + type: 'ip', + }, + 'netflow.source_ipv6_prefix': { + category: 'netflow', + name: 'netflow.source_ipv6_prefix', + type: 'ip', + }, + 'netflow.post_octet_total_count': { + category: 'netflow', + name: 'netflow.post_octet_total_count', + type: 'long', + }, + 'netflow.post_packet_total_count': { + category: 'netflow', + name: 'netflow.post_packet_total_count', + type: 'long', + }, + 'netflow.flow_key_indicator': { + category: 'netflow', + name: 'netflow.flow_key_indicator', + type: 'long', + }, + 'netflow.post_mcast_packet_total_count': { + category: 'netflow', + name: 'netflow.post_mcast_packet_total_count', + type: 'long', + }, + 'netflow.post_mcast_octet_total_count': { + category: 'netflow', + name: 'netflow.post_mcast_octet_total_count', + type: 'long', + }, + 'netflow.icmp_type_ipv4': { + category: 'netflow', + name: 'netflow.icmp_type_ipv4', + type: 'short', + }, + 'netflow.icmp_code_ipv4': { + category: 'netflow', + name: 'netflow.icmp_code_ipv4', + type: 'short', + }, + 'netflow.icmp_type_ipv6': { + category: 'netflow', + name: 'netflow.icmp_type_ipv6', + type: 'short', + }, + 'netflow.icmp_code_ipv6': { + category: 'netflow', + name: 'netflow.icmp_code_ipv6', + type: 'short', + }, + 'netflow.udp_source_port': { + category: 'netflow', + name: 'netflow.udp_source_port', + type: 'integer', + }, + 'netflow.udp_destination_port': { + category: 'netflow', + name: 'netflow.udp_destination_port', + type: 'integer', + }, + 'netflow.tcp_source_port': { + category: 'netflow', + name: 'netflow.tcp_source_port', + type: 'integer', + }, + 'netflow.tcp_destination_port': { + category: 'netflow', + name: 'netflow.tcp_destination_port', + type: 'integer', + }, + 'netflow.tcp_sequence_number': { + category: 'netflow', + name: 'netflow.tcp_sequence_number', + type: 'long', + }, + 'netflow.tcp_acknowledgement_number': { + category: 'netflow', + name: 'netflow.tcp_acknowledgement_number', + type: 'long', + }, + 'netflow.tcp_window_size': { + category: 'netflow', + name: 'netflow.tcp_window_size', + type: 'integer', + }, + 'netflow.tcp_urgent_pointer': { + category: 'netflow', + name: 'netflow.tcp_urgent_pointer', + type: 'integer', + }, + 'netflow.tcp_header_length': { + category: 'netflow', + name: 'netflow.tcp_header_length', + type: 'short', + }, + 'netflow.ip_header_length': { + category: 'netflow', + name: 'netflow.ip_header_length', + type: 'short', + }, + 'netflow.total_length_ipv4': { + category: 'netflow', + name: 'netflow.total_length_ipv4', + type: 'integer', + }, + 'netflow.payload_length_ipv6': { + category: 'netflow', + name: 'netflow.payload_length_ipv6', + type: 'integer', + }, + 'netflow.ip_ttl': { + category: 'netflow', + name: 'netflow.ip_ttl', + type: 'short', + }, + 'netflow.next_header_ipv6': { + category: 'netflow', + name: 'netflow.next_header_ipv6', + type: 'short', + }, + 'netflow.mpls_payload_length': { + category: 'netflow', + name: 'netflow.mpls_payload_length', + type: 'long', + }, + 'netflow.ip_diff_serv_code_point': { + category: 'netflow', + name: 'netflow.ip_diff_serv_code_point', + type: 'short', + }, + 'netflow.ip_precedence': { + category: 'netflow', + name: 'netflow.ip_precedence', + type: 'short', + }, + 'netflow.fragment_flags': { + category: 'netflow', + name: 'netflow.fragment_flags', + type: 'short', + }, + 'netflow.octet_delta_sum_of_squares': { + category: 'netflow', + name: 'netflow.octet_delta_sum_of_squares', + type: 'long', + }, + 'netflow.octet_total_sum_of_squares': { + category: 'netflow', + name: 'netflow.octet_total_sum_of_squares', + type: 'long', + }, + 'netflow.mpls_top_label_ttl': { + category: 'netflow', + name: 'netflow.mpls_top_label_ttl', + type: 'short', + }, + 'netflow.mpls_label_stack_length': { + category: 'netflow', + name: 'netflow.mpls_label_stack_length', + type: 'long', + }, + 'netflow.mpls_label_stack_depth': { + category: 'netflow', + name: 'netflow.mpls_label_stack_depth', + type: 'long', + }, + 'netflow.mpls_top_label_exp': { + category: 'netflow', + name: 'netflow.mpls_top_label_exp', + type: 'short', + }, + 'netflow.ip_payload_length': { + category: 'netflow', + name: 'netflow.ip_payload_length', + type: 'long', + }, + 'netflow.udp_message_length': { + category: 'netflow', + name: 'netflow.udp_message_length', + type: 'integer', + }, + 'netflow.is_multicast': { + category: 'netflow', + name: 'netflow.is_multicast', + type: 'short', + }, + 'netflow.ipv4_ihl': { + category: 'netflow', + name: 'netflow.ipv4_ihl', + type: 'short', + }, + 'netflow.ipv4_options': { + category: 'netflow', + name: 'netflow.ipv4_options', + type: 'long', + }, + 'netflow.tcp_options': { + category: 'netflow', + name: 'netflow.tcp_options', + type: 'long', + }, + 'netflow.padding_octets': { + category: 'netflow', + name: 'netflow.padding_octets', + type: 'short', + }, + 'netflow.collector_ipv4_address': { + category: 'netflow', + name: 'netflow.collector_ipv4_address', + type: 'ip', + }, + 'netflow.collector_ipv6_address': { + category: 'netflow', + name: 'netflow.collector_ipv6_address', + type: 'ip', + }, + 'netflow.export_interface': { + category: 'netflow', + name: 'netflow.export_interface', + type: 'long', + }, + 'netflow.export_protocol_version': { + category: 'netflow', + name: 'netflow.export_protocol_version', + type: 'short', + }, + 'netflow.export_transport_protocol': { + category: 'netflow', + name: 'netflow.export_transport_protocol', + type: 'short', + }, + 'netflow.collector_transport_port': { + category: 'netflow', + name: 'netflow.collector_transport_port', + type: 'integer', + }, + 'netflow.exporter_transport_port': { + category: 'netflow', + name: 'netflow.exporter_transport_port', + type: 'integer', + }, + 'netflow.tcp_syn_total_count': { + category: 'netflow', + name: 'netflow.tcp_syn_total_count', + type: 'long', + }, + 'netflow.tcp_fin_total_count': { + category: 'netflow', + name: 'netflow.tcp_fin_total_count', + type: 'long', + }, + 'netflow.tcp_rst_total_count': { + category: 'netflow', + name: 'netflow.tcp_rst_total_count', + type: 'long', + }, + 'netflow.tcp_psh_total_count': { + category: 'netflow', + name: 'netflow.tcp_psh_total_count', + type: 'long', + }, + 'netflow.tcp_ack_total_count': { + category: 'netflow', + name: 'netflow.tcp_ack_total_count', + type: 'long', + }, + 'netflow.tcp_urg_total_count': { + category: 'netflow', + name: 'netflow.tcp_urg_total_count', + type: 'long', + }, + 'netflow.ip_total_length': { + category: 'netflow', + name: 'netflow.ip_total_length', + type: 'long', + }, + 'netflow.post_nat_source_ipv4_address': { + category: 'netflow', + name: 'netflow.post_nat_source_ipv4_address', + type: 'ip', + }, + 'netflow.post_nat_destination_ipv4_address': { + category: 'netflow', + name: 'netflow.post_nat_destination_ipv4_address', + type: 'ip', + }, + 'netflow.post_napt_source_transport_port': { + category: 'netflow', + name: 'netflow.post_napt_source_transport_port', + type: 'integer', + }, + 'netflow.post_napt_destination_transport_port': { + category: 'netflow', + name: 'netflow.post_napt_destination_transport_port', + type: 'integer', + }, + 'netflow.nat_originating_address_realm': { + category: 'netflow', + name: 'netflow.nat_originating_address_realm', + type: 'short', + }, + 'netflow.nat_event': { + category: 'netflow', + name: 'netflow.nat_event', + type: 'short', + }, + 'netflow.initiator_octets': { + category: 'netflow', + name: 'netflow.initiator_octets', + type: 'long', + }, + 'netflow.responder_octets': { + category: 'netflow', + name: 'netflow.responder_octets', + type: 'long', + }, + 'netflow.firewall_event': { + category: 'netflow', + name: 'netflow.firewall_event', + type: 'short', + }, + 'netflow.ingress_vrfid': { + category: 'netflow', + name: 'netflow.ingress_vrfid', + type: 'long', + }, + 'netflow.egress_vrfid': { + category: 'netflow', + name: 'netflow.egress_vrfid', + type: 'long', + }, + 'netflow.vr_fname': { + category: 'netflow', + name: 'netflow.vr_fname', + type: 'keyword', + }, + 'netflow.post_mpls_top_label_exp': { + category: 'netflow', + name: 'netflow.post_mpls_top_label_exp', + type: 'short', + }, + 'netflow.tcp_window_scale': { + category: 'netflow', + name: 'netflow.tcp_window_scale', + type: 'integer', + }, + 'netflow.biflow_direction': { + category: 'netflow', + name: 'netflow.biflow_direction', + type: 'short', + }, + 'netflow.ethernet_header_length': { + category: 'netflow', + name: 'netflow.ethernet_header_length', + type: 'short', + }, + 'netflow.ethernet_payload_length': { + category: 'netflow', + name: 'netflow.ethernet_payload_length', + type: 'integer', + }, + 'netflow.ethernet_total_length': { + category: 'netflow', + name: 'netflow.ethernet_total_length', + type: 'integer', + }, + 'netflow.dot1q_vlan_id': { + category: 'netflow', + name: 'netflow.dot1q_vlan_id', + type: 'integer', + }, + 'netflow.dot1q_priority': { + category: 'netflow', + name: 'netflow.dot1q_priority', + type: 'short', + }, + 'netflow.dot1q_customer_vlan_id': { + category: 'netflow', + name: 'netflow.dot1q_customer_vlan_id', + type: 'integer', + }, + 'netflow.dot1q_customer_priority': { + category: 'netflow', + name: 'netflow.dot1q_customer_priority', + type: 'short', + }, + 'netflow.metro_evc_id': { + category: 'netflow', + name: 'netflow.metro_evc_id', + type: 'keyword', + }, + 'netflow.metro_evc_type': { + category: 'netflow', + name: 'netflow.metro_evc_type', + type: 'short', + }, + 'netflow.pseudo_wire_id': { + category: 'netflow', + name: 'netflow.pseudo_wire_id', + type: 'long', + }, + 'netflow.pseudo_wire_type': { + category: 'netflow', + name: 'netflow.pseudo_wire_type', + type: 'integer', + }, + 'netflow.pseudo_wire_control_word': { + category: 'netflow', + name: 'netflow.pseudo_wire_control_word', + type: 'long', + }, + 'netflow.ingress_physical_interface': { + category: 'netflow', + name: 'netflow.ingress_physical_interface', + type: 'long', + }, + 'netflow.egress_physical_interface': { + category: 'netflow', + name: 'netflow.egress_physical_interface', + type: 'long', + }, + 'netflow.post_dot1q_vlan_id': { + category: 'netflow', + name: 'netflow.post_dot1q_vlan_id', + type: 'integer', + }, + 'netflow.post_dot1q_customer_vlan_id': { + category: 'netflow', + name: 'netflow.post_dot1q_customer_vlan_id', + type: 'integer', + }, + 'netflow.ethernet_type': { + category: 'netflow', + name: 'netflow.ethernet_type', + type: 'integer', + }, + 'netflow.post_ip_precedence': { + category: 'netflow', + name: 'netflow.post_ip_precedence', + type: 'short', + }, + 'netflow.collection_time_milliseconds': { + category: 'netflow', + name: 'netflow.collection_time_milliseconds', + type: 'date', + }, + 'netflow.export_sctp_stream_id': { + category: 'netflow', + name: 'netflow.export_sctp_stream_id', + type: 'integer', + }, + 'netflow.max_export_seconds': { + category: 'netflow', + name: 'netflow.max_export_seconds', + type: 'date', + }, + 'netflow.max_flow_end_seconds': { + category: 'netflow', + name: 'netflow.max_flow_end_seconds', + type: 'date', + }, + 'netflow.message_md5_checksum': { + category: 'netflow', + name: 'netflow.message_md5_checksum', + type: 'short', + }, + 'netflow.message_scope': { + category: 'netflow', + name: 'netflow.message_scope', + type: 'short', + }, + 'netflow.min_export_seconds': { + category: 'netflow', + name: 'netflow.min_export_seconds', + type: 'date', + }, + 'netflow.min_flow_start_seconds': { + category: 'netflow', + name: 'netflow.min_flow_start_seconds', + type: 'date', + }, + 'netflow.opaque_octets': { + category: 'netflow', + name: 'netflow.opaque_octets', + type: 'short', + }, + 'netflow.session_scope': { + category: 'netflow', + name: 'netflow.session_scope', + type: 'short', + }, + 'netflow.max_flow_end_microseconds': { + category: 'netflow', + name: 'netflow.max_flow_end_microseconds', + type: 'date', + }, + 'netflow.max_flow_end_milliseconds': { + category: 'netflow', + name: 'netflow.max_flow_end_milliseconds', + type: 'date', + }, + 'netflow.max_flow_end_nanoseconds': { + category: 'netflow', + name: 'netflow.max_flow_end_nanoseconds', + type: 'date', + }, + 'netflow.min_flow_start_microseconds': { + category: 'netflow', + name: 'netflow.min_flow_start_microseconds', + type: 'date', + }, + 'netflow.min_flow_start_milliseconds': { + category: 'netflow', + name: 'netflow.min_flow_start_milliseconds', + type: 'date', + }, + 'netflow.min_flow_start_nanoseconds': { + category: 'netflow', + name: 'netflow.min_flow_start_nanoseconds', + type: 'date', + }, + 'netflow.collector_certificate': { + category: 'netflow', + name: 'netflow.collector_certificate', + type: 'short', + }, + 'netflow.exporter_certificate': { + category: 'netflow', + name: 'netflow.exporter_certificate', + type: 'short', + }, + 'netflow.data_records_reliability': { + category: 'netflow', + name: 'netflow.data_records_reliability', + type: 'boolean', + }, + 'netflow.observation_point_type': { + category: 'netflow', + name: 'netflow.observation_point_type', + type: 'short', + }, + 'netflow.new_connection_delta_count': { + category: 'netflow', + name: 'netflow.new_connection_delta_count', + type: 'long', + }, + 'netflow.connection_sum_duration_seconds': { + category: 'netflow', + name: 'netflow.connection_sum_duration_seconds', + type: 'long', + }, + 'netflow.connection_transaction_id': { + category: 'netflow', + name: 'netflow.connection_transaction_id', + type: 'long', + }, + 'netflow.post_nat_source_ipv6_address': { + category: 'netflow', + name: 'netflow.post_nat_source_ipv6_address', + type: 'ip', + }, + 'netflow.post_nat_destination_ipv6_address': { + category: 'netflow', + name: 'netflow.post_nat_destination_ipv6_address', + type: 'ip', + }, + 'netflow.nat_pool_id': { + category: 'netflow', + name: 'netflow.nat_pool_id', + type: 'long', + }, + 'netflow.nat_pool_name': { + category: 'netflow', + name: 'netflow.nat_pool_name', + type: 'keyword', + }, + 'netflow.anonymization_flags': { + category: 'netflow', + name: 'netflow.anonymization_flags', + type: 'integer', + }, + 'netflow.anonymization_technique': { + category: 'netflow', + name: 'netflow.anonymization_technique', + type: 'integer', + }, + 'netflow.information_element_index': { + category: 'netflow', + name: 'netflow.information_element_index', + type: 'integer', + }, + 'netflow.p2p_technology': { + category: 'netflow', + name: 'netflow.p2p_technology', + type: 'keyword', + }, + 'netflow.tunnel_technology': { + category: 'netflow', + name: 'netflow.tunnel_technology', + type: 'keyword', + }, + 'netflow.encrypted_technology': { + category: 'netflow', + name: 'netflow.encrypted_technology', + type: 'keyword', + }, + 'netflow.bgp_validity_state': { + category: 'netflow', + name: 'netflow.bgp_validity_state', + type: 'short', + }, + 'netflow.ip_sec_spi': { + category: 'netflow', + name: 'netflow.ip_sec_spi', + type: 'long', + }, + 'netflow.gre_key': { + category: 'netflow', + name: 'netflow.gre_key', + type: 'long', + }, + 'netflow.nat_type': { + category: 'netflow', + name: 'netflow.nat_type', + type: 'short', + }, + 'netflow.initiator_packets': { + category: 'netflow', + name: 'netflow.initiator_packets', + type: 'long', + }, + 'netflow.responder_packets': { + category: 'netflow', + name: 'netflow.responder_packets', + type: 'long', + }, + 'netflow.observation_domain_name': { + category: 'netflow', + name: 'netflow.observation_domain_name', + type: 'keyword', + }, + 'netflow.selection_sequence_id': { + category: 'netflow', + name: 'netflow.selection_sequence_id', + type: 'long', + }, + 'netflow.selector_id': { + category: 'netflow', + name: 'netflow.selector_id', + type: 'long', + }, + 'netflow.information_element_id': { + category: 'netflow', + name: 'netflow.information_element_id', + type: 'integer', + }, + 'netflow.selector_algorithm': { + category: 'netflow', + name: 'netflow.selector_algorithm', + type: 'integer', + }, + 'netflow.sampling_packet_interval': { + category: 'netflow', + name: 'netflow.sampling_packet_interval', + type: 'long', + }, + 'netflow.sampling_packet_space': { + category: 'netflow', + name: 'netflow.sampling_packet_space', + type: 'long', + }, + 'netflow.sampling_time_interval': { + category: 'netflow', + name: 'netflow.sampling_time_interval', + type: 'long', + }, + 'netflow.sampling_time_space': { + category: 'netflow', + name: 'netflow.sampling_time_space', + type: 'long', + }, + 'netflow.sampling_size': { + category: 'netflow', + name: 'netflow.sampling_size', + type: 'long', + }, + 'netflow.sampling_population': { + category: 'netflow', + name: 'netflow.sampling_population', + type: 'long', + }, + 'netflow.sampling_probability': { + category: 'netflow', + name: 'netflow.sampling_probability', + type: 'double', + }, + 'netflow.data_link_frame_size': { + category: 'netflow', + name: 'netflow.data_link_frame_size', + type: 'integer', + }, + 'netflow.ip_header_packet_section': { + category: 'netflow', + name: 'netflow.ip_header_packet_section', + type: 'short', + }, + 'netflow.ip_payload_packet_section': { + category: 'netflow', + name: 'netflow.ip_payload_packet_section', + type: 'short', + }, + 'netflow.data_link_frame_section': { + category: 'netflow', + name: 'netflow.data_link_frame_section', + type: 'short', + }, + 'netflow.mpls_label_stack_section': { + category: 'netflow', + name: 'netflow.mpls_label_stack_section', + type: 'short', + }, + 'netflow.mpls_payload_packet_section': { + category: 'netflow', + name: 'netflow.mpls_payload_packet_section', + type: 'short', + }, + 'netflow.selector_id_total_pkts_observed': { + category: 'netflow', + name: 'netflow.selector_id_total_pkts_observed', + type: 'long', + }, + 'netflow.selector_id_total_pkts_selected': { + category: 'netflow', + name: 'netflow.selector_id_total_pkts_selected', + type: 'long', + }, + 'netflow.absolute_error': { + category: 'netflow', + name: 'netflow.absolute_error', + type: 'double', + }, + 'netflow.relative_error': { + category: 'netflow', + name: 'netflow.relative_error', + type: 'double', + }, + 'netflow.observation_time_seconds': { + category: 'netflow', + name: 'netflow.observation_time_seconds', + type: 'date', + }, + 'netflow.observation_time_milliseconds': { + category: 'netflow', + name: 'netflow.observation_time_milliseconds', + type: 'date', + }, + 'netflow.observation_time_microseconds': { + category: 'netflow', + name: 'netflow.observation_time_microseconds', + type: 'date', + }, + 'netflow.observation_time_nanoseconds': { + category: 'netflow', + name: 'netflow.observation_time_nanoseconds', + type: 'date', + }, + 'netflow.digest_hash_value': { + category: 'netflow', + name: 'netflow.digest_hash_value', + type: 'long', + }, + 'netflow.hash_ip_payload_offset': { + category: 'netflow', + name: 'netflow.hash_ip_payload_offset', + type: 'long', + }, + 'netflow.hash_ip_payload_size': { + category: 'netflow', + name: 'netflow.hash_ip_payload_size', + type: 'long', + }, + 'netflow.hash_output_range_min': { + category: 'netflow', + name: 'netflow.hash_output_range_min', + type: 'long', + }, + 'netflow.hash_output_range_max': { + category: 'netflow', + name: 'netflow.hash_output_range_max', + type: 'long', + }, + 'netflow.hash_selected_range_min': { + category: 'netflow', + name: 'netflow.hash_selected_range_min', + type: 'long', + }, + 'netflow.hash_selected_range_max': { + category: 'netflow', + name: 'netflow.hash_selected_range_max', + type: 'long', + }, + 'netflow.hash_digest_output': { + category: 'netflow', + name: 'netflow.hash_digest_output', + type: 'boolean', + }, + 'netflow.hash_initialiser_value': { + category: 'netflow', + name: 'netflow.hash_initialiser_value', + type: 'long', + }, + 'netflow.selector_name': { + category: 'netflow', + name: 'netflow.selector_name', + type: 'keyword', + }, + 'netflow.upper_ci_limit': { + category: 'netflow', + name: 'netflow.upper_ci_limit', + type: 'double', + }, + 'netflow.lower_ci_limit': { + category: 'netflow', + name: 'netflow.lower_ci_limit', + type: 'double', + }, + 'netflow.confidence_level': { + category: 'netflow', + name: 'netflow.confidence_level', + type: 'double', + }, + 'netflow.information_element_data_type': { + category: 'netflow', + name: 'netflow.information_element_data_type', + type: 'short', + }, + 'netflow.information_element_description': { + category: 'netflow', + name: 'netflow.information_element_description', + type: 'keyword', + }, + 'netflow.information_element_name': { + category: 'netflow', + name: 'netflow.information_element_name', + type: 'keyword', + }, + 'netflow.information_element_range_begin': { + category: 'netflow', + name: 'netflow.information_element_range_begin', + type: 'long', + }, + 'netflow.information_element_range_end': { + category: 'netflow', + name: 'netflow.information_element_range_end', + type: 'long', + }, + 'netflow.information_element_semantics': { + category: 'netflow', + name: 'netflow.information_element_semantics', + type: 'short', + }, + 'netflow.information_element_units': { + category: 'netflow', + name: 'netflow.information_element_units', + type: 'integer', + }, + 'netflow.private_enterprise_number': { + category: 'netflow', + name: 'netflow.private_enterprise_number', + type: 'long', + }, + 'netflow.virtual_station_interface_id': { + category: 'netflow', + name: 'netflow.virtual_station_interface_id', + type: 'short', + }, + 'netflow.virtual_station_interface_name': { + category: 'netflow', + name: 'netflow.virtual_station_interface_name', + type: 'keyword', + }, + 'netflow.virtual_station_uuid': { + category: 'netflow', + name: 'netflow.virtual_station_uuid', + type: 'short', + }, + 'netflow.virtual_station_name': { + category: 'netflow', + name: 'netflow.virtual_station_name', + type: 'keyword', + }, + 'netflow.layer2_segment_id': { + category: 'netflow', + name: 'netflow.layer2_segment_id', + type: 'long', + }, + 'netflow.layer2_octet_delta_count': { + category: 'netflow', + name: 'netflow.layer2_octet_delta_count', + type: 'long', + }, + 'netflow.layer2_octet_total_count': { + category: 'netflow', + name: 'netflow.layer2_octet_total_count', + type: 'long', + }, + 'netflow.ingress_unicast_packet_total_count': { + category: 'netflow', + name: 'netflow.ingress_unicast_packet_total_count', + type: 'long', + }, + 'netflow.ingress_multicast_packet_total_count': { + category: 'netflow', + name: 'netflow.ingress_multicast_packet_total_count', + type: 'long', + }, + 'netflow.ingress_broadcast_packet_total_count': { + category: 'netflow', + name: 'netflow.ingress_broadcast_packet_total_count', + type: 'long', + }, + 'netflow.egress_unicast_packet_total_count': { + category: 'netflow', + name: 'netflow.egress_unicast_packet_total_count', + type: 'long', + }, + 'netflow.egress_broadcast_packet_total_count': { + category: 'netflow', + name: 'netflow.egress_broadcast_packet_total_count', + type: 'long', + }, + 'netflow.monitoring_interval_start_milli_seconds': { + category: 'netflow', + name: 'netflow.monitoring_interval_start_milli_seconds', + type: 'date', + }, + 'netflow.monitoring_interval_end_milli_seconds': { + category: 'netflow', + name: 'netflow.monitoring_interval_end_milli_seconds', + type: 'date', + }, + 'netflow.port_range_start': { + category: 'netflow', + name: 'netflow.port_range_start', + type: 'integer', + }, + 'netflow.port_range_end': { + category: 'netflow', + name: 'netflow.port_range_end', + type: 'integer', + }, + 'netflow.port_range_step_size': { + category: 'netflow', + name: 'netflow.port_range_step_size', + type: 'integer', + }, + 'netflow.port_range_num_ports': { + category: 'netflow', + name: 'netflow.port_range_num_ports', + type: 'integer', + }, + 'netflow.sta_mac_address': { + category: 'netflow', + name: 'netflow.sta_mac_address', + type: 'keyword', + }, + 'netflow.sta_ipv4_address': { + category: 'netflow', + name: 'netflow.sta_ipv4_address', + type: 'ip', + }, + 'netflow.wtp_mac_address': { + category: 'netflow', + name: 'netflow.wtp_mac_address', + type: 'keyword', + }, + 'netflow.ingress_interface_type': { + category: 'netflow', + name: 'netflow.ingress_interface_type', + type: 'long', + }, + 'netflow.egress_interface_type': { + category: 'netflow', + name: 'netflow.egress_interface_type', + type: 'long', + }, + 'netflow.rtp_sequence_number': { + category: 'netflow', + name: 'netflow.rtp_sequence_number', + type: 'integer', + }, + 'netflow.user_name': { + category: 'netflow', + name: 'netflow.user_name', + type: 'keyword', + }, + 'netflow.application_category_name': { + category: 'netflow', + name: 'netflow.application_category_name', + type: 'keyword', + }, + 'netflow.application_sub_category_name': { + category: 'netflow', + name: 'netflow.application_sub_category_name', + type: 'keyword', + }, + 'netflow.application_group_name': { + category: 'netflow', + name: 'netflow.application_group_name', + type: 'keyword', + }, + 'netflow.original_flows_present': { + category: 'netflow', + name: 'netflow.original_flows_present', + type: 'long', + }, + 'netflow.original_flows_initiated': { + category: 'netflow', + name: 'netflow.original_flows_initiated', + type: 'long', + }, + 'netflow.original_flows_completed': { + category: 'netflow', + name: 'netflow.original_flows_completed', + type: 'long', + }, + 'netflow.distinct_count_of_source_ip_address': { + category: 'netflow', + name: 'netflow.distinct_count_of_source_ip_address', + type: 'long', + }, + 'netflow.distinct_count_of_destination_ip_address': { + category: 'netflow', + name: 'netflow.distinct_count_of_destination_ip_address', + type: 'long', + }, + 'netflow.distinct_count_of_source_ipv4_address': { + category: 'netflow', + name: 'netflow.distinct_count_of_source_ipv4_address', + type: 'long', + }, + 'netflow.distinct_count_of_destination_ipv4_address': { + category: 'netflow', + name: 'netflow.distinct_count_of_destination_ipv4_address', + type: 'long', + }, + 'netflow.distinct_count_of_source_ipv6_address': { + category: 'netflow', + name: 'netflow.distinct_count_of_source_ipv6_address', + type: 'long', + }, + 'netflow.distinct_count_of_destination_ipv6_address': { + category: 'netflow', + name: 'netflow.distinct_count_of_destination_ipv6_address', + type: 'long', + }, + 'netflow.value_distribution_method': { + category: 'netflow', + name: 'netflow.value_distribution_method', + type: 'short', + }, + 'netflow.rfc3550_jitter_milliseconds': { + category: 'netflow', + name: 'netflow.rfc3550_jitter_milliseconds', + type: 'long', + }, + 'netflow.rfc3550_jitter_microseconds': { + category: 'netflow', + name: 'netflow.rfc3550_jitter_microseconds', + type: 'long', + }, + 'netflow.rfc3550_jitter_nanoseconds': { + category: 'netflow', + name: 'netflow.rfc3550_jitter_nanoseconds', + type: 'long', + }, + 'netflow.dot1q_dei': { + category: 'netflow', + name: 'netflow.dot1q_dei', + type: 'boolean', + }, + 'netflow.dot1q_customer_dei': { + category: 'netflow', + name: 'netflow.dot1q_customer_dei', + type: 'boolean', + }, + 'netflow.flow_selector_algorithm': { + category: 'netflow', + name: 'netflow.flow_selector_algorithm', + type: 'integer', + }, + 'netflow.flow_selected_octet_delta_count': { + category: 'netflow', + name: 'netflow.flow_selected_octet_delta_count', + type: 'long', + }, + 'netflow.flow_selected_packet_delta_count': { + category: 'netflow', + name: 'netflow.flow_selected_packet_delta_count', + type: 'long', + }, + 'netflow.flow_selected_flow_delta_count': { + category: 'netflow', + name: 'netflow.flow_selected_flow_delta_count', + type: 'long', + }, + 'netflow.selector_id_total_flows_observed': { + category: 'netflow', + name: 'netflow.selector_id_total_flows_observed', + type: 'long', + }, + 'netflow.selector_id_total_flows_selected': { + category: 'netflow', + name: 'netflow.selector_id_total_flows_selected', + type: 'long', + }, + 'netflow.sampling_flow_interval': { + category: 'netflow', + name: 'netflow.sampling_flow_interval', + type: 'long', + }, + 'netflow.sampling_flow_spacing': { + category: 'netflow', + name: 'netflow.sampling_flow_spacing', + type: 'long', + }, + 'netflow.flow_sampling_time_interval': { + category: 'netflow', + name: 'netflow.flow_sampling_time_interval', + type: 'long', + }, + 'netflow.flow_sampling_time_spacing': { + category: 'netflow', + name: 'netflow.flow_sampling_time_spacing', + type: 'long', + }, + 'netflow.hash_flow_domain': { + category: 'netflow', + name: 'netflow.hash_flow_domain', + type: 'integer', + }, + 'netflow.transport_octet_delta_count': { + category: 'netflow', + name: 'netflow.transport_octet_delta_count', + type: 'long', + }, + 'netflow.transport_packet_delta_count': { + category: 'netflow', + name: 'netflow.transport_packet_delta_count', + type: 'long', + }, + 'netflow.original_exporter_ipv4_address': { + category: 'netflow', + name: 'netflow.original_exporter_ipv4_address', + type: 'ip', + }, + 'netflow.original_exporter_ipv6_address': { + category: 'netflow', + name: 'netflow.original_exporter_ipv6_address', + type: 'ip', + }, + 'netflow.original_observation_domain_id': { + category: 'netflow', + name: 'netflow.original_observation_domain_id', + type: 'long', + }, + 'netflow.intermediate_process_id': { + category: 'netflow', + name: 'netflow.intermediate_process_id', + type: 'long', + }, + 'netflow.ignored_data_record_total_count': { + category: 'netflow', + name: 'netflow.ignored_data_record_total_count', + type: 'long', + }, + 'netflow.data_link_frame_type': { + category: 'netflow', + name: 'netflow.data_link_frame_type', + type: 'integer', + }, + 'netflow.section_offset': { + category: 'netflow', + name: 'netflow.section_offset', + type: 'integer', + }, + 'netflow.section_exported_octets': { + category: 'netflow', + name: 'netflow.section_exported_octets', + type: 'integer', + }, + 'netflow.dot1q_service_instance_tag': { + category: 'netflow', + name: 'netflow.dot1q_service_instance_tag', + type: 'short', + }, + 'netflow.dot1q_service_instance_id': { + category: 'netflow', + name: 'netflow.dot1q_service_instance_id', + type: 'long', + }, + 'netflow.dot1q_service_instance_priority': { + category: 'netflow', + name: 'netflow.dot1q_service_instance_priority', + type: 'short', + }, + 'netflow.dot1q_customer_source_mac_address': { + category: 'netflow', + name: 'netflow.dot1q_customer_source_mac_address', + type: 'keyword', + }, + 'netflow.dot1q_customer_destination_mac_address': { + category: 'netflow', + name: 'netflow.dot1q_customer_destination_mac_address', + type: 'keyword', + }, + 'netflow.post_layer2_octet_delta_count': { + category: 'netflow', + name: 'netflow.post_layer2_octet_delta_count', + type: 'long', + }, + 'netflow.post_mcast_layer2_octet_delta_count': { + category: 'netflow', + name: 'netflow.post_mcast_layer2_octet_delta_count', + type: 'long', + }, + 'netflow.post_layer2_octet_total_count': { + category: 'netflow', + name: 'netflow.post_layer2_octet_total_count', + type: 'long', + }, + 'netflow.post_mcast_layer2_octet_total_count': { + category: 'netflow', + name: 'netflow.post_mcast_layer2_octet_total_count', + type: 'long', + }, + 'netflow.minimum_layer2_total_length': { + category: 'netflow', + name: 'netflow.minimum_layer2_total_length', + type: 'long', + }, + 'netflow.maximum_layer2_total_length': { + category: 'netflow', + name: 'netflow.maximum_layer2_total_length', + type: 'long', + }, + 'netflow.dropped_layer2_octet_delta_count': { + category: 'netflow', + name: 'netflow.dropped_layer2_octet_delta_count', + type: 'long', + }, + 'netflow.dropped_layer2_octet_total_count': { + category: 'netflow', + name: 'netflow.dropped_layer2_octet_total_count', + type: 'long', + }, + 'netflow.ignored_layer2_octet_total_count': { + category: 'netflow', + name: 'netflow.ignored_layer2_octet_total_count', + type: 'long', + }, + 'netflow.not_sent_layer2_octet_total_count': { + category: 'netflow', + name: 'netflow.not_sent_layer2_octet_total_count', + type: 'long', + }, + 'netflow.layer2_octet_delta_sum_of_squares': { + category: 'netflow', + name: 'netflow.layer2_octet_delta_sum_of_squares', + type: 'long', + }, + 'netflow.layer2_octet_total_sum_of_squares': { + category: 'netflow', + name: 'netflow.layer2_octet_total_sum_of_squares', + type: 'long', + }, + 'netflow.layer2_frame_delta_count': { + category: 'netflow', + name: 'netflow.layer2_frame_delta_count', + type: 'long', + }, + 'netflow.layer2_frame_total_count': { + category: 'netflow', + name: 'netflow.layer2_frame_total_count', + type: 'long', + }, + 'netflow.pseudo_wire_destination_ipv4_address': { + category: 'netflow', + name: 'netflow.pseudo_wire_destination_ipv4_address', + type: 'ip', + }, + 'netflow.ignored_layer2_frame_total_count': { + category: 'netflow', + name: 'netflow.ignored_layer2_frame_total_count', + type: 'long', + }, + 'netflow.mib_object_value_integer': { + category: 'netflow', + name: 'netflow.mib_object_value_integer', + type: 'integer', + }, + 'netflow.mib_object_value_octet_string': { + category: 'netflow', + name: 'netflow.mib_object_value_octet_string', + type: 'short', + }, + 'netflow.mib_object_value_oid': { + category: 'netflow', + name: 'netflow.mib_object_value_oid', + type: 'short', + }, + 'netflow.mib_object_value_bits': { + category: 'netflow', + name: 'netflow.mib_object_value_bits', + type: 'short', + }, + 'netflow.mib_object_value_ip_address': { + category: 'netflow', + name: 'netflow.mib_object_value_ip_address', + type: 'ip', + }, + 'netflow.mib_object_value_counter': { + category: 'netflow', + name: 'netflow.mib_object_value_counter', + type: 'long', + }, + 'netflow.mib_object_value_gauge': { + category: 'netflow', + name: 'netflow.mib_object_value_gauge', + type: 'long', + }, + 'netflow.mib_object_value_time_ticks': { + category: 'netflow', + name: 'netflow.mib_object_value_time_ticks', + type: 'long', + }, + 'netflow.mib_object_value_unsigned': { + category: 'netflow', + name: 'netflow.mib_object_value_unsigned', + type: 'long', + }, + 'netflow.mib_object_identifier': { + category: 'netflow', + name: 'netflow.mib_object_identifier', + type: 'short', + }, + 'netflow.mib_sub_identifier': { + category: 'netflow', + name: 'netflow.mib_sub_identifier', + type: 'long', + }, + 'netflow.mib_index_indicator': { + category: 'netflow', + name: 'netflow.mib_index_indicator', + type: 'long', + }, + 'netflow.mib_capture_time_semantics': { + category: 'netflow', + name: 'netflow.mib_capture_time_semantics', + type: 'short', + }, + 'netflow.mib_context_engine_id': { + category: 'netflow', + name: 'netflow.mib_context_engine_id', + type: 'short', + }, + 'netflow.mib_context_name': { + category: 'netflow', + name: 'netflow.mib_context_name', + type: 'keyword', + }, + 'netflow.mib_object_name': { + category: 'netflow', + name: 'netflow.mib_object_name', + type: 'keyword', + }, + 'netflow.mib_object_description': { + category: 'netflow', + name: 'netflow.mib_object_description', + type: 'keyword', + }, + 'netflow.mib_object_syntax': { + category: 'netflow', + name: 'netflow.mib_object_syntax', + type: 'keyword', + }, + 'netflow.mib_module_name': { + category: 'netflow', + name: 'netflow.mib_module_name', + type: 'keyword', + }, + 'netflow.mobile_imsi': { + category: 'netflow', + name: 'netflow.mobile_imsi', + type: 'keyword', + }, + 'netflow.mobile_msisdn': { + category: 'netflow', + name: 'netflow.mobile_msisdn', + type: 'keyword', + }, + 'netflow.http_status_code': { + category: 'netflow', + name: 'netflow.http_status_code', + type: 'integer', + }, + 'netflow.source_transport_ports_limit': { + category: 'netflow', + name: 'netflow.source_transport_ports_limit', + type: 'integer', + }, + 'netflow.http_request_method': { + category: 'netflow', + name: 'netflow.http_request_method', + type: 'keyword', + }, + 'netflow.http_request_host': { + category: 'netflow', + name: 'netflow.http_request_host', + type: 'keyword', + }, + 'netflow.http_request_target': { + category: 'netflow', + name: 'netflow.http_request_target', + type: 'keyword', + }, + 'netflow.http_message_version': { + category: 'netflow', + name: 'netflow.http_message_version', + type: 'keyword', + }, + 'netflow.nat_instance_id': { + category: 'netflow', + name: 'netflow.nat_instance_id', + type: 'long', + }, + 'netflow.internal_address_realm': { + category: 'netflow', + name: 'netflow.internal_address_realm', + type: 'short', + }, + 'netflow.external_address_realm': { + category: 'netflow', + name: 'netflow.external_address_realm', + type: 'short', + }, + 'netflow.nat_quota_exceeded_event': { + category: 'netflow', + name: 'netflow.nat_quota_exceeded_event', + type: 'long', + }, + 'netflow.nat_threshold_event': { + category: 'netflow', + name: 'netflow.nat_threshold_event', + type: 'long', + }, + 'netflow.http_user_agent': { + category: 'netflow', + name: 'netflow.http_user_agent', + type: 'keyword', + }, + 'netflow.http_content_type': { + category: 'netflow', + name: 'netflow.http_content_type', + type: 'keyword', + }, + 'netflow.http_reason_phrase': { + category: 'netflow', + name: 'netflow.http_reason_phrase', + type: 'keyword', + }, + 'netflow.max_session_entries': { + category: 'netflow', + name: 'netflow.max_session_entries', + type: 'long', + }, + 'netflow.max_bib_entries': { + category: 'netflow', + name: 'netflow.max_bib_entries', + type: 'long', + }, + 'netflow.max_entries_per_user': { + category: 'netflow', + name: 'netflow.max_entries_per_user', + type: 'long', + }, + 'netflow.max_subscribers': { + category: 'netflow', + name: 'netflow.max_subscribers', + type: 'long', + }, + 'netflow.max_fragments_pending_reassembly': { + category: 'netflow', + name: 'netflow.max_fragments_pending_reassembly', + type: 'long', + }, + 'netflow.address_pool_high_threshold': { + category: 'netflow', + name: 'netflow.address_pool_high_threshold', + type: 'long', + }, + 'netflow.address_pool_low_threshold': { + category: 'netflow', + name: 'netflow.address_pool_low_threshold', + type: 'long', + }, + 'netflow.address_port_mapping_high_threshold': { + category: 'netflow', + name: 'netflow.address_port_mapping_high_threshold', + type: 'long', + }, + 'netflow.address_port_mapping_low_threshold': { + category: 'netflow', + name: 'netflow.address_port_mapping_low_threshold', + type: 'long', + }, + 'netflow.address_port_mapping_per_user_high_threshold': { + category: 'netflow', + name: 'netflow.address_port_mapping_per_user_high_threshold', + type: 'long', + }, + 'netflow.global_address_mapping_high_threshold': { + category: 'netflow', + name: 'netflow.global_address_mapping_high_threshold', + type: 'long', + }, + 'netflow.vpn_identifier': { + category: 'netflow', + name: 'netflow.vpn_identifier', + type: 'short', + }, + bucket_name: { + category: 'base', + description: 'Name of the S3 bucket that this log retrieved from. ', + name: 'bucket_name', + type: 'keyword', + }, + object_key: { + category: 'base', + description: 'Name of the S3 object that this log retrieved from. ', + name: 'object_key', + type: 'keyword', + }, + 'cef.version': { + category: 'cef', + description: 'Version of the CEF specification used by the message. ', + name: 'cef.version', + type: 'keyword', + }, + 'cef.device.vendor': { + category: 'cef', + description: 'Vendor of the device that produced the message. ', + name: 'cef.device.vendor', + type: 'keyword', + }, + 'cef.device.product': { + category: 'cef', + description: 'Product of the device that produced the message. ', + name: 'cef.device.product', + type: 'keyword', + }, + 'cef.device.version': { + category: 'cef', + description: 'Version of the product that produced the message. ', + name: 'cef.device.version', + type: 'keyword', + }, + 'cef.device.event_class_id': { + category: 'cef', + description: 'Unique identifier of the event type. ', + name: 'cef.device.event_class_id', + type: 'keyword', + }, + 'cef.severity': { + category: 'cef', + description: + 'Importance of the event. The valid string values are Unknown, Low, Medium, High, and Very-High. The valid integer values are 0-3=Low, 4-6=Medium, 7- 8=High, and 9-10=Very-High. ', + example: 'Very-High', + name: 'cef.severity', + type: 'keyword', + }, + 'cef.name': { + category: 'cef', + description: 'Short description of the event. ', + name: 'cef.name', + type: 'keyword', + }, + 'cef.extensions.agentAddress': { + category: 'cef', + description: 'The IP address of the ArcSight connector that processed the event.', + name: 'cef.extensions.agentAddress', + type: 'ip', + }, + 'cef.extensions.agentDnsDomain': { + category: 'cef', + description: 'The DNS domain name of the ArcSight connector that processed the event.', + name: 'cef.extensions.agentDnsDomain', + type: 'keyword', + }, + 'cef.extensions.agentHostName': { + category: 'cef', + description: 'The hostname of the ArcSight connector that processed the event.', + name: 'cef.extensions.agentHostName', + type: 'keyword', + }, + 'cef.extensions.agentId': { + category: 'cef', + description: 'The agent ID of the ArcSight connector that processed the event.', + name: 'cef.extensions.agentId', + type: 'keyword', + }, + 'cef.extensions.agentMacAddress': { + category: 'cef', + description: 'The MAC address of the ArcSight connector that processed the event.', + name: 'cef.extensions.agentMacAddress', + type: 'keyword', + }, + 'cef.extensions.agentNtDomain': { + category: 'cef', + description: 'null', + name: 'cef.extensions.agentNtDomain', + type: 'keyword', + }, + 'cef.extensions.agentReceiptTime': { + category: 'cef', + description: + 'The time at which information about the event was received by the ArcSight connector.', + name: 'cef.extensions.agentReceiptTime', + type: 'date', + }, + 'cef.extensions.agentTimeZone': { + category: 'cef', + description: 'The agent time zone of the ArcSight connector that processed the event.', + name: 'cef.extensions.agentTimeZone', + type: 'keyword', + }, + 'cef.extensions.agentTranslatedAddress': { + category: 'cef', + description: 'null', + name: 'cef.extensions.agentTranslatedAddress', + type: 'ip', + }, + 'cef.extensions.agentTranslatedZoneExternalID': { + category: 'cef', + description: 'null', + name: 'cef.extensions.agentTranslatedZoneExternalID', + type: 'keyword', + }, + 'cef.extensions.agentTranslatedZoneURI': { + category: 'cef', + description: 'null', + name: 'cef.extensions.agentTranslatedZoneURI', + type: 'keyword', + }, + 'cef.extensions.agentType': { + category: 'cef', + description: 'The agent type of the ArcSight connector that processed the event', + name: 'cef.extensions.agentType', + type: 'keyword', + }, + 'cef.extensions.agentVersion': { + category: 'cef', + description: 'The version of the ArcSight connector that processed the event.', + name: 'cef.extensions.agentVersion', + type: 'keyword', + }, + 'cef.extensions.agentZoneExternalID': { + category: 'cef', + description: 'null', + name: 'cef.extensions.agentZoneExternalID', + type: 'keyword', + }, + 'cef.extensions.agentZoneURI': { + category: 'cef', + description: 'null', + name: 'cef.extensions.agentZoneURI', + type: 'keyword', + }, + 'cef.extensions.applicationProtocol': { + category: 'cef', + description: + 'Application level protocol, example values are HTTP, HTTPS, SSHv2, Telnet, POP, IMPA, IMAPS, and so on.', + name: 'cef.extensions.applicationProtocol', + type: 'keyword', + }, + 'cef.extensions.baseEventCount': { + category: 'cef', + description: + 'A count associated with this event. How many times was this same event observed? Count can be omitted if it is 1.', + name: 'cef.extensions.baseEventCount', + type: 'long', + }, + 'cef.extensions.bytesIn': { + category: 'cef', + description: + 'Number of bytes transferred inbound, relative to the source to destination relationship, meaning that data was flowing from source to destination.', + name: 'cef.extensions.bytesIn', + type: 'long', + }, + 'cef.extensions.bytesOut': { + category: 'cef', + description: + 'Number of bytes transferred outbound relative to the source to destination relationship. For example, the byte number of data flowing from the destination to the source.', + name: 'cef.extensions.bytesOut', + type: 'long', + }, + 'cef.extensions.customerExternalID': { + category: 'cef', + description: 'null', + name: 'cef.extensions.customerExternalID', + type: 'keyword', + }, + 'cef.extensions.customerURI': { + category: 'cef', + description: 'null', + name: 'cef.extensions.customerURI', + type: 'keyword', + }, + 'cef.extensions.destinationAddress': { + category: 'cef', + description: + 'Identifies the destination address that the event refers to in an IP network. The format is an IPv4 address.', + name: 'cef.extensions.destinationAddress', + type: 'ip', + }, + 'cef.extensions.destinationDnsDomain': { + category: 'cef', + description: 'The DNS domain part of the complete fully qualified domain name (FQDN).', + name: 'cef.extensions.destinationDnsDomain', + type: 'keyword', + }, + 'cef.extensions.destinationGeoLatitude': { + category: 'cef', + description: "The latitudinal value from which the destination's IP address belongs.", + name: 'cef.extensions.destinationGeoLatitude', + type: 'double', + }, + 'cef.extensions.destinationGeoLongitude': { + category: 'cef', + description: "The longitudinal value from which the destination's IP address belongs.", + name: 'cef.extensions.destinationGeoLongitude', + type: 'double', + }, + 'cef.extensions.destinationHostName': { + category: 'cef', + description: + 'Identifies the destination that an event refers to in an IP network. The format should be a fully qualified domain name (FQDN) associated with the destination node, when a node is available.', + name: 'cef.extensions.destinationHostName', + type: 'keyword', + }, + 'cef.extensions.destinationMacAddress': { + category: 'cef', + description: 'Six colon-seperated hexadecimal numbers.', + name: 'cef.extensions.destinationMacAddress', + type: 'keyword', + }, + 'cef.extensions.destinationNtDomain': { + category: 'cef', + description: 'The Windows domain name of the destination address.', + name: 'cef.extensions.destinationNtDomain', + type: 'keyword', + }, + 'cef.extensions.destinationPort': { + category: 'cef', + description: 'The valid port numbers are between 0 and 65535.', + name: 'cef.extensions.destinationPort', + type: 'long', + }, + 'cef.extensions.destinationProcessId': { + category: 'cef', + description: + 'Provides the ID of the destination process associated with the event. For example, if an event contains process ID 105, "105" is the process ID.', + name: 'cef.extensions.destinationProcessId', + type: 'long', + }, + 'cef.extensions.destinationProcessName': { + category: 'cef', + description: "The name of the event's destination process.", + name: 'cef.extensions.destinationProcessName', + type: 'keyword', + }, + 'cef.extensions.destinationServiceName': { + category: 'cef', + description: 'The service targeted by this event.', + name: 'cef.extensions.destinationServiceName', + type: 'keyword', + }, + 'cef.extensions.destinationTranslatedAddress': { + category: 'cef', + description: 'Identifies the translated destination that the event refers to in an IP network.', + name: 'cef.extensions.destinationTranslatedAddress', + type: 'ip', + }, + 'cef.extensions.destinationTranslatedPort': { + category: 'cef', + description: + 'Port after it was translated; for example, a firewall. Valid port numbers are 0 to 65535.', + name: 'cef.extensions.destinationTranslatedPort', + type: 'long', + }, + 'cef.extensions.destinationTranslatedZoneExternalID': { + category: 'cef', + description: 'null', + name: 'cef.extensions.destinationTranslatedZoneExternalID', + type: 'keyword', + }, + 'cef.extensions.destinationTranslatedZoneURI': { + category: 'cef', + description: + 'The URI for the Translated Zone that the destination asset has been assigned to in ArcSight.', + name: 'cef.extensions.destinationTranslatedZoneURI', + type: 'keyword', + }, + 'cef.extensions.destinationUserId': { + category: 'cef', + description: + 'Identifies the destination user by ID. For example, in UNIX, the root user is generally associated with user ID 0.', + name: 'cef.extensions.destinationUserId', + type: 'keyword', + }, + 'cef.extensions.destinationUserName': { + category: 'cef', + description: + "Identifies the destination user by name. This is the user associated with the event's destination. Email addresses are often mapped into the UserName fields. The recipient is a candidate to put into this field.", + name: 'cef.extensions.destinationUserName', + type: 'keyword', + }, + 'cef.extensions.destinationUserPrivileges': { + category: 'cef', + description: + 'The typical values are "Administrator", "User", and "Guest". This identifies the destination user\'s privileges. In UNIX, for example, activity executed on the root user would be identified with destinationUser Privileges of "Administrator".', + name: 'cef.extensions.destinationUserPrivileges', + type: 'keyword', + }, + 'cef.extensions.destinationZoneExternalID': { + category: 'cef', + description: 'null', + name: 'cef.extensions.destinationZoneExternalID', + type: 'keyword', + }, + 'cef.extensions.destinationZoneURI': { + category: 'cef', + description: + 'The URI for the Zone that the destination asset has been assigned to in ArcSight.', + name: 'cef.extensions.destinationZoneURI', + type: 'keyword', + }, + 'cef.extensions.deviceAction': { + category: 'cef', + description: 'Action taken by the device.', + name: 'cef.extensions.deviceAction', + type: 'keyword', + }, + 'cef.extensions.deviceAddress': { + category: 'cef', + description: 'Identifies the device address that an event refers to in an IP network.', + name: 'cef.extensions.deviceAddress', + type: 'ip', + }, + 'cef.extensions.deviceCustomFloatingPoint1Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomFloatingPoint1Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomFloatingPoint3Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomFloatingPoint3Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomFloatingPoint4Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomFloatingPoint4Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomDate1': { + category: 'cef', + description: + 'One of two timestamp fields available to map fields that do not apply to any other in this dictionary.', + name: 'cef.extensions.deviceCustomDate1', + type: 'date', + }, + 'cef.extensions.deviceCustomDate1Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomDate1Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomDate2': { + category: 'cef', + description: + 'One of two timestamp fields available to map fields that do not apply to any other in this dictionary.', + name: 'cef.extensions.deviceCustomDate2', + type: 'date', + }, + 'cef.extensions.deviceCustomDate2Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomDate2Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomFloatingPoint1': { + category: 'cef', + description: + 'One of four floating point fields available to map fields that do not apply to any other in this dictionary.', + name: 'cef.extensions.deviceCustomFloatingPoint1', + type: 'double', + }, + 'cef.extensions.deviceCustomFloatingPoint2': { + category: 'cef', + description: + 'One of four floating point fields available to map fields that do not apply to any other in this dictionary.', + name: 'cef.extensions.deviceCustomFloatingPoint2', + type: 'double', + }, + 'cef.extensions.deviceCustomFloatingPoint2Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomFloatingPoint2Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomFloatingPoint3': { + category: 'cef', + description: + 'One of four floating point fields available to map fields that do not apply to any other in this dictionary.', + name: 'cef.extensions.deviceCustomFloatingPoint3', + type: 'double', + }, + 'cef.extensions.deviceCustomFloatingPoint4': { + category: 'cef', + description: + 'One of four floating point fields available to map fields that do not apply to any other in this dictionary.', + name: 'cef.extensions.deviceCustomFloatingPoint4', + type: 'double', + }, + 'cef.extensions.deviceCustomIPv6Address1': { + category: 'cef', + description: + 'One of four IPv6 address fields available to map fields that do not apply to any other in this dictionary.', + name: 'cef.extensions.deviceCustomIPv6Address1', + type: 'ip', + }, + 'cef.extensions.deviceCustomIPv6Address1Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomIPv6Address1Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomIPv6Address2': { + category: 'cef', + description: + 'One of four IPv6 address fields available to map fields that do not apply to any other in this dictionary.', + name: 'cef.extensions.deviceCustomIPv6Address2', + type: 'ip', + }, + 'cef.extensions.deviceCustomIPv6Address2Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomIPv6Address2Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomIPv6Address3': { + category: 'cef', + description: + 'One of four IPv6 address fields available to map fields that do not apply to any other in this dictionary.', + name: 'cef.extensions.deviceCustomIPv6Address3', + type: 'ip', + }, + 'cef.extensions.deviceCustomIPv6Address3Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomIPv6Address3Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomIPv6Address4': { + category: 'cef', + description: + 'One of four IPv6 address fields available to map fields that do not apply to any other in this dictionary.', + name: 'cef.extensions.deviceCustomIPv6Address4', + type: 'ip', + }, + 'cef.extensions.deviceCustomIPv6Address4Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomIPv6Address4Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomNumber1': { + category: 'cef', + description: + 'One of three number fields available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', + name: 'cef.extensions.deviceCustomNumber1', + type: 'long', + }, + 'cef.extensions.deviceCustomNumber1Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomNumber1Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomNumber2': { + category: 'cef', + description: + 'One of three number fields available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', + name: 'cef.extensions.deviceCustomNumber2', + type: 'long', + }, + 'cef.extensions.deviceCustomNumber2Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomNumber2Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomNumber3': { + category: 'cef', + description: + 'One of three number fields available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', + name: 'cef.extensions.deviceCustomNumber3', + type: 'long', + }, + 'cef.extensions.deviceCustomNumber3Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomNumber3Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomString1': { + category: 'cef', + description: + 'One of six strings available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', + name: 'cef.extensions.deviceCustomString1', + type: 'keyword', + }, + 'cef.extensions.deviceCustomString1Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomString1Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomString2': { + category: 'cef', + description: + 'One of six strings available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', + name: 'cef.extensions.deviceCustomString2', + type: 'keyword', + }, + 'cef.extensions.deviceCustomString2Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomString2Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomString3': { + category: 'cef', + description: + 'One of six strings available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', + name: 'cef.extensions.deviceCustomString3', + type: 'keyword', + }, + 'cef.extensions.deviceCustomString3Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomString3Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomString4': { + category: 'cef', + description: + 'One of six strings available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', + name: 'cef.extensions.deviceCustomString4', + type: 'keyword', + }, + 'cef.extensions.deviceCustomString4Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomString4Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomString5': { + category: 'cef', + description: + 'One of six strings available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', + name: 'cef.extensions.deviceCustomString5', + type: 'keyword', + }, + 'cef.extensions.deviceCustomString5Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomString5Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomString6': { + category: 'cef', + description: + 'One of six strings available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', + name: 'cef.extensions.deviceCustomString6', + type: 'keyword', + }, + 'cef.extensions.deviceCustomString6Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomString6Label', + type: 'keyword', + }, + 'cef.extensions.deviceDirection': { + category: 'cef', + description: + 'Any information about what direction the observed communication has taken. The following values are supported - "0" for inbound or "1" for outbound.', + name: 'cef.extensions.deviceDirection', + type: 'long', + }, + 'cef.extensions.deviceDnsDomain': { + category: 'cef', + description: 'The DNS domain part of the complete fully qualified domain name (FQDN).', + name: 'cef.extensions.deviceDnsDomain', + type: 'keyword', + }, + 'cef.extensions.deviceEventCategory': { + category: 'cef', + description: + 'Represents the category assigned by the originating device. Devices often use their own categorization schema to classify event. Example "/Monitor/Disk/Read".', + name: 'cef.extensions.deviceEventCategory', + type: 'keyword', + }, + 'cef.extensions.deviceExternalId': { + category: 'cef', + description: 'A name that uniquely identifies the device generating this event.', + name: 'cef.extensions.deviceExternalId', + type: 'keyword', + }, + 'cef.extensions.deviceFacility': { + category: 'cef', + description: + 'The facility generating this event. For example, Syslog has an explicit facility associated with every event.', + name: 'cef.extensions.deviceFacility', + type: 'keyword', + }, + 'cef.extensions.deviceFlexNumber1': { + category: 'cef', + description: + 'One of two alternative number fields available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', + name: 'cef.extensions.deviceFlexNumber1', + type: 'long', + }, + 'cef.extensions.deviceFlexNumber1Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceFlexNumber1Label', + type: 'keyword', + }, + 'cef.extensions.deviceFlexNumber2': { + category: 'cef', + description: + 'One of two alternative number fields available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', + name: 'cef.extensions.deviceFlexNumber2', + type: 'long', + }, + 'cef.extensions.deviceFlexNumber2Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceFlexNumber2Label', + type: 'keyword', + }, + 'cef.extensions.deviceHostName': { + category: 'cef', + description: + 'The format should be a fully qualified domain name (FQDN) associated with the device node, when a node is available.', + name: 'cef.extensions.deviceHostName', + type: 'keyword', + }, + 'cef.extensions.deviceInboundInterface': { + category: 'cef', + description: 'Interface on which the packet or data entered the device.', + name: 'cef.extensions.deviceInboundInterface', + type: 'keyword', + }, + 'cef.extensions.deviceMacAddress': { + category: 'cef', + description: 'Six colon-separated hexadecimal numbers.', + name: 'cef.extensions.deviceMacAddress', + type: 'keyword', + }, + 'cef.extensions.deviceNtDomain': { + category: 'cef', + description: 'The Windows domain name of the device address.', + name: 'cef.extensions.deviceNtDomain', + type: 'keyword', + }, + 'cef.extensions.deviceOutboundInterface': { + category: 'cef', + description: 'Interface on which the packet or data left the device.', + name: 'cef.extensions.deviceOutboundInterface', + type: 'keyword', + }, + 'cef.extensions.devicePayloadId': { + category: 'cef', + description: 'Unique identifier for the payload associated with the event.', + name: 'cef.extensions.devicePayloadId', + type: 'keyword', + }, + 'cef.extensions.deviceProcessId': { + category: 'cef', + description: 'Provides the ID of the process on the device generating the event.', + name: 'cef.extensions.deviceProcessId', + type: 'long', + }, + 'cef.extensions.deviceProcessName': { + category: 'cef', + description: + 'Process name associated with the event. An example might be the process generating the syslog entry in UNIX.', + name: 'cef.extensions.deviceProcessName', + type: 'keyword', + }, + 'cef.extensions.deviceReceiptTime': { + category: 'cef', + description: + 'The time at which the event related to the activity was received. The format is MMM dd yyyy HH:mm:ss or milliseconds since epoch (Jan 1st 1970)', + name: 'cef.extensions.deviceReceiptTime', + type: 'date', + }, + 'cef.extensions.deviceTimeZone': { + category: 'cef', + description: 'The time zone for the device generating the event.', + name: 'cef.extensions.deviceTimeZone', + type: 'keyword', + }, + 'cef.extensions.deviceTranslatedAddress': { + category: 'cef', + description: + 'Identifies the translated device address that the event refers to in an IP network.', + name: 'cef.extensions.deviceTranslatedAddress', + type: 'ip', + }, + 'cef.extensions.deviceTranslatedZoneExternalID': { + category: 'cef', + description: 'null', + name: 'cef.extensions.deviceTranslatedZoneExternalID', + type: 'keyword', + }, + 'cef.extensions.deviceTranslatedZoneURI': { + category: 'cef', + description: + 'The URI for the Translated Zone that the device asset has been assigned to in ArcSight.', + name: 'cef.extensions.deviceTranslatedZoneURI', + type: 'keyword', + }, + 'cef.extensions.deviceZoneExternalID': { + category: 'cef', + description: 'null', + name: 'cef.extensions.deviceZoneExternalID', + type: 'keyword', + }, + 'cef.extensions.deviceZoneURI': { + category: 'cef', + description: 'Thee URI for the Zone that the device asset has been assigned to in ArcSight.', + name: 'cef.extensions.deviceZoneURI', + type: 'keyword', + }, + 'cef.extensions.endTime': { + category: 'cef', + description: + 'The time at which the activity related to the event ended. The format is MMM dd yyyy HH:mm:ss or milliseconds since epoch (Jan 1st1970). An example would be reporting the end of a session.', + name: 'cef.extensions.endTime', + type: 'date', + }, + 'cef.extensions.eventId': { + category: 'cef', + description: 'This is a unique ID that ArcSight assigns to each event.', + name: 'cef.extensions.eventId', + type: 'long', + }, + 'cef.extensions.eventOutcome': { + category: 'cef', + description: "Displays the outcome, usually as 'success' or 'failure'.", + name: 'cef.extensions.eventOutcome', + type: 'keyword', + }, + 'cef.extensions.externalId': { + category: 'cef', + description: + 'The ID used by an originating device. They are usually increasing numbers, associated with events.', + name: 'cef.extensions.externalId', + type: 'keyword', + }, + 'cef.extensions.fileCreateTime': { + category: 'cef', + description: 'Time when the file was created.', + name: 'cef.extensions.fileCreateTime', + type: 'date', + }, + 'cef.extensions.fileHash': { + category: 'cef', + description: 'Hash of a file.', + name: 'cef.extensions.fileHash', + type: 'keyword', + }, + 'cef.extensions.fileId': { + category: 'cef', + description: 'An ID associated with a file could be the inode.', + name: 'cef.extensions.fileId', + type: 'keyword', + }, + 'cef.extensions.fileModificationTime': { + category: 'cef', + description: 'Time when the file was last modified.', + name: 'cef.extensions.fileModificationTime', + type: 'date', + }, + 'cef.extensions.filename': { + category: 'cef', + description: 'Name of the file only (without its path).', + name: 'cef.extensions.filename', + type: 'keyword', + }, + 'cef.extensions.filePath': { + category: 'cef', + description: 'Full path to the file, including file name itself.', + name: 'cef.extensions.filePath', + type: 'keyword', + }, + 'cef.extensions.filePermission': { + category: 'cef', + description: 'Permissions of the file.', + name: 'cef.extensions.filePermission', + type: 'keyword', + }, + 'cef.extensions.fileSize': { + category: 'cef', + description: 'Size of the file.', + name: 'cef.extensions.fileSize', + type: 'long', + }, + 'cef.extensions.fileType': { + category: 'cef', + description: 'Type of file (pipe, socket, etc.)', + name: 'cef.extensions.fileType', + type: 'keyword', + }, + 'cef.extensions.flexDate1': { + category: 'cef', + description: + 'A timestamp field available to map a timestamp that does not apply to any other defined timestamp field in this dictionary. Use all flex fields sparingly and seek a more specific, dictionary supplied field when possible. These fields are typically reserved for customer use and should not be set by vendors unless necessary.', + name: 'cef.extensions.flexDate1', + type: 'date', + }, + 'cef.extensions.flexDate1Label': { + category: 'cef', + description: 'The label field is a string and describes the purpose of the flex field.', + name: 'cef.extensions.flexDate1Label', + type: 'keyword', + }, + 'cef.extensions.flexString1': { + category: 'cef', + description: + 'One of four floating point fields available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible. These fields are typically reserved for customer use and should not be set by vendors unless necessary.', + name: 'cef.extensions.flexString1', + type: 'keyword', + }, + 'cef.extensions.flexString2': { + category: 'cef', + description: + 'One of four floating point fields available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible. These fields are typically reserved for customer use and should not be set by vendors unless necessary.', + name: 'cef.extensions.flexString2', + type: 'keyword', + }, + 'cef.extensions.flexString1Label': { + category: 'cef', + description: 'The label field is a string and describes the purpose of the flex field.', + name: 'cef.extensions.flexString1Label', + type: 'keyword', + }, + 'cef.extensions.flexString2Label': { + category: 'cef', + description: 'The label field is a string and describes the purpose of the flex field.', + name: 'cef.extensions.flexString2Label', + type: 'keyword', + }, + 'cef.extensions.message': { + category: 'cef', + description: + 'An arbitrary message giving more details about the event. Multi-line entries can be produced by using \\n as the new line separator.', + name: 'cef.extensions.message', + type: 'keyword', + }, + 'cef.extensions.oldFileCreateTime': { + category: 'cef', + description: 'Time when old file was created.', + name: 'cef.extensions.oldFileCreateTime', + type: 'date', + }, + 'cef.extensions.oldFileHash': { + category: 'cef', + description: 'Hash of the old file.', + name: 'cef.extensions.oldFileHash', + type: 'keyword', + }, + 'cef.extensions.oldFileId': { + category: 'cef', + description: 'An ID associated with the old file could be the inode.', + name: 'cef.extensions.oldFileId', + type: 'keyword', + }, + 'cef.extensions.oldFileModificationTime': { + category: 'cef', + description: 'Time when old file was last modified.', + name: 'cef.extensions.oldFileModificationTime', + type: 'date', + }, + 'cef.extensions.oldFileName': { + category: 'cef', + description: 'Name of the old file.', + name: 'cef.extensions.oldFileName', + type: 'keyword', + }, + 'cef.extensions.oldFilePath': { + category: 'cef', + description: 'Full path to the old file, including the file name itself.', + name: 'cef.extensions.oldFilePath', + type: 'keyword', + }, + 'cef.extensions.oldFilePermission': { + category: 'cef', + description: 'Permissions of the old file.', + name: 'cef.extensions.oldFilePermission', + type: 'keyword', + }, + 'cef.extensions.oldFileSize': { + category: 'cef', + description: 'Size of the old file.', + name: 'cef.extensions.oldFileSize', + type: 'long', + }, + 'cef.extensions.oldFileType': { + category: 'cef', + description: 'Type of the old file (pipe, socket, etc.)', + name: 'cef.extensions.oldFileType', + type: 'keyword', + }, + 'cef.extensions.rawEvent': { + category: 'cef', + description: 'null', + name: 'cef.extensions.rawEvent', + type: 'keyword', + }, + 'cef.extensions.Reason': { + category: 'cef', + description: + 'The reason an audit event was generated. For example "bad password" or "unknown user". This could also be an error or return code. Example "0x1234".', + name: 'cef.extensions.Reason', + type: 'keyword', + }, + 'cef.extensions.requestClientApplication': { + category: 'cef', + description: 'The User-Agent associated with the request.', + name: 'cef.extensions.requestClientApplication', + type: 'keyword', + }, + 'cef.extensions.requestContext': { + category: 'cef', + description: + 'Description of the content from which the request originated (for example, HTTP Referrer)', + name: 'cef.extensions.requestContext', + type: 'keyword', + }, + 'cef.extensions.requestCookies': { + category: 'cef', + description: 'Cookies associated with the request.', + name: 'cef.extensions.requestCookies', + type: 'keyword', + }, + 'cef.extensions.requestMethod': { + category: 'cef', + description: 'The HTTP method used to access a URL.', + name: 'cef.extensions.requestMethod', + type: 'keyword', + }, + 'cef.extensions.requestUrl': { + category: 'cef', + description: + 'In the case of an HTTP request, this field contains the URL accessed. The URL should contain the protocol as well.', + name: 'cef.extensions.requestUrl', + type: 'keyword', + }, + 'cef.extensions.sourceAddress': { + category: 'cef', + description: 'Identifies the source that an event refers to in an IP network.', + name: 'cef.extensions.sourceAddress', + type: 'ip', + }, + 'cef.extensions.sourceDnsDomain': { + category: 'cef', + description: 'The DNS domain part of the complete fully qualified domain name (FQDN).', + name: 'cef.extensions.sourceDnsDomain', + type: 'keyword', + }, + 'cef.extensions.sourceGeoLatitude': { + category: 'cef', + description: 'null', + name: 'cef.extensions.sourceGeoLatitude', + type: 'double', + }, + 'cef.extensions.sourceGeoLongitude': { + category: 'cef', + description: 'null', + name: 'cef.extensions.sourceGeoLongitude', + type: 'double', + }, + 'cef.extensions.sourceHostName': { + category: 'cef', + description: + "Identifies the source that an event refers to in an IP network. The format should be a fully qualified domain name (FQDN) associated with the source node, when a mode is available. Examples: 'host' or 'host.domain.com'. ", + name: 'cef.extensions.sourceHostName', + type: 'keyword', + }, + 'cef.extensions.sourceMacAddress': { + category: 'cef', + description: 'Six colon-separated hexadecimal numbers.', + example: '00:0d:60:af:1b:61', + name: 'cef.extensions.sourceMacAddress', + type: 'keyword', + }, + 'cef.extensions.sourceNtDomain': { + category: 'cef', + description: 'The Windows domain name for the source address.', + name: 'cef.extensions.sourceNtDomain', + type: 'keyword', + }, + 'cef.extensions.sourcePort': { + category: 'cef', + description: 'The valid port numbers are 0 to 65535.', + name: 'cef.extensions.sourcePort', + type: 'long', + }, + 'cef.extensions.sourceProcessId': { + category: 'cef', + description: 'The ID of the source process associated with the event.', + name: 'cef.extensions.sourceProcessId', + type: 'long', + }, + 'cef.extensions.sourceProcessName': { + category: 'cef', + description: "The name of the event's source process.", + name: 'cef.extensions.sourceProcessName', + type: 'keyword', + }, + 'cef.extensions.sourceServiceName': { + category: 'cef', + description: 'The service that is responsible for generating this event.', + name: 'cef.extensions.sourceServiceName', + type: 'keyword', + }, + 'cef.extensions.sourceTranslatedAddress': { + category: 'cef', + description: 'Identifies the translated source that the event refers to in an IP network.', + name: 'cef.extensions.sourceTranslatedAddress', + type: 'ip', + }, + 'cef.extensions.sourceTranslatedPort': { + category: 'cef', + description: + 'A port number after being translated by, for example, a firewall. Valid port numbers are 0 to 65535.', + name: 'cef.extensions.sourceTranslatedPort', + type: 'long', + }, + 'cef.extensions.sourceTranslatedZoneExternalID': { + category: 'cef', + description: 'null', + name: 'cef.extensions.sourceTranslatedZoneExternalID', + type: 'keyword', + }, + 'cef.extensions.sourceTranslatedZoneURI': { + category: 'cef', + description: + 'The URI for the Translated Zone that the destination asset has been assigned to in ArcSight.', + name: 'cef.extensions.sourceTranslatedZoneURI', + type: 'keyword', + }, + 'cef.extensions.sourceUserId': { + category: 'cef', + description: + 'Identifies the source user by ID. This is the user associated with the source of the event. For example, in UNIX, the root user is generally associated with user ID 0.', + name: 'cef.extensions.sourceUserId', + type: 'keyword', + }, + 'cef.extensions.sourceUserName': { + category: 'cef', + description: + 'Identifies the source user by name. Email addresses are also mapped into the UserName fields. The sender is a candidate to put into this field.', + name: 'cef.extensions.sourceUserName', + type: 'keyword', + }, + 'cef.extensions.sourceUserPrivileges': { + category: 'cef', + description: + 'The typical values are "Administrator", "User", and "Guest". It identifies the source user\'s privileges. In UNIX, for example, activity executed by the root user would be identified with "Administrator".', + name: 'cef.extensions.sourceUserPrivileges', + type: 'keyword', + }, + 'cef.extensions.sourceZoneExternalID': { + category: 'cef', + description: 'null', + name: 'cef.extensions.sourceZoneExternalID', + type: 'keyword', + }, + 'cef.extensions.sourceZoneURI': { + category: 'cef', + description: 'The URI for the Zone that the source asset has been assigned to in ArcSight.', + name: 'cef.extensions.sourceZoneURI', + type: 'keyword', + }, + 'cef.extensions.startTime': { + category: 'cef', + description: + 'The time when the activity the event referred to started. The format is MMM dd yyyy HH:mm:ss or milliseconds since epoch (Jan 1st 1970)', + name: 'cef.extensions.startTime', + type: 'date', + }, + 'cef.extensions.transportProtocol': { + category: 'cef', + description: + 'Identifies the Layer-4 protocol used. The possible values are protocols such as TCP or UDP.', + name: 'cef.extensions.transportProtocol', + type: 'keyword', + }, + 'cef.extensions.type': { + category: 'cef', + description: + '0 means base event, 1 means aggregated, 2 means correlation, and 3 means action. This field can be omitted for base events (type 0).', + name: 'cef.extensions.type', + type: 'long', + }, + 'cef.extensions.categoryDeviceType': { + category: 'cef', + description: 'Device type. Examples - Proxy, IDS, Web Server', + name: 'cef.extensions.categoryDeviceType', + type: 'keyword', + }, + 'cef.extensions.categoryObject': { + category: 'cef', + description: + 'Object that the event is about. For example it can be an operating sytem, database, file, etc.', + name: 'cef.extensions.categoryObject', + type: 'keyword', + }, + 'cef.extensions.categoryBehavior': { + category: 'cef', + description: + "Action or a behavior associated with an event. It's what is being done to the object.", + name: 'cef.extensions.categoryBehavior', + type: 'keyword', + }, + 'cef.extensions.categoryTechnique': { + category: 'cef', + description: 'Technique being used (e.g. /DoS).', + name: 'cef.extensions.categoryTechnique', + type: 'keyword', + }, + 'cef.extensions.categoryDeviceGroup': { + category: 'cef', + description: 'General device group like Firewall.', + name: 'cef.extensions.categoryDeviceGroup', + type: 'keyword', + }, + 'cef.extensions.categorySignificance': { + category: 'cef', + description: 'Characterization of the importance of the event.', + name: 'cef.extensions.categorySignificance', + type: 'keyword', + }, + 'cef.extensions.categoryOutcome': { + category: 'cef', + description: 'Outcome of the event (e.g. sucess, failure, or attempt).', + name: 'cef.extensions.categoryOutcome', + type: 'keyword', + }, + 'cef.extensions.managerReceiptTime': { + category: 'cef', + description: 'When the Arcsight ESM received the event.', + name: 'cef.extensions.managerReceiptTime', + type: 'date', + }, + 'source.service.name': { + category: 'source', + description: 'Service that is the source of the event.', + name: 'source.service.name', + type: 'keyword', + }, + 'destination.service.name': { + category: 'destination', + description: 'Service that is the target of the event.', + name: 'destination.service.name', + type: 'keyword', + }, + type: { + category: 'base', + description: + 'The type of the transaction (for example, HTTP, MySQL, Redis, or RUM) or "flow" in case of flows. ', + name: 'type', + }, + 'server.process.name': { + category: 'server', + description: 'The name of the process that served the transaction. ', + name: 'server.process.name', + }, + 'server.process.args': { + category: 'server', + description: 'The command-line of the process that served the transaction. ', + name: 'server.process.args', + }, + 'server.process.executable': { + category: 'server', + description: 'Absolute path to the server process executable. ', + name: 'server.process.executable', + }, + 'server.process.working_directory': { + category: 'server', + description: 'The working directory of the server process. ', + name: 'server.process.working_directory', + }, + 'server.process.start': { + category: 'server', + description: 'The time the server process started. ', + name: 'server.process.start', + }, + 'client.process.name': { + category: 'client', + description: 'The name of the process that initiated the transaction. ', + name: 'client.process.name', + }, + 'client.process.args': { + category: 'client', + description: 'The command-line of the process that initiated the transaction. ', + name: 'client.process.args', + }, + 'client.process.executable': { + category: 'client', + description: 'Absolute path to the client process executable. ', + name: 'client.process.executable', + }, + 'client.process.working_directory': { + category: 'client', + description: 'The working directory of the client process. ', + name: 'client.process.working_directory', + }, + 'client.process.start': { + category: 'client', + description: 'The time the client process started. ', + name: 'client.process.start', + }, + real_ip: { + category: 'base', + description: + 'If the server initiating the transaction is a proxy, this field contains the original client IP address. For HTTP, for example, the IP address extracted from a configurable HTTP header, by default `X-Forwarded-For`. Unless this field is disabled, it always has a value, and it matches the `client_ip` for non proxy clients. ', + name: 'real_ip', + type: 'alias', + }, + transport: { + category: 'base', + description: + 'The transport protocol used for the transaction. If not specified, then tcp is assumed. ', + name: 'transport', + type: 'alias', + }, + 'flow.final': { + category: 'flow', + description: + 'Indicates if event is last event in flow. If final is false, the event reports an intermediate flow state only. ', + name: 'flow.final', + type: 'boolean', + }, + 'flow.id': { + category: 'flow', + description: 'Internal flow ID based on connection meta data and address. ', + name: 'flow.id', + }, + 'flow.vlan': { + category: 'flow', + description: + "VLAN identifier from the 802.1q frame. In case of a multi-tagged frame this field will be an array with the outer tag's VLAN identifier listed first. ", + name: 'flow.vlan', + type: 'long', + }, + flow_id: { + category: 'base', + name: 'flow_id', + type: 'alias', + }, + final: { + category: 'base', + name: 'final', + type: 'alias', + }, + vlan: { + category: 'base', + name: 'vlan', + type: 'alias', + }, + 'source.stats.net_bytes_total': { + category: 'source', + name: 'source.stats.net_bytes_total', + type: 'alias', + }, + 'source.stats.net_packets_total': { + category: 'source', + name: 'source.stats.net_packets_total', + type: 'alias', + }, + 'dest.stats.net_bytes_total': { + category: 'dest', + name: 'dest.stats.net_bytes_total', + type: 'alias', + }, + 'dest.stats.net_packets_total': { + category: 'dest', + name: 'dest.stats.net_packets_total', + type: 'alias', + }, + status: { + category: 'base', + description: + 'The high level status of the transaction. The way to compute this value depends on the protocol, but the result has a meaning independent of the protocol. ', + name: 'status', + }, + method: { + category: 'base', + description: + 'The command/verb/method of the transaction. For HTTP, this is the method name (GET, POST, PUT, and so on), for SQL this is the verb (SELECT, UPDATE, DELETE, and so on). ', + name: 'method', + }, + resource: { + category: 'base', + description: + 'The logical resource that this transaction refers to. For HTTP, this is the URL path up to the last slash (/). For example, if the URL is `/users/1`, the resource is `/users`. For databases, the resource is typically the table name. The field is not filled for all transaction types. ', + name: 'resource', + }, + path: { + category: 'base', + description: + 'The path the transaction refers to. For HTTP, this is the URL. For SQL databases, this is the table name. For key-value stores, this is the key. ', + name: 'path', + }, + query: { + category: 'base', + description: + 'The query in a human readable format. For HTTP, it will typically be something like `GET /users/_search?name=test`. For MySQL, it is something like `SELECT id from users where name=test`. ', + name: 'query', + type: 'keyword', + }, + params: { + category: 'base', + description: + 'The request parameters. For HTTP, these are the POST or GET parameters. For Thrift-RPC, these are the parameters from the request. ', + name: 'params', + type: 'text', + }, + notes: { + category: 'base', + description: + 'Messages from Packetbeat itself. This field usually contains error messages for interpreting the raw data. This information can be helpful for troubleshooting. ', + name: 'notes', + type: 'alias', + }, + request: { + category: 'base', + description: + 'For text protocols, this is the request as seen on the wire (application layer only). For binary protocols this is our representation of the request. ', + name: 'request', + type: 'text', + }, + response: { + category: 'base', + description: + 'For text protocols, this is the response as seen on the wire (application layer only). For binary protocols this is our representation of the request. ', + name: 'response', + type: 'text', + }, + bytes_in: { + category: 'base', + description: + 'The number of bytes of the request. Note that this size is the application layer message length, without the length of the IP or TCP headers. ', + name: 'bytes_in', + type: 'alias', + }, + bytes_out: { + category: 'base', + description: + 'The number of bytes of the response. Note that this size is the application layer message length, without the length of the IP or TCP headers. ', + name: 'bytes_out', + type: 'alias', + }, + 'amqp.reply-code': { + category: 'amqp', + description: 'AMQP reply code to an error, similar to http reply-code ', + example: 404, + name: 'amqp.reply-code', + type: 'long', + }, + 'amqp.reply-text': { + category: 'amqp', + description: 'Text explaining the error. ', + name: 'amqp.reply-text', + type: 'keyword', + }, + 'amqp.class-id': { + category: 'amqp', + description: 'Failing method class. ', + name: 'amqp.class-id', + type: 'long', + }, + 'amqp.method-id': { + category: 'amqp', + description: 'Failing method ID. ', + name: 'amqp.method-id', + type: 'long', + }, + 'amqp.exchange': { + category: 'amqp', + description: 'Name of the exchange. ', + name: 'amqp.exchange', + type: 'keyword', + }, + 'amqp.exchange-type': { + category: 'amqp', + description: 'Exchange type. ', + example: 'fanout', + name: 'amqp.exchange-type', + type: 'keyword', + }, + 'amqp.passive': { + category: 'amqp', + description: 'If set, do not create exchange/queue. ', + name: 'amqp.passive', + type: 'boolean', + }, + 'amqp.durable': { + category: 'amqp', + description: 'If set, request a durable exchange/queue. ', + name: 'amqp.durable', + type: 'boolean', + }, + 'amqp.exclusive': { + category: 'amqp', + description: 'If set, request an exclusive queue. ', + name: 'amqp.exclusive', + type: 'boolean', + }, + 'amqp.auto-delete': { + category: 'amqp', + description: 'If set, auto-delete queue when unused. ', + name: 'amqp.auto-delete', + type: 'boolean', + }, + 'amqp.no-wait': { + category: 'amqp', + description: 'If set, the server will not respond to the method. ', + name: 'amqp.no-wait', + type: 'boolean', + }, + 'amqp.consumer-tag': { + category: 'amqp', + description: 'Identifier for the consumer, valid within the current channel. ', + name: 'amqp.consumer-tag', + }, + 'amqp.delivery-tag': { + category: 'amqp', + description: 'The server-assigned and channel-specific delivery tag. ', + name: 'amqp.delivery-tag', + type: 'long', + }, + 'amqp.message-count': { + category: 'amqp', + description: + 'The number of messages in the queue, which will be zero for newly-declared queues. ', + name: 'amqp.message-count', + type: 'long', + }, + 'amqp.consumer-count': { + category: 'amqp', + description: 'The number of consumers of a queue. ', + name: 'amqp.consumer-count', + type: 'long', + }, + 'amqp.routing-key': { + category: 'amqp', + description: 'Message routing key. ', + name: 'amqp.routing-key', + type: 'keyword', + }, + 'amqp.no-ack': { + category: 'amqp', + description: 'If set, the server does not expect acknowledgements for messages. ', + name: 'amqp.no-ack', + type: 'boolean', + }, + 'amqp.no-local': { + category: 'amqp', + description: + 'If set, the server will not send messages to the connection that published them. ', + name: 'amqp.no-local', + type: 'boolean', + }, + 'amqp.if-unused': { + category: 'amqp', + description: 'Delete only if unused. ', + name: 'amqp.if-unused', + type: 'boolean', + }, + 'amqp.if-empty': { + category: 'amqp', + description: 'Delete only if empty. ', + name: 'amqp.if-empty', + type: 'boolean', + }, + 'amqp.queue': { + category: 'amqp', + description: 'The queue name identifies the queue within the vhost. ', + name: 'amqp.queue', + type: 'keyword', + }, + 'amqp.redelivered': { + category: 'amqp', + description: + 'Indicates that the message has been previously delivered to this or another client. ', + name: 'amqp.redelivered', + type: 'boolean', + }, + 'amqp.multiple': { + category: 'amqp', + description: 'Acknowledge multiple messages. ', + name: 'amqp.multiple', + type: 'boolean', + }, + 'amqp.arguments': { + category: 'amqp', + description: 'Optional additional arguments passed to some methods. Can be of various types. ', + name: 'amqp.arguments', + type: 'object', + }, + 'amqp.mandatory': { + category: 'amqp', + description: 'Indicates mandatory routing. ', + name: 'amqp.mandatory', + type: 'boolean', + }, + 'amqp.immediate': { + category: 'amqp', + description: 'Request immediate delivery. ', + name: 'amqp.immediate', + type: 'boolean', + }, + 'amqp.content-type': { + category: 'amqp', + description: 'MIME content type. ', + example: 'text/plain', + name: 'amqp.content-type', + type: 'keyword', + }, + 'amqp.content-encoding': { + category: 'amqp', + description: 'MIME content encoding. ', + name: 'amqp.content-encoding', + type: 'keyword', + }, + 'amqp.headers': { + category: 'amqp', + description: 'Message header field table. ', + name: 'amqp.headers', + type: 'object', + }, + 'amqp.delivery-mode': { + category: 'amqp', + description: 'Non-persistent (1) or persistent (2). ', + name: 'amqp.delivery-mode', + type: 'keyword', + }, + 'amqp.priority': { + category: 'amqp', + description: 'Message priority, 0 to 9. ', + name: 'amqp.priority', + type: 'long', + }, + 'amqp.correlation-id': { + category: 'amqp', + description: 'Application correlation identifier. ', + name: 'amqp.correlation-id', + type: 'keyword', + }, + 'amqp.reply-to': { + category: 'amqp', + description: 'Address to reply to. ', + name: 'amqp.reply-to', + type: 'keyword', + }, + 'amqp.expiration': { + category: 'amqp', + description: 'Message expiration specification. ', + name: 'amqp.expiration', + type: 'keyword', + }, + 'amqp.message-id': { + category: 'amqp', + description: 'Application message identifier. ', + name: 'amqp.message-id', + type: 'keyword', + }, + 'amqp.timestamp': { + category: 'amqp', + description: 'Message timestamp. ', + name: 'amqp.timestamp', + type: 'keyword', + }, + 'amqp.type': { + category: 'amqp', + description: 'Message type name. ', + name: 'amqp.type', + type: 'keyword', + }, + 'amqp.user-id': { + category: 'amqp', + description: 'Creating user id. ', + name: 'amqp.user-id', + type: 'keyword', + }, + 'amqp.app-id': { + category: 'amqp', + description: 'Creating application id. ', + name: 'amqp.app-id', + type: 'keyword', + }, + no_request: { + category: 'base', + name: 'no_request', + type: 'alias', + }, + 'cassandra.no_request': { + category: 'cassandra', + description: 'Indicates that there is no request because this is a PUSH message. ', + name: 'cassandra.no_request', + type: 'boolean', + }, + 'cassandra.request.headers.version': { + category: 'cassandra', + description: 'The version of the protocol.', + name: 'cassandra.request.headers.version', + type: 'long', + }, + 'cassandra.request.headers.flags': { + category: 'cassandra', + description: 'Flags applying to this frame.', + name: 'cassandra.request.headers.flags', + type: 'keyword', + }, + 'cassandra.request.headers.stream': { + category: 'cassandra', + description: + 'A frame has a stream id. If a client sends a request message with the stream id X, it is guaranteed that the stream id of the response to that message will be X.', + name: 'cassandra.request.headers.stream', + type: 'keyword', + }, + 'cassandra.request.headers.op': { + category: 'cassandra', + description: 'An operation type that distinguishes the actual message.', + name: 'cassandra.request.headers.op', + type: 'keyword', + }, + 'cassandra.request.headers.length': { + category: 'cassandra', + description: + 'A integer representing the length of the body of the frame (a frame is limited to 256MB in length).', + name: 'cassandra.request.headers.length', + type: 'long', + }, + 'cassandra.request.query': { + category: 'cassandra', + description: 'The CQL query which client send to cassandra.', + name: 'cassandra.request.query', + type: 'keyword', + }, + 'cassandra.response.headers.version': { + category: 'cassandra', + description: 'The version of the protocol.', + name: 'cassandra.response.headers.version', + type: 'long', + }, + 'cassandra.response.headers.flags': { + category: 'cassandra', + description: 'Flags applying to this frame.', + name: 'cassandra.response.headers.flags', + type: 'keyword', + }, + 'cassandra.response.headers.stream': { + category: 'cassandra', + description: + 'A frame has a stream id. If a client sends a request message with the stream id X, it is guaranteed that the stream id of the response to that message will be X.', + name: 'cassandra.response.headers.stream', + type: 'keyword', + }, + 'cassandra.response.headers.op': { + category: 'cassandra', + description: 'An operation type that distinguishes the actual message.', + name: 'cassandra.response.headers.op', + type: 'keyword', + }, + 'cassandra.response.headers.length': { + category: 'cassandra', + description: + 'A integer representing the length of the body of the frame (a frame is limited to 256MB in length).', + name: 'cassandra.response.headers.length', + type: 'long', + }, + 'cassandra.response.result.type': { + category: 'cassandra', + description: 'Cassandra result type.', + name: 'cassandra.response.result.type', + type: 'keyword', + }, + 'cassandra.response.result.rows.num_rows': { + category: 'cassandra', + description: 'Representing the number of rows present in this result.', + name: 'cassandra.response.result.rows.num_rows', + type: 'long', + }, + 'cassandra.response.result.rows.meta.keyspace': { + category: 'cassandra', + description: 'Only present after set Global_tables_spec, the keyspace name.', + name: 'cassandra.response.result.rows.meta.keyspace', + type: 'keyword', + }, + 'cassandra.response.result.rows.meta.table': { + category: 'cassandra', + description: 'Only present after set Global_tables_spec, the table name.', + name: 'cassandra.response.result.rows.meta.table', + type: 'keyword', + }, + 'cassandra.response.result.rows.meta.flags': { + category: 'cassandra', + description: 'Provides information on the formatting of the remaining information.', + name: 'cassandra.response.result.rows.meta.flags', + type: 'keyword', + }, + 'cassandra.response.result.rows.meta.col_count': { + category: 'cassandra', + description: + 'Representing the number of columns selected by the query that produced this result.', + name: 'cassandra.response.result.rows.meta.col_count', + type: 'long', + }, + 'cassandra.response.result.rows.meta.pkey_columns': { + category: 'cassandra', + description: 'Representing the PK columns index and counts.', + name: 'cassandra.response.result.rows.meta.pkey_columns', + type: 'long', + }, + 'cassandra.response.result.rows.meta.paging_state': { + category: 'cassandra', + description: + 'The paging_state is a bytes value that should be used in QUERY/EXECUTE to continue paging and retrieve the remainder of the result for this query.', + name: 'cassandra.response.result.rows.meta.paging_state', + type: 'keyword', + }, + 'cassandra.response.result.keyspace': { + category: 'cassandra', + description: 'Indicating the name of the keyspace that has been set.', + name: 'cassandra.response.result.keyspace', + type: 'keyword', + }, + 'cassandra.response.result.schema_change.change': { + category: 'cassandra', + description: 'Representing the type of changed involved.', + name: 'cassandra.response.result.schema_change.change', + type: 'keyword', + }, + 'cassandra.response.result.schema_change.keyspace': { + category: 'cassandra', + description: 'This describes which keyspace has changed.', + name: 'cassandra.response.result.schema_change.keyspace', + type: 'keyword', + }, + 'cassandra.response.result.schema_change.table': { + category: 'cassandra', + description: 'This describes which table has changed.', + name: 'cassandra.response.result.schema_change.table', + type: 'keyword', + }, + 'cassandra.response.result.schema_change.object': { + category: 'cassandra', + description: + 'This describes the name of said affected object (either the table, user type, function, or aggregate name).', + name: 'cassandra.response.result.schema_change.object', + type: 'keyword', + }, + 'cassandra.response.result.schema_change.target': { + category: 'cassandra', + description: 'Target could be "FUNCTION" or "AGGREGATE", multiple arguments.', + name: 'cassandra.response.result.schema_change.target', + type: 'keyword', + }, + 'cassandra.response.result.schema_change.name': { + category: 'cassandra', + description: 'The function/aggregate name.', + name: 'cassandra.response.result.schema_change.name', + type: 'keyword', + }, + 'cassandra.response.result.schema_change.args': { + category: 'cassandra', + description: 'One string for each argument type (as CQL type).', + name: 'cassandra.response.result.schema_change.args', + type: 'keyword', + }, + 'cassandra.response.result.prepared.prepared_id': { + category: 'cassandra', + description: 'Representing the prepared query ID.', + name: 'cassandra.response.result.prepared.prepared_id', + type: 'keyword', + }, + 'cassandra.response.result.prepared.req_meta.keyspace': { + category: 'cassandra', + description: 'Only present after set Global_tables_spec, the keyspace name.', + name: 'cassandra.response.result.prepared.req_meta.keyspace', + type: 'keyword', + }, + 'cassandra.response.result.prepared.req_meta.table': { + category: 'cassandra', + description: 'Only present after set Global_tables_spec, the table name.', + name: 'cassandra.response.result.prepared.req_meta.table', + type: 'keyword', + }, + 'cassandra.response.result.prepared.req_meta.flags': { + category: 'cassandra', + description: 'Provides information on the formatting of the remaining information.', + name: 'cassandra.response.result.prepared.req_meta.flags', + type: 'keyword', + }, + 'cassandra.response.result.prepared.req_meta.col_count': { + category: 'cassandra', + description: + 'Representing the number of columns selected by the query that produced this result.', + name: 'cassandra.response.result.prepared.req_meta.col_count', + type: 'long', + }, + 'cassandra.response.result.prepared.req_meta.pkey_columns': { + category: 'cassandra', + description: 'Representing the PK columns index and counts.', + name: 'cassandra.response.result.prepared.req_meta.pkey_columns', + type: 'long', + }, + 'cassandra.response.result.prepared.req_meta.paging_state': { + category: 'cassandra', + description: + 'The paging_state is a bytes value that should be used in QUERY/EXECUTE to continue paging and retrieve the remainder of the result for this query.', + name: 'cassandra.response.result.prepared.req_meta.paging_state', + type: 'keyword', + }, + 'cassandra.response.result.prepared.resp_meta.keyspace': { + category: 'cassandra', + description: 'Only present after set Global_tables_spec, the keyspace name.', + name: 'cassandra.response.result.prepared.resp_meta.keyspace', + type: 'keyword', + }, + 'cassandra.response.result.prepared.resp_meta.table': { + category: 'cassandra', + description: 'Only present after set Global_tables_spec, the table name.', + name: 'cassandra.response.result.prepared.resp_meta.table', + type: 'keyword', + }, + 'cassandra.response.result.prepared.resp_meta.flags': { + category: 'cassandra', + description: 'Provides information on the formatting of the remaining information.', + name: 'cassandra.response.result.prepared.resp_meta.flags', + type: 'keyword', + }, + 'cassandra.response.result.prepared.resp_meta.col_count': { + category: 'cassandra', + description: + 'Representing the number of columns selected by the query that produced this result.', + name: 'cassandra.response.result.prepared.resp_meta.col_count', + type: 'long', + }, + 'cassandra.response.result.prepared.resp_meta.pkey_columns': { + category: 'cassandra', + description: 'Representing the PK columns index and counts.', + name: 'cassandra.response.result.prepared.resp_meta.pkey_columns', + type: 'long', + }, + 'cassandra.response.result.prepared.resp_meta.paging_state': { + category: 'cassandra', + description: + 'The paging_state is a bytes value that should be used in QUERY/EXECUTE to continue paging and retrieve the remainder of the result for this query.', + name: 'cassandra.response.result.prepared.resp_meta.paging_state', + type: 'keyword', + }, + 'cassandra.response.supported': { + category: 'cassandra', + description: + 'Indicates which startup options are supported by the server. This message comes as a response to an OPTIONS message.', + name: 'cassandra.response.supported', + type: 'object', + }, + 'cassandra.response.authentication.class': { + category: 'cassandra', + description: 'Indicates the full class name of the IAuthenticator in use', + name: 'cassandra.response.authentication.class', + type: 'keyword', + }, + 'cassandra.response.warnings': { + category: 'cassandra', + description: 'The text of the warnings, only occur when Warning flag was set.', + name: 'cassandra.response.warnings', + type: 'keyword', + }, + 'cassandra.response.event.type': { + category: 'cassandra', + description: 'Representing the event type.', + name: 'cassandra.response.event.type', + type: 'keyword', + }, + 'cassandra.response.event.change': { + category: 'cassandra', + description: + 'The message corresponding respectively to the type of change followed by the address of the new/removed node.', + name: 'cassandra.response.event.change', + type: 'keyword', + }, + 'cassandra.response.event.host': { + category: 'cassandra', + description: 'Representing the node ip.', + name: 'cassandra.response.event.host', + type: 'keyword', + }, + 'cassandra.response.event.port': { + category: 'cassandra', + description: 'Representing the node port.', + name: 'cassandra.response.event.port', + type: 'long', + }, + 'cassandra.response.event.schema_change.change': { + category: 'cassandra', + description: 'Representing the type of changed involved.', + name: 'cassandra.response.event.schema_change.change', + type: 'keyword', + }, + 'cassandra.response.event.schema_change.keyspace': { + category: 'cassandra', + description: 'This describes which keyspace has changed.', + name: 'cassandra.response.event.schema_change.keyspace', + type: 'keyword', + }, + 'cassandra.response.event.schema_change.table': { + category: 'cassandra', + description: 'This describes which table has changed.', + name: 'cassandra.response.event.schema_change.table', + type: 'keyword', + }, + 'cassandra.response.event.schema_change.object': { + category: 'cassandra', + description: + 'This describes the name of said affected object (either the table, user type, function, or aggregate name).', + name: 'cassandra.response.event.schema_change.object', + type: 'keyword', + }, + 'cassandra.response.event.schema_change.target': { + category: 'cassandra', + description: 'Target could be "FUNCTION" or "AGGREGATE", multiple arguments.', + name: 'cassandra.response.event.schema_change.target', + type: 'keyword', + }, + 'cassandra.response.event.schema_change.name': { + category: 'cassandra', + description: 'The function/aggregate name.', + name: 'cassandra.response.event.schema_change.name', + type: 'keyword', + }, + 'cassandra.response.event.schema_change.args': { + category: 'cassandra', + description: 'One string for each argument type (as CQL type).', + name: 'cassandra.response.event.schema_change.args', + type: 'keyword', + }, + 'cassandra.response.error.code': { + category: 'cassandra', + description: 'The error code of the Cassandra response.', + name: 'cassandra.response.error.code', + type: 'long', + }, + 'cassandra.response.error.msg': { + category: 'cassandra', + description: 'The error message of the Cassandra response.', + name: 'cassandra.response.error.msg', + type: 'keyword', + }, + 'cassandra.response.error.type': { + category: 'cassandra', + description: 'The error type of the Cassandra response.', + name: 'cassandra.response.error.type', + type: 'keyword', + }, + 'cassandra.response.error.details.read_consistency': { + category: 'cassandra', + description: 'Representing the consistency level of the query that triggered the exception.', + name: 'cassandra.response.error.details.read_consistency', + type: 'keyword', + }, + 'cassandra.response.error.details.required': { + category: 'cassandra', + description: + 'Representing the number of nodes that should be alive to respect consistency level.', + name: 'cassandra.response.error.details.required', + type: 'long', + }, + 'cassandra.response.error.details.alive': { + category: 'cassandra', + description: + 'Representing the number of replicas that were known to be alive when the request had been processed (since an unavailable exception has been triggered).', + name: 'cassandra.response.error.details.alive', + type: 'long', + }, + 'cassandra.response.error.details.received': { + category: 'cassandra', + description: 'Representing the number of nodes having acknowledged the request.', + name: 'cassandra.response.error.details.received', + type: 'long', + }, + 'cassandra.response.error.details.blockfor': { + category: 'cassandra', + description: + 'Representing the number of replicas whose acknowledgement is required to achieve consistency level.', + name: 'cassandra.response.error.details.blockfor', + type: 'long', + }, + 'cassandra.response.error.details.write_type': { + category: 'cassandra', + description: 'Describe the type of the write that timed out.', + name: 'cassandra.response.error.details.write_type', + type: 'keyword', + }, + 'cassandra.response.error.details.data_present': { + category: 'cassandra', + description: 'It means the replica that was asked for data had responded.', + name: 'cassandra.response.error.details.data_present', + type: 'boolean', + }, + 'cassandra.response.error.details.keyspace': { + category: 'cassandra', + description: 'The keyspace of the failed function.', + name: 'cassandra.response.error.details.keyspace', + type: 'keyword', + }, + 'cassandra.response.error.details.table': { + category: 'cassandra', + description: 'The keyspace of the failed function.', + name: 'cassandra.response.error.details.table', + type: 'keyword', + }, + 'cassandra.response.error.details.stmt_id': { + category: 'cassandra', + description: 'Representing the unknown ID.', + name: 'cassandra.response.error.details.stmt_id', + type: 'keyword', + }, + 'cassandra.response.error.details.num_failures': { + category: 'cassandra', + description: + 'Representing the number of nodes that experience a failure while executing the request.', + name: 'cassandra.response.error.details.num_failures', + type: 'keyword', + }, + 'cassandra.response.error.details.function': { + category: 'cassandra', + description: 'The name of the failed function.', + name: 'cassandra.response.error.details.function', + type: 'keyword', + }, + 'cassandra.response.error.details.arg_types': { + category: 'cassandra', + description: 'One string for each argument type (as CQL type) of the failed function.', + name: 'cassandra.response.error.details.arg_types', + type: 'keyword', + }, + 'dhcpv4.transaction_id': { + category: 'dhcpv4', + description: + 'Transaction ID, a random number chosen by the client, used by the client and server to associate messages and responses between a client and a server. ', + name: 'dhcpv4.transaction_id', + type: 'keyword', + }, + 'dhcpv4.seconds': { + category: 'dhcpv4', + description: + 'Number of seconds elapsed since client began address acquisition or renewal process. ', + name: 'dhcpv4.seconds', + type: 'long', + }, + 'dhcpv4.flags': { + category: 'dhcpv4', + description: + 'Flags are set by the client to indicate how the DHCP server should its reply -- either unicast or broadcast. ', + name: 'dhcpv4.flags', + type: 'keyword', + }, + 'dhcpv4.client_ip': { + category: 'dhcpv4', + description: 'The current IP address of the client.', + name: 'dhcpv4.client_ip', + type: 'ip', + }, + 'dhcpv4.assigned_ip': { + category: 'dhcpv4', + description: + 'The IP address that the DHCP server is assigning to the client. This field is also known as "your" IP address. ', + name: 'dhcpv4.assigned_ip', + type: 'ip', + }, + 'dhcpv4.server_ip': { + category: 'dhcpv4', + description: + 'The IP address of the DHCP server that the client should use for the next step in the bootstrap process. ', + name: 'dhcpv4.server_ip', + type: 'ip', + }, + 'dhcpv4.relay_ip': { + category: 'dhcpv4', + description: + 'The relay IP address used by the client to contact the server (i.e. a DHCP relay server). ', + name: 'dhcpv4.relay_ip', + type: 'ip', + }, + 'dhcpv4.client_mac': { + category: 'dhcpv4', + description: "The client's MAC address (layer two).", + name: 'dhcpv4.client_mac', + type: 'keyword', + }, + 'dhcpv4.server_name': { + category: 'dhcpv4', + description: + 'The name of the server sending the message. Optional. Used in DHCPOFFER or DHCPACK messages. ', + name: 'dhcpv4.server_name', + type: 'keyword', + }, + 'dhcpv4.op_code': { + category: 'dhcpv4', + description: 'The message op code (bootrequest or bootreply). ', + example: 'bootreply', + name: 'dhcpv4.op_code', + type: 'keyword', + }, + 'dhcpv4.hops': { + category: 'dhcpv4', + description: 'The number of hops the DHCP message went through.', + name: 'dhcpv4.hops', + type: 'long', + }, + 'dhcpv4.hardware_type': { + category: 'dhcpv4', + description: 'The type of hardware used for the local network (Ethernet, LocalTalk, etc). ', + name: 'dhcpv4.hardware_type', + type: 'keyword', + }, + 'dhcpv4.option.message_type': { + category: 'dhcpv4', + description: + 'The specific type of DHCP message being sent (e.g. discover, offer, request, decline, ack, nak, release, inform). ', + example: 'ack', + name: 'dhcpv4.option.message_type', + type: 'keyword', + }, + 'dhcpv4.option.parameter_request_list': { + category: 'dhcpv4', + description: + 'This option is used by a DHCP client to request values for specified configuration parameters. ', + name: 'dhcpv4.option.parameter_request_list', + type: 'keyword', + }, + 'dhcpv4.option.requested_ip_address': { + category: 'dhcpv4', + description: + 'This option is used in a client request (DHCPDISCOVER) to allow the client to request that a particular IP address be assigned. ', + name: 'dhcpv4.option.requested_ip_address', + type: 'ip', + }, + 'dhcpv4.option.server_identifier': { + category: 'dhcpv4', + description: 'IP address of the individual DHCP server which handled this message. ', + name: 'dhcpv4.option.server_identifier', + type: 'ip', + }, + 'dhcpv4.option.broadcast_address': { + category: 'dhcpv4', + description: "This option specifies the broadcast address in use on the client's subnet. ", + name: 'dhcpv4.option.broadcast_address', + type: 'ip', + }, + 'dhcpv4.option.max_dhcp_message_size': { + category: 'dhcpv4', + description: + 'This option specifies the maximum length DHCP message that the client is willing to accept. ', + name: 'dhcpv4.option.max_dhcp_message_size', + type: 'long', + }, + 'dhcpv4.option.class_identifier': { + category: 'dhcpv4', + description: + "This option is used by DHCP clients to optionally identify the vendor type and configuration of a DHCP client. Vendors may choose to define specific vendor class identifiers to convey particular configuration or other identification information about a client. For example, the identifier may encode the client's hardware configuration. ", + name: 'dhcpv4.option.class_identifier', + type: 'keyword', + }, + 'dhcpv4.option.domain_name': { + category: 'dhcpv4', + description: + 'This option specifies the domain name that client should use when resolving hostnames via the Domain Name System. ', + name: 'dhcpv4.option.domain_name', + type: 'keyword', + }, + 'dhcpv4.option.dns_servers': { + category: 'dhcpv4', + description: + 'The domain name server option specifies a list of Domain Name System servers available to the client. ', + name: 'dhcpv4.option.dns_servers', + type: 'ip', + }, + 'dhcpv4.option.vendor_identifying_options': { + category: 'dhcpv4', + description: + 'A DHCP client may use this option to unambiguously identify the vendor that manufactured the hardware on which the client is running, the software in use, or an industry consortium to which the vendor belongs. This field is described in RFC 3925. ', + name: 'dhcpv4.option.vendor_identifying_options', + type: 'object', + }, + 'dhcpv4.option.subnet_mask': { + category: 'dhcpv4', + description: 'The subnet mask that the client should use on the currnet network. ', + name: 'dhcpv4.option.subnet_mask', + type: 'ip', + }, + 'dhcpv4.option.utc_time_offset_sec': { + category: 'dhcpv4', + description: + "The time offset field specifies the offset of the client's subnet in seconds from Coordinated Universal Time (UTC). ", + name: 'dhcpv4.option.utc_time_offset_sec', + type: 'long', + }, + 'dhcpv4.option.router': { + category: 'dhcpv4', + description: + "The router option specifies a list of IP addresses for routers on the client's subnet. ", + name: 'dhcpv4.option.router', + type: 'ip', + }, + 'dhcpv4.option.time_servers': { + category: 'dhcpv4', + description: + 'The time server option specifies a list of RFC 868 time servers available to the client. ', + name: 'dhcpv4.option.time_servers', + type: 'ip', + }, + 'dhcpv4.option.ntp_servers': { + category: 'dhcpv4', + description: + 'This option specifies a list of IP addresses indicating NTP servers available to the client. ', + name: 'dhcpv4.option.ntp_servers', + type: 'ip', + }, + 'dhcpv4.option.hostname': { + category: 'dhcpv4', + description: 'This option specifies the name of the client. ', + name: 'dhcpv4.option.hostname', + type: 'keyword', + }, + 'dhcpv4.option.ip_address_lease_time_sec': { + category: 'dhcpv4', + description: + 'This option is used in a client request (DHCPDISCOVER or DHCPREQUEST) to allow the client to request a lease time for the IP address. In a server reply (DHCPOFFER), a DHCP server uses this option to specify the lease time it is willing to offer. ', + name: 'dhcpv4.option.ip_address_lease_time_sec', + type: 'long', + }, + 'dhcpv4.option.message': { + category: 'dhcpv4', + description: + 'This option is used by a DHCP server to provide an error message to a DHCP client in a DHCPNAK message in the event of a failure. A client may use this option in a DHCPDECLINE message to indicate the why the client declined the offered parameters. ', + name: 'dhcpv4.option.message', + type: 'text', + }, + 'dhcpv4.option.renewal_time_sec': { + category: 'dhcpv4', + description: + 'This option specifies the time interval from address assignment until the client transitions to the RENEWING state. ', + name: 'dhcpv4.option.renewal_time_sec', + type: 'long', + }, + 'dhcpv4.option.rebinding_time_sec': { + category: 'dhcpv4', + description: + 'This option specifies the time interval from address assignment until the client transitions to the REBINDING state. ', + name: 'dhcpv4.option.rebinding_time_sec', + type: 'long', + }, + 'dhcpv4.option.boot_file_name': { + category: 'dhcpv4', + description: + "This option is used to identify a bootfile when the 'file' field in the DHCP header has been used for DHCP options. ", + name: 'dhcpv4.option.boot_file_name', + type: 'keyword', + }, + 'dns.flags.authoritative': { + category: 'dns', + description: + 'A DNS flag specifying that the responding server is an authority for the domain name used in the question. ', + name: 'dns.flags.authoritative', + type: 'boolean', + }, + 'dns.flags.recursion_available': { + category: 'dns', + description: + 'A DNS flag specifying whether recursive query support is available in the name server. ', + name: 'dns.flags.recursion_available', + type: 'boolean', + }, + 'dns.flags.recursion_desired': { + category: 'dns', + description: + 'A DNS flag specifying that the client directs the server to pursue a query recursively. Recursive query support is optional. ', + name: 'dns.flags.recursion_desired', + type: 'boolean', + }, + 'dns.flags.authentic_data': { + category: 'dns', + description: + 'A DNS flag specifying that the recursive server considers the response authentic. ', + name: 'dns.flags.authentic_data', + type: 'boolean', + }, + 'dns.flags.checking_disabled': { + category: 'dns', + description: + 'A DNS flag specifying that the client disables the server signature validation of the query. ', + name: 'dns.flags.checking_disabled', + type: 'boolean', + }, + 'dns.flags.truncated_response': { + category: 'dns', + description: 'A DNS flag specifying that only the first 512 bytes of the reply were returned. ', + name: 'dns.flags.truncated_response', + type: 'boolean', + }, + 'dns.question.etld_plus_one': { + category: 'dns', + description: + 'The effective top-level domain (eTLD) plus one more label. For example, the eTLD+1 for "foo.bar.golang.org." is "golang.org.". The data for determining the eTLD comes from an embedded copy of the data from http://publicsuffix.org.', + example: 'amazon.co.uk.', + name: 'dns.question.etld_plus_one', + }, + 'dns.answers_count': { + category: 'dns', + description: 'The number of resource records contained in the `dns.answers` field. ', + name: 'dns.answers_count', + type: 'long', + }, + 'dns.authorities': { + category: 'dns', + description: 'An array containing a dictionary for each authority section from the answer. ', + name: 'dns.authorities', + type: 'object', + }, + 'dns.authorities_count': { + category: 'dns', + description: + 'The number of resource records contained in the `dns.authorities` field. The `dns.authorities` field may or may not be included depending on the configuration of Packetbeat. ', + name: 'dns.authorities_count', + type: 'long', + }, + 'dns.authorities.name': { + category: 'dns', + description: 'The domain name to which this resource record pertains.', + example: 'example.com.', + name: 'dns.authorities.name', + }, + 'dns.authorities.type': { + category: 'dns', + description: 'The type of data contained in this resource record.', + example: 'NS', + name: 'dns.authorities.type', + }, + 'dns.authorities.class': { + category: 'dns', + description: 'The class of DNS data contained in this resource record.', + example: 'IN', + name: 'dns.authorities.class', + }, + 'dns.additionals': { + category: 'dns', + description: 'An array containing a dictionary for each additional section from the answer. ', + name: 'dns.additionals', + type: 'object', + }, + 'dns.additionals_count': { + category: 'dns', + description: + 'The number of resource records contained in the `dns.additionals` field. The `dns.additionals` field may or may not be included depending on the configuration of Packetbeat. ', + name: 'dns.additionals_count', + type: 'long', + }, + 'dns.additionals.name': { + category: 'dns', + description: 'The domain name to which this resource record pertains.', + example: 'example.com.', + name: 'dns.additionals.name', + }, + 'dns.additionals.type': { + category: 'dns', + description: 'The type of data contained in this resource record.', + example: 'NS', + name: 'dns.additionals.type', + }, + 'dns.additionals.class': { + category: 'dns', + description: 'The class of DNS data contained in this resource record.', + example: 'IN', + name: 'dns.additionals.class', + }, + 'dns.additionals.ttl': { + category: 'dns', + description: + 'The time interval in seconds that this resource record may be cached before it should be discarded. Zero values mean that the data should not be cached. ', + name: 'dns.additionals.ttl', + type: 'long', + }, + 'dns.additionals.data': { + category: 'dns', + description: + 'The data describing the resource. The meaning of this data depends on the type and class of the resource record. ', + name: 'dns.additionals.data', + }, + 'dns.opt.version': { + category: 'dns', + description: 'The EDNS version.', + example: '0', + name: 'dns.opt.version', + }, + 'dns.opt.do': { + category: 'dns', + description: 'If set, the transaction uses DNSSEC.', + name: 'dns.opt.do', + type: 'boolean', + }, + 'dns.opt.ext_rcode': { + category: 'dns', + description: 'Extended response code field.', + example: 'BADVERS', + name: 'dns.opt.ext_rcode', + }, + 'dns.opt.udp_size': { + category: 'dns', + description: "Requestor's UDP payload size (in bytes).", + name: 'dns.opt.udp_size', + type: 'long', + }, + 'http.request.headers': { + category: 'http', + description: + 'A map containing the captured header fields from the request. Which headers to capture is configurable. If headers with the same header name are present in the message, they will be separated by commas. ', + name: 'http.request.headers', + type: 'object', + }, + 'http.request.params': { + category: 'http', + name: 'http.request.params', + type: 'alias', + }, + 'http.response.status_phrase': { + category: 'http', + description: 'The HTTP status phrase.', + example: 'Not Found', + name: 'http.response.status_phrase', + }, + 'http.response.headers': { + category: 'http', + description: + 'A map containing the captured header fields from the response. Which headers to capture is configurable. If headers with the same header name are present in the message, they will be separated by commas. ', + name: 'http.response.headers', + type: 'object', + }, + 'http.response.code': { + category: 'http', + name: 'http.response.code', + type: 'alias', + }, + 'http.response.phrase': { + category: 'http', + name: 'http.response.phrase', + type: 'alias', + }, + 'icmp.version': { + category: 'icmp', + description: 'The version of the ICMP protocol.', + name: 'icmp.version', + }, + 'icmp.request.message': { + category: 'icmp', + description: 'A human readable form of the request.', + name: 'icmp.request.message', + type: 'keyword', + }, + 'icmp.request.type': { + category: 'icmp', + description: 'The request type.', + name: 'icmp.request.type', + type: 'long', + }, + 'icmp.request.code': { + category: 'icmp', + description: 'The request code.', + name: 'icmp.request.code', + type: 'long', + }, + 'icmp.response.message': { + category: 'icmp', + description: 'A human readable form of the response.', + name: 'icmp.response.message', + type: 'keyword', + }, + 'icmp.response.type': { + category: 'icmp', + description: 'The response type.', + name: 'icmp.response.type', + type: 'long', + }, + 'icmp.response.code': { + category: 'icmp', + description: 'The response code.', + name: 'icmp.response.code', + type: 'long', + }, + 'memcache.protocol_type': { + category: 'memcache', + description: + 'The memcache protocol implementation. The value can be "binary" for binary-based, "text" for text-based, or "unknown" for an unknown memcache protocol type. ', + name: 'memcache.protocol_type', + type: 'keyword', + }, + 'memcache.request.line': { + category: 'memcache', + description: 'The raw command line for unknown commands ONLY. ', + name: 'memcache.request.line', + type: 'keyword', + }, + 'memcache.request.command': { + category: 'memcache', + description: + 'The memcache command being requested in the memcache text protocol. For example "set" or "get". The binary protocol opcodes are translated into memcache text protocol commands. ', + name: 'memcache.request.command', + type: 'keyword', + }, + 'memcache.response.command': { + category: 'memcache', + description: + 'Either the text based protocol response message type or the name of the originating request if binary protocol is used. ', + name: 'memcache.response.command', + type: 'keyword', + }, + 'memcache.request.type': { + category: 'memcache', + description: + 'The memcache command classification. This value can be "UNKNOWN", "Load", "Store", "Delete", "Counter", "Info", "SlabCtrl", "LRUCrawler", "Stats", "Success", "Fail", or "Auth". ', + name: 'memcache.request.type', + type: 'keyword', + }, + 'memcache.response.type': { + category: 'memcache', + description: + 'The memcache command classification. This value can be "UNKNOWN", "Load", "Store", "Delete", "Counter", "Info", "SlabCtrl", "LRUCrawler", "Stats", "Success", "Fail", or "Auth". The text based protocol will employ any of these, whereas the binary based protocol will mirror the request commands only (see `memcache.response.status` for binary protocol). ', + name: 'memcache.response.type', + type: 'keyword', + }, + 'memcache.response.error_msg': { + category: 'memcache', + description: 'The optional error message in the memcache response (text based protocol only). ', + name: 'memcache.response.error_msg', + type: 'keyword', + }, + 'memcache.request.opcode': { + category: 'memcache', + description: 'The binary protocol message opcode name. ', + name: 'memcache.request.opcode', + type: 'keyword', + }, + 'memcache.response.opcode': { + category: 'memcache', + description: 'The binary protocol message opcode name. ', + name: 'memcache.response.opcode', + type: 'keyword', + }, + 'memcache.request.opcode_value': { + category: 'memcache', + description: 'The binary protocol message opcode value. ', + name: 'memcache.request.opcode_value', + type: 'long', + }, + 'memcache.response.opcode_value': { + category: 'memcache', + description: 'The binary protocol message opcode value. ', + name: 'memcache.response.opcode_value', + type: 'long', + }, + 'memcache.request.opaque': { + category: 'memcache', + description: + 'The binary protocol opaque header value used for correlating request with response messages. ', + name: 'memcache.request.opaque', + type: 'long', + }, + 'memcache.response.opaque': { + category: 'memcache', + description: + 'The binary protocol opaque header value used for correlating request with response messages. ', + name: 'memcache.response.opaque', + type: 'long', + }, + 'memcache.request.vbucket': { + category: 'memcache', + description: 'The vbucket index sent in the binary message. ', + name: 'memcache.request.vbucket', + type: 'long', + }, + 'memcache.response.status': { + category: 'memcache', + description: 'The textual representation of the response error code (binary protocol only). ', + name: 'memcache.response.status', + type: 'keyword', + }, + 'memcache.response.status_code': { + category: 'memcache', + description: 'The status code value returned in the response (binary protocol only). ', + name: 'memcache.response.status_code', + type: 'long', + }, + 'memcache.request.keys': { + category: 'memcache', + description: 'The list of keys sent in the store or load commands. ', + name: 'memcache.request.keys', + type: 'array', + }, + 'memcache.response.keys': { + category: 'memcache', + description: 'The list of keys returned for the load command (if present). ', + name: 'memcache.response.keys', + type: 'array', + }, + 'memcache.request.count_values': { + category: 'memcache', + description: + 'The number of values found in the memcache request message. If the command does not send any data, this field is missing. ', + name: 'memcache.request.count_values', + type: 'long', + }, + 'memcache.response.count_values': { + category: 'memcache', + description: + 'The number of values found in the memcache response message. If the command does not send any data, this field is missing. ', + name: 'memcache.response.count_values', + type: 'long', + }, + 'memcache.request.values': { + category: 'memcache', + description: 'The list of base64 encoded values sent with the request (if present). ', + name: 'memcache.request.values', + type: 'array', + }, + 'memcache.response.values': { + category: 'memcache', + description: 'The list of base64 encoded values sent with the response (if present). ', + name: 'memcache.response.values', + type: 'array', + }, + 'memcache.request.bytes': { + category: 'memcache', + description: 'The byte count of the values being transferred. ', + name: 'memcache.request.bytes', + type: 'long', + format: 'bytes', + }, + 'memcache.response.bytes': { + category: 'memcache', + description: 'The byte count of the values being transferred. ', + name: 'memcache.response.bytes', + type: 'long', + format: 'bytes', + }, + 'memcache.request.delta': { + category: 'memcache', + description: 'The counter increment/decrement delta value. ', + name: 'memcache.request.delta', + type: 'long', + }, + 'memcache.request.initial': { + category: 'memcache', + description: 'The counter increment/decrement initial value parameter (binary protocol only). ', + name: 'memcache.request.initial', + type: 'long', + }, + 'memcache.request.verbosity': { + category: 'memcache', + description: 'The value of the memcache "verbosity" command. ', + name: 'memcache.request.verbosity', + type: 'long', + }, + 'memcache.request.raw_args': { + category: 'memcache', + description: + 'The text protocol raw arguments for the "stats ..." and "lru crawl ..." commands. ', + name: 'memcache.request.raw_args', + type: 'keyword', + }, + 'memcache.request.source_class': { + category: 'memcache', + description: "The source class id in 'slab reassign' command. ", + name: 'memcache.request.source_class', + type: 'long', + }, + 'memcache.request.dest_class': { + category: 'memcache', + description: "The destination class id in 'slab reassign' command. ", + name: 'memcache.request.dest_class', + type: 'long', + }, + 'memcache.request.automove': { + category: 'memcache', + description: + 'The automove mode in the \'slab automove\' command expressed as a string. This value can be "standby"(=0), "slow"(=1), "aggressive"(=2), or the raw value if the value is unknown. ', + name: 'memcache.request.automove', + type: 'keyword', + }, + 'memcache.request.flags': { + category: 'memcache', + description: 'The memcache command flags sent in the request (if present). ', + name: 'memcache.request.flags', + type: 'long', + }, + 'memcache.response.flags': { + category: 'memcache', + description: 'The memcache message flags sent in the response (if present). ', + name: 'memcache.response.flags', + type: 'long', + }, + 'memcache.request.exptime': { + category: 'memcache', + description: + 'The data expiry time in seconds sent with the memcache command (if present). If the value is <30 days, the expiry time is relative to "now", or else it is an absolute Unix time in seconds (32-bit). ', + name: 'memcache.request.exptime', + type: 'long', + }, + 'memcache.request.sleep_us': { + category: 'memcache', + description: "The sleep setting in microseconds for the 'lru_crawler sleep' command. ", + name: 'memcache.request.sleep_us', + type: 'long', + }, + 'memcache.response.value': { + category: 'memcache', + description: 'The counter value returned by a counter operation. ', + name: 'memcache.response.value', + type: 'long', + }, + 'memcache.request.noreply': { + category: 'memcache', + description: + 'Set to true if noreply was set in the request. The `memcache.response` field will be missing. ', + name: 'memcache.request.noreply', + type: 'boolean', + }, + 'memcache.request.quiet': { + category: 'memcache', + description: 'Set to true if the binary protocol message is to be treated as a quiet message. ', + name: 'memcache.request.quiet', + type: 'boolean', + }, + 'memcache.request.cas_unique': { + category: 'memcache', + description: 'The CAS (compare-and-swap) identifier if present. ', + name: 'memcache.request.cas_unique', + type: 'long', + }, + 'memcache.response.cas_unique': { + category: 'memcache', + description: + 'The CAS (compare-and-swap) identifier to be used with CAS-based updates (if present). ', + name: 'memcache.response.cas_unique', + type: 'long', + }, + 'memcache.response.stats': { + category: 'memcache', + description: + 'The list of statistic values returned. Each entry is a dictionary with the fields "name" and "value". ', + name: 'memcache.response.stats', + type: 'array', + }, + 'memcache.response.version': { + category: 'memcache', + description: 'The returned memcache version string. ', + name: 'memcache.response.version', + type: 'keyword', + }, + 'mongodb.error': { + category: 'mongodb', + description: + 'If the MongoDB request has resulted in an error, this field contains the error message returned by the server. ', + name: 'mongodb.error', + }, + 'mongodb.fullCollectionName': { + category: 'mongodb', + description: + 'The full collection name. The full collection name is the concatenation of the database name with the collection name, using a dot (.) for the concatenation. For example, for the database foo and the collection bar, the full collection name is foo.bar. ', + name: 'mongodb.fullCollectionName', + }, + 'mongodb.numberToSkip': { + category: 'mongodb', + description: + 'Sets the number of documents to omit - starting from the first document in the resulting dataset - when returning the result of the query. ', + name: 'mongodb.numberToSkip', + type: 'long', + }, + 'mongodb.numberToReturn': { + category: 'mongodb', + description: 'The requested maximum number of documents to be returned. ', + name: 'mongodb.numberToReturn', + type: 'long', + }, + 'mongodb.numberReturned': { + category: 'mongodb', + description: 'The number of documents in the reply. ', + name: 'mongodb.numberReturned', + type: 'long', + }, + 'mongodb.startingFrom': { + category: 'mongodb', + description: 'Where in the cursor this reply is starting. ', + name: 'mongodb.startingFrom', + }, + 'mongodb.query': { + category: 'mongodb', + description: + 'A JSON document that represents the query. The query will contain one or more elements, all of which must match for a document to be included in the result set. Possible elements include $query, $orderby, $hint, $explain, and $snapshot. ', + name: 'mongodb.query', + }, + 'mongodb.returnFieldsSelector': { + category: 'mongodb', + description: + 'A JSON document that limits the fields in the returned documents. The returnFieldsSelector contains one or more elements, each of which is the name of a field that should be returned, and the integer value 1. ', + name: 'mongodb.returnFieldsSelector', + }, + 'mongodb.selector': { + category: 'mongodb', + description: + 'A BSON document that specifies the query for selecting the document to update or delete. ', + name: 'mongodb.selector', + }, + 'mongodb.update': { + category: 'mongodb', + description: + 'A BSON document that specifies the update to be performed. For information on specifying updates, see the Update Operations documentation from the MongoDB Manual. ', + name: 'mongodb.update', + }, + 'mongodb.cursorId': { + category: 'mongodb', + description: + 'The cursor identifier returned in the OP_REPLY. This must be the value that was returned from the database. ', + name: 'mongodb.cursorId', + }, + 'mysql.affected_rows': { + category: 'mysql', + description: + 'If the MySQL command is successful, this field contains the affected number of rows of the last statement. ', + name: 'mysql.affected_rows', + type: 'long', + }, + 'mysql.insert_id': { + category: 'mysql', + description: + 'If the INSERT query is successful, this field contains the id of the newly inserted row. ', + name: 'mysql.insert_id', + }, + 'mysql.num_fields': { + category: 'mysql', + description: + 'If the SELECT query is successful, this field is set to the number of fields returned. ', + name: 'mysql.num_fields', + }, + 'mysql.num_rows': { + category: 'mysql', + description: + 'If the SELECT query is successful, this field is set to the number of rows returned. ', + name: 'mysql.num_rows', + }, + 'mysql.query': { + category: 'mysql', + description: "The row mysql query as read from the transaction's request. ", + name: 'mysql.query', + }, + 'mysql.error_code': { + category: 'mysql', + description: 'The error code returned by MySQL. ', + name: 'mysql.error_code', + type: 'long', + }, + 'mysql.error_message': { + category: 'mysql', + description: 'The error info message returned by MySQL. ', + name: 'mysql.error_message', + }, + 'nfs.version': { + category: 'nfs', + description: 'NFS protocol version number.', + name: 'nfs.version', + type: 'long', + }, + 'nfs.minor_version': { + category: 'nfs', + description: 'NFS protocol minor version number.', + name: 'nfs.minor_version', + type: 'long', + }, + 'nfs.tag': { + category: 'nfs', + description: 'NFS v4 COMPOUND operation tag.', + name: 'nfs.tag', + }, + 'nfs.opcode': { + category: 'nfs', + description: 'NFS operation name, or main operation name, in case of COMPOUND calls. ', + name: 'nfs.opcode', + }, + 'nfs.status': { + category: 'nfs', + description: 'NFS operation reply status.', + name: 'nfs.status', + }, + 'rpc.xid': { + category: 'rpc', + description: 'RPC message transaction identifier.', + name: 'rpc.xid', + }, + 'rpc.status': { + category: 'rpc', + description: 'RPC message reply status.', + name: 'rpc.status', + }, + 'rpc.auth_flavor': { + category: 'rpc', + description: 'RPC authentication flavor.', + name: 'rpc.auth_flavor', + }, + 'rpc.cred.uid': { + category: 'rpc', + description: "RPC caller's user id, in case of auth-unix.", + name: 'rpc.cred.uid', + type: 'long', + }, + 'rpc.cred.gid': { + category: 'rpc', + description: "RPC caller's group id, in case of auth-unix.", + name: 'rpc.cred.gid', + type: 'long', + }, + 'rpc.cred.gids': { + category: 'rpc', + description: "RPC caller's secondary group ids, in case of auth-unix.", + name: 'rpc.cred.gids', + }, + 'rpc.cred.stamp': { + category: 'rpc', + description: 'Arbitrary ID which the caller machine may generate.', + name: 'rpc.cred.stamp', + type: 'long', + }, + 'rpc.cred.machinename': { + category: 'rpc', + description: "The name of the caller's machine.", + name: 'rpc.cred.machinename', + }, + 'rpc.call_size': { + category: 'rpc', + description: 'RPC call size with argument.', + name: 'rpc.call_size', + type: 'alias', + }, + 'rpc.reply_size': { + category: 'rpc', + description: 'RPC reply size with argument.', + name: 'rpc.reply_size', + type: 'alias', + }, + 'pgsql.error_code': { + category: 'pgsql', + description: 'The PostgreSQL error code.', + name: 'pgsql.error_code', + type: 'long', + }, + 'pgsql.error_message': { + category: 'pgsql', + description: 'The PostgreSQL error message.', + name: 'pgsql.error_message', + }, + 'pgsql.error_severity': { + category: 'pgsql', + description: 'The PostgreSQL error severity.', + name: 'pgsql.error_severity', + }, + 'pgsql.num_fields': { + category: 'pgsql', + description: + 'If the SELECT query if successful, this field is set to the number of fields returned. ', + name: 'pgsql.num_fields', + }, + 'pgsql.num_rows': { + category: 'pgsql', + description: + 'If the SELECT query if successful, this field is set to the number of rows returned. ', + name: 'pgsql.num_rows', + }, + 'redis.return_value': { + category: 'redis', + description: 'The return value of the Redis command in a human readable format. ', + name: 'redis.return_value', + }, + 'redis.error': { + category: 'redis', + description: + 'If the Redis command has resulted in an error, this field contains the error message returned by the Redis server. ', + name: 'redis.error', + }, + 'thrift.params': { + category: 'thrift', + description: + 'The RPC method call parameters in a human readable format. If the IDL files are available, the parameters use names whenever possible. Otherwise, the IDs from the message are used. ', + name: 'thrift.params', + }, + 'thrift.service': { + category: 'thrift', + description: 'The name of the Thrift-RPC service as defined in the IDL files. ', + name: 'thrift.service', + }, + 'thrift.return_value': { + category: 'thrift', + description: + 'The value returned by the Thrift-RPC call. This is encoded in a human readable format. ', + name: 'thrift.return_value', + }, + 'thrift.exceptions': { + category: 'thrift', + description: + 'If the call resulted in exceptions, this field contains the exceptions in a human readable format. ', + name: 'thrift.exceptions', + }, + 'tls.client.x509.version': { + category: 'tls', + description: 'Version of x509 format.', + example: 3, + name: 'tls.client.x509.version', + type: 'keyword', + }, + 'tls.client.x509.version_number': { + category: 'tls', + description: 'Version of x509 format.', + example: 3, + name: 'tls.client.x509.version_number', + type: 'keyword', + }, + 'tls.client.x509.serial_number': { + category: 'tls', + description: + 'Unique serial number issued by the certificate authority. For consistency, if this value is alphanumeric, it should be formatted without colons and uppercase characters. ', + example: '55FBB9C7DEBF09809D12CCAA', + name: 'tls.client.x509.serial_number', + type: 'keyword', + }, + 'tls.client.x509.issuer.distinguished_name': { + category: 'tls', + description: 'Distinguished name (DN) of issuing certificate authority.', + example: 'C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert SHA2 High Assurance Server CA', + name: 'tls.client.x509.issuer.distinguished_name', + type: 'keyword', + }, + 'tls.client.x509.issuer.common_name': { + category: 'tls', + description: 'List of common name (CN) of issuing certificate authority.', + example: 'DigiCert SHA2 High Assurance Server CA', + name: 'tls.client.x509.issuer.common_name', + type: 'keyword', + }, + 'tls.client.x509.issuer.organizational_unit': { + category: 'tls', + description: 'List of organizational units (OU) of issuing certificate authority.', + example: 'www.digicert.com', + name: 'tls.client.x509.issuer.organizational_unit', + type: 'keyword', + }, + 'tls.client.x509.issuer.organization': { + category: 'tls', + description: 'List of organizations (O) of issuing certificate authority.', + example: 'DigiCert Inc', + name: 'tls.client.x509.issuer.organization', + type: 'keyword', + }, + 'tls.client.x509.issuer.locality': { + category: 'tls', + description: 'List of locality names (L)', + example: 'Mountain View', + name: 'tls.client.x509.issuer.locality', + type: 'keyword', + }, + 'tls.client.x509.issuer.province': { + category: 'tls', + description: 'Province or region within country.', + name: 'tls.client.x509.issuer.province', + type: 'keyword', + }, + 'tls.client.x509.issuer.state_or_province': { + category: 'tls', + description: 'List of state or province names (ST, S, or P)', + example: 'California', + name: 'tls.client.x509.issuer.state_or_province', + type: 'keyword', + }, + 'tls.client.x509.issuer.country': { + category: 'tls', + description: 'List of country (C) codes', + example: 'US', + name: 'tls.client.x509.issuer.country', + type: 'keyword', + }, + 'tls.client.x509.signature_algorithm': { + category: 'tls', + description: + 'Identifier for certificate signature algorithm. Recommend using names found in Go Lang Crypto library (See https://github.com/golang/go/blob/go1.14/src/crypto/x509/x509.go#L337-L353).', + example: 'SHA256-RSA', + name: 'tls.client.x509.signature_algorithm', + type: 'keyword', + }, + 'tls.client.x509.not_before': { + category: 'tls', + description: 'Time at which the certificate is first considered valid.', + example: '"2019-08-16T01:40:25.000Z"', + name: 'tls.client.x509.not_before', + type: 'date', + }, + 'tls.client.x509.not_after': { + category: 'tls', + description: 'Time at which the certificate is no longer considered valid.', + example: '"2020-07-16T03:15:39.000Z"', + name: 'tls.client.x509.not_after', + type: 'date', + }, + 'tls.client.x509.subject.distinguished_name': { + category: 'tls', + description: 'Distinguished name (DN) of the certificate subject entity.', + example: 'C=US, ST=California, L=San Francisco, O=Fastly, Inc., CN=r2.shared.global.fastly.net', + name: 'tls.client.x509.subject.distinguished_name', + type: 'keyword', + }, + 'tls.client.x509.subject.common_name': { + category: 'tls', + description: 'List of common names (CN) of subject.', + example: 'r2.shared.global.fastly.net', + name: 'tls.client.x509.subject.common_name', + type: 'keyword', + }, + 'tls.client.x509.subject.organizational_unit': { + category: 'tls', + description: 'List of organizational units (OU) of subject.', + name: 'tls.client.x509.subject.organizational_unit', + type: 'keyword', + }, + 'tls.client.x509.subject.organization': { + category: 'tls', + description: 'List of organizations (O) of subject.', + example: 'Fastly, Inc.', + name: 'tls.client.x509.subject.organization', + type: 'keyword', + }, + 'tls.client.x509.subject.locality': { + category: 'tls', + description: 'List of locality names (L)', + example: 'San Francisco', + name: 'tls.client.x509.subject.locality', + type: 'keyword', + }, + 'tls.client.x509.subject.province': { + category: 'tls', + description: 'Province or region within country.', + name: 'tls.client.x509.subject.province', + type: 'keyword', + }, + 'tls.client.x509.subject.state_or_province': { + category: 'tls', + description: 'List of state or province names (ST, S, or P)', + example: 'California', + name: 'tls.client.x509.subject.state_or_province', + type: 'keyword', + }, + 'tls.client.x509.subject.country': { + category: 'tls', + description: 'List of country (C) code', + example: 'US', + name: 'tls.client.x509.subject.country', + type: 'keyword', + }, + 'tls.client.x509.public_key_algorithm': { + category: 'tls', + description: 'Algorithm used to generate the public key.', + example: 'RSA', + name: 'tls.client.x509.public_key_algorithm', + type: 'keyword', + }, + 'tls.client.x509.public_key_size': { + category: 'tls', + description: 'The size of the public key space in bits.', + example: 2048, + name: 'tls.client.x509.public_key_size', + type: 'long', + }, + 'tls.client.x509.alternative_names': { + category: 'tls', + description: + 'List of subject alternative names (SAN). Name types vary by certificate authority and certificate type but commonly contain IP addresses, DNS names (and wildcards), and email addresses.', + example: '*.elastic.co', + name: 'tls.client.x509.alternative_names', + type: 'keyword', + }, + 'tls.server.x509.version': { + category: 'tls', + description: 'Version of x509 format.', + example: 3, + name: 'tls.server.x509.version', + type: 'keyword', + }, + 'tls.server.x509.version_number': { + category: 'tls', + description: 'Version of x509 format.', + example: 3, + name: 'tls.server.x509.version_number', + type: 'keyword', + }, + 'tls.server.x509.serial_number': { + category: 'tls', + description: + 'Unique serial number issued by the certificate authority. For consistency, if this value is alphanumeric, it should be formatted without colons and uppercase characters. ', + example: '55FBB9C7DEBF09809D12CCAA', + name: 'tls.server.x509.serial_number', + type: 'keyword', + }, + 'tls.server.x509.issuer.distinguished_name': { + category: 'tls', + description: 'Distinguished name (DN) of issuing certificate authority.', + example: 'C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert SHA2 High Assurance Server CA', + name: 'tls.server.x509.issuer.distinguished_name', + type: 'keyword', + }, + 'tls.server.x509.issuer.common_name': { + category: 'tls', + description: 'List of common name (CN) of issuing certificate authority.', + example: 'DigiCert SHA2 High Assurance Server CA', + name: 'tls.server.x509.issuer.common_name', + type: 'keyword', + }, + 'tls.server.x509.issuer.organizational_unit': { + category: 'tls', + description: 'List of organizational units (OU) of issuing certificate authority.', + example: 'www.digicert.com', + name: 'tls.server.x509.issuer.organizational_unit', + type: 'keyword', + }, + 'tls.server.x509.issuer.organization': { + category: 'tls', + description: 'List of organizations (O) of issuing certificate authority.', + example: 'DigiCert Inc', + name: 'tls.server.x509.issuer.organization', + type: 'keyword', + }, + 'tls.server.x509.issuer.locality': { + category: 'tls', + description: 'List of locality names (L)', + example: 'Mountain View', + name: 'tls.server.x509.issuer.locality', + type: 'keyword', + }, + 'tls.server.x509.issuer.province': { + category: 'tls', + description: 'Province or region within country.', + name: 'tls.server.x509.issuer.province', + type: 'keyword', + }, + 'tls.server.x509.issuer.state_or_province': { + category: 'tls', + description: 'List of state or province names (ST, S, or P)', + example: 'California', + name: 'tls.server.x509.issuer.state_or_province', + type: 'keyword', + }, + 'tls.server.x509.issuer.country': { + category: 'tls', + description: 'List of country (C) codes', + example: 'US', + name: 'tls.server.x509.issuer.country', + type: 'keyword', + }, + 'tls.server.x509.signature_algorithm': { + category: 'tls', + description: + 'Identifier for certificate signature algorithm. Recommend using names found in Go Lang Crypto library (See https://github.com/golang/go/blob/go1.14/src/crypto/x509/x509.go#L337-L353).', + example: 'SHA256-RSA', + name: 'tls.server.x509.signature_algorithm', + type: 'keyword', + }, + 'tls.server.x509.not_before': { + category: 'tls', + description: 'Time at which the certificate is first considered valid.', + example: '"2019-08-16T01:40:25.000Z"', + name: 'tls.server.x509.not_before', + type: 'date', + }, + 'tls.server.x509.not_after': { + category: 'tls', + description: 'Time at which the certificate is no longer considered valid.', + example: '"2020-07-16T03:15:39.000Z"', + name: 'tls.server.x509.not_after', + type: 'date', + }, + 'tls.server.x509.subject.distinguished_name': { + category: 'tls', + description: 'Distinguished name (DN) of the certificate subject entity.', + example: 'C=US, ST=California, L=San Francisco, O=Fastly, Inc., CN=r2.shared.global.fastly.net', + name: 'tls.server.x509.subject.distinguished_name', + type: 'keyword', + }, + 'tls.server.x509.subject.common_name': { + category: 'tls', + description: 'List of common names (CN) of subject.', + example: 'r2.shared.global.fastly.net', + name: 'tls.server.x509.subject.common_name', + type: 'keyword', + }, + 'tls.server.x509.subject.organizational_unit': { + category: 'tls', + description: 'List of organizational units (OU) of subject.', + name: 'tls.server.x509.subject.organizational_unit', + type: 'keyword', + }, + 'tls.server.x509.subject.organization': { + category: 'tls', + description: 'List of organizations (O) of subject.', + example: 'Fastly, Inc.', + name: 'tls.server.x509.subject.organization', + type: 'keyword', + }, + 'tls.server.x509.subject.locality': { + category: 'tls', + description: 'List of locality names (L)', + example: 'San Francisco', + name: 'tls.server.x509.subject.locality', + type: 'keyword', + }, + 'tls.server.x509.subject.province': { + category: 'tls', + description: 'Province or region within country.', + name: 'tls.server.x509.subject.province', + type: 'keyword', + }, + 'tls.server.x509.subject.state_or_province': { + category: 'tls', + description: 'List of state or province names (ST, S, or P)', + example: 'California', + name: 'tls.server.x509.subject.state_or_province', + type: 'keyword', + }, + 'tls.server.x509.subject.country': { + category: 'tls', + description: 'List of country (C) code', + example: 'US', + name: 'tls.server.x509.subject.country', + type: 'keyword', + }, + 'tls.server.x509.public_key_algorithm': { + category: 'tls', + description: 'Algorithm used to generate the public key.', + example: 'RSA', + name: 'tls.server.x509.public_key_algorithm', + type: 'keyword', + }, + 'tls.server.x509.public_key_size': { + category: 'tls', + description: 'The size of the public key space in bits.', + example: 2048, + name: 'tls.server.x509.public_key_size', + type: 'long', + }, + 'tls.server.x509.alternative_names': { + category: 'tls', + description: + 'List of subject alternative names (SAN). Name types vary by certificate authority and certificate type but commonly contain IP addresses, DNS names (and wildcards), and email addresses.', + example: '*.elastic.co', + name: 'tls.server.x509.alternative_names', + type: 'keyword', + }, + 'tls.detailed.version': { + category: 'tls', + description: 'The version of the TLS protocol used. ', + example: 'TLS 1.3', + name: 'tls.detailed.version', + type: 'keyword', + }, + 'tls.detailed.resumption_method': { + category: 'tls', + description: + 'If the session has been resumed, the underlying method used. One of "id" for TLS session ID or "ticket" for TLS ticket extension. ', + name: 'tls.detailed.resumption_method', + type: 'keyword', + }, + 'tls.detailed.client_certificate_requested': { + category: 'tls', + description: + 'Whether the server has requested the client to authenticate itself using a client certificate. ', + name: 'tls.detailed.client_certificate_requested', + type: 'boolean', + }, + 'tls.detailed.client_hello.version': { + category: 'tls', + description: + 'The version of the TLS protocol by which the client wishes to communicate during this session. ', + name: 'tls.detailed.client_hello.version', + type: 'keyword', + }, + 'tls.detailed.client_hello.session_id': { + category: 'tls', + description: + 'Unique number to identify the session for the corresponding connection with the client. ', + name: 'tls.detailed.client_hello.session_id', + type: 'keyword', + }, + 'tls.detailed.client_hello.supported_compression_methods': { + category: 'tls', + description: + 'The list of compression methods the client supports. See https://www.iana.org/assignments/comp-meth-ids/comp-meth-ids.xhtml ', + name: 'tls.detailed.client_hello.supported_compression_methods', + type: 'keyword', + }, + 'tls.detailed.client_hello.extensions.server_name_indication': { + category: 'tls', + description: 'List of hostnames', + name: 'tls.detailed.client_hello.extensions.server_name_indication', + type: 'keyword', + }, + 'tls.detailed.client_hello.extensions.application_layer_protocol_negotiation': { + category: 'tls', + description: 'List of application-layer protocols the client is willing to use. ', + name: 'tls.detailed.client_hello.extensions.application_layer_protocol_negotiation', + type: 'keyword', + }, + 'tls.detailed.client_hello.extensions.session_ticket': { + category: 'tls', + description: + 'Length of the session ticket, if provided, or an empty string to advertise support for tickets. ', + name: 'tls.detailed.client_hello.extensions.session_ticket', + type: 'keyword', + }, + 'tls.detailed.client_hello.extensions.supported_versions': { + category: 'tls', + description: 'List of TLS versions that the client is willing to use. ', + name: 'tls.detailed.client_hello.extensions.supported_versions', + type: 'keyword', + }, + 'tls.detailed.client_hello.extensions.supported_groups': { + category: 'tls', + description: 'List of Elliptic Curve Cryptography (ECC) curve groups supported by the client. ', + name: 'tls.detailed.client_hello.extensions.supported_groups', + type: 'keyword', + }, + 'tls.detailed.client_hello.extensions.signature_algorithms': { + category: 'tls', + description: 'List of signature algorithms that may be use in digital signatures. ', + name: 'tls.detailed.client_hello.extensions.signature_algorithms', + type: 'keyword', + }, + 'tls.detailed.client_hello.extensions.ec_points_formats': { + category: 'tls', + description: + 'List of Elliptic Curve (EC) point formats. Indicates the set of point formats that the client can parse. ', + name: 'tls.detailed.client_hello.extensions.ec_points_formats', + type: 'keyword', + }, + 'tls.detailed.client_hello.extensions._unparsed_': { + category: 'tls', + description: 'List of extensions that were left unparsed by Packetbeat. ', + name: 'tls.detailed.client_hello.extensions._unparsed_', + type: 'keyword', + }, + 'tls.detailed.server_hello.version': { + category: 'tls', + description: + 'The version of the TLS protocol that is used for this session. It is the highest version supported by the server not exceeding the version requested in the client hello. ', + name: 'tls.detailed.server_hello.version', + type: 'keyword', + }, + 'tls.detailed.server_hello.selected_compression_method': { + category: 'tls', + description: + 'The compression method selected by the server from the list provided in the client hello. ', + name: 'tls.detailed.server_hello.selected_compression_method', + type: 'keyword', + }, + 'tls.detailed.server_hello.session_id': { + category: 'tls', + description: + 'Unique number to identify the session for the corresponding connection with the client. ', + name: 'tls.detailed.server_hello.session_id', + type: 'keyword', + }, + 'tls.detailed.server_hello.extensions.application_layer_protocol_negotiation': { + category: 'tls', + description: 'Negotiated application layer protocol', + name: 'tls.detailed.server_hello.extensions.application_layer_protocol_negotiation', + type: 'keyword', + }, + 'tls.detailed.server_hello.extensions.session_ticket': { + category: 'tls', + description: + 'Used to announce that a session ticket will be provided by the server. Always an empty string. ', + name: 'tls.detailed.server_hello.extensions.session_ticket', + type: 'keyword', + }, + 'tls.detailed.server_hello.extensions.supported_versions': { + category: 'tls', + description: 'Negotiated TLS version to be used. ', + name: 'tls.detailed.server_hello.extensions.supported_versions', + type: 'keyword', + }, + 'tls.detailed.server_hello.extensions.ec_points_formats': { + category: 'tls', + description: + 'List of Elliptic Curve (EC) point formats. Indicates the set of point formats that the server can parse. ', + name: 'tls.detailed.server_hello.extensions.ec_points_formats', + type: 'keyword', + }, + 'tls.detailed.server_hello.extensions._unparsed_': { + category: 'tls', + description: 'List of extensions that were left unparsed by Packetbeat. ', + name: 'tls.detailed.server_hello.extensions._unparsed_', + type: 'keyword', + }, + 'tls.detailed.client_certificate.version': { + category: 'tls', + description: 'X509 format version.', + name: 'tls.detailed.client_certificate.version', + type: 'long', + }, + 'tls.detailed.client_certificate.version_number': { + category: 'tls', + description: 'Version of x509 format.', + example: 3, + name: 'tls.detailed.client_certificate.version_number', + type: 'keyword', + }, + 'tls.detailed.client_certificate.serial_number': { + category: 'tls', + description: "The certificate's serial number.", + name: 'tls.detailed.client_certificate.serial_number', + type: 'keyword', + }, + 'tls.detailed.client_certificate.not_before': { + category: 'tls', + description: 'Date before which the certificate is not valid.', + name: 'tls.detailed.client_certificate.not_before', + type: 'date', + }, + 'tls.detailed.client_certificate.not_after': { + category: 'tls', + description: 'Date after which the certificate expires.', + name: 'tls.detailed.client_certificate.not_after', + type: 'date', + }, + 'tls.detailed.client_certificate.public_key_algorithm': { + category: 'tls', + description: "The algorithm used for this certificate's public key. One of RSA, DSA or ECDSA. ", + name: 'tls.detailed.client_certificate.public_key_algorithm', + type: 'keyword', + }, + 'tls.detailed.client_certificate.public_key_size': { + category: 'tls', + description: 'Size of the public key.', + name: 'tls.detailed.client_certificate.public_key_size', + type: 'long', + }, + 'tls.detailed.client_certificate.signature_algorithm': { + category: 'tls', + description: "The algorithm used for the certificate's signature. ", + name: 'tls.detailed.client_certificate.signature_algorithm', + type: 'keyword', + }, + 'tls.detailed.client_certificate.alternative_names': { + category: 'tls', + description: 'Subject Alternative Names for this certificate.', + name: 'tls.detailed.client_certificate.alternative_names', + type: 'keyword', + }, + 'tls.detailed.client_certificate.subject.country': { + category: 'tls', + description: 'Country code.', + name: 'tls.detailed.client_certificate.subject.country', + type: 'keyword', + }, + 'tls.detailed.client_certificate.subject.organization': { + category: 'tls', + description: 'Organization name.', + name: 'tls.detailed.client_certificate.subject.organization', + type: 'keyword', + }, + 'tls.detailed.client_certificate.subject.organizational_unit': { + category: 'tls', + description: 'Unit within organization.', + name: 'tls.detailed.client_certificate.subject.organizational_unit', + type: 'keyword', + }, + 'tls.detailed.client_certificate.subject.province': { + category: 'tls', + description: 'Province or region within country.', + name: 'tls.detailed.client_certificate.subject.province', + type: 'keyword', + }, + 'tls.detailed.client_certificate.subject.common_name': { + category: 'tls', + description: 'Name or host name identified by the certificate.', + name: 'tls.detailed.client_certificate.subject.common_name', + type: 'keyword', + }, + 'tls.detailed.client_certificate.subject.locality': { + category: 'tls', + description: 'Locality.', + name: 'tls.detailed.client_certificate.subject.locality', + type: 'keyword', + }, + 'tls.detailed.client_certificate.subject.distinguished_name': { + category: 'tls', + description: 'Distinguished name (DN) of the certificate subject entity.', + example: 'C=US, ST=California, L=San Francisco, O=Fastly, Inc., CN=r2.shared.global.fastly.net', + name: 'tls.detailed.client_certificate.subject.distinguished_name', + type: 'keyword', + }, + 'tls.detailed.client_certificate.issuer.country': { + category: 'tls', + description: 'Country code.', + name: 'tls.detailed.client_certificate.issuer.country', + type: 'keyword', + }, + 'tls.detailed.client_certificate.issuer.organization': { + category: 'tls', + description: 'Organization name.', + name: 'tls.detailed.client_certificate.issuer.organization', + type: 'keyword', + }, + 'tls.detailed.client_certificate.issuer.organizational_unit': { + category: 'tls', + description: 'Unit within organization.', + name: 'tls.detailed.client_certificate.issuer.organizational_unit', + type: 'keyword', + }, + 'tls.detailed.client_certificate.issuer.province': { + category: 'tls', + description: 'Province or region within country.', + name: 'tls.detailed.client_certificate.issuer.province', + type: 'keyword', + }, + 'tls.detailed.client_certificate.issuer.common_name': { + category: 'tls', + description: 'Name or host name identified by the certificate.', + name: 'tls.detailed.client_certificate.issuer.common_name', + type: 'keyword', + }, + 'tls.detailed.client_certificate.issuer.locality': { + category: 'tls', + description: 'Locality.', + name: 'tls.detailed.client_certificate.issuer.locality', + type: 'keyword', + }, + 'tls.detailed.client_certificate.issuer.distinguished_name': { + category: 'tls', + description: 'Distinguished name (DN) of the certificate issuer entity.', + example: 'C=US, ST=California, L=San Francisco, O=Fastly, Inc., CN=r2.shared.global.fastly.net', + name: 'tls.detailed.client_certificate.issuer.distinguished_name', + type: 'keyword', + }, + 'tls.detailed.server_certificate.version': { + category: 'tls', + description: 'X509 format version.', + name: 'tls.detailed.server_certificate.version', + type: 'long', + }, + 'tls.detailed.server_certificate.version_number': { + category: 'tls', + description: 'Version of x509 format.', + example: 3, + name: 'tls.detailed.server_certificate.version_number', + type: 'keyword', + }, + 'tls.detailed.server_certificate.serial_number': { + category: 'tls', + description: "The certificate's serial number.", + name: 'tls.detailed.server_certificate.serial_number', + type: 'keyword', + }, + 'tls.detailed.server_certificate.not_before': { + category: 'tls', + description: 'Date before which the certificate is not valid.', + name: 'tls.detailed.server_certificate.not_before', + type: 'date', + }, + 'tls.detailed.server_certificate.not_after': { + category: 'tls', + description: 'Date after which the certificate expires.', + name: 'tls.detailed.server_certificate.not_after', + type: 'date', + }, + 'tls.detailed.server_certificate.public_key_algorithm': { + category: 'tls', + description: "The algorithm used for this certificate's public key. One of RSA, DSA or ECDSA. ", + name: 'tls.detailed.server_certificate.public_key_algorithm', + type: 'keyword', + }, + 'tls.detailed.server_certificate.public_key_size': { + category: 'tls', + description: 'Size of the public key.', + name: 'tls.detailed.server_certificate.public_key_size', + type: 'long', + }, + 'tls.detailed.server_certificate.signature_algorithm': { + category: 'tls', + description: "The algorithm used for the certificate's signature. ", + name: 'tls.detailed.server_certificate.signature_algorithm', + type: 'keyword', + }, + 'tls.detailed.server_certificate.alternative_names': { + category: 'tls', + description: 'Subject Alternative Names for this certificate.', + name: 'tls.detailed.server_certificate.alternative_names', + type: 'keyword', + }, + 'tls.detailed.server_certificate.subject.country': { + category: 'tls', + description: 'Country code.', + name: 'tls.detailed.server_certificate.subject.country', + type: 'keyword', + }, + 'tls.detailed.server_certificate.subject.organization': { + category: 'tls', + description: 'Organization name.', + name: 'tls.detailed.server_certificate.subject.organization', + type: 'keyword', + }, + 'tls.detailed.server_certificate.subject.organizational_unit': { + category: 'tls', + description: 'Unit within organization.', + name: 'tls.detailed.server_certificate.subject.organizational_unit', + type: 'keyword', + }, + 'tls.detailed.server_certificate.subject.province': { + category: 'tls', + description: 'Province or region within country.', + name: 'tls.detailed.server_certificate.subject.province', + type: 'keyword', + }, + 'tls.detailed.server_certificate.subject.state_or_province': { + category: 'tls', + description: 'Province or region within country.', + name: 'tls.detailed.server_certificate.subject.state_or_province', + type: 'keyword', + }, + 'tls.detailed.server_certificate.subject.common_name': { + category: 'tls', + description: 'Name or host name identified by the certificate.', + name: 'tls.detailed.server_certificate.subject.common_name', + type: 'keyword', + }, + 'tls.detailed.server_certificate.subject.locality': { + category: 'tls', + description: 'Locality.', + name: 'tls.detailed.server_certificate.subject.locality', + type: 'keyword', + }, + 'tls.detailed.server_certificate.subject.distinguished_name': { + category: 'tls', + description: 'Distinguished name (DN) of the certificate subject entity.', + example: 'C=US, ST=California, L=San Francisco, O=Fastly, Inc., CN=r2.shared.global.fastly.net', + name: 'tls.detailed.server_certificate.subject.distinguished_name', + type: 'keyword', + }, + 'tls.detailed.server_certificate.issuer.country': { + category: 'tls', + description: 'Country code.', + name: 'tls.detailed.server_certificate.issuer.country', + type: 'keyword', + }, + 'tls.detailed.server_certificate.issuer.organization': { + category: 'tls', + description: 'Organization name.', + name: 'tls.detailed.server_certificate.issuer.organization', + type: 'keyword', + }, + 'tls.detailed.server_certificate.issuer.organizational_unit': { + category: 'tls', + description: 'Unit within organization.', + name: 'tls.detailed.server_certificate.issuer.organizational_unit', + type: 'keyword', + }, + 'tls.detailed.server_certificate.issuer.province': { + category: 'tls', + description: 'Province or region within country.', + name: 'tls.detailed.server_certificate.issuer.province', + type: 'keyword', + }, + 'tls.detailed.server_certificate.issuer.state_or_province': { + category: 'tls', + description: 'Province or region within country.', + name: 'tls.detailed.server_certificate.issuer.state_or_province', + type: 'keyword', + }, + 'tls.detailed.server_certificate.issuer.common_name': { + category: 'tls', + description: 'Name or host name identified by the certificate.', + name: 'tls.detailed.server_certificate.issuer.common_name', + type: 'keyword', + }, + 'tls.detailed.server_certificate.issuer.locality': { + category: 'tls', + description: 'Locality.', + name: 'tls.detailed.server_certificate.issuer.locality', + type: 'keyword', + }, + 'tls.detailed.server_certificate.issuer.distinguished_name': { + category: 'tls', + description: 'Distinguished name (DN) of the certificate issuer entity.', + example: 'C=US, ST=California, L=San Francisco, O=Fastly, Inc., CN=r2.shared.global.fastly.net', + name: 'tls.detailed.server_certificate.issuer.distinguished_name', + type: 'keyword', + }, + 'tls.detailed.server_certificate_chain': { + category: 'tls', + description: 'Chain of trust for the server certificate.', + name: 'tls.detailed.server_certificate_chain', + type: 'array', + }, + 'tls.detailed.client_certificate_chain': { + category: 'tls', + description: 'Chain of trust for the client certificate.', + name: 'tls.detailed.client_certificate_chain', + type: 'array', + }, + 'tls.detailed.alert_types': { + category: 'tls', + description: 'An array containing the TLS alert type for every alert received. ', + name: 'tls.detailed.alert_types', + type: 'keyword', + }, + 'tls.handshake_completed': { + category: 'tls', + name: 'tls.handshake_completed', + type: 'alias', + }, + 'tls.client_hello.supported_ciphers': { + category: 'tls', + name: 'tls.client_hello.supported_ciphers', + type: 'alias', + }, + 'tls.server_hello.selected_cipher': { + category: 'tls', + name: 'tls.server_hello.selected_cipher', + type: 'alias', + }, + 'tls.fingerprints.ja3': { + category: 'tls', + name: 'tls.fingerprints.ja3', + type: 'alias', + }, + 'tls.resumption_method': { + category: 'tls', + name: 'tls.resumption_method', + type: 'alias', + }, + 'tls.client_certificate_requested': { + category: 'tls', + name: 'tls.client_certificate_requested', + type: 'alias', + }, + 'tls.client_hello.version': { + category: 'tls', + name: 'tls.client_hello.version', + type: 'alias', + }, + 'tls.client_hello.session_id': { + category: 'tls', + name: 'tls.client_hello.session_id', + type: 'alias', + }, + 'tls.client_hello.supported_compression_methods': { + category: 'tls', + name: 'tls.client_hello.supported_compression_methods', + type: 'alias', + }, + 'tls.client_hello.extensions.server_name_indication': { + category: 'tls', + name: 'tls.client_hello.extensions.server_name_indication', + type: 'alias', + }, + 'tls.client_hello.extensions.application_layer_protocol_negotiation': { + category: 'tls', + name: 'tls.client_hello.extensions.application_layer_protocol_negotiation', + type: 'alias', + }, + 'tls.client_hello.extensions.session_ticket': { + category: 'tls', + name: 'tls.client_hello.extensions.session_ticket', + type: 'alias', + }, + 'tls.client_hello.extensions.supported_versions': { + category: 'tls', + name: 'tls.client_hello.extensions.supported_versions', + type: 'alias', + }, + 'tls.client_hello.extensions.supported_groups': { + category: 'tls', + name: 'tls.client_hello.extensions.supported_groups', + type: 'alias', + }, + 'tls.client_hello.extensions.signature_algorithms': { + category: 'tls', + name: 'tls.client_hello.extensions.signature_algorithms', + type: 'alias', + }, + 'tls.client_hello.extensions.ec_points_formats': { + category: 'tls', + name: 'tls.client_hello.extensions.ec_points_formats', + type: 'alias', + }, + 'tls.client_hello.extensions._unparsed_': { + category: 'tls', + name: 'tls.client_hello.extensions._unparsed_', + type: 'alias', + }, + 'tls.server_hello.version': { + category: 'tls', + name: 'tls.server_hello.version', + type: 'alias', + }, + 'tls.server_hello.selected_compression_method': { + category: 'tls', + name: 'tls.server_hello.selected_compression_method', + type: 'alias', + }, + 'tls.server_hello.session_id': { + category: 'tls', + name: 'tls.server_hello.session_id', + type: 'alias', + }, + 'tls.server_hello.extensions.application_layer_protocol_negotiation': { + category: 'tls', + name: 'tls.server_hello.extensions.application_layer_protocol_negotiation', + type: 'alias', + }, + 'tls.server_hello.extensions.session_ticket': { + category: 'tls', + name: 'tls.server_hello.extensions.session_ticket', + type: 'alias', + }, + 'tls.server_hello.extensions.supported_versions': { + category: 'tls', + name: 'tls.server_hello.extensions.supported_versions', + type: 'alias', + }, + 'tls.server_hello.extensions.ec_points_formats': { + category: 'tls', + name: 'tls.server_hello.extensions.ec_points_formats', + type: 'alias', + }, + 'tls.server_hello.extensions._unparsed_': { + category: 'tls', + name: 'tls.server_hello.extensions._unparsed_', + type: 'alias', + }, + 'tls.client_certificate.version': { + category: 'tls', + name: 'tls.client_certificate.version', + type: 'alias', + }, + 'tls.client_certificate.serial_number': { + category: 'tls', + name: 'tls.client_certificate.serial_number', + type: 'alias', + }, + 'tls.client_certificate.not_before': { + category: 'tls', + name: 'tls.client_certificate.not_before', + type: 'alias', + }, + 'tls.client_certificate.not_after': { + category: 'tls', + name: 'tls.client_certificate.not_after', + type: 'alias', + }, + 'tls.client_certificate.public_key_algorithm': { + category: 'tls', + name: 'tls.client_certificate.public_key_algorithm', + type: 'alias', + }, + 'tls.client_certificate.public_key_size': { + category: 'tls', + name: 'tls.client_certificate.public_key_size', + type: 'alias', + }, + 'tls.client_certificate.signature_algorithm': { + category: 'tls', + name: 'tls.client_certificate.signature_algorithm', + type: 'alias', + }, + 'tls.client_certificate.alternative_names': { + category: 'tls', + name: 'tls.client_certificate.alternative_names', + type: 'alias', + }, + 'tls.client_certificate.subject.country': { + category: 'tls', + name: 'tls.client_certificate.subject.country', + type: 'alias', + }, + 'tls.client_certificate.subject.organization': { + category: 'tls', + name: 'tls.client_certificate.subject.organization', + type: 'alias', + }, + 'tls.client_certificate.subject.organizational_unit': { + category: 'tls', + name: 'tls.client_certificate.subject.organizational_unit', + type: 'alias', + }, + 'tls.client_certificate.subject.province': { + category: 'tls', + name: 'tls.client_certificate.subject.province', + type: 'alias', + }, + 'tls.client_certificate.subject.common_name': { + category: 'tls', + name: 'tls.client_certificate.subject.common_name', + type: 'alias', + }, + 'tls.client_certificate.subject.locality': { + category: 'tls', + name: 'tls.client_certificate.subject.locality', + type: 'alias', + }, + 'tls.client_certificate.issuer.country': { + category: 'tls', + name: 'tls.client_certificate.issuer.country', + type: 'alias', + }, + 'tls.client_certificate.issuer.organization': { + category: 'tls', + name: 'tls.client_certificate.issuer.organization', + type: 'alias', + }, + 'tls.client_certificate.issuer.organizational_unit': { + category: 'tls', + name: 'tls.client_certificate.issuer.organizational_unit', + type: 'alias', + }, + 'tls.client_certificate.issuer.province': { + category: 'tls', + name: 'tls.client_certificate.issuer.province', + type: 'alias', + }, + 'tls.client_certificate.issuer.common_name': { + category: 'tls', + name: 'tls.client_certificate.issuer.common_name', + type: 'alias', + }, + 'tls.client_certificate.issuer.locality': { + category: 'tls', + name: 'tls.client_certificate.issuer.locality', + type: 'alias', + }, + 'tls.server_certificate.version': { + category: 'tls', + name: 'tls.server_certificate.version', + type: 'alias', + }, + 'tls.server_certificate.serial_number': { + category: 'tls', + name: 'tls.server_certificate.serial_number', + type: 'alias', + }, + 'tls.server_certificate.not_before': { + category: 'tls', + name: 'tls.server_certificate.not_before', + type: 'alias', + }, + 'tls.server_certificate.not_after': { + category: 'tls', + name: 'tls.server_certificate.not_after', + type: 'alias', + }, + 'tls.server_certificate.public_key_algorithm': { + category: 'tls', + name: 'tls.server_certificate.public_key_algorithm', + type: 'alias', + }, + 'tls.server_certificate.public_key_size': { + category: 'tls', + name: 'tls.server_certificate.public_key_size', + type: 'alias', + }, + 'tls.server_certificate.signature_algorithm': { + category: 'tls', + name: 'tls.server_certificate.signature_algorithm', + type: 'alias', + }, + 'tls.server_certificate.alternative_names': { + category: 'tls', + name: 'tls.server_certificate.alternative_names', + type: 'alias', + }, + 'tls.server_certificate.subject.country': { + category: 'tls', + name: 'tls.server_certificate.subject.country', + type: 'alias', + }, + 'tls.server_certificate.subject.organization': { + category: 'tls', + name: 'tls.server_certificate.subject.organization', + type: 'alias', + }, + 'tls.server_certificate.subject.organizational_unit': { + category: 'tls', + name: 'tls.server_certificate.subject.organizational_unit', + type: 'alias', + }, + 'tls.server_certificate.subject.province': { + category: 'tls', + name: 'tls.server_certificate.subject.province', + type: 'alias', + }, + 'tls.server_certificate.subject.common_name': { + category: 'tls', + name: 'tls.server_certificate.subject.common_name', + type: 'alias', + }, + 'tls.server_certificate.subject.locality': { + category: 'tls', + name: 'tls.server_certificate.subject.locality', + type: 'alias', + }, + 'tls.server_certificate.issuer.country': { + category: 'tls', + name: 'tls.server_certificate.issuer.country', + type: 'alias', + }, + 'tls.server_certificate.issuer.organization': { + category: 'tls', + name: 'tls.server_certificate.issuer.organization', + type: 'alias', + }, + 'tls.server_certificate.issuer.organizational_unit': { + category: 'tls', + name: 'tls.server_certificate.issuer.organizational_unit', + type: 'alias', + }, + 'tls.server_certificate.issuer.province': { + category: 'tls', + name: 'tls.server_certificate.issuer.province', + type: 'alias', + }, + 'tls.server_certificate.issuer.common_name': { + category: 'tls', + name: 'tls.server_certificate.issuer.common_name', + type: 'alias', + }, + 'tls.server_certificate.issuer.locality': { + category: 'tls', + name: 'tls.server_certificate.issuer.locality', + type: 'alias', + }, + 'tls.alert_types': { + category: 'tls', + name: 'tls.alert_types', + type: 'alias', + }, + 'winlog.api': { + category: 'winlog', + description: + 'The event log API type used to read the record. The possible values are "wineventlog" for the Windows Event Log API or "eventlogging" for the Event Logging API. The Event Logging API was designed for Windows Server 2003 or Windows 2000 operating systems. In Windows Vista, the event logging infrastructure was redesigned. On Windows Vista or later operating systems, the Windows Event Log API is used. Winlogbeat automatically detects which API to use for reading event logs. ', + name: 'winlog.api', + }, + 'winlog.activity_id': { + category: 'winlog', + description: + 'A globally unique identifier that identifies the current activity. The events that are published with this identifier are part of the same activity. ', + name: 'winlog.activity_id', + type: 'keyword', + }, + 'winlog.computer_name': { + category: 'winlog', + description: + 'The name of the computer that generated the record. When using Windows event forwarding, this name can differ from `agent.hostname`. ', + name: 'winlog.computer_name', + type: 'keyword', + }, + 'winlog.event_data': { + category: 'winlog', + description: + 'The event-specific data. This field is mutually exclusive with `user_data`. If you are capturing event data on versions prior to Windows Vista, the parameters in `event_data` are named `param1`, `param2`, and so on, because event log parameters are unnamed in earlier versions of Windows. ', + name: 'winlog.event_data', + type: 'object', + }, + 'winlog.event_data.AuthenticationPackageName': { + category: 'winlog', + name: 'winlog.event_data.AuthenticationPackageName', + type: 'keyword', + }, + 'winlog.event_data.Binary': { + category: 'winlog', + name: 'winlog.event_data.Binary', + type: 'keyword', + }, + 'winlog.event_data.BitlockerUserInputTime': { + category: 'winlog', + name: 'winlog.event_data.BitlockerUserInputTime', + type: 'keyword', + }, + 'winlog.event_data.BootMode': { + category: 'winlog', + name: 'winlog.event_data.BootMode', + type: 'keyword', + }, + 'winlog.event_data.BootType': { + category: 'winlog', + name: 'winlog.event_data.BootType', + type: 'keyword', + }, + 'winlog.event_data.BuildVersion': { + category: 'winlog', + name: 'winlog.event_data.BuildVersion', + type: 'keyword', + }, + 'winlog.event_data.Company': { + category: 'winlog', + name: 'winlog.event_data.Company', + type: 'keyword', + }, + 'winlog.event_data.CorruptionActionState': { + category: 'winlog', + name: 'winlog.event_data.CorruptionActionState', + type: 'keyword', + }, + 'winlog.event_data.CreationUtcTime': { + category: 'winlog', + name: 'winlog.event_data.CreationUtcTime', + type: 'keyword', + }, + 'winlog.event_data.Description': { + category: 'winlog', + name: 'winlog.event_data.Description', + type: 'keyword', + }, + 'winlog.event_data.Detail': { + category: 'winlog', + name: 'winlog.event_data.Detail', + type: 'keyword', + }, + 'winlog.event_data.DeviceName': { + category: 'winlog', + name: 'winlog.event_data.DeviceName', + type: 'keyword', + }, + 'winlog.event_data.DeviceNameLength': { + category: 'winlog', + name: 'winlog.event_data.DeviceNameLength', + type: 'keyword', + }, + 'winlog.event_data.DeviceTime': { + category: 'winlog', + name: 'winlog.event_data.DeviceTime', + type: 'keyword', + }, + 'winlog.event_data.DeviceVersionMajor': { + category: 'winlog', + name: 'winlog.event_data.DeviceVersionMajor', + type: 'keyword', + }, + 'winlog.event_data.DeviceVersionMinor': { + category: 'winlog', + name: 'winlog.event_data.DeviceVersionMinor', + type: 'keyword', + }, + 'winlog.event_data.DriveName': { + category: 'winlog', + name: 'winlog.event_data.DriveName', + type: 'keyword', + }, + 'winlog.event_data.DriverName': { + category: 'winlog', + name: 'winlog.event_data.DriverName', + type: 'keyword', + }, + 'winlog.event_data.DriverNameLength': { + category: 'winlog', + name: 'winlog.event_data.DriverNameLength', + type: 'keyword', + }, + 'winlog.event_data.DwordVal': { + category: 'winlog', + name: 'winlog.event_data.DwordVal', + type: 'keyword', + }, + 'winlog.event_data.EntryCount': { + category: 'winlog', + name: 'winlog.event_data.EntryCount', + type: 'keyword', + }, + 'winlog.event_data.ExtraInfo': { + category: 'winlog', + name: 'winlog.event_data.ExtraInfo', + type: 'keyword', + }, + 'winlog.event_data.FailureName': { + category: 'winlog', + name: 'winlog.event_data.FailureName', + type: 'keyword', + }, + 'winlog.event_data.FailureNameLength': { + category: 'winlog', + name: 'winlog.event_data.FailureNameLength', + type: 'keyword', + }, + 'winlog.event_data.FileVersion': { + category: 'winlog', + name: 'winlog.event_data.FileVersion', + type: 'keyword', + }, + 'winlog.event_data.FinalStatus': { + category: 'winlog', + name: 'winlog.event_data.FinalStatus', + type: 'keyword', + }, + 'winlog.event_data.Group': { + category: 'winlog', + name: 'winlog.event_data.Group', + type: 'keyword', + }, + 'winlog.event_data.IdleImplementation': { + category: 'winlog', + name: 'winlog.event_data.IdleImplementation', + type: 'keyword', + }, + 'winlog.event_data.IdleStateCount': { + category: 'winlog', + name: 'winlog.event_data.IdleStateCount', + type: 'keyword', + }, + 'winlog.event_data.ImpersonationLevel': { + category: 'winlog', + name: 'winlog.event_data.ImpersonationLevel', + type: 'keyword', + }, + 'winlog.event_data.IntegrityLevel': { + category: 'winlog', + name: 'winlog.event_data.IntegrityLevel', + type: 'keyword', + }, + 'winlog.event_data.IpAddress': { + category: 'winlog', + name: 'winlog.event_data.IpAddress', + type: 'keyword', + }, + 'winlog.event_data.IpPort': { + category: 'winlog', + name: 'winlog.event_data.IpPort', + type: 'keyword', + }, + 'winlog.event_data.KeyLength': { + category: 'winlog', + name: 'winlog.event_data.KeyLength', + type: 'keyword', + }, + 'winlog.event_data.LastBootGood': { + category: 'winlog', + name: 'winlog.event_data.LastBootGood', + type: 'keyword', + }, + 'winlog.event_data.LastShutdownGood': { + category: 'winlog', + name: 'winlog.event_data.LastShutdownGood', + type: 'keyword', + }, + 'winlog.event_data.LmPackageName': { + category: 'winlog', + name: 'winlog.event_data.LmPackageName', + type: 'keyword', + }, + 'winlog.event_data.LogonGuid': { + category: 'winlog', + name: 'winlog.event_data.LogonGuid', + type: 'keyword', + }, + 'winlog.event_data.LogonId': { + category: 'winlog', + name: 'winlog.event_data.LogonId', + type: 'keyword', + }, + 'winlog.event_data.LogonProcessName': { + category: 'winlog', + name: 'winlog.event_data.LogonProcessName', + type: 'keyword', + }, + 'winlog.event_data.LogonType': { + category: 'winlog', + name: 'winlog.event_data.LogonType', + type: 'keyword', + }, + 'winlog.event_data.MajorVersion': { + category: 'winlog', + name: 'winlog.event_data.MajorVersion', + type: 'keyword', + }, + 'winlog.event_data.MaximumPerformancePercent': { + category: 'winlog', + name: 'winlog.event_data.MaximumPerformancePercent', + type: 'keyword', + }, + 'winlog.event_data.MemberName': { + category: 'winlog', + name: 'winlog.event_data.MemberName', + type: 'keyword', + }, + 'winlog.event_data.MemberSid': { + category: 'winlog', + name: 'winlog.event_data.MemberSid', + type: 'keyword', + }, + 'winlog.event_data.MinimumPerformancePercent': { + category: 'winlog', + name: 'winlog.event_data.MinimumPerformancePercent', + type: 'keyword', + }, + 'winlog.event_data.MinimumThrottlePercent': { + category: 'winlog', + name: 'winlog.event_data.MinimumThrottlePercent', + type: 'keyword', + }, + 'winlog.event_data.MinorVersion': { + category: 'winlog', + name: 'winlog.event_data.MinorVersion', + type: 'keyword', + }, + 'winlog.event_data.NewProcessId': { + category: 'winlog', + name: 'winlog.event_data.NewProcessId', + type: 'keyword', + }, + 'winlog.event_data.NewProcessName': { + category: 'winlog', + name: 'winlog.event_data.NewProcessName', + type: 'keyword', + }, + 'winlog.event_data.NewSchemeGuid': { + category: 'winlog', + name: 'winlog.event_data.NewSchemeGuid', + type: 'keyword', + }, + 'winlog.event_data.NewTime': { + category: 'winlog', + name: 'winlog.event_data.NewTime', + type: 'keyword', + }, + 'winlog.event_data.NominalFrequency': { + category: 'winlog', + name: 'winlog.event_data.NominalFrequency', + type: 'keyword', + }, + 'winlog.event_data.Number': { + category: 'winlog', + name: 'winlog.event_data.Number', + type: 'keyword', + }, + 'winlog.event_data.OldSchemeGuid': { + category: 'winlog', + name: 'winlog.event_data.OldSchemeGuid', + type: 'keyword', + }, + 'winlog.event_data.OldTime': { + category: 'winlog', + name: 'winlog.event_data.OldTime', + type: 'keyword', + }, + 'winlog.event_data.OriginalFileName': { + category: 'winlog', + name: 'winlog.event_data.OriginalFileName', + type: 'keyword', + }, + 'winlog.event_data.Path': { + category: 'winlog', + name: 'winlog.event_data.Path', + type: 'keyword', + }, + 'winlog.event_data.PerformanceImplementation': { + category: 'winlog', + name: 'winlog.event_data.PerformanceImplementation', + type: 'keyword', + }, + 'winlog.event_data.PreviousCreationUtcTime': { + category: 'winlog', + name: 'winlog.event_data.PreviousCreationUtcTime', + type: 'keyword', + }, + 'winlog.event_data.PreviousTime': { + category: 'winlog', + name: 'winlog.event_data.PreviousTime', + type: 'keyword', + }, + 'winlog.event_data.PrivilegeList': { + category: 'winlog', + name: 'winlog.event_data.PrivilegeList', + type: 'keyword', + }, + 'winlog.event_data.ProcessId': { + category: 'winlog', + name: 'winlog.event_data.ProcessId', + type: 'keyword', + }, + 'winlog.event_data.ProcessName': { + category: 'winlog', + name: 'winlog.event_data.ProcessName', + type: 'keyword', + }, + 'winlog.event_data.ProcessPath': { + category: 'winlog', + name: 'winlog.event_data.ProcessPath', + type: 'keyword', + }, + 'winlog.event_data.ProcessPid': { + category: 'winlog', + name: 'winlog.event_data.ProcessPid', + type: 'keyword', + }, + 'winlog.event_data.Product': { + category: 'winlog', + name: 'winlog.event_data.Product', + type: 'keyword', + }, + 'winlog.event_data.PuaCount': { + category: 'winlog', + name: 'winlog.event_data.PuaCount', + type: 'keyword', + }, + 'winlog.event_data.PuaPolicyId': { + category: 'winlog', + name: 'winlog.event_data.PuaPolicyId', + type: 'keyword', + }, + 'winlog.event_data.QfeVersion': { + category: 'winlog', + name: 'winlog.event_data.QfeVersion', + type: 'keyword', + }, + 'winlog.event_data.Reason': { + category: 'winlog', + name: 'winlog.event_data.Reason', + type: 'keyword', + }, + 'winlog.event_data.SchemaVersion': { + category: 'winlog', + name: 'winlog.event_data.SchemaVersion', + type: 'keyword', + }, + 'winlog.event_data.ScriptBlockText': { + category: 'winlog', + name: 'winlog.event_data.ScriptBlockText', + type: 'keyword', + }, + 'winlog.event_data.ServiceName': { + category: 'winlog', + name: 'winlog.event_data.ServiceName', + type: 'keyword', + }, + 'winlog.event_data.ServiceVersion': { + category: 'winlog', + name: 'winlog.event_data.ServiceVersion', + type: 'keyword', + }, + 'winlog.event_data.ShutdownActionType': { + category: 'winlog', + name: 'winlog.event_data.ShutdownActionType', + type: 'keyword', + }, + 'winlog.event_data.ShutdownEventCode': { + category: 'winlog', + name: 'winlog.event_data.ShutdownEventCode', + type: 'keyword', + }, + 'winlog.event_data.ShutdownReason': { + category: 'winlog', + name: 'winlog.event_data.ShutdownReason', + type: 'keyword', + }, + 'winlog.event_data.Signature': { + category: 'winlog', + name: 'winlog.event_data.Signature', + type: 'keyword', + }, + 'winlog.event_data.SignatureStatus': { + category: 'winlog', + name: 'winlog.event_data.SignatureStatus', + type: 'keyword', + }, + 'winlog.event_data.Signed': { + category: 'winlog', + name: 'winlog.event_data.Signed', + type: 'keyword', + }, + 'winlog.event_data.StartTime': { + category: 'winlog', + name: 'winlog.event_data.StartTime', + type: 'keyword', + }, + 'winlog.event_data.State': { + category: 'winlog', + name: 'winlog.event_data.State', + type: 'keyword', + }, + 'winlog.event_data.Status': { + category: 'winlog', + name: 'winlog.event_data.Status', + type: 'keyword', + }, + 'winlog.event_data.StopTime': { + category: 'winlog', + name: 'winlog.event_data.StopTime', + type: 'keyword', + }, + 'winlog.event_data.SubjectDomainName': { + category: 'winlog', + name: 'winlog.event_data.SubjectDomainName', + type: 'keyword', + }, + 'winlog.event_data.SubjectLogonId': { + category: 'winlog', + name: 'winlog.event_data.SubjectLogonId', + type: 'keyword', + }, + 'winlog.event_data.SubjectUserName': { + category: 'winlog', + name: 'winlog.event_data.SubjectUserName', + type: 'keyword', + }, + 'winlog.event_data.SubjectUserSid': { + category: 'winlog', + name: 'winlog.event_data.SubjectUserSid', + type: 'keyword', + }, + 'winlog.event_data.TSId': { + category: 'winlog', + name: 'winlog.event_data.TSId', + type: 'keyword', + }, + 'winlog.event_data.TargetDomainName': { + category: 'winlog', + name: 'winlog.event_data.TargetDomainName', + type: 'keyword', + }, + 'winlog.event_data.TargetInfo': { + category: 'winlog', + name: 'winlog.event_data.TargetInfo', + type: 'keyword', + }, + 'winlog.event_data.TargetLogonGuid': { + category: 'winlog', + name: 'winlog.event_data.TargetLogonGuid', + type: 'keyword', + }, + 'winlog.event_data.TargetLogonId': { + category: 'winlog', + name: 'winlog.event_data.TargetLogonId', + type: 'keyword', + }, + 'winlog.event_data.TargetServerName': { + category: 'winlog', + name: 'winlog.event_data.TargetServerName', + type: 'keyword', + }, + 'winlog.event_data.TargetUserName': { + category: 'winlog', + name: 'winlog.event_data.TargetUserName', + type: 'keyword', + }, + 'winlog.event_data.TargetUserSid': { + category: 'winlog', + name: 'winlog.event_data.TargetUserSid', + type: 'keyword', + }, + 'winlog.event_data.TerminalSessionId': { + category: 'winlog', + name: 'winlog.event_data.TerminalSessionId', + type: 'keyword', + }, + 'winlog.event_data.TokenElevationType': { + category: 'winlog', + name: 'winlog.event_data.TokenElevationType', + type: 'keyword', + }, + 'winlog.event_data.TransmittedServices': { + category: 'winlog', + name: 'winlog.event_data.TransmittedServices', + type: 'keyword', + }, + 'winlog.event_data.UserSid': { + category: 'winlog', + name: 'winlog.event_data.UserSid', + type: 'keyword', + }, + 'winlog.event_data.Version': { + category: 'winlog', + name: 'winlog.event_data.Version', + type: 'keyword', + }, + 'winlog.event_data.Workstation': { + category: 'winlog', + name: 'winlog.event_data.Workstation', + type: 'keyword', + }, + 'winlog.event_data.param1': { + category: 'winlog', + name: 'winlog.event_data.param1', + type: 'keyword', + }, + 'winlog.event_data.param2': { + category: 'winlog', + name: 'winlog.event_data.param2', + type: 'keyword', + }, + 'winlog.event_data.param3': { + category: 'winlog', + name: 'winlog.event_data.param3', + type: 'keyword', + }, + 'winlog.event_data.param4': { + category: 'winlog', + name: 'winlog.event_data.param4', + type: 'keyword', + }, + 'winlog.event_data.param5': { + category: 'winlog', + name: 'winlog.event_data.param5', + type: 'keyword', + }, + 'winlog.event_data.param6': { + category: 'winlog', + name: 'winlog.event_data.param6', + type: 'keyword', + }, + 'winlog.event_data.param7': { + category: 'winlog', + name: 'winlog.event_data.param7', + type: 'keyword', + }, + 'winlog.event_data.param8': { + category: 'winlog', + name: 'winlog.event_data.param8', + type: 'keyword', + }, + 'winlog.event_id': { + category: 'winlog', + description: 'The event identifier. The value is specific to the source of the event. ', + name: 'winlog.event_id', + type: 'keyword', + }, + 'winlog.keywords': { + category: 'winlog', + description: 'The keywords are used to classify an event. ', + name: 'winlog.keywords', + type: 'keyword', + }, + 'winlog.channel': { + category: 'winlog', + description: + 'The name of the channel from which this record was read. This value is one of the names from the `event_logs` collection in the configuration. ', + name: 'winlog.channel', + type: 'keyword', + }, + 'winlog.record_id': { + category: 'winlog', + description: + 'The record ID of the event log record. The first record written to an event log is record number 1, and other records are numbered sequentially. If the record number reaches the maximum value (2^32^ for the Event Logging API and 2^64^ for the Windows Event Log API), the next record number will be 0. ', + name: 'winlog.record_id', + type: 'keyword', + }, + 'winlog.related_activity_id': { + category: 'winlog', + description: + 'A globally unique identifier that identifies the activity to which control was transferred to. The related events would then have this identifier as their `activity_id` identifier. ', + name: 'winlog.related_activity_id', + type: 'keyword', + }, + 'winlog.opcode': { + category: 'winlog', + description: + 'The opcode defined in the event. Task and opcode are typically used to identify the location in the application from where the event was logged. ', + name: 'winlog.opcode', + type: 'keyword', + }, + 'winlog.provider_guid': { + category: 'winlog', + description: + 'A globally unique identifier that identifies the provider that logged the event. ', + name: 'winlog.provider_guid', + type: 'keyword', + }, + 'winlog.process.pid': { + category: 'winlog', + description: 'The process_id of the Client Server Runtime Process. ', + name: 'winlog.process.pid', + type: 'long', + }, + 'winlog.provider_name': { + category: 'winlog', + description: + 'The source of the event log record (the application or service that logged the record). ', + name: 'winlog.provider_name', + type: 'keyword', + }, + 'winlog.task': { + category: 'winlog', + description: + 'The task defined in the event. Task and opcode are typically used to identify the location in the application from where the event was logged. The category used by the Event Logging API (on pre Windows Vista operating systems) is written to this field. ', + name: 'winlog.task', + type: 'keyword', + }, + 'winlog.process.thread.id': { + category: 'winlog', + name: 'winlog.process.thread.id', + type: 'long', + }, + 'winlog.user_data': { + category: 'winlog', + description: 'The event specific data. This field is mutually exclusive with `event_data`. ', + name: 'winlog.user_data', + type: 'object', + }, + 'winlog.user.identifier': { + category: 'winlog', + description: + 'The Windows security identifier (SID) of the account associated with this event. If Winlogbeat cannot resolve the SID to a name, then the `user.name`, `user.domain`, and `user.type` fields will be omitted from the event. If you discover Winlogbeat not resolving SIDs, review the log for clues as to what the problem may be. ', + example: 'S-1-5-21-3541430928-2051711210-1391384369-1001', + name: 'winlog.user.identifier', + type: 'keyword', + }, + 'winlog.user.name': { + category: 'winlog', + description: 'Name of the user associated with this event. ', + name: 'winlog.user.name', + type: 'keyword', + }, + 'winlog.user.domain': { + category: 'winlog', + description: 'The domain that the account associated with this event is a member of. ', + name: 'winlog.user.domain', + type: 'keyword', + }, + 'winlog.user.type': { + category: 'winlog', + description: 'The type of account associated with this event. ', + name: 'winlog.user.type', + type: 'keyword', + }, + 'winlog.version': { + category: 'winlog', + description: "The version number of the event's definition.", + name: 'winlog.version', + type: 'long', + }, + activity_id: { + category: 'base', + name: 'activity_id', + type: 'alias', + }, + computer_name: { + category: 'base', + name: 'computer_name', + type: 'alias', + }, + event_id: { + category: 'base', + name: 'event_id', + type: 'alias', + }, + keywords: { + category: 'base', + name: 'keywords', + type: 'alias', + }, + log_name: { + category: 'base', + name: 'log_name', + type: 'alias', + }, + message_error: { + category: 'base', + name: 'message_error', + type: 'alias', + }, + record_number: { + category: 'base', + name: 'record_number', + type: 'alias', + }, + related_activity_id: { + category: 'base', + name: 'related_activity_id', + type: 'alias', + }, + opcode: { + category: 'base', + name: 'opcode', + type: 'alias', + }, + provider_guid: { + category: 'base', + name: 'provider_guid', + type: 'alias', + }, + process_id: { + category: 'base', + name: 'process_id', + type: 'alias', + }, + source_name: { + category: 'base', + name: 'source_name', + type: 'alias', + }, + task: { + category: 'base', + name: 'task', + type: 'alias', + }, + thread_id: { + category: 'base', + name: 'thread_id', + type: 'alias', + }, + 'user.identifier': { + category: 'user', + name: 'user.identifier', + type: 'alias', + }, + 'user.type': { + category: 'user', + name: 'user.type', + type: 'alias', + }, + version: { + category: 'base', + name: 'version', + type: 'alias', + }, + xml: { + category: 'base', + name: 'xml', + type: 'alias', + }, + 'powershell.id': { + category: 'powershell', + description: 'Shell Id.', + example: 'Microsoft Powershell', + name: 'powershell.id', + type: 'keyword', + }, + 'powershell.pipeline_id': { + category: 'powershell', + description: 'Pipeline id.', + example: '1', + name: 'powershell.pipeline_id', + type: 'keyword', + }, + 'powershell.runspace_id': { + category: 'powershell', + description: 'Runspace id.', + example: '4fa9074d-45ab-4e53-9195-e91981ac2bbb', + name: 'powershell.runspace_id', + type: 'keyword', + }, + 'powershell.sequence': { + category: 'powershell', + description: 'Sequence number of the powershell execution.', + example: 1, + name: 'powershell.sequence', + type: 'long', + }, + 'powershell.total': { + category: 'powershell', + description: 'Total number of messages in the sequence.', + example: 10, + name: 'powershell.total', + type: 'long', + }, + 'powershell.command.path': { + category: 'powershell', + description: 'Path of the executed command.', + example: 'C:\\Windows\\system32\\cmd.exe', + name: 'powershell.command.path', + type: 'keyword', + }, + 'powershell.command.name': { + category: 'powershell', + description: 'Name of the executed command.', + example: 'cmd.exe', + name: 'powershell.command.name', + type: 'keyword', + }, + 'powershell.command.type': { + category: 'powershell', + description: 'Type of the executed command.', + example: 'Application', + name: 'powershell.command.type', + type: 'keyword', + }, + 'powershell.command.value': { + category: 'powershell', + description: 'The invoked command.', + example: 'Import-LocalizedData LocalizedData -filename ArchiveResources', + name: 'powershell.command.value', + type: 'text', + }, + 'powershell.command.invocation_details': { + category: 'powershell', + description: 'An array of objects containing detailed information of the executed command. ', + name: 'powershell.command.invocation_details', + type: 'array', + }, + 'powershell.command.invocation_details.type': { + category: 'powershell', + description: 'The type of detail.', + example: 'CommandInvocation', + name: 'powershell.command.invocation_details.type', + type: 'keyword', + }, + 'powershell.command.invocation_details.related_command': { + category: 'powershell', + description: 'The command to which the detail is related to.', + example: 'Add-Type', + name: 'powershell.command.invocation_details.related_command', + type: 'keyword', + }, + 'powershell.command.invocation_details.name': { + category: 'powershell', + description: 'Only used for ParameterBinding detail type. Indicates the parameter name. ', + example: 'AssemblyName', + name: 'powershell.command.invocation_details.name', + type: 'keyword', + }, + 'powershell.command.invocation_details.value': { + category: 'powershell', + description: 'The value of the detail. The meaning of it will depend on the detail type. ', + example: 'System.IO.Compression.FileSystem', + name: 'powershell.command.invocation_details.value', + type: 'text', + }, + 'powershell.connected_user.domain': { + category: 'powershell', + description: 'User domain.', + example: 'VAGRANT', + name: 'powershell.connected_user.domain', + type: 'keyword', + }, + 'powershell.connected_user.name': { + category: 'powershell', + description: 'User name.', + example: 'vagrant', + name: 'powershell.connected_user.name', + type: 'keyword', + }, + 'powershell.engine.version': { + category: 'powershell', + description: 'Version of the PowerShell engine version used to execute the command.', + example: '5.1.17763.1007', + name: 'powershell.engine.version', + type: 'keyword', + }, + 'powershell.engine.previous_state': { + category: 'powershell', + description: 'Previous state of the PowerShell engine. ', + example: 'Available', + name: 'powershell.engine.previous_state', + type: 'keyword', + }, + 'powershell.engine.new_state': { + category: 'powershell', + description: 'New state of the PowerShell engine. ', + example: 'Stopped', + name: 'powershell.engine.new_state', + type: 'keyword', + }, + 'powershell.file.script_block_id': { + category: 'powershell', + description: 'Id of the executed script block.', + example: '50d2dbda-7361-4926-a94d-d9eadfdb43fa', + name: 'powershell.file.script_block_id', + type: 'keyword', + }, + 'powershell.file.script_block_text': { + category: 'powershell', + description: 'Text of the executed script block. ', + example: '.\\a_script.ps1', + name: 'powershell.file.script_block_text', + type: 'text', + }, + 'powershell.process.executable_version': { + category: 'powershell', + description: 'Version of the engine hosting process executable.', + example: '5.1.17763.1007', + name: 'powershell.process.executable_version', + type: 'keyword', + }, + 'powershell.provider.new_state': { + category: 'powershell', + description: 'New state of the PowerShell provider. ', + example: 'Active', + name: 'powershell.provider.new_state', + type: 'keyword', + }, + 'powershell.provider.name': { + category: 'powershell', + description: 'Provider name. ', + example: 'Variable', + name: 'powershell.provider.name', + type: 'keyword', + }, + 'winlog.logon.type': { + category: 'winlog', + description: + 'Logon type name. This is the descriptive version of the `winlog.event_data.LogonType` ordinal. This is an enrichment added by the Security module. ', + example: 'RemoteInteractive', + name: 'winlog.logon.type', + type: 'keyword', + }, + 'winlog.logon.id': { + category: 'winlog', + description: + 'Logon ID that can be used to associate this logon with other events related to the same logon session. ', + name: 'winlog.logon.id', + type: 'keyword', + }, + 'winlog.logon.failure.reason': { + category: 'winlog', + description: 'The reason the logon failed. ', + name: 'winlog.logon.failure.reason', + type: 'keyword', + }, + 'winlog.logon.failure.status': { + category: 'winlog', + description: + 'The reason the logon failed. This is textual description based on the value of the hexadecimal `Status` field. ', + name: 'winlog.logon.failure.status', + type: 'keyword', + }, + 'winlog.logon.failure.sub_status': { + category: 'winlog', + description: + 'Additional information about the logon failure. This is a textual description based on the value of the hexidecimal `SubStatus` field. ', + name: 'winlog.logon.failure.sub_status', + type: 'keyword', + }, + 'sysmon.dns.status': { + category: 'sysmon', + description: 'Windows status code returned for the DNS query.', + name: 'sysmon.dns.status', + type: 'keyword', + }, + 'sysmon.file.archived': { + category: 'sysmon', + description: 'Indicates if the deleted file was archived.', + name: 'sysmon.file.archived', + type: 'boolean', + }, + 'sysmon.file.is_executable': { + category: 'sysmon', + description: 'Indicates if the deleted file was an executable.', + name: 'sysmon.file.is_executable', + type: 'boolean', + }, +}; diff --git a/x-pack/plugins/timelines/server/utils/build_query.ts b/x-pack/plugins/timelines/server/utils/build_query.ts new file mode 100644 index 0000000000000..bc7c48a538664 --- /dev/null +++ b/x-pack/plugins/timelines/server/utils/build_query.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 { isEmpty, isString } from 'lodash/fp'; + +import { ESQuery } from '../../common/typed_json'; + +export const createQueryFilterClauses = (filterQuery: ESQuery | string | undefined) => + !isEmpty(filterQuery) ? [isString(filterQuery) ? JSON.parse(filterQuery) : filterQuery] : []; + +export const inspectStringifyObject = (obj: unknown) => { + try { + return JSON.stringify(obj, null, 2); + } catch { + return 'Sorry about that, something went wrong.'; + } +}; diff --git a/x-pack/plugins/timelines/server/utils/filters.ts b/x-pack/plugins/timelines/server/utils/filters.ts new file mode 100644 index 0000000000000..166c70400d5b2 --- /dev/null +++ b/x-pack/plugins/timelines/server/utils/filters.ts @@ -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. + */ + +import { isEmpty, isString } from 'lodash/fp'; +import { ESQuery } from '../../common/typed_json'; + +export const createQueryFilterClauses = (filterQuery: ESQuery | string | undefined) => + !isEmpty(filterQuery) ? [isString(filterQuery) ? JSON.parse(filterQuery) : filterQuery] : []; diff --git a/x-pack/plugins/timelines/tsconfig.json b/x-pack/plugins/timelines/tsconfig.json index 67e606e798c03..1bc60a696fcef 100644 --- a/x-pack/plugins/timelines/tsconfig.json +++ b/x-pack/plugins/timelines/tsconfig.json @@ -1,19 +1,29 @@ + { - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "composite": true, - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": [ - // add all the folders contains files to be compiled - "common/**/*", - "public/**/*", - "server/**/*" - ], - "references": [ - { "path": "../../../src/core/tsconfig.json" }, - ] -} + "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", + "public/**/*.json", + "../../../typings/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json" }, + { "path": "../data_enhanced/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + { "path": "../licensing/tsconfig.json" }, + { "path": "../spaces/tsconfig.json" } + ] + } diff --git a/x-pack/plugins/transform/public/app/app.tsx b/x-pack/plugins/transform/public/app/app.tsx index d4936783a0297..9219f29e4d9f0 100644 --- a/x-pack/plugins/transform/public/app/app.tsx +++ b/x-pack/plugins/transform/public/app/app.tsx @@ -10,7 +10,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { Router, Route, Switch } from 'react-router-dom'; import { ScopedHistory } from 'kibana/public'; -import { EuiErrorBoundary } from '@elastic/eui'; +import { EuiErrorBoundary, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -35,7 +35,7 @@ export const App: FC<{ history: ScopedHistory }> = ({ history }) => { title={ <FormattedMessage id="xpack.transform.app.checkingPrivilegesErrorMessage" - defaultMessage="Error fetching user privileges from the server." + defaultMessage="Error fetching user privileges from the server" /> } error={apiError} @@ -44,21 +44,23 @@ export const App: FC<{ history: ScopedHistory }> = ({ history }) => { } return ( - <div data-test-subj="transformApp"> - <Router history={history}> - <Switch> - <Route - path={`/${SECTION_SLUG.CLONE_TRANSFORM}/:transformId`} - component={CloneTransformSection} - /> - <Route - path={`/${SECTION_SLUG.CREATE_TRANSFORM}/:savedObjectId`} - component={CreateTransformSection} - /> - <Route path={`/`} component={TransformManagementSection} /> - </Switch> - </Router> - </div> + <EuiFlexGroup justifyContent="spaceAround" data-test-subj="transformApp"> + <EuiFlexItem grow={true}> + <Router history={history}> + <Switch> + <Route + path={`/${SECTION_SLUG.CLONE_TRANSFORM}/:transformId`} + component={CloneTransformSection} + /> + <Route + path={`/${SECTION_SLUG.CREATE_TRANSFORM}/:savedObjectId`} + component={CreateTransformSection} + /> + <Route path={`/`} component={TransformManagementSection} /> + </Switch> + </Router> + </EuiFlexItem> + </EuiFlexGroup> ); }; diff --git a/x-pack/plugins/transform/public/app/components/section_error.tsx b/x-pack/plugins/transform/public/app/components/section_error.tsx index 2af0c19fb8817..964c13d775d4b 100644 --- a/x-pack/plugins/transform/public/app/components/section_error.tsx +++ b/x-pack/plugins/transform/public/app/components/section_error.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiCallOut } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiPageContent } from '@elastic/eui'; import React from 'react'; interface Props { @@ -23,9 +23,17 @@ export const SectionError: React.FunctionComponent<Props> = ({ const errorMessage = error?.message ?? JSON.stringify(error, null, 2); return ( - <EuiCallOut title={title} color="danger" iconType="alert" {...rest}> - <pre>{errorMessage}</pre> - {actions ? actions : null} - </EuiCallOut> + <EuiPageContent verticalPosition="center" horizontalPosition="center" color="danger"> + <EuiEmptyPrompt + iconType="alert" + title={<h2>{title}</h2>} + body={ + <p> + <pre>{errorMessage}</pre> + {actions ? actions : null} + </p> + } + /> + </EuiPageContent> ); }; diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx index c3dc9ab4bb8a1..68f6fea3aa943 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx @@ -6,7 +6,7 @@ */ import React, { FC } from 'react'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import '@testing-library/jest-dom/extend-expect'; import { render, screen, waitFor } from '@testing-library/react'; diff --git a/x-pack/plugins/transform/public/app/lib/authorization/components/with_privileges.tsx b/x-pack/plugins/transform/public/app/lib/authorization/components/with_privileges.tsx index ef009e6a125e7..cdf4407b4233f 100644 --- a/x-pack/plugins/transform/public/app/lib/authorization/components/with_privileges.tsx +++ b/x-pack/plugins/transform/public/app/lib/authorization/components/with_privileges.tsx @@ -7,7 +7,7 @@ import React, { useContext, FC } from 'react'; -import { EuiPageContent } from '@elastic/eui'; +import { EuiFlexItem, EuiFlexGroup, EuiPageContent } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -74,27 +74,31 @@ const MissingClusterPrivileges: FC<MissingClusterPrivilegesProps> = ({ missingPrivileges, privilegesCount, }) => ( - <EuiPageContent> - <NotAuthorizedSection - title={ - <FormattedMessage - id="xpack.transform.app.deniedPrivilegeTitle" - defaultMessage="You're missing cluster privileges" - /> - } - message={ - <FormattedMessage - id="xpack.transform.app.deniedPrivilegeDescription" - defaultMessage="To use this section of Transforms, you must have {privilegesCount, + <EuiFlexGroup justifyContent="spaceAround"> + <EuiFlexItem grow={false}> + <EuiPageContent verticalPosition="center" horizontalPosition="center" color="danger"> + <NotAuthorizedSection + title={ + <FormattedMessage + id="xpack.transform.app.deniedPrivilegeTitle" + defaultMessage="You're missing cluster privileges" + /> + } + message={ + <FormattedMessage + id="xpack.transform.app.deniedPrivilegeDescription" + defaultMessage="To use this section of Transforms, you must have {privilegesCount, plural, one {this cluster privilege} other {these cluster privileges}}: {missingPrivileges}." - values={{ - missingPrivileges, - privilegesCount, - }} + values={{ + missingPrivileges, + privilegesCount, + }} + /> + } /> - } - /> - </EuiPageContent> + </EuiPageContent> + </EuiFlexItem> + </EuiFlexGroup> ); export const PrivilegesWrapper: FC<{ privileges: string | string[] }> = ({ diff --git a/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx b/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx index e4ecc0418d782..8aecf403186c5 100644 --- a/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx +++ b/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx @@ -15,12 +15,9 @@ import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty, EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiPageContent, EuiPageContentBody, + EuiPageHeader, EuiSpacer, - EuiTitle, } from '@elastic/eui'; import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants'; @@ -105,37 +102,38 @@ export const CloneTransformSection: FC<Props> = ({ match, location }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const docsLink = ( + <EuiButtonEmpty + href={esTransform} + target="_blank" + iconType="help" + data-test-subj="documentationLink" + > + <FormattedMessage + id="xpack.transform.transformsWizard.transformDocsLinkText" + defaultMessage="Transform docs" + /> + </EuiButtonEmpty> + ); + return ( <PrivilegesWrapper privileges={APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES}> - <EuiPageContent data-test-subj="transformPageCloneTransform"> - <EuiTitle size="l"> - <EuiFlexGroup alignItems="center"> - <EuiFlexItem grow={true}> - <h1> - <FormattedMessage - id="xpack.transform.transformsWizard.cloneTransformTitle" - defaultMessage="Clone transform" - /> - </h1> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButtonEmpty - href={esTransform} - target="_blank" - iconType="help" - data-test-subj="documentationLink" - > - <FormattedMessage - id="xpack.transform.transformsWizard.transformDocsLinkText" - defaultMessage="Transform docs" - /> - </EuiButtonEmpty> - </EuiFlexItem> - </EuiFlexGroup> - </EuiTitle> - <EuiPageContentBody> - <EuiSpacer size="l" /> - {typeof errorMessage !== 'undefined' && ( + <EuiPageHeader + pageTitle={ + <FormattedMessage + id="xpack.transform.transformsWizard.cloneTransformTitle" + defaultMessage="Clone transform" + /> + } + rightSideItems={[docsLink]} + bottomBorder + /> + + <EuiSpacer size="l" /> + + <EuiPageContentBody data-test-subj="transformPageCloneTransform"> + {typeof errorMessage !== 'undefined' && ( + <> <EuiCallOut title={i18n.translate('xpack.transform.clone.errorPromptTitle', { defaultMessage: 'An error occurred getting the transform configuration.', @@ -145,12 +143,13 @@ export const CloneTransformSection: FC<Props> = ({ match, location }) => { > <pre>{JSON.stringify(errorMessage)}</pre> </EuiCallOut> - )} - {searchItems !== undefined && isInitialized === true && transformConfig !== undefined && ( - <Wizard cloneConfig={transformConfig} searchItems={searchItems} /> - )} - </EuiPageContentBody> - </EuiPageContent> + <EuiSpacer size="l" /> + </> + )} + {searchItems !== undefined && isInitialized === true && transformConfig !== undefined && ( + <Wizard cloneConfig={transformConfig} searchItems={searchItems} /> + )} + </EuiPageContentBody> </PrivilegesWrapper> ); }; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx index b88eb8ce48601..d736bd60f2df6 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/create_transform_section.tsx @@ -13,12 +13,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty, EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiPageContent, EuiPageContentBody, + EuiPageHeader, EuiSpacer, - EuiTitle, } from '@elastic/eui'; import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants'; @@ -42,42 +39,44 @@ export const CreateTransformSection: FC<Props> = ({ match }) => { const { error: searchItemsError, searchItems } = useSearchItems(match.params.savedObjectId); + const docsLink = ( + <EuiButtonEmpty + href={esTransform} + target="_blank" + iconType="help" + data-test-subj="documentationLink" + > + <FormattedMessage + id="xpack.transform.transformsWizard.transformDocsLinkText" + defaultMessage="Transform docs" + /> + </EuiButtonEmpty> + ); + return ( <PrivilegesWrapper privileges={APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES}> - <EuiPageContent data-test-subj="transformPageCreateTransform"> - <EuiTitle size="l"> - <EuiFlexGroup alignItems="center"> - <EuiFlexItem grow={true}> - <h1> - <FormattedMessage - id="xpack.transform.transformsWizard.createTransformTitle" - defaultMessage="Create transform" - /> - </h1> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButtonEmpty - href={esTransform} - target="_blank" - iconType="help" - data-test-subj="documentationLink" - > - <FormattedMessage - id="xpack.transform.transformsWizard.transformDocsLinkText" - defaultMessage="Transform docs" - /> - </EuiButtonEmpty> - </EuiFlexItem> - </EuiFlexGroup> - </EuiTitle> - <EuiPageContentBody> - <EuiSpacer size="l" /> - {searchItemsError !== undefined && ( + <EuiPageHeader + pageTitle={ + <FormattedMessage + id="xpack.transform.transformsWizard.createTransformTitle" + defaultMessage="Create transform" + /> + } + rightSideItems={[docsLink]} + bottomBorder + /> + + <EuiSpacer size="l" /> + + <EuiPageContentBody data-test-subj="transformPageCreateTransform"> + {searchItemsError !== undefined && ( + <> <EuiCallOut title={searchItemsError} color="danger" iconType="alert" /> - )} - {searchItems !== undefined && <Wizard searchItems={searchItems} />} - </EuiPageContentBody> - </EuiPageContent> + <EuiSpacer size="l" /> + </> + )} + {searchItems !== undefined && <Wizard searchItems={searchItems} />} + </EuiPageContentBody> </PrivilegesWrapper> ); }; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/discover_action_name.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/discover_action_name.test.tsx index 8dba93399792c..dc6fae40ee0d1 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/discover_action_name.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_discover/discover_action_name.test.tsx @@ -7,7 +7,7 @@ import { cloneDeep } from 'lodash'; import React from 'react'; -import { IntlProvider } from 'react-intl'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; import { render, waitFor, screen } from '@testing-library/react'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx index 2f956db07f2ac..faa304678c0fa 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx @@ -19,7 +19,6 @@ import { EuiFlyoutBody, EuiFlyoutFooter, EuiFlyoutHeader, - EuiOverlayMask, EuiSpacer, EuiTitle, } from '@elastic/eui'; @@ -78,70 +77,68 @@ export const EditTransformFlyout: FC<EditTransformFlyoutProps> = ({ closeFlyout, const isUpdateButtonDisabled = !state.isFormValid || !state.isFormTouched; return ( - <EuiOverlayMask> - <EuiFlyout - onClose={closeFlyout} - hideCloseButton - aria-labelledby="transformEditFlyoutTitle" - data-test-subj="transformEditFlyout" - > - <EuiFlyoutHeader hasBorder> - <EuiTitle size="m"> - <h2 id="transformEditFlyoutTitle"> - {i18n.translate('xpack.transform.transformList.editFlyoutTitle', { - defaultMessage: 'Edit {transformId}', - values: { - transformId: config.id, - }, + <EuiFlyout + onClose={closeFlyout} + hideCloseButton + aria-labelledby="transformEditFlyoutTitle" + data-test-subj="transformEditFlyout" + > + <EuiFlyoutHeader hasBorder> + <EuiTitle size="m"> + <h2 id="transformEditFlyoutTitle"> + {i18n.translate('xpack.transform.transformList.editFlyoutTitle', { + defaultMessage: 'Edit {transformId}', + values: { + transformId: config.id, + }, + })} + </h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody banner={<EditTransformFlyoutCallout />}> + <EditTransformFlyoutForm editTransformFlyout={[state, dispatch]} /> + {errorMessage !== undefined && ( + <> + <EuiSpacer size="m" /> + <EuiCallOut + title={i18n.translate( + 'xpack.transform.transformList.editTransformGenericErrorMessage', + { + defaultMessage: + 'An error occurred calling the API endpoint to update transforms.', + } + )} + color="danger" + iconType="alert" + > + <p>{errorMessage}</p> + </EuiCallOut> + </> + )} + </EuiFlyoutBody> + <EuiFlyoutFooter> + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty iconType="cross" onClick={closeFlyout} flush="left"> + {i18n.translate('xpack.transform.transformList.editFlyoutCancelButtonText', { + defaultMessage: 'Cancel', })} - </h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody banner={<EditTransformFlyoutCallout />}> - <EditTransformFlyoutForm editTransformFlyout={[state, dispatch]} /> - {errorMessage !== undefined && ( - <> - <EuiSpacer size="m" /> - <EuiCallOut - title={i18n.translate( - 'xpack.transform.transformList.editTransformGenericErrorMessage', - { - defaultMessage: - 'An error occurred calling the API endpoint to update transforms.', - } - )} - color="danger" - iconType="alert" - > - <p>{errorMessage}</p> - </EuiCallOut> - </> - )} - </EuiFlyoutBody> - <EuiFlyoutFooter> - <EuiFlexGroup justifyContent="spaceBetween"> - <EuiFlexItem grow={false}> - <EuiButtonEmpty iconType="cross" onClick={closeFlyout} flush="left"> - {i18n.translate('xpack.transform.transformList.editFlyoutCancelButtonText', { - defaultMessage: 'Cancel', - })} - </EuiButtonEmpty> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButton - data-test-subj="transformEditFlyoutUpdateButton" - onClick={submitFormHandler} - fill - isDisabled={isUpdateButtonDisabled} - > - {i18n.translate('xpack.transform.transformList.editFlyoutUpdateButtonText', { - defaultMessage: 'Update', - })} - </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlyoutFooter> - </EuiFlyout> - </EuiOverlayMask> + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + data-test-subj="transformEditFlyoutUpdateButton" + onClick={submitFormHandler} + fill + isDisabled={isUpdateButtonDisabled} + > + {i18n.translate('xpack.transform.transformList.editFlyoutUpdateButtonText', { + defaultMessage: 'Update', + })} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlyoutFooter> + </EuiFlyout> ); }; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/transform_list.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/transform_list.test.tsx.snap index e2de4c0ea1f6c..cf80421711355 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/transform_list.test.tsx.snap +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/transform_list.test.tsx.snap @@ -1,23 +1,42 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Transform: Transform List <TransformList /> Minimal initialization 1`] = ` -<EuiEmptyPrompt - actions={ - Array [ - <EuiButtonEmpty - data-test-subj="transformCreateFirstButton" - isDisabled={true} - onClick={[MockFunction]} - > - Create your first transform - </EuiButtonEmpty>, - ] - } - data-test-subj="transformNoTransformsFound" - title={ - <h2> - No transforms found - </h2> - } -/> +<EuiFlexGroup + justifyContent="spaceAround" +> + <EuiFlexItem + grow={false} + > + <EuiSpacer + size="l" + /> + <EuiPageContent + color="subdued" + horizontalPosition="center" + verticalPosition="center" + > + <EuiEmptyPrompt + actions={ + Array [ + <EuiButton + color="primary" + data-test-subj="transformCreateFirstButton" + fill={true} + isDisabled={true} + onClick={[MockFunction]} + > + Create your first transform + </EuiButton>, + ] + } + data-test-subj="transformNoTransformsFound" + title={ + <h2> + No transforms found + </h2> + } + /> + </EuiPageContent> + </EuiFlexItem> +</EuiFlexGroup> `; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx index bacf8f9deccae..ab30f4793a315 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx @@ -10,12 +10,15 @@ import React, { MouseEventHandler, FC, useContext, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { + EuiButton, EuiButtonEmpty, EuiButtonIcon, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, + EuiPageContent, EuiPopover, + EuiSpacer, EuiTitle, EuiInMemoryTable, EuiSearchBarProps, @@ -135,27 +138,36 @@ export const TransformList: FC<TransformListProps> = ({ if (transforms.length === 0) { return ( - <EuiEmptyPrompt - title={ - <h2> - {i18n.translate('xpack.transform.list.emptyPromptTitle', { - defaultMessage: 'No transforms found', - })} - </h2> - } - actions={[ - <EuiButtonEmpty - onClick={onCreateTransform} - isDisabled={disabled} - data-test-subj="transformCreateFirstButton" - > - {i18n.translate('xpack.transform.list.emptyPromptButtonText', { - defaultMessage: 'Create your first transform', - })} - </EuiButtonEmpty>, - ]} - data-test-subj="transformNoTransformsFound" - /> + <EuiFlexGroup justifyContent="spaceAround"> + <EuiFlexItem grow={false}> + <EuiSpacer size="l" /> + <EuiPageContent verticalPosition="center" horizontalPosition="center" color="subdued"> + <EuiEmptyPrompt + title={ + <h2> + {i18n.translate('xpack.transform.list.emptyPromptTitle', { + defaultMessage: 'No transforms found', + })} + </h2> + } + actions={[ + <EuiButton + color="primary" + fill + onClick={onCreateTransform} + isDisabled={disabled} + data-test-subj="transformCreateFirstButton" + > + {i18n.translate('xpack.transform.list.emptyPromptButtonText', { + defaultMessage: 'Create your first transform', + })} + </EuiButton>, + ]} + data-test-subj="transformNoTransformsFound" + /> + </EuiPageContent> + </EuiFlexItem> + </EuiFlexGroup> ); } diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx index cc4c502f21eb5..2479d34f1579a 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx @@ -5,23 +5,21 @@ * 2.0. */ -import React, { FC, Fragment, useEffect, useState } from 'react'; +import React, { FC, useEffect, useState } from 'react'; -import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty, - EuiCallOut, + EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiLoadingContent, EuiModal, EuiPageContent, EuiPageContentBody, + EuiPageHeader, EuiSpacer, - EuiText, - EuiTitle, } from '@elastic/eui'; import { APP_GET_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants'; @@ -77,73 +75,91 @@ export const TransformManagement: FC = () => { setSavedObjectId(id); }; + const docsLink = ( + <EuiButtonEmpty + href={esTransform} + target="_blank" + iconType="help" + data-test-subj="documentationLink" + > + <FormattedMessage + id="xpack.transform.transformList.transformDocsLinkText" + defaultMessage="Transform docs" + /> + </EuiButtonEmpty> + ); + return ( - <Fragment> - <EuiPageContent data-test-subj="transformPageTransformList"> - <EuiTitle size="l"> - <EuiFlexGroup alignItems="center"> - <EuiFlexItem grow={true}> - <h1 data-test-subj="transformAppTitle"> - <FormattedMessage - id="xpack.transform.transformList.transformTitle" - defaultMessage="Transforms" - /> - </h1> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButtonEmpty - href={esTransform} - target="_blank" - iconType="help" - data-test-subj="documentationLink" - > - <FormattedMessage - id="xpack.transform.transformList.transformDocsLinkText" - defaultMessage="Transform docs" - /> - </EuiButtonEmpty> - </EuiFlexItem> - </EuiFlexGroup> - </EuiTitle> - <EuiSpacer size="s" /> - <EuiTitle size="s"> - <EuiText color="subdued"> + <> + <EuiPageHeader + pageTitle={ + <span data-test-subj="transformAppTitle"> <FormattedMessage - id="xpack.transform.transformList.transformDescription" - defaultMessage="Use transforms to pivot existing Elasticsearch indices into summarized entity-centric indices or to create an indexed view of the latest documents for fast access." + id="xpack.transform.transformList.transformTitle" + defaultMessage="Transforms" /> - </EuiText> - </EuiTitle> - <EuiPageContentBody> - <EuiSpacer size="l" /> - {!isInitialized && <EuiLoadingContent lines={2} />} - {isInitialized && ( - <> - <TransformStatsBar transformNodes={transformNodes} transformsList={transforms} /> - <EuiSpacer size="s" /> - {typeof errorMessage !== 'undefined' && ( - <EuiCallOut - title={i18n.translate('xpack.transform.list.errorPromptTitle', { - defaultMessage: 'An error occurred getting the transform list.', - })} - color="danger" - iconType="alert" - > - <pre>{JSON.stringify(errorMessage)}</pre> - </EuiCallOut> - )} - {typeof errorMessage === 'undefined' && ( - <TransformList - onCreateTransform={onOpenModal} - transformNodes={transformNodes} - transforms={transforms} - transformsLoading={transformsLoading} - /> - )} - </> - )} - </EuiPageContentBody> - </EuiPageContent> + </span> + } + description={ + <FormattedMessage + id="xpack.transform.transformList.transformDescription" + defaultMessage="Use transforms to pivot existing Elasticsearch indices into summarized entity-centric indices or to create an indexed view of the latest documents for fast access." + /> + } + rightSideItems={[docsLink]} + bottomBorder + /> + + <EuiSpacer size="l" /> + + <EuiPageContentBody data-test-subj="transformPageTransformList"> + {!isInitialized && <EuiLoadingContent lines={2} />} + {isInitialized && ( + <> + <TransformStatsBar transformNodes={transformNodes} transformsList={transforms} /> + <EuiSpacer size="s" /> + {typeof errorMessage !== 'undefined' && ( + <EuiFlexGroup justifyContent="spaceAround"> + <EuiFlexItem grow={false}> + <EuiSpacer size="l" /> + <EuiPageContent + verticalPosition="center" + horizontalPosition="center" + color="danger" + > + <EuiEmptyPrompt + iconType="alert" + title={ + <h2> + <FormattedMessage + id="xpack.transform.list.errorPromptTitle" + defaultMessage="An error occurred getting the transform list" + /> + </h2> + } + body={ + <p> + <pre>{JSON.stringify(errorMessage)}</pre> + </p> + } + actions={[]} + /> + </EuiPageContent> + </EuiFlexItem> + </EuiFlexGroup> + )} + {typeof errorMessage === 'undefined' && ( + <TransformList + onCreateTransform={onOpenModal} + transformNodes={transformNodes} + transforms={transforms} + transformsLoading={transformsLoading} + /> + )} + </> + )} + </EuiPageContentBody> + {isSearchSelectionVisible && ( <EuiModal onClose={onCloseModal} @@ -153,7 +169,7 @@ export const TransformManagement: FC = () => { <SearchSelection onSearchSelected={onSearchSelected} /> </EuiModal> )} - </Fragment> + </> ); }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index bc8318e803c8f..837716ec9dd5a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4941,8 +4941,6 @@ "visTypePie.editors.pie.showLabelsLabel": "ラベルを表示", "visTypePie.editors.pie.showTopLevelOnlyLabel": "トップレベルのみ表示", "visTypePie.editors.pie.showValuesLabel": "値を表示", - "visualizations.advancedSettings.visualization.legacyChartsLibrary.description": "Visualizeでエリア、折れ線、棒グラフのレガシーグラフライブラリを有効にします。", - "visualizations.advancedSettings.visualization.legacyChartsLibrary.name": "レガシーグラフライブラリ", "visualizations.advancedSettings.visualizeEnableLabsText": "ユーザーが実験的なビジュアライゼーションを作成、表示、編集できるようになります。無効の場合、\n ユーザーは本番準備が整ったビジュアライゼーションのみを利用できます。", "visualizations.advancedSettings.visualizeEnableLabsTitle": "実験的なビジュアライゼーションを有効にする", "visualizations.disabledLabVisualizationLink": "ドキュメンテーションを表示", @@ -5057,7 +5055,6 @@ "visTypeXy.editors.pointSeries.thresholdLine.valueLabel": "しきい値", "visTypeXy.editors.pointSeries.thresholdLine.widthLabel": "線の幅", "visTypeXy.editors.pointSeries.thresholdLineSettingsTitle": "しきい線", - "visTypeXy.emptyTextColumnValue": " (空) ", "visTypeXy.fittingFunctionsTitle.carry": "最後 (ギャップを最後の値で埋める) ", "visTypeXy.fittingFunctionsTitle.linear": "線形 (ギャップを線で埋める) ", "visTypeXy.fittingFunctionsTitle.lookahead": "次 (ギャップを次の値で埋める) ", @@ -5976,9 +5973,6 @@ "xpack.banners.settings.textColor.description": "バナーテキストの色を設定します。{subscriptionLink}", "xpack.banners.settings.textColor.title": "バナーテキスト色", "xpack.banners.settings.textContent.title": "バナーテキスト", - "xpack.canvas.app.loadErrorMessage": "メッセージ:{error}", - "xpack.canvas.app.loadErrorTitle": "Canvas の読み込みに失敗", - "xpack.canvas.app.loadingMessage": "Canvas を読み込み中", "xpack.canvas.appDescription": "データを完璧に美しく表現します。", "xpack.canvas.argAddPopover.addAriaLabel": "引数を追加", "xpack.canvas.argFormAdvancedFailure.applyButtonLabel": "適用", @@ -5996,8 +5990,6 @@ "xpack.canvas.asset.deleteAssetTooltip": "削除", "xpack.canvas.asset.downloadAssetTooltip": "ダウンロード", "xpack.canvas.asset.thumbnailAltText": "アセットのサムネイル", - "xpack.canvas.assetManager.manageButtonLabel": "アセットの管理", - "xpack.canvas.assetModal.copyAssetMessage": "「{id}」をクリップボードにコピーしました", "xpack.canvas.assetModal.emptyAssetsDescription": "アセットをインポートして開始します", "xpack.canvas.assetModal.filePickerPromptText": "画像を選択するかドラッグ &amp; ドロップしてください", "xpack.canvas.assetModal.loadingText": "画像をアップロード中", @@ -6020,7 +6012,6 @@ "xpack.canvas.customElementModal.nameInputLabel": "名前", "xpack.canvas.customElementModal.remainingCharactersDescription": "残り {numberOfRemainingCharacter} 文字", "xpack.canvas.customElementModal.saveButtonLabel": "保存", - "xpack.canvas.datasourceDatasourceComponent.changeButtonLabel": "要素データソースの変更", "xpack.canvas.datasourceDatasourceComponent.expressionArgDescription": "データソースの引数は式で制御されます。式エディターを使用して、データソースを修正します。", "xpack.canvas.datasourceDatasourceComponent.previewButtonLabel": "データをプレビュー", "xpack.canvas.datasourceDatasourceComponent.saveButtonLabel": "保存", @@ -6449,8 +6440,6 @@ "xpack.canvas.groupSettings.saveGroupDescription": "ワークパッド全体で再利用できるように、このグループを新規エレメントとして保存します。", "xpack.canvas.groupSettings.ungroupDescription": "個々のエレメントの設定を編集できるように、 ({uKey}) のグループを解除します。", "xpack.canvas.helpMenu.appName": "Canvas", - "xpack.canvas.helpMenu.description": "{CANVAS} に関する情報", - "xpack.canvas.helpMenu.documentationLinkLabel": "{CANVAS} ドキュメント", "xpack.canvas.helpMenu.keyboardShortcutsLinkLabel": "キーボードショートカット", "xpack.canvas.home.myWorkpadsTabLabel": "マイワークパッド", "xpack.canvas.home.workpadTemplatesTabLabel": "テンプレート", @@ -6517,7 +6506,6 @@ "xpack.canvas.lib.palettes.yellowBlueLabel": "黄、青", "xpack.canvas.lib.palettes.yellowGreenLabel": "黄、緑", "xpack.canvas.lib.palettes.yellowRedLabel": "黄、赤", - "xpack.canvas.link.errorMessage": "リンクエラー:{message}", "xpack.canvas.pageConfig.backgroundColorDescription": "HEX、RGB、また HTML 色名が使用できます", "xpack.canvas.pageConfig.backgroundColorLabel": "背景", "xpack.canvas.pageConfig.title": "ページ設定", @@ -6527,7 +6515,6 @@ "xpack.canvas.pageManager.addPageTooltip": "新しいページをこのワークパッドに追加", "xpack.canvas.pageManager.confirmRemoveDescription": "このページを削除してよろしいですか?", "xpack.canvas.pageManager.confirmRemoveTitle": "ページを削除", - "xpack.canvas.pageManager.pageNumberAriaLabel": "ページ番号 {pageNumber} を読み込む", "xpack.canvas.pageManager.removeButtonLabel": "削除", "xpack.canvas.pagePreviewPageControls.clonePageAriaLabel": "ページのクローンを作成", "xpack.canvas.pagePreviewPageControls.clonePageTooltip": "クローンを作成", @@ -6579,10 +6566,8 @@ "xpack.canvas.savedElementsModal.deleteElementDescription": "このエレメントを削除してよろしいですか?", "xpack.canvas.savedElementsModal.deleteElementTitle": "要素'{elementName}'を削除しますか?", "xpack.canvas.savedElementsModal.editElementTitle": "エレメントを編集", - "xpack.canvas.savedElementsModal.elementsTitle": "エレメント", "xpack.canvas.savedElementsModal.findElementPlaceholder": "エレメントを検索", "xpack.canvas.savedElementsModal.modalTitle": "マイエレメント", - "xpack.canvas.savedElementsModal.myElementsTitle": "マイエレメント", "xpack.canvas.shareWebsiteFlyout.description": "外部 Web サイトでこのワークパッドの不動バージョンを共有するには、これらの手順に従ってください。現在のワークパッドのビジュアルスナップショットになり、ライブデータにはアクセスできません。", "xpack.canvas.shareWebsiteFlyout.flyoutCalloutDescription": "共有するには、このワークパッド、{CANVAS} シェアラブルワークパッドランタイム、サンプル {HTML} ファイルを含む {link} を使用します。", "xpack.canvas.shareWebsiteFlyout.flyoutTitle": "Webサイトで共有", @@ -6637,13 +6622,10 @@ "xpack.canvas.textStylePicker.styleItalicOption": "斜体", "xpack.canvas.textStylePicker.styleOptionsControl": "スタイルオプション", "xpack.canvas.textStylePicker.styleUnderlineOption": "下線", - "xpack.canvas.timePicker.applyButtonLabel": "適用", "xpack.canvas.toolbar.editorButtonLabel": "表現エディター", - "xpack.canvas.toolbar.errorMessage": "ツールバーエラー:{message}", "xpack.canvas.toolbar.nextPageAriaLabel": "次のページ", "xpack.canvas.toolbar.pageButtonLabel": "{pageNum}{rest} ページ", "xpack.canvas.toolbar.previousPageAriaLabel": "前のページ", - "xpack.canvas.toolbar.workpadManagerCloseButtonLabel": "閉じる", "xpack.canvas.toolbarTray.closeTrayAriaLabel": "トレイのクローンを作成", "xpack.canvas.transitions.fade.displayName": "フェード", "xpack.canvas.transitions.fade.help": "ページからページへフェードします", @@ -6905,20 +6887,8 @@ "xpack.canvas.units.quickRange.today": "今日", "xpack.canvas.units.quickRange.yesterday": "昨日", "xpack.canvas.useCloneWorkpad.clonedWorkpadName": "{workpadName} のコピー", - "xpack.canvas.varConfig.addButtonLabel": "変数の追加", - "xpack.canvas.varConfig.addTooltipLabel": "変数の追加", - "xpack.canvas.varConfig.copyActionButtonLabel": "スニペットをコピー", - "xpack.canvas.varConfig.copyActionTooltipLabel": "変数構文をクリップボードにコピー", "xpack.canvas.varConfig.copyNotificationDescription": "変数構文がクリップボードにコピーされました", - "xpack.canvas.varConfig.deleteActionButtonLabel": "変数の削除", "xpack.canvas.varConfig.deleteNotificationDescription": "変数の削除が正常に完了しました", - "xpack.canvas.varConfig.editActionButtonLabel": "変数の編集", - "xpack.canvas.varConfig.emptyDescription": "このワークパッドには現在変数がありません。変数を追加して、共通の値を格納したり、編集したりすることができます。これらの変数は、要素または式エディターで使用できます。", - "xpack.canvas.varConfig.tableNameLabel": "名前", - "xpack.canvas.varConfig.tableTypeLabel": "型", - "xpack.canvas.varConfig.tableValueLabel": "値", - "xpack.canvas.varConfig.titleLabel": "変数", - "xpack.canvas.varConfig.titleTooltip": "変数を追加して、共通の値を格納したり、編集したりします", "xpack.canvas.varConfigDeleteVar.cancelButtonLabel": "キャンセル", "xpack.canvas.varConfigDeleteVar.deleteButtonLabel": "変数の削除", "xpack.canvas.varConfigDeleteVar.titleLabel": "変数を削除しますか?", @@ -6952,7 +6922,6 @@ "xpack.canvas.workpadConfig.USLetterButtonLabel": "US レター", "xpack.canvas.workpadConfig.widthLabel": "幅", "xpack.canvas.workpadCreate.createButtonLabel": "ワークパッドを作成", - "xpack.canvas.workpadHeader.addElementButtonLabel": "エレメントを追加", "xpack.canvas.workpadHeader.addElementModalCloseButtonLabel": "閉じる", "xpack.canvas.workpadHeader.fullscreenButtonAriaLabel": "全画面表示", "xpack.canvas.workpadHeader.fullscreenTooltip": "全画面モードを開始します", @@ -6999,10 +6968,8 @@ "xpack.canvas.workpadHeaderElementMenu.textMenuItemLabel": "テキスト", "xpack.canvas.workpadHeaderKioskControl.controlTitle": "全画面ページのサイクル", "xpack.canvas.workpadHeaderKioskControl.cycleFormLabel": "サイクル間隔を変更", - "xpack.canvas.workpadHeaderKioskControl.cycleToggleSwitch": "スライドを自動的にサイクル", "xpack.canvas.workpadHeaderRefreshControlSettings.refreshAriaLabel": "エレメントを更新", "xpack.canvas.workpadHeaderRefreshControlSettings.refreshTooltip": "データを更新", - "xpack.canvas.workpadHeaderShareMenu.copyPDFMessage": "{PDF}生成{URL}がクリップボードにコピーされました。", "xpack.canvas.workpadHeaderShareMenu.copyShareConfigMessage": "共有マークアップがクリップボードにコピーされました", "xpack.canvas.workpadHeaderShareMenu.shareDownloadJSONTitle": "{JSON} をダウンロード", "xpack.canvas.workpadHeaderShareMenu.shareDownloadPDFTitle": "{PDF}レポート", @@ -7012,8 +6979,6 @@ "xpack.canvas.workpadHeaderShareMenu.shareWorkpadMessage": "このワークパッドを共有", "xpack.canvas.workpadHeaderShareMenu.unknownExportErrorMessage": "不明なエクスポートタイプ:{type}", "xpack.canvas.workpadHeaderShareMenu.unsupportedRendererWarning": "このワークパッドには、{CANVAS}シェアラブルワークパッドランタイムがサポートしていないレンダリング関数が含まれています。これらのエレメントはレンダリングされません:", - "xpack.canvas.workpadHeaderViewMenu.autoplayOffMenuItemLabel": "自動再生をオフにする", - "xpack.canvas.workpadHeaderViewMenu.autoplayOnMenuItemLabel": "自動再生をオンにする", "xpack.canvas.workpadHeaderViewMenu.autoplaySettingsMenuItemLabel": "自動再生設定", "xpack.canvas.workpadHeaderViewMenu.fullscreenMenuLabel": "全画面モードを開始します", "xpack.canvas.workpadHeaderViewMenu.hideEditModeLabel": "編集コントロールを非表示にします", @@ -7022,13 +6987,10 @@ "xpack.canvas.workpadHeaderViewMenu.showEditModeLabel": "編集コントロールを表示します", "xpack.canvas.workpadHeaderViewMenu.viewMenuButtonLabel": "表示", "xpack.canvas.workpadHeaderViewMenu.viewMenuLabel": "表示オプション", - "xpack.canvas.workpadHeaderViewMenu.zoomControlsAriaLabel": "ズームコントロール", - "xpack.canvas.workpadHeaderViewMenu.zoomControlsTooltip": "ズームコントロール", "xpack.canvas.workpadHeaderViewMenu.zoomFitToWindowText": "ウィンドウに合わせる", "xpack.canvas.workpadHeaderViewMenu.zoomInText": "ズームイン", "xpack.canvas.workpadHeaderViewMenu.zoomMenuItemLabel": "ズーム", "xpack.canvas.workpadHeaderViewMenu.zoomOutText": "ズームアウト", - "xpack.canvas.workpadHeaderViewMenu.zoomPanelTitle": "ズーム", "xpack.canvas.workpadHeaderViewMenu.zoomPrecentageValue": "リセット", "xpack.canvas.workpadHeaderViewMenu.zoomResetText": "{scalePercentage}%", "xpack.canvas.workpadImport.filePickerPlaceholder": "ワークパッド {JSON} ファイルをインポート", @@ -7521,7 +7483,6 @@ "xpack.enterpriseSearch.appSearch.credentials.title": "資格情報", "xpack.enterpriseSearch.appSearch.credentials.updateWarning": "既存の API キーはユーザー間で共有できます。このキーのアクセス権を変更すると、このキーにアクセスできるすべてのユーザーに影響します。", "xpack.enterpriseSearch.appSearch.credentials.updateWarningTitle": "十分ご注意ください!", - "xpack.enterpriseSearch.appSearch.deleteRoleMappingMessage": "このマッピングを完全に削除しますか?このアクションは元に戻せません。一部のユーザーがアクセスを失う可能性があります。", "xpack.enterpriseSearch.appSearch.DEV_ROLE_TYPE_DESCRIPTION": "開発者はエンジンのすべての要素を管理できます。", "xpack.enterpriseSearch.appSearch.documentCreation.api.description": "{documentsApiLink}を使用すると、新しいドキュメントをエンジンに追加できるほか、ドキュメントの更新、IDによるドキュメントの取得、ドキュメントの削除が可能です。基本操作を説明するさまざまな{clientLibrariesLink}があります。", "xpack.enterpriseSearch.appSearch.documentCreation.api.example": "実行中のAPIを表示するには、コマンドラインまたはクライアントライブラリを使用して、次の要求の例で実験することができます。", @@ -7857,7 +7818,6 @@ "xpack.enterpriseSearch.appSearch.multiInputRows.removeValueButtonLabel": "値を削除", "xpack.enterpriseSearch.appSearch.ownerRoleTypeDescription": "所有者はすべての操作を実行できます。アカウントには複数の所有者がいる場合がありますが、一度に少なくとも1人の所有者が必要です。", "xpack.enterpriseSearch.appSearch.productCardDescription": "Elastic App Search には、強力な検索を設計し、Web サイトや Web/モバイルアプリケーションにデプロイするための使いやすいツールがあります。", - "xpack.enterpriseSearch.appSearch.productCta": "App Searchの起動", "xpack.enterpriseSearch.appSearch.productDescription": "ダッシュボード、分析、APIを活用し、高度なアプリケーション検索をシンプルにします。", "xpack.enterpriseSearch.appSearch.productName": "App Search", "xpack.enterpriseSearch.appSearch.result.documentDetailLink": "ドキュメントの詳細を表示", @@ -7907,6 +7867,7 @@ "xpack.enterpriseSearch.appSearch.tokens.search.description": "エンドポイントのみの検索では、公開検索キーが使用されます。", "xpack.enterpriseSearch.appSearch.tokens.search.name": "公開検索キー", "xpack.enterpriseSearch.appSearch.tokens.update": "正常に API キーを更新しました。", + "xpack.enterpriseSearch.emailLabel": "メール", "xpack.enterpriseSearch.enterpriseSearch.setupGuide.description": "場所を問わず、何でも検索。組織を支える多忙なチームのために、パワフルでモダンな検索エクスペリエンスを簡単に導入できます。Webサイトやアプリ、ワークプレイスに事前調整済みの検索をすばやく追加しましょう。何でもシンプルに検索できます。", "xpack.enterpriseSearch.enterpriseSearch.setupGuide.notConfigured": "エンタープライズサーチはまだKibanaインスタンスで構成されていません。", "xpack.enterpriseSearch.enterpriseSearch.setupGuide.videoAlt": "エンタープライズ サーチの基本操作", @@ -7949,15 +7910,14 @@ "xpack.enterpriseSearch.roleMapping.attributeSelectorTitle": "属性マッピング", "xpack.enterpriseSearch.roleMapping.attributeValueLabel": "属性値", "xpack.enterpriseSearch.roleMapping.authProviderLabel": "認証プロバイダー", - "xpack.enterpriseSearch.roleMapping.deleteRoleMappingButton": "マッピングを削除", "xpack.enterpriseSearch.roleMapping.deleteRoleMappingDescription": "マッピングの削除は永久的であり、元に戻すことはできません", - "xpack.enterpriseSearch.roleMapping.deleteRoleMappingTitle": "このロールマッピングを削除", "xpack.enterpriseSearch.roleMapping.externalAttributeLabel": "外部属性", "xpack.enterpriseSearch.roleMapping.filterRoleMappingsPlaceholder": "ロールをフィルタリング...", "xpack.enterpriseSearch.roleMapping.individualAuthProviderLabel": "個別の認証プロバイダーを選択", "xpack.enterpriseSearch.roleMapping.manageRoleMappingTitle": "ロールマッピングを管理", "xpack.enterpriseSearch.roleMapping.noResults.message": "の結果が見つかりません。", "xpack.enterpriseSearch.roleMapping.newRoleMappingTitle": "ロールマッピングを追加", + "xpack.enterpriseSearch.roleMapping.removeRoleMappingTitle": "このロールマッピングを削除", "xpack.enterpriseSearch.roleMapping.roleLabel": "ロール", "xpack.enterpriseSearch.roleMapping.roleMappingsTitle": "ユーザーとロール", "xpack.enterpriseSearch.roleMapping.saveRoleMappingButtonLabel": "ロールマッピングの保存", @@ -7994,6 +7954,7 @@ "xpack.enterpriseSearch.troubleshooting.differentEsClusters.title": "{productName}とKibanaは別のElasticsearchクラスターにあります", "xpack.enterpriseSearch.troubleshooting.standardAuth.description": "このプラグインは、{standardAuthLink}の{productName}を完全にはサポートしていません。{productName}で作成されたユーザーはKibanaアクセス権が必要です。Kibanaで作成されたユーザーは、ナビゲーションメニューに{productName}が表示されません。", "xpack.enterpriseSearch.troubleshooting.standardAuth.title": "標準認証の{productName}はサポートされていません", + "xpack.enterpriseSearch.usernameLabel": "ユーザー名", "xpack.enterpriseSearch.workplaceSearch.accountNav.account.link": "マイアカウント", "xpack.enterpriseSearch.workplaceSearch.accountNav.logout.link": "ログアウト", "xpack.enterpriseSearch.workplaceSearch.accountNav.orgDashboard.link": "組織ダッシュボードに移動", @@ -8164,8 +8125,6 @@ "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.groupTableHeader": "グループ", "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.sourcesTableHeader": "コンテンツソース", "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.usersTableHeader": "ユーザー", - "xpack.enterpriseSearch.workplaceSearch.groups.groupsUsersTable.emailTableHeader": "メール", - "xpack.enterpriseSearch.workplaceSearch.groups.groupsUsersTable.usernameTableHeader": "ユーザー名", "xpack.enterpriseSearch.workplaceSearch.groups.groupUpdatedText": "前回更新日時{updatedAt}。", "xpack.enterpriseSearch.workplaceSearch.groups.groupUsersUpdated": "このグループのユーザーが正常に更新されました。", "xpack.enterpriseSearch.workplaceSearch.groups.heading": "グループを管理", @@ -8265,7 +8224,6 @@ "xpack.enterpriseSearch.workplaceSearch.reset.button": "リセット", "xpack.enterpriseSearch.workplaceSearch.roleMapping.adminRoleTypeDescription": "管理者は、コンテンツソース、グループ、ユーザー管理機能など、すべての組織レベルの設定に無制限にアクセスできます。", "xpack.enterpriseSearch.workplaceSearch.roleMapping.defaultGroupName": "デフォルト", - "xpack.enterpriseSearch.workplaceSearch.roleMapping.deleteRoleMappingButtonMessage": "このマッピングを完全に削除しますか?このアクションは元に戻せません。一部のユーザーがアクセスを失う可能性があります。", "xpack.enterpriseSearch.workplaceSearch.roleMapping.groupAssignmentInvalidError": "1つ以上の割り当てられたグループが必要です。", "xpack.enterpriseSearch.workplaceSearch.roleMapping.roleMappingsTableHeader": "グループアクセス", "xpack.enterpriseSearch.workplaceSearch.roleMapping.userRoleTypeDescription": "ユーザーの機能アクセスは検索インターフェースと個人設定管理に制限されます。", @@ -9795,8 +9753,6 @@ "xpack.idxMgmt.home.componentTemplates.emptyPromptDocumentionLink": "詳細情報", "xpack.idxMgmt.home.componentTemplates.emptyPromptTitle": "コンポーネントテンプレートを作成して開始", "xpack.idxMgmt.home.componentTemplates.list.componentTemplatesDescription": "コンポーネントテンプレートを使用して、複数のインデックステンプレートで設定、マッピング、エイリアス構成を再利用します。{learnMoreLink}", - "xpack.idxMgmt.home.componentTemplates.list.loadErrorReloadLinkLabel": "再試行してください。", - "xpack.idxMgmt.home.componentTemplates.list.loadErrorTitle": "コンポーネントテンプレートを読み込めません。{reloadLink}", "xpack.idxMgmt.home.componentTemplates.list.loadingMessage": "コンポーネントテンプレートを読み込んでいます…", "xpack.idxMgmt.home.componentTemplatesTabTitle": "コンポーネントテンプレート", "xpack.idxMgmt.home.dataStreamsTabTitle": "データストリーム", @@ -12617,7 +12573,6 @@ "xpack.lens.indexPattern.emptyDimensionButton": "空のディメンション", "xpack.lens.indexPattern.emptyFieldsLabel": "空のフィールド", "xpack.lens.indexPattern.emptyFieldsLabelHelp": "空のフィールドには、フィルターに基づく最初の 500 件のドキュメントの値が含まれていませんでした。", - "xpack.lens.indexpattern.emptyTextColumnValue": " (空) ", "xpack.lens.indexPattern.existenceErrorAriaLabel": "存在の取り込みに失敗しました", "xpack.lens.indexPattern.existenceErrorLabel": "フィールド情報を読み込めません", "xpack.lens.indexPattern.existenceTimeoutAriaLabel": "存在の取り込みがタイムアウトしました", @@ -12737,7 +12692,6 @@ "xpack.lens.indexPattern.terms.orderByHelp": "上位の値がランク付けされる条件となるディメンションを指定します。", "xpack.lens.indexPattern.terms.orderDescending": "降順", "xpack.lens.indexPattern.terms.orderDirection": "ランク方向", - "xpack.lens.indexPattern.terms.orderDirectionHelp": "上位の値のランク順序を指定します。", "xpack.lens.indexPattern.terms.otherBucketDescription": "他の値を「その他」としてグループ化", "xpack.lens.indexPattern.terms.otherLabel": "その他", "xpack.lens.indexPattern.terms.size": "値の数", @@ -17259,7 +17213,6 @@ "xpack.observability.expView.seriesEditor.clearFilter": "フィルターを消去", "xpack.observability.expView.seriesEditor.filters": "フィルター", "xpack.observability.expView.seriesEditor.name": "名前", - "xpack.observability.expView.seriesEditor.notFound": "系列が見つかりません。系列を追加してください。", "xpack.observability.expView.seriesEditor.removeSeries": "クリックすると、系列を削除します", "xpack.observability.expView.seriesEditor.time": "時間", "xpack.observability.featureCatalogueDescription": "専用UIで、ログ、メトリック、アプリケーショントレース、システム可用性を連結します。", @@ -20322,7 +20275,6 @@ "xpack.securitySolution.eventsViewer.alerts.defaultHeaders.versionTitle": "バージョン", "xpack.securitySolution.eventsViewer.errorFetchingEventsData": "イベントデータをクエリできませんでした", "xpack.securitySolution.eventsViewer.eventsLabel": "イベント", - "xpack.securitySolution.eventsViewer.footer.loadingEventsDataLabel": "イベントを読み込み中", "xpack.securitySolution.eventsViewer.showingLabel": "表示中", "xpack.securitySolution.exceptions.addException.addEndpointException": "エンドポイント例外の追加", "xpack.securitySolution.exceptions.addException.addException": "ルール例外の追加", @@ -20449,8 +20401,6 @@ "xpack.securitySolution.header.editableTitle.cancel": "キャンセル", "xpack.securitySolution.header.editableTitle.editButtonAria": "クリックすると {title} を編集できます", "xpack.securitySolution.header.editableTitle.save": "保存", - "xpack.securitySolution.headerGlobal.buttonAddData": "データの追加", - "xpack.securitySolution.headerGlobal.securitySolution": "セキュリティソリューション", "xpack.securitySolution.headerPage.pageSubtitle": "前回のイベント:{beat}", "xpack.securitySolution.hooks.useAddToTimeline.addedFieldMessage": "{fieldOrValue}をタイムラインに追加しました", "xpack.securitySolution.hooks.useAddToTimeline.template.addedFieldMessage": "{fieldOrValue}をタイムラインテンプレートに追加しました", @@ -20540,8 +20490,6 @@ "xpack.securitySolution.kpiNetwork.uniquePrivateIps.title": "固有のプライベート IP", "xpack.securitySolution.lastEventTime.errorSearchDescription": "前回のイベント時刻検索でエラーが発生しました。", "xpack.securitySolution.lastEventTime.failSearchDescription": "前回のイベント時刻で検索を実行できませんでした", - "xpack.securitySolution.lastUpdated.updated": "更新しました", - "xpack.securitySolution.lastUpdated.updating": "更新中...", "xpack.securitySolution.licensing.unsupportedMachineLearningMessage": "ご使用のライセンスは機械翻訳をサポートしていません。ライセンスをアップグレードしてください。", "xpack.securitySolution.lists.cancelValueListsUploadTitle": "アップロードのキャンセル", "xpack.securitySolution.lists.closeValueListsModalTitle": "閉じる", @@ -22345,7 +22293,6 @@ "xpack.stackAlerts.threshold.ui.visualization.loadingAlertVisualizationDescription": "アラートビジュアライゼーションを読み込み中…", "xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.dataDoesNotExistTextMessage": "時間範囲とフィルターが正しいことを確認してください。", "xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.noDataTitle": "このクエリに一致するデータはありません", - "xpack.timelines.placeholder": "プラグイン:{name} タイムライン:{timelineId}", "xpack.transform.actionDeleteTransform.bulkDeleteDestinationIndexTitle": "ディスティネーションインデックスの削除", "xpack.transform.actionDeleteTransform.bulkDeleteDestIndexPatternTitle": "ディスティネーションインデックスパターンの削除", "xpack.transform.actionDeleteTransform.deleteDestinationIndexTitle": "ディスティネーションインデックス{destinationIndex}の削除", @@ -23492,7 +23439,6 @@ "xpack.uptime.alerts.tls.criteriaExpression.ariaLabel": "このアラートで監視されるモニターの条件を示す式", "xpack.uptime.alerts.tls.criteriaExpression.description": "タイミング", "xpack.uptime.alerts.tls.criteriaExpression.value": "任意のモニター", - "xpack.uptime.alerts.tls.defaultActionMessage": "期限切れになるか古くなりすぎた{count} TLS個のTLS証明書証明書を検知しました。\n\n{expiringConditionalOpen}\n期限切れになる証明書数:{expiringCount}\n期限切れになる証明書:{expiringCommonNameAndDate}\n{expiringConditionalClose}\n\n{agingConditionalOpen}\n古い証明書数:{agingCount}\n古い証明書:{agingCommonNameAndDate}\n{agingConditionalClose}\n", "xpack.uptime.alerts.tls.description": "アップタイム監視の TLS 証明書の有効期限が近いときにアラートを発行します。", "xpack.uptime.alerts.tls.expirationExpression.ariaLabel": "証明書有効期限の TLS アラートをトリガーするしきい値を示す式", "xpack.uptime.alerts.tls.expirationExpression.description": "証明書が", @@ -24343,4 +24289,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。", "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f867407ff2d9b..0192566db0731 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4968,8 +4968,6 @@ "visTypePie.editors.pie.showLabelsLabel": "显示标签", "visTypePie.editors.pie.showTopLevelOnlyLabel": "仅显示顶级", "visTypePie.editors.pie.showValuesLabel": "显示值", - "visualizations.advancedSettings.visualization.legacyChartsLibrary.description": "在 Visualize 中启用面积图、折线图和条形图的旧版图表库。", - "visualizations.advancedSettings.visualization.legacyChartsLibrary.name": "旧版图表库", "visualizations.advancedSettings.visualizeEnableLabsText": "允许用户创建、查看和编辑实验性可视化。如果禁用,\n 仅被视为生产就绪的可视化可供用户使用。", "visualizations.advancedSettings.visualizeEnableLabsTitle": "启用实验性可视化", "visualizations.disabledLabVisualizationLink": "阅读文档", @@ -5085,7 +5083,6 @@ "visTypeXy.editors.pointSeries.thresholdLine.valueLabel": "阈值", "visTypeXy.editors.pointSeries.thresholdLine.widthLabel": "线条宽度", "visTypeXy.editors.pointSeries.thresholdLineSettingsTitle": "阈值线条", - "visTypeXy.emptyTextColumnValue": " (空) ", "visTypeXy.fittingFunctionsTitle.carry": "最后一个 (使用最后一个值填充缺口) ", "visTypeXy.fittingFunctionsTitle.linear": "线 (使用线填充缺口) ", "visTypeXy.fittingFunctionsTitle.lookahead": "下一个 (使用下一个值填充缺口) ", @@ -6015,9 +6012,6 @@ "xpack.banners.settings.textColor.description": "设置横幅广告文本的颜色。{subscriptionLink}", "xpack.banners.settings.textColor.title": "横幅广告文本颜色", "xpack.banners.settings.textContent.title": "横幅广告文本", - "xpack.canvas.app.loadErrorMessage": "消息:{error}", - "xpack.canvas.app.loadErrorTitle": "Canvas 加载失败", - "xpack.canvas.app.loadingMessage": "Canvas 正在加载", "xpack.canvas.appDescription": "以最佳像素展示您的数据。", "xpack.canvas.argAddPopover.addAriaLabel": "添加参数", "xpack.canvas.argFormAdvancedFailure.applyButtonLabel": "应用", @@ -6035,8 +6029,6 @@ "xpack.canvas.asset.deleteAssetTooltip": "删除", "xpack.canvas.asset.downloadAssetTooltip": "下载", "xpack.canvas.asset.thumbnailAltText": "资产缩略图", - "xpack.canvas.assetManager.manageButtonLabel": "管理资产", - "xpack.canvas.assetModal.copyAssetMessage": "已将“{id}”复制到剪贴板", "xpack.canvas.assetModal.emptyAssetsDescription": "导入您的资产以开始", "xpack.canvas.assetModal.filePickerPromptText": "选择或拖放图像", "xpack.canvas.assetModal.loadingText": "正在上传图像", @@ -6059,7 +6051,6 @@ "xpack.canvas.customElementModal.nameInputLabel": "名称", "xpack.canvas.customElementModal.remainingCharactersDescription": "还剩 {numberOfRemainingCharacter} 个字符", "xpack.canvas.customElementModal.saveButtonLabel": "保存", - "xpack.canvas.datasourceDatasourceComponent.changeButtonLabel": "更改元素数据源", "xpack.canvas.datasourceDatasourceComponent.expressionArgDescription": "数据源包含由表达式控制的参数。使用表达式编辑器可修改数据源。", "xpack.canvas.datasourceDatasourceComponent.previewButtonLabel": "预览数据", "xpack.canvas.datasourceDatasourceComponent.saveButtonLabel": "保存", @@ -6489,8 +6480,6 @@ "xpack.canvas.groupSettings.saveGroupDescription": "将此组另存为新元素,以在整个 Workpad 重复使用。", "xpack.canvas.groupSettings.ungroupDescription": "取消分组 ({uKey}) 以编辑各个元素设置。", "xpack.canvas.helpMenu.appName": "Canvas", - "xpack.canvas.helpMenu.description": "有关 {CANVAS} 特定信息", - "xpack.canvas.helpMenu.documentationLinkLabel": "{CANVAS} 文档", "xpack.canvas.helpMenu.keyboardShortcutsLinkLabel": "快捷键", "xpack.canvas.home.myWorkpadsTabLabel": "我的 Workpad", "xpack.canvas.home.workpadTemplatesTabLabel": "模板", @@ -6557,7 +6546,6 @@ "xpack.canvas.lib.palettes.yellowBlueLabel": "黄、蓝", "xpack.canvas.lib.palettes.yellowGreenLabel": "黄、绿", "xpack.canvas.lib.palettes.yellowRedLabel": "黄、红", - "xpack.canvas.link.errorMessage": "链接错误:{message}", "xpack.canvas.pageConfig.backgroundColorDescription": "接受 HEX、RGB 或 HTML 颜色名称", "xpack.canvas.pageConfig.backgroundColorLabel": "背景", "xpack.canvas.pageConfig.title": "页面设置", @@ -6567,7 +6555,6 @@ "xpack.canvas.pageManager.addPageTooltip": "将新页面添加到此 Workpad", "xpack.canvas.pageManager.confirmRemoveDescription": "确定要移除此页面?", "xpack.canvas.pageManager.confirmRemoveTitle": "移除页面", - "xpack.canvas.pageManager.pageNumberAriaLabel": "加载页码 {pageNumber}", "xpack.canvas.pageManager.removeButtonLabel": "移除", "xpack.canvas.pagePreviewPageControls.clonePageAriaLabel": "克隆页面", "xpack.canvas.pagePreviewPageControls.clonePageTooltip": "克隆", @@ -6619,10 +6606,8 @@ "xpack.canvas.savedElementsModal.deleteElementDescription": "确定要删除此元素?", "xpack.canvas.savedElementsModal.deleteElementTitle": "删除元素“{elementName}”?", "xpack.canvas.savedElementsModal.editElementTitle": "编辑元素", - "xpack.canvas.savedElementsModal.elementsTitle": "元素", "xpack.canvas.savedElementsModal.findElementPlaceholder": "查找元素", "xpack.canvas.savedElementsModal.modalTitle": "我的元素", - "xpack.canvas.savedElementsModal.myElementsTitle": "我的元素", "xpack.canvas.shareWebsiteFlyout.description": "按照以下步骤在外部网站上共享此 Workpad 的静态版本。其将是当前 Workpad 的可视化快照,对实时数据没有访问权限。", "xpack.canvas.shareWebsiteFlyout.flyoutCalloutDescription": "要尝试共享,可以{link},其包含此 Workpad、{CANVAS} Shareable Workpad Runtime 及示例 {HTML} 文件。", "xpack.canvas.shareWebsiteFlyout.flyoutTitle": "在网站上共享", @@ -6677,13 +6662,10 @@ "xpack.canvas.textStylePicker.styleItalicOption": "斜体", "xpack.canvas.textStylePicker.styleOptionsControl": "样式选项", "xpack.canvas.textStylePicker.styleUnderlineOption": "下划线", - "xpack.canvas.timePicker.applyButtonLabel": "应用", "xpack.canvas.toolbar.editorButtonLabel": "表达式编辑器", - "xpack.canvas.toolbar.errorMessage": "工具栏错误:{message}", "xpack.canvas.toolbar.nextPageAriaLabel": "下一页", "xpack.canvas.toolbar.pageButtonLabel": "第 {pageNum}{rest} 页", "xpack.canvas.toolbar.previousPageAriaLabel": "上一页", - "xpack.canvas.toolbar.workpadManagerCloseButtonLabel": "关闭", "xpack.canvas.toolbarTray.closeTrayAriaLabel": "关闭托盘", "xpack.canvas.transitions.fade.displayName": "淡化", "xpack.canvas.transitions.fade.help": "从一页淡入到下一页", @@ -6949,20 +6931,8 @@ "xpack.canvas.units.time.minutes": "{minutes, plural, other {# 分钟}}", "xpack.canvas.units.time.seconds": "{seconds, plural, other {# 秒}}", "xpack.canvas.useCloneWorkpad.clonedWorkpadName": "{workpadName} 副本", - "xpack.canvas.varConfig.addButtonLabel": "添加变量", - "xpack.canvas.varConfig.addTooltipLabel": "添加变量", - "xpack.canvas.varConfig.copyActionButtonLabel": "复制代码片段", - "xpack.canvas.varConfig.copyActionTooltipLabel": "将变量语法复制到剪贴板", "xpack.canvas.varConfig.copyNotificationDescription": "变量语法已复制到剪贴板", - "xpack.canvas.varConfig.deleteActionButtonLabel": "删除变量", "xpack.canvas.varConfig.deleteNotificationDescription": "变量已成功删除", - "xpack.canvas.varConfig.editActionButtonLabel": "编辑变量", - "xpack.canvas.varConfig.emptyDescription": "此 Workpad 当前没有变量。您可以添加变量以存储和编辑公用值。这样,便可以在元素中或表达式编辑器中使用这些变量。", - "xpack.canvas.varConfig.tableNameLabel": "名称", - "xpack.canvas.varConfig.tableTypeLabel": "类型", - "xpack.canvas.varConfig.tableValueLabel": "值", - "xpack.canvas.varConfig.titleLabel": "变量", - "xpack.canvas.varConfig.titleTooltip": "添加变量以存储和编辑公用值", "xpack.canvas.varConfigDeleteVar.cancelButtonLabel": "取消", "xpack.canvas.varConfigDeleteVar.deleteButtonLabel": "删除变量", "xpack.canvas.varConfigDeleteVar.titleLabel": "删除变量?", @@ -6996,7 +6966,6 @@ "xpack.canvas.workpadConfig.USLetterButtonLabel": "美国信函", "xpack.canvas.workpadConfig.widthLabel": "宽", "xpack.canvas.workpadCreate.createButtonLabel": "创建 Workpad", - "xpack.canvas.workpadHeader.addElementButtonLabel": "添加元素", "xpack.canvas.workpadHeader.addElementModalCloseButtonLabel": "关闭", "xpack.canvas.workpadHeader.cycleIntervalDaysText": "每 {days} {days, plural, other {天}}", "xpack.canvas.workpadHeader.cycleIntervalHoursText": "每 {hours} {hours, plural, other {小时}}", @@ -7047,10 +7016,8 @@ "xpack.canvas.workpadHeaderElementMenu.textMenuItemLabel": "文本", "xpack.canvas.workpadHeaderKioskControl.controlTitle": "循环播放全屏页面", "xpack.canvas.workpadHeaderKioskControl.cycleFormLabel": "更改循环播放时间间隔", - "xpack.canvas.workpadHeaderKioskControl.cycleToggleSwitch": "自动循环播放幻灯片", "xpack.canvas.workpadHeaderRefreshControlSettings.refreshAriaLabel": "刷新元素", "xpack.canvas.workpadHeaderRefreshControlSettings.refreshTooltip": "刷新数据", - "xpack.canvas.workpadHeaderShareMenu.copyPDFMessage": "{PDF} 生成 {URL} 已复制到您的剪贴板。", "xpack.canvas.workpadHeaderShareMenu.copyShareConfigMessage": "已将共享标记复制到剪贴板", "xpack.canvas.workpadHeaderShareMenu.shareDownloadJSONTitle": "下载为 {JSON}", "xpack.canvas.workpadHeaderShareMenu.shareDownloadPDFTitle": "{PDF} 报告", @@ -7060,8 +7027,6 @@ "xpack.canvas.workpadHeaderShareMenu.shareWorkpadMessage": "共享此 Workpad", "xpack.canvas.workpadHeaderShareMenu.unknownExportErrorMessage": "未知导出类型:{type}", "xpack.canvas.workpadHeaderShareMenu.unsupportedRendererWarning": "此 Workpad 包含 {CANVAS} Shareable Workpad Runtime 不支持的呈现函数。将不会呈现以下元素:", - "xpack.canvas.workpadHeaderViewMenu.autoplayOffMenuItemLabel": "关闭自动播放", - "xpack.canvas.workpadHeaderViewMenu.autoplayOnMenuItemLabel": "打开自动播放", "xpack.canvas.workpadHeaderViewMenu.autoplaySettingsMenuItemLabel": "自动播放设置", "xpack.canvas.workpadHeaderViewMenu.fullscreenMenuLabel": "进入全屏模式", "xpack.canvas.workpadHeaderViewMenu.hideEditModeLabel": "隐藏编辑控件", @@ -7070,13 +7035,10 @@ "xpack.canvas.workpadHeaderViewMenu.showEditModeLabel": "显示编辑控制", "xpack.canvas.workpadHeaderViewMenu.viewMenuButtonLabel": "查看", "xpack.canvas.workpadHeaderViewMenu.viewMenuLabel": "查看选项", - "xpack.canvas.workpadHeaderViewMenu.zoomControlsAriaLabel": "缩放控制", - "xpack.canvas.workpadHeaderViewMenu.zoomControlsTooltip": "缩放控制", "xpack.canvas.workpadHeaderViewMenu.zoomFitToWindowText": "适应窗口大小", "xpack.canvas.workpadHeaderViewMenu.zoomInText": "放大", "xpack.canvas.workpadHeaderViewMenu.zoomMenuItemLabel": "缩放", "xpack.canvas.workpadHeaderViewMenu.zoomOutText": "缩小", - "xpack.canvas.workpadHeaderViewMenu.zoomPanelTitle": "缩放", "xpack.canvas.workpadHeaderViewMenu.zoomPrecentageValue": "重置", "xpack.canvas.workpadHeaderViewMenu.zoomResetText": "{scalePercentage}%", "xpack.canvas.workpadImport.filePickerPlaceholder": "导入 Workpad {JSON} 文件", @@ -7580,7 +7542,6 @@ "xpack.enterpriseSearch.appSearch.credentials.title": "凭据", "xpack.enterpriseSearch.appSearch.credentials.updateWarning": "现有 API 密钥可在用户之间共享。更改此密钥的权限将影响有权访问此密钥的所有用户。", "xpack.enterpriseSearch.appSearch.credentials.updateWarningTitle": "谨慎操作!", - "xpack.enterpriseSearch.appSearch.deleteRoleMappingMessage": "确定要永久删除此映射?此操作不可逆转,且某些用户可能会失去访问权限。", "xpack.enterpriseSearch.appSearch.DEV_ROLE_TYPE_DESCRIPTION": "开发人员可以管理引擎的所有方面。", "xpack.enterpriseSearch.appSearch.documentCreation.api.description": "{documentsApiLink} 可用于将新文档添加到您的引擎、更新文档、按 ID 检索文档以及删除文档。有各种{clientLibrariesLink}可帮助您入门。", "xpack.enterpriseSearch.appSearch.documentCreation.api.example": "要了解如何使用 API,可以在下面通过命令行或客户端库试用示例请求。", @@ -7924,7 +7885,6 @@ "xpack.enterpriseSearch.appSearch.multiInputRows.removeValueButtonLabel": "删除值", "xpack.enterpriseSearch.appSearch.ownerRoleTypeDescription": "所有者可以执行任何操作。该帐户可以有很多所有者,但任何时候必须至少有一个所有者。", "xpack.enterpriseSearch.appSearch.productCardDescription": "Elastic App Search 提供用户友好的工具,用于设计强大的搜索功能,并将其部署到您的网站或 Web/移动应用程序。", - "xpack.enterpriseSearch.appSearch.productCta": "启动 App Search", "xpack.enterpriseSearch.appSearch.productDescription": "利用仪表板、分析和 API 执行高级应用程序搜索简单易行。", "xpack.enterpriseSearch.appSearch.productName": "App Search", "xpack.enterpriseSearch.appSearch.result.documentDetailLink": "访问文档详情", @@ -7975,6 +7935,7 @@ "xpack.enterpriseSearch.appSearch.tokens.search.description": "公有搜索密钥仅用于搜索终端。", "xpack.enterpriseSearch.appSearch.tokens.search.name": "公有搜索密钥", "xpack.enterpriseSearch.appSearch.tokens.update": "成功更新 API 密钥。", + "xpack.enterpriseSearch.emailLabel": "电子邮件", "xpack.enterpriseSearch.enterpriseSearch.setupGuide.description": "随时随地进行全面搜索。为工作繁忙的团队轻松实现强大的现代搜索体验。将预先调整的搜索功能快速添加到您的网站、应用或工作区。全面搜索就是这么简单。", "xpack.enterpriseSearch.enterpriseSearch.setupGuide.notConfigured": "企业搜索尚未在您的 Kibana 实例中配置。", "xpack.enterpriseSearch.enterpriseSearch.setupGuide.videoAlt": "企业搜索入门", @@ -8017,9 +7978,7 @@ "xpack.enterpriseSearch.roleMapping.attributeSelectorTitle": "属性映射", "xpack.enterpriseSearch.roleMapping.attributeValueLabel": "属性值", "xpack.enterpriseSearch.roleMapping.authProviderLabel": "身份验证提供程序", - "xpack.enterpriseSearch.roleMapping.deleteRoleMappingButton": "删除映射", "xpack.enterpriseSearch.roleMapping.deleteRoleMappingDescription": "请注意,删除映射是永久性的,无法撤消", - "xpack.enterpriseSearch.roleMapping.deleteRoleMappingTitle": "移除此角色映射", "xpack.enterpriseSearch.roleMapping.externalAttributeLabel": "外部属性", "xpack.enterpriseSearch.roleMapping.filterRoleMappingsPlaceholder": "筛选角色......", "xpack.enterpriseSearch.roleMapping.individualAuthProviderLabel": "选择单个身份验证提供程序", @@ -8028,6 +7987,7 @@ "xpack.enterpriseSearch.roleMapping.newRoleMappingTitle": "添加角色映射", "xpack.enterpriseSearch.roleMapping.roleLabel": "角色", "xpack.enterpriseSearch.roleMapping.roleMappingsTitle": "用户和角色", + "xpack.enterpriseSearch.roleMapping.removeRoleMappingTitle": "移除此角色映射", "xpack.enterpriseSearch.roleMapping.saveRoleMappingButtonLabel": "保存角色映射", "xpack.enterpriseSearch.roleMapping.updateRoleMappingButtonLabel": "更新角色映射", "xpack.enterpriseSearch.schema.addFieldModal.fieldNameNote.correct": "字段名称只能包含小写字母、数字和下划线", @@ -8062,6 +8022,7 @@ "xpack.enterpriseSearch.troubleshooting.differentEsClusters.title": "{productName} 和 Kibana 在不同的 Elasticsearch 集群中", "xpack.enterpriseSearch.troubleshooting.standardAuth.description": "此插件不完全支持使用 {standardAuthLink} 的 {productName}。{productName} 中创建的用户必须具有 Kibana 访问权限。Kibana 中创建的用户在导航菜单中将看不到 {productName}。", "xpack.enterpriseSearch.troubleshooting.standardAuth.title": "不支持使用标准身份验证的 {productName}", + "xpack.enterpriseSearch.usernameLabel": "用户名", "xpack.enterpriseSearch.workplaceSearch.accountNav.account.link": "我的帐户", "xpack.enterpriseSearch.workplaceSearch.accountNav.logout.link": "注销", "xpack.enterpriseSearch.workplaceSearch.accountNav.orgDashboard.link": "前往组织仪表板", @@ -8232,8 +8193,6 @@ "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.groupTableHeader": "组", "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.sourcesTableHeader": "内容源", "xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.usersTableHeader": "用户", - "xpack.enterpriseSearch.workplaceSearch.groups.groupsUsersTable.emailTableHeader": "电子邮件", - "xpack.enterpriseSearch.workplaceSearch.groups.groupsUsersTable.usernameTableHeader": "用户名", "xpack.enterpriseSearch.workplaceSearch.groups.groupUpdatedText": "上次更新于 {updatedAt}。", "xpack.enterpriseSearch.workplaceSearch.groups.groupUsersUpdated": "已成功更新此组的用户", "xpack.enterpriseSearch.workplaceSearch.groups.heading": "管理组", @@ -8333,7 +8292,6 @@ "xpack.enterpriseSearch.workplaceSearch.reset.button": "重置", "xpack.enterpriseSearch.workplaceSearch.roleMapping.adminRoleTypeDescription": "管理员对所有组织范围设置 (包括内容源、组和用户管理功能) 具有完全权限。", "xpack.enterpriseSearch.workplaceSearch.roleMapping.defaultGroupName": "默认", - "xpack.enterpriseSearch.workplaceSearch.roleMapping.deleteRoleMappingButtonMessage": "确定要永久删除此映射?此操作不可逆转,且某些用户可能会失去访问权限。", "xpack.enterpriseSearch.workplaceSearch.roleMapping.groupAssignmentInvalidError": "至少需要一个分配的组。", "xpack.enterpriseSearch.workplaceSearch.roleMapping.roleMappingsTableHeader": "组访问权限", "xpack.enterpriseSearch.workplaceSearch.roleMapping.userRoleTypeDescription": "用户的功能访问权限仅限于搜索界面和个人设置管理。", @@ -9902,8 +9860,6 @@ "xpack.idxMgmt.home.componentTemplates.emptyPromptDocumentionLink": "了解详情。", "xpack.idxMgmt.home.componentTemplates.emptyPromptTitle": "首先创建组件模板", "xpack.idxMgmt.home.componentTemplates.list.componentTemplatesDescription": "使用组件模板可在多个索引模板中重复使用设置、映射和别名。{learnMoreLink}", - "xpack.idxMgmt.home.componentTemplates.list.loadErrorReloadLinkLabel": "请重试。", - "xpack.idxMgmt.home.componentTemplates.list.loadErrorTitle": "无法加载组件模板。{reloadLink}", "xpack.idxMgmt.home.componentTemplates.list.loadingMessage": "正在加载组件模板……", "xpack.idxMgmt.home.componentTemplatesTabTitle": "组件模板", "xpack.idxMgmt.home.dataStreamsTabTitle": "数据流", @@ -12787,7 +12743,6 @@ "xpack.lens.indexPattern.emptyDimensionButton": "空维度", "xpack.lens.indexPattern.emptyFieldsLabel": "空字段", "xpack.lens.indexPattern.emptyFieldsLabelHelp": "空字段在基于您的筛选的前 500 个文档中不包含任何值。", - "xpack.lens.indexpattern.emptyTextColumnValue": " (空) ", "xpack.lens.indexPattern.existenceErrorAriaLabel": "现有内容提取失败", "xpack.lens.indexPattern.existenceErrorLabel": "无法加载字段信息", "xpack.lens.indexPattern.existenceTimeoutAriaLabel": "现有内容提取超时", @@ -12908,7 +12863,6 @@ "xpack.lens.indexPattern.terms.orderByHelp": "指定排名靠前值排名所依据的维度。", "xpack.lens.indexPattern.terms.orderDescending": "降序", "xpack.lens.indexPattern.terms.orderDirection": "排名方向", - "xpack.lens.indexPattern.terms.orderDirectionHelp": "指定排名靠前值的排名顺序。", "xpack.lens.indexPattern.terms.otherBucketDescription": "将其他值分组为“其他”", "xpack.lens.indexPattern.terms.otherLabel": "其他", "xpack.lens.indexPattern.terms.size": "值数目", @@ -17495,7 +17449,6 @@ "xpack.observability.expView.seriesEditor.clearFilter": "清除筛选", "xpack.observability.expView.seriesEditor.filters": "筛选", "xpack.observability.expView.seriesEditor.name": "名称", - "xpack.observability.expView.seriesEditor.notFound": "未找到序列,请添加序列。", "xpack.observability.expView.seriesEditor.removeSeries": "单击移除序列", "xpack.observability.expView.seriesEditor.time": "时间", "xpack.observability.featureCatalogueDescription": "通过专用 UI 整合您的日志、指标、应用程序跟踪和系统可用性。", @@ -20623,7 +20576,6 @@ "xpack.securitySolution.eventsViewer.alerts.defaultHeaders.versionTitle": "版本", "xpack.securitySolution.eventsViewer.errorFetchingEventsData": "无法查询事件数据", "xpack.securitySolution.eventsViewer.eventsLabel": "事件", - "xpack.securitySolution.eventsViewer.footer.loadingEventsDataLabel": "正在加载事件", "xpack.securitySolution.eventsViewer.showingLabel": "正在显示", "xpack.securitySolution.eventsViewer.unit": "{totalCount, plural, other {个事件}}", "xpack.securitySolution.exceptions.addException.addEndpointException": "添加终端例外", @@ -20758,8 +20710,6 @@ "xpack.securitySolution.header.editableTitle.cancel": "取消", "xpack.securitySolution.header.editableTitle.editButtonAria": "通过单击,可以编辑 {title}", "xpack.securitySolution.header.editableTitle.save": "保存", - "xpack.securitySolution.headerGlobal.buttonAddData": "添加数据", - "xpack.securitySolution.headerGlobal.securitySolution": "安全解决方案", "xpack.securitySolution.headerPage.pageSubtitle": "上一事件:{beat}", "xpack.securitySolution.hooks.useAddToTimeline.addedFieldMessage": "已将 {fieldOrValue} 添加到时间线", "xpack.securitySolution.hooks.useAddToTimeline.template.addedFieldMessage": "已将 {fieldOrValue} 添加到时间线模板", @@ -20851,8 +20801,6 @@ "xpack.securitySolution.kpiNetwork.uniquePrivateIps.title": "唯一专用 IP", "xpack.securitySolution.lastEventTime.errorSearchDescription": "搜索上次事件时间时发生错误", "xpack.securitySolution.lastEventTime.failSearchDescription": "无法对上次事件时间执行搜索", - "xpack.securitySolution.lastUpdated.updated": "更新时间", - "xpack.securitySolution.lastUpdated.updating": "正在更新......", "xpack.securitySolution.licensing.unsupportedMachineLearningMessage": "您的许可证不支持 Machine Learning。请升级您的许可证。", "xpack.securitySolution.lists.cancelValueListsUploadTitle": "取消上传", "xpack.securitySolution.lists.closeValueListsModalTitle": "关闭", @@ -22699,7 +22647,6 @@ "xpack.stackAlerts.threshold.ui.visualization.loadingAlertVisualizationDescription": "正在加载告警可视化……", "xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.dataDoesNotExistTextMessage": "确认您的时间范围和筛选正确。", "xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.noDataTitle": "没有数据匹配此查询", - "xpack.timelines.placeholder": "插件:{name} 时间线:{timelineId}", "xpack.transform.actionDeleteTransform.bulkDeleteDestinationIndexTitle": "删除目标索引", "xpack.transform.actionDeleteTransform.bulkDeleteDestIndexPatternTitle": "删除目标索引模式", "xpack.transform.actionDeleteTransform.deleteDestinationIndexTitle": "删除目标索引 {destinationIndex}", @@ -23858,7 +23805,6 @@ "xpack.uptime.alerts.tls.criteriaExpression.ariaLabel": "显示此告警监视的监测条件的表达式", "xpack.uptime.alerts.tls.criteriaExpression.description": "当", "xpack.uptime.alerts.tls.criteriaExpression.value": "任意监测", - "xpack.uptime.alerts.tls.defaultActionMessage": "已检测到 {count} 个即将过期或即将过时的 TLS 证书。\n\n{expiringConditionalOpen}\n即将过期的证书计数:{expiringCount}\n即将过期的证书:{expiringCommonNameAndDate}\n{expiringConditionalClose}\n\n{agingConditionalOpen}\n过时的证书计数:{agingCount}\n过时的证书:{agingCommonNameAndDate}\n{agingConditionalClose}\n", "xpack.uptime.alerts.tls.description": "运行时间监测的 TLS 证书即将过期时告警。", "xpack.uptime.alerts.tls.expirationExpression.ariaLabel": "显示将触发证书过期 TLS 告警的阈值的表达式", "xpack.uptime.alerts.tls.expirationExpression.description": "具有将在", @@ -24719,4 +24665,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。", "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts index 2eda435d045a4..4266822bda1fc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts @@ -10,6 +10,7 @@ import { getSlackActionType } from './slack'; import { getEmailActionType } from './email'; import { getIndexActionType } from './es_index'; import { getPagerDutyActionType } from './pagerduty'; +import { getSwimlaneActionType } from './swimlane'; import { getWebhookActionType } from './webhook'; import { TypeRegistry } from '../../type_registry'; import { ActionTypeModel } from '../../../types'; @@ -28,6 +29,7 @@ export function registerBuiltInActionTypes({ actionTypeRegistry.register(getEmailActionType()); actionTypeRegistry.register(getIndexActionType()); actionTypeRegistry.register(getPagerDutyActionType()); + actionTypeRegistry.register(getSwimlaneActionType()); actionTypeRegistry.register(getWebhookActionType()); actionTypeRegistry.register(getServiceNowITSMActionType()); actionTypeRegistry.register(getServiceNowSIRActionType()); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx index b89f71b0fc354..be5250ccf8b29 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx @@ -12,7 +12,7 @@ import { JiraActionConnector } from './types'; jest.mock('../../../../common/lib/kibana'); describe('JiraActionConnectorFields renders', () => { - test('alerting Jira connector fields is rendered', () => { + test('alerting Jira connector fields are rendered', () => { const actionConnector = { secrets: { email: 'email', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx index 5897de46f94df..99d7e9510454f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -63,6 +63,7 @@ const JiraParamsFields: React.FunctionComponent<ActionParamsProps<JiraActionPara actionConnector, issueType: incident.issueType ?? '', }); + const editSubActionProperty = useCallback( (key: string, value: any) => { if (key === 'issueType') { @@ -75,9 +76,11 @@ const JiraParamsFields: React.FunctionComponent<ActionParamsProps<JiraActionPara index ); } + if (key === 'comments') { return editAction('subActionParams', { incident, comments: value }, index); } + return editAction( 'subActionParams', { @@ -124,6 +127,7 @@ const JiraParamsFields: React.FunctionComponent<ActionParamsProps<JiraActionPara text: type.name ?? '', })); }, [editSubActionProperty, incident, issueTypes]); + const prioritiesSelectOptions: EuiSelectOption[] = useMemo(() => { if (incident.issueType != null && fields != null) { const priorities = fields.priority != null ? fields.priority.allowedValues : []; @@ -141,6 +145,7 @@ const JiraParamsFields: React.FunctionComponent<ActionParamsProps<JiraActionPara } return []; }, [editSubActionProperty, fields, incident.issueType, incident.priority]); + useEffect(() => { if (!hasPriority && incident.priority != null) { editSubActionProperty('priority', null); @@ -167,6 +172,7 @@ const JiraParamsFields: React.FunctionComponent<ActionParamsProps<JiraActionPara } // eslint-disable-next-line react-hooks/exhaustive-deps }, [actionConnector]); + useEffect(() => { if (!actionParams.subAction) { editAction('subAction', 'pushToService', index); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx index b7b68b9485d8a..bbd237a7cec89 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx @@ -12,7 +12,7 @@ import { ResilientActionConnector } from './types'; jest.mock('../../../../common/lib/kibana'); describe('ResilientActionConnectorFields renders', () => { - test('alerting Resilient connector fields is rendered', () => { + test('alerting Resilient connector fields are rendered', () => { const actionConnector = { secrets: { apiKeyId: 'key', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.tsx index 54a138a2bc7cf..b0f5198b6b5fd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.tsx @@ -147,6 +147,7 @@ const ResilientParamsFields: React.FunctionComponent<ActionParamsProps<Resilient } // eslint-disable-next-line react-hooks/exhaustive-deps }, [actionConnector]); + useEffect(() => { if (!actionParams.subAction) { editAction('subAction', 'pushToService', index); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx index 330844b93b6b5..4993c51f350ad 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx @@ -12,7 +12,7 @@ import { ServiceNowActionConnector } from './types'; jest.mock('../../../../common/lib/kibana'); describe('ServiceNowActionConnectorFields renders', () => { - test('alerting servicenow connector fields is rendered', () => { + test('alerting servicenow connector fields are rendered', () => { const actionConnector = { secrets: { username: 'user', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts new file mode 100644 index 0000000000000..90bab65b83bfd --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts @@ -0,0 +1,145 @@ +/* + * 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 { getApplication } from './api'; + +const getApplicationResponse = { + fields: [], +}; + +describe('Swimlane API', () => { + let fetchMock: jest.SpyInstance<Promise<unknown>>; + + beforeAll(() => jest.spyOn(window, 'fetch')); + beforeEach(() => { + jest.resetAllMocks(); + fetchMock = jest.spyOn(window, 'fetch'); + }); + + describe('getApplication', () => { + it('should call getApplication API correctly', async () => { + const abortCtrl = new AbortController(); + + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => getApplicationResponse, + }); + const res = await getApplication({ + signal: abortCtrl.signal, + apiToken: '', + appId: '', + url: '', + }); + + expect(res).toEqual(getApplicationResponse); + }); + + it('returns an error when the response fails', async () => { + const abortCtrl = new AbortController(); + + fetchMock.mockResolvedValueOnce({ + ok: false, + status: 401, + json: async () => getApplicationResponse, + }); + + try { + await getApplication({ + signal: abortCtrl.signal, + apiToken: '', + appId: '', + url: '', + }); + } catch (e) { + expect(e.message).toContain('Received status:'); + } + }); + + it('returns an error when parsing the json fails', async () => { + const abortCtrl = new AbortController(); + + fetchMock.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => { + throw new Error('bad'); + }, + }); + + try { + await getApplication({ + signal: abortCtrl.signal, + apiToken: '', + appId: '', + url: '', + }); + } catch (e) { + expect(e.message).toContain('bad'); + } + }); + + it('it removes unsafe fields', async () => { + const abortCtrl = new AbortController(); + + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + fields: [ + { + id: '__proto__', + name: 'Alert Id', + key: 'alert-id', + fieldType: 'text', + }, + { + id: 'a6ide', + name: '__proto__', + key: 'alert-id', + fieldType: 'text', + }, + { + id: 'a6ide', + name: 'Alert Id', + key: '__proto__', + fieldType: 'text', + }, + { + id: 'a6ide', + name: 'Alert Id', + key: 'alert-id', + fieldType: '__proto__', + }, + { + id: 'safe-id', + name: 'Safe', + key: 'safe-key', + fieldType: 'safe-text', + }, + ], + }), + }); + + const res = await getApplication({ + signal: abortCtrl.signal, + apiToken: '', + appId: '', + url: '', + }); + + expect(res).toEqual({ + fields: [ + { + id: 'safe-id', + name: 'Safe', + key: 'safe-key', + fieldType: 'safe-text', + }, + ], + }); + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts new file mode 100644 index 0000000000000..c6f9d4bee3e13 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SwimlaneFieldMappingConfig } from './types'; + +const removeUnsafeFields = (fields: SwimlaneFieldMappingConfig[]): SwimlaneFieldMappingConfig[] => + fields.filter( + (filter) => + filter.id !== '__proto__' && + filter.key !== '__proto__' && + filter.name !== '__proto__' && + filter.fieldType !== '__proto__' + ); +export async function getApplication({ + signal, + url, + appId, + apiToken, +}: { + signal: AbortSignal; + url: string; + appId: string; + apiToken: string; +}): Promise<Record<string, any>> { + const headers: Record<string, string> = { + 'Content-Type': 'application/json', + 'Private-Token': `${apiToken}`, + }; + + const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url; + const apiUrl = urlWithoutTrailingSlash.endsWith('api') + ? urlWithoutTrailingSlash + : urlWithoutTrailingSlash + '/api'; + const applicationUrl = `${apiUrl}/app/{appId}`; + + const getApplicationUrl = (id: string) => applicationUrl.replace('{appId}', id); + + try { + const response = await fetch(getApplicationUrl(appId), { + method: 'GET', + headers, + signal, + }); + + /** + * Fetch do not throw when there is an HTTP error (status >= 400). + * We need to do it manually. + */ + + if (!response.ok) { + throw new Error( + `Received status: ${response.status} when attempting to get application with id: ${appId}` + ); + } + + const data = await response.json(); + return { ...data, fields: removeUnsafeFields(data?.fields ?? []) }; + } catch (error) { + throw new Error(`Unable to get application with id ${appId}. Error: ${error.message}`); + } +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts new file mode 100644 index 0000000000000..413b952675b8c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts @@ -0,0 +1,62 @@ +/* + * 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 { SwimlaneConnectorType, SwimlaneMappingConfig, MappingConfigurationKeys } from './types'; +import * as i18n from './translations'; + +const casesRequiredFields: MappingConfigurationKeys[] = [ + 'caseNameConfig', + 'descriptionConfig', + 'commentsConfig', + 'caseIdConfig', +]; +const casesFields = [...casesRequiredFields]; +const alertsRequiredFields: MappingConfigurationKeys[] = ['ruleNameConfig', 'alertIdConfig']; +const alertsFields = ['severityConfig', 'commentsConfig', ...alertsRequiredFields]; + +const translationMapping: Record<string, string> = { + caseIdConfig: i18n.SW_REQUIRED_CASE_ID, + alertIdConfig: i18n.SW_REQUIRED_ALERT_ID, + caseNameConfig: i18n.SW_REQUIRED_CASE_NAME, + descriptionConfig: i18n.SW_REQUIRED_DESCRIPTION, + commentsConfig: i18n.SW_REQUIRED_COMMENTS, + ruleNameConfig: i18n.SW_REQUIRED_RULE_NAME, + severityConfig: i18n.SW_REQUIRED_SEVERITY, +}; + +export const isValidFieldForConnector = ( + connector: SwimlaneConnectorType, + field: MappingConfigurationKeys +): boolean => { + if (connector === SwimlaneConnectorType.All) { + return true; + } + + return connector === SwimlaneConnectorType.Alerts + ? alertsFields.includes(field) + : casesFields.includes(field); +}; + +export const validateMappingForConnector = ( + connectorType: SwimlaneConnectorType, + mapping: SwimlaneMappingConfig +): Record<string, string> => { + if (connectorType === SwimlaneConnectorType.All || connectorType == null) { + return {}; + } + + const requiredFields = + connectorType === SwimlaneConnectorType.Alerts ? alertsRequiredFields : casesRequiredFields; + + return requiredFields.reduce((errors, field) => { + if (mapping?.[field] == null) { + errors = { ...errors, [field]: translationMapping[field] }; + } + + return errors; + }, {} as Record<string, string>); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/index.ts new file mode 100644 index 0000000000000..39a57e1bccb61 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/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 { getActionType as getSwimlaneActionType } from './swimlane'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/logo.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/logo.tsx new file mode 100644 index 0000000000000..d22ff809fe74d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/logo.tsx @@ -0,0 +1,53 @@ +/* + * 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'; + +const Logo = () => { + return ( + <svg + xmlns="http://www.w3.org/2000/svg" + width="32" + height="32" + fill="none" + stroke="null" + vectorEffect="non-scaling-stroke" + > + <g fillRule="evenodd" stroke="null" clipRule="evenodd"> + <path + fill="#19CCC0" + d="M21.536 9.338a1.053 1.053 0 00-1.29 0l-4.564 3.566a.988.988 0 000 1.566l4.725 3.691c.02.016.05.016.07 0l6.12-4.782a.054.054 0 000-.086l-5.061-3.955z" + vectorEffect="non-scaling-stroke" + /> + <path + fill="#00FFF4" + d="M15.684 31.61l10.728-8.382a.627.627 0 00.244-.494v-9.401l-11.787 9.21a.627.627 0 00-.244.494v8.08c0 .531.633.827 1.059.494z" + vectorEffect="non-scaling-stroke" + /> + <path + fill="#27AFA2" + d="M26.655 13.331L20.44 18.19l-1.703-1.331 7.917-3.527z" + vectorEffect="non-scaling-stroke" + /> + <path + fill="#028ACF" + d="M10.464 22.663c.377.294.914.294 1.291 0l4.563-3.566a.987.987 0 000-1.565l-4.724-3.692a.058.058 0 00-.071 0l-6.12 4.782a.054.054 0 000 .086l5.061 3.955z" + vectorEffect="non-scaling-stroke" + /> + <path + fill="#02AAFF" + d="M16.316.39L5.588 8.771a.628.628 0 00-.244.494v9.401l11.787-9.21a.628.628 0 00.244-.494V.883c0-.531-.633-.827-1.059-.494z" + vectorEffect="non-scaling-stroke" + /> + <path fill="#0578A5" d="M5.344 18.67l6.214-4.858 1.704 1.331-7.918 3.527z" /> + </g> + </svg> + ); +}; + +// eslint-disable-next-line import/no-default-export +export { Logo as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts new file mode 100644 index 0000000000000..1574dfe2f5384 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts @@ -0,0 +1,61 @@ +/* + * 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 applicationFields = [ + { + id: 'a6ide', + name: 'Alert Id', + key: 'alert-id', + fieldType: 'text', + }, + { + id: 'adnlas', + name: 'Severity', + key: 'severity', + fieldType: 'text', + }, + { + id: 'adnfls', + name: 'Rule Name', + key: 'rule-name', + fieldType: 'text', + }, + { + id: 'a6sst', + name: 'Case Id', + key: 'case-id-name', + fieldType: 'text', + }, + { + id: 'a6fst', + name: 'Case Name', + key: 'case-name', + fieldType: 'text', + }, + { + id: 'a6fdf', + name: 'Comments', + key: 'notes', + fieldType: 'comments', + }, + { + id: 'a6fde', + name: 'Description', + key: 'description', + fieldType: 'text', + }, +]; + +export const mappings = { + alertIdConfig: applicationFields[0], + severityConfig: applicationFields[1], + ruleNameConfig: applicationFields[2], + caseIdConfig: applicationFields[3], + caseNameConfig: applicationFields[4], + commentsConfig: applicationFields[5], + descriptionConfig: applicationFields[6], +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/index.ts new file mode 100644 index 0000000000000..ca7c39bf1378c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/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 { SwimlaneConnection } from './swimlane_connection'; +export { SwimlaneFields } from './swimlane_fields'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx new file mode 100644 index 0000000000000..05b6d8d63f1cf --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx @@ -0,0 +1,201 @@ +/* + * 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, + EuiCallOut, + EuiFieldText, + EuiFormRow, + EuiLink, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import React, { useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import * as i18n from '../translations'; +import { useKibana } from '../../../../../common/lib/kibana'; +import { useGetApplication } from '../use_get_application'; +import { SwimlaneActionConnector, SwimlaneFieldMappingConfig } from '../types'; +import { IErrorObject } from '../../../../../types'; + +interface Props { + action: SwimlaneActionConnector; + editActionConfig: (property: string, value: any) => void; + editActionSecrets: (property: string, value: any) => void; + errors: IErrorObject; + readOnly: boolean; + updateCurrentStep: (step: number) => void; + updateFields: (items: SwimlaneFieldMappingConfig[]) => void; +} + +const SwimlaneConnectionComponent: React.FunctionComponent<Props> = ({ + action, + editActionConfig, + editActionSecrets, + errors, + readOnly, + updateCurrentStep, + updateFields, +}) => { + const { + notifications: { toasts }, + } = useKibana().services; + const { apiUrl, appId } = action.config; + const { apiToken } = action.secrets; + const { docLinks } = useKibana().services; + const { getApplication } = useGetApplication({ + toastNotifications: toasts, + apiToken, + appId, + apiUrl, + }); + const isValid = apiUrl && apiToken && appId; + + const connectSwimlane = useCallback(async () => { + // fetch swimlane application configuration + const application = await getApplication(); + + if (application?.fields) { + const allFields = application.fields; + updateFields(allFields); + updateCurrentStep(2); + } + }, [getApplication, updateCurrentStep, updateFields]); + + const onChangeConfig = useCallback( + (e: React.ChangeEvent<HTMLInputElement>, key: 'apiUrl' | 'appId') => { + editActionConfig(key, e.target.value); + }, + [editActionConfig] + ); + + const onBlurConfig = useCallback( + (key: 'apiUrl' | 'appId') => { + if (!action.config[key]) { + editActionConfig(key, ''); + } + }, + [action.config, editActionConfig] + ); + + const onChangeSecrets = useCallback( + (e: React.ChangeEvent<HTMLInputElement>) => { + editActionSecrets('apiToken', e.target.value); + }, + [editActionSecrets] + ); + + const onBlurSecrets = useCallback(() => { + if (!apiToken) { + editActionSecrets('apiToken', ''); + } + }, [apiToken, editActionSecrets]); + + const isApiUrlInvalid = errors.apiUrl?.length > 0 && apiToken !== undefined; + const isAppIdInvalid = errors.appId?.length > 0 && apiToken !== undefined; + const isApiTokenInvalid = errors.apiToken?.length > 0 && apiToken !== undefined; + + return ( + <> + <EuiFormRow + id="apiUrl" + fullWidth + label={i18n.SW_API_URL_TEXT_FIELD_LABEL} + error={errors.apiUrl} + isInvalid={isApiUrlInvalid} + > + <EuiFieldText + fullWidth + name="apiUrl" + value={apiUrl ?? ''} + readOnly={readOnly} + isInvalid={isApiUrlInvalid} + data-test-subj="swimlaneApiUrlInput" + onChange={(e) => onChangeConfig(e, 'apiUrl')} + onBlur={() => onBlurConfig('apiUrl')} + /> + </EuiFormRow> + <EuiFormRow + id="appId" + fullWidth + label={i18n.SW_APP_ID_TEXT_FIELD_LABEL} + error={errors.appId} + isInvalid={isAppIdInvalid} + > + <EuiFieldText + fullWidth + name="appId" + value={appId ?? ''} + readOnly={readOnly} + isInvalid={isAppIdInvalid} + data-test-subj="swimlaneAppIdInput" + onChange={(e) => onChangeConfig(e, 'appId')} + onBlur={() => onBlurConfig('appId')} + /> + </EuiFormRow> + <EuiFormRow + id="apiToken" + fullWidth + helpText={ + <EuiLink + href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/swimlane-action-type.html`} + target="_blank" + > + <FormattedMessage + id="xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiTokenNameHelpLabel" + defaultMessage="Provide a Swimlane API Token" + /> + </EuiLink> + } + error={errors.apiToken} + isInvalid={isApiTokenInvalid} + label={i18n.SW_API_TOKEN_TEXT_FIELD_LABEL} + > + <> + {!action.id ? ( + <> + <EuiSpacer size="s" /> + <EuiText size="s" data-test-subj="rememberValuesMessage"> + {i18n.SW_REMEMBER_VALUE_LABEL} + </EuiText> + <EuiSpacer size="s" /> + </> + ) : ( + <> + <EuiSpacer size="s" /> + <EuiCallOut + size="s" + iconType="iInCircle" + data-test-subj="reenterValuesMessage" + title={i18n.SW_REENTER_VALUE_LABEL} + /> + <EuiSpacer size="m" /> + </> + )} + <EuiFieldText + fullWidth + isInvalid={isApiTokenInvalid} + readOnly={readOnly} + value={apiToken ?? ''} + data-test-subj="swimlaneApiTokenInput" + onChange={onChangeSecrets} + onBlur={onBlurSecrets} + /> + </> + </EuiFormRow> + <EuiSpacer /> + <EuiButton + disabled={!isValid} + onClick={connectSwimlane} + data-test-subj="swimlaneConfigureMapping" + > + {i18n.SW_RETRIEVE_CONFIGURATION_LABEL} + </EuiButton> + </> + ); +}; + +export const SwimlaneConnection = React.memo(SwimlaneConnectionComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx new file mode 100644 index 0000000000000..87d0964322e14 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx @@ -0,0 +1,313 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, useCallback, useEffect, useRef } from 'react'; +import { + EuiButton, + EuiFormRow, + EuiComboBox, + EuiComboBoxOptionOption, + EuiButtonGroup, +} from '@elastic/eui'; +import * as i18n from '../translations'; +import { + SwimlaneActionConnector, + SwimlaneConnectorType, + SwimlaneFieldMappingConfig, + SwimlaneMappingConfig, +} from '../types'; +import { IErrorObject } from '../../../../../types'; +import { isValidFieldForConnector } from '../helpers'; + +const SINGLE_SELECTION = { asPlainText: true }; +const EMPTY_COMBO_BOX_ARRAY: Array<EuiComboBoxOptionOption<string>> | undefined = []; + +const formatOption = (field: SwimlaneFieldMappingConfig) => ({ + label: `${field.name} (${field.key})`, + value: field.id, +}); + +const createSelectedOption = (field: SwimlaneFieldMappingConfig | null | undefined) => + field != null ? [formatOption(field)] : EMPTY_COMBO_BOX_ARRAY; + +interface Props { + action: SwimlaneActionConnector; + editActionConfig: (property: string, value: any) => void; + updateCurrentStep: (step: number) => void; + fields: SwimlaneFieldMappingConfig[]; + errors: IErrorObject; +} + +const connectorTypeButtons = [ + { id: 'all', label: 'All' }, + { id: 'alerts', label: 'Alerts' }, + { id: 'cases', label: 'Cases' }, +]; + +const SwimlaneFieldsComponent: React.FC<Props> = ({ + action, + editActionConfig, + updateCurrentStep, + fields, + errors, +}) => { + const { mappings, connectorType = SwimlaneConnectorType.All } = action.config; + const prevConnectorType = useRef<SwimlaneConnectorType>(connectorType); + const hasChangedConnectorType = connectorType !== prevConnectorType.current; + + const [fieldTypeMap, fieldIdMap] = useMemo( + () => + fields.reduce( + ([typeMap, idMap], field) => { + if (field != null) { + typeMap.set(field.fieldType, [ + ...(typeMap.get(field.fieldType) ?? []), + formatOption(field), + ]); + idMap.set(field.id, field); + } + + return [typeMap, idMap]; + }, + [ + new Map<string, Array<EuiComboBoxOptionOption<string>>>(), + new Map<string, SwimlaneFieldMappingConfig>(), + ] + ), + [fields] + ); + + const textOptions = useMemo(() => fieldTypeMap.get('text') ?? [], [fieldTypeMap]); + const commentsOptions = useMemo(() => fieldTypeMap.get('comments') ?? [], [fieldTypeMap]); + + const state = useMemo( + () => ({ + alertIdConfig: createSelectedOption(mappings?.alertIdConfig), + severityConfig: createSelectedOption(mappings?.severityConfig), + ruleNameConfig: createSelectedOption(mappings?.ruleNameConfig), + caseIdConfig: createSelectedOption(mappings?.caseIdConfig), + caseNameConfig: createSelectedOption(mappings?.caseNameConfig), + commentsConfig: createSelectedOption(mappings?.commentsConfig), + descriptionConfig: createSelectedOption(mappings?.descriptionConfig), + }), + [mappings] + ); + + const mappingErrors: Record<string, string> = useMemo( + () => (Array.isArray(errors?.mappings) ? errors?.mappings[0] : {}), + [errors] + ); + + const resetConnection = useCallback(() => { + updateCurrentStep(1); + }, [updateCurrentStep]); + + const editMappings = useCallback( + (key: keyof SwimlaneMappingConfig, e: Array<EuiComboBoxOptionOption<string>>) => { + if (e.length === 0) { + const newProps = { + ...mappings, + [key]: null, + }; + editActionConfig('mappings', newProps); + return; + } + + const option = e[0]; + const item = fieldIdMap.get(option.value ?? ''); + if (!item) { + return; + } + + const newProps = { + ...mappings, + [key]: { id: item.id, name: item.name, key: item.key, fieldType: item.fieldType }, + }; + editActionConfig('mappings', newProps); + }, + [editActionConfig, fieldIdMap, mappings] + ); + + /** + * Connector type needs to be updated on mount to All. + * Otherwise it is undefined and this will cause an error + * if the user saves the connector without any mapping + */ + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => editActionConfig('connectorType', connectorType), []); + + useEffect(() => { + if (connectorType !== prevConnectorType.current) { + prevConnectorType.current = connectorType; + } + }, [connectorType]); + + return ( + <> + <EuiFormRow id="connectorType" fullWidth label={i18n.SW_CONNECTOR_TYPE_LABEL}> + <EuiButtonGroup + name="connectorType" + legend={i18n.SW_CONNECTOR_TYPE_LABEL} + options={connectorTypeButtons} + idSelected={connectorType} + onChange={(type) => editActionConfig('connectorType', type)} + buttonSize="compressed" + /> + </EuiFormRow> + {isValidFieldForConnector(connectorType as SwimlaneConnectorType.All, 'alertIdConfig') && ( + <> + <EuiFormRow + id="alertIdConfig" + fullWidth + label={i18n.SW_ALERT_ID_FIELD_LABEL} + error={mappingErrors?.alertIdConfig} + isInvalid={mappingErrors?.alertIdConfig != null && !hasChangedConnectorType} + > + <EuiComboBox + fullWidth + selectedOptions={state.alertIdConfig} + options={textOptions} + singleSelection={SINGLE_SELECTION} + data-test-subj="swimlaneAlertIdInput" + onChange={(e) => editMappings('alertIdConfig', e)} + isInvalid={mappingErrors?.alertIdConfig != null && !hasChangedConnectorType} + /> + </EuiFormRow> + </> + )} + {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'ruleNameConfig') && ( + <> + <EuiFormRow + id="ruleNameConfig" + fullWidth + label={i18n.SW_RULE_NAME_FIELD_LABEL} + error={mappingErrors?.ruleNameConfig} + isInvalid={mappingErrors?.ruleNameConfig != null && !hasChangedConnectorType} + > + <EuiComboBox + fullWidth + selectedOptions={state.ruleNameConfig} + options={textOptions} + singleSelection={SINGLE_SELECTION} + data-test-subj="swimlaneAlertNameInput" + onChange={(e) => editMappings('ruleNameConfig', e)} + isInvalid={mappingErrors?.ruleNameConfig != null && !hasChangedConnectorType} + /> + </EuiFormRow> + </> + )} + {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'severityConfig') && ( + <> + <EuiFormRow + id="severityConfig" + fullWidth + label={i18n.SW_SEVERITY_FIELD_LABEL} + error={mappingErrors?.severityConfig} + isInvalid={mappingErrors?.severityConfig != null && !hasChangedConnectorType} + > + <EuiComboBox + fullWidth + selectedOptions={state.severityConfig} + options={textOptions} + singleSelection={SINGLE_SELECTION} + data-test-subj="swimlaneSeverityInput" + onChange={(e) => editMappings('severityConfig', e)} + isInvalid={mappingErrors?.severityConfig != null && !hasChangedConnectorType} + /> + </EuiFormRow> + </> + )} + {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'caseIdConfig') && ( + <> + <EuiFormRow + id="caseIdConfig" + fullWidth + label={i18n.SW_CASE_ID_FIELD_LABEL} + error={mappingErrors?.caseIdConfig} + isInvalid={mappingErrors?.caseIdConfig != null && !hasChangedConnectorType} + > + <EuiComboBox + fullWidth + selectedOptions={state.caseIdConfig} + options={textOptions} + singleSelection={SINGLE_SELECTION} + data-test-subj="swimlaneCaseIdConfig" + onChange={(e) => editMappings('caseIdConfig', e)} + isInvalid={mappingErrors?.caseIdConfig != null && !hasChangedConnectorType} + /> + </EuiFormRow> + </> + )} + {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'caseNameConfig') && ( + <> + <EuiFormRow + id="caseNameConfig" + fullWidth + label={i18n.SW_CASE_NAME_FIELD_LABEL} + error={mappingErrors?.caseNameConfig} + isInvalid={mappingErrors?.caseNameConfig != null && !hasChangedConnectorType} + > + <EuiComboBox + fullWidth + selectedOptions={state.caseNameConfig} + options={textOptions} + singleSelection={SINGLE_SELECTION} + data-test-subj="swimlaneCaseNameConfig" + onChange={(e) => editMappings('caseNameConfig', e)} + isInvalid={mappingErrors?.caseNameConfig != null && !hasChangedConnectorType} + /> + </EuiFormRow> + </> + )} + {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'commentsConfig') && ( + <> + <EuiFormRow + id="commentsConfig" + fullWidth + label={i18n.SW_COMMENTS_FIELD_LABEL} + error={mappingErrors?.commentsConfig} + isInvalid={mappingErrors?.commentsConfig != null && !hasChangedConnectorType} + > + <EuiComboBox + fullWidth + selectedOptions={state.commentsConfig} + options={commentsOptions} + singleSelection={SINGLE_SELECTION} + data-test-subj="swimlaneCommentsConfig" + onChange={(e) => editMappings('commentsConfig', e)} + isInvalid={mappingErrors?.commentsConfig != null && !hasChangedConnectorType} + /> + </EuiFormRow> + </> + )} + {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'descriptionConfig') && ( + <> + <EuiFormRow + id="descriptionConfig" + fullWidth + label={i18n.SW_DESCRIPTION_FIELD_LABEL} + error={mappingErrors?.descriptionConfig} + isInvalid={mappingErrors?.descriptionConfig != null && !hasChangedConnectorType} + > + <EuiComboBox + fullWidth + selectedOptions={state.descriptionConfig} + options={textOptions} + singleSelection={SINGLE_SELECTION} + data-test-subj="swimlaneDescriptionConfig" + onChange={(e) => editMappings('descriptionConfig', e)} + isInvalid={mappingErrors?.descriptionConfig != null && !hasChangedConnectorType} + /> + </EuiFormRow> + </> + )} + <EuiButton onClick={resetConnection}>{i18n.SW_CONFIGURE_API_LABEL}</EuiButton> + </> + ); +}; + +export const SwimlaneFields = React.memo(SwimlaneFieldsComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx new file mode 100644 index 0000000000000..07d78a8885c51 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx @@ -0,0 +1,219 @@ +/* + * 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 { TypeRegistry } from '../../../type_registry'; +import { registerBuiltInActionTypes } from '.././index'; +import { ActionTypeModel } from '../../../../types'; +import { SwimlaneActionConnector } from './types'; + +const ACTION_TYPE_ID = '.swimlane'; +let actionTypeModel: ActionTypeModel; + +beforeAll(() => { + const actionTypeRegistry = new TypeRegistry<ActionTypeModel>(); + registerBuiltInActionTypes({ actionTypeRegistry }); + const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); + if (getResult !== null) { + actionTypeModel = getResult; + } +}); + +describe('actionTypeRegistry.get() works', () => { + test('action type static data is as expected', () => { + expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); + }); +}); + +describe('swimlane connector validation', () => { + test('connector validation succeeds when connector is valid', async () => { + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'all', + mappings: { + alertIdConfig: { id: '1234' }, + severityConfig: { id: '1234' }, + ruleNameConfig: { id: '1234' }, + caseIdConfig: { id: '1234' }, + caseNameConfig: { id: '1234' }, + descriptionConfig: { id: '1234' }, + commentsConfig: { id: '1234' }, + }, + }, + } as SwimlaneActionConnector; + + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ + config: { errors: { apiUrl: [], appId: [], mappings: [], connectorType: [] } }, + secrets: { errors: { apiToken: [] } }, + }); + }); + + test('it validates correctly when connectorType=all', async () => { + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'all', + mappings: {}, + }, + } as SwimlaneActionConnector; + + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ + config: { errors: { apiUrl: [], appId: [], mappings: [], connectorType: [] } }, + secrets: { errors: { apiToken: [] } }, + }); + }); + + test('it validates correctly when connectorType=cases', async () => { + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'cases', + mappings: {}, + }, + } as SwimlaneActionConnector; + + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ + config: { + errors: { + apiUrl: [], + appId: [], + mappings: [ + { + caseIdConfig: 'Case ID is required.', + caseNameConfig: 'Case name is required.', + commentsConfig: 'Comments are required.', + descriptionConfig: 'Description is required.', + }, + ], + connectorType: [], + }, + }, + secrets: { errors: { apiToken: [] } }, + }); + }); + + test('it validates correctly when connectorType=alerts', async () => { + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'alerts', + mappings: {}, + }, + } as SwimlaneActionConnector; + + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ + config: { + errors: { + apiUrl: [], + appId: [], + mappings: [ + { + alertIdConfig: 'Alert ID is required.', + ruleNameConfig: 'Rule name is required.', + }, + ], + connectorType: [], + }, + }, + secrets: { errors: { apiToken: [] } }, + }); + }); + + test('it validates correctly required config/secrets fields', async () => { + const actionConnector = { + secrets: {}, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: {}, + } as SwimlaneActionConnector; + + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ + config: { + errors: { + apiUrl: ['URL is required.'], + appId: ['An App ID is required.'], + mappings: [], + connectorType: [], + }, + }, + secrets: { errors: { apiToken: ['An API token is required.'] } }, + }); + }); +}); + +describe('swimlane action params validation', () => { + test('action params validation succeeds when action params is valid', async () => { + const actionParams = { + subActionParams: { + ruleName: 'Rule Name', + alertId: 'alert-id', + }, + }; + + expect(await actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + 'subActionParams.incident.ruleName': [], + 'subActionParams.incident.alertId': [], + }, + }); + }); + + test('it validates correctly required fields', async () => { + const actionParams = { + subActionParams: { incident: {} }, + }; + + expect(await actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + 'subActionParams.incident.ruleName': ['Rule name is required.'], + 'subActionParams.incident.alertId': ['Alert ID is required.'], + }, + }); + }); + + test('it succeeds when missing incident', async () => { + const actionParams = { + subActionParams: {}, + }; + + expect(await actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + 'subActionParams.incident.ruleName': [], + 'subActionParams.incident.alertId': [], + }, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx new file mode 100644 index 0000000000000..5e06e3935eebd --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx @@ -0,0 +1,106 @@ +/* + * 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 { isEmpty } from 'lodash'; +import { lazy } from 'react'; +import { + ActionTypeModel, + ConnectorValidationResult, + GenericValidationResult, +} from '../../../../types'; +import { + SwimlaneActionConnector, + SwimlaneConfig, + SwimlaneSecrets, + SwimlaneActionParams, +} from './types'; +import * as i18n from './translations'; +import { isValidUrl } from '../../../lib/value_validators'; +import { validateMappingForConnector } from './helpers'; + +export function getActionType(): ActionTypeModel< + SwimlaneConfig, + SwimlaneSecrets, + SwimlaneActionParams +> { + return { + id: '.swimlane', + iconClass: lazy(() => import('./logo')), + selectMessage: i18n.SW_SELECT_MESSAGE_TEXT, + actionTypeTitle: i18n.SW_ACTION_TYPE_TITLE, + validateConnector: async ( + action: SwimlaneActionConnector + ): Promise<ConnectorValidationResult<SwimlaneConfig, SwimlaneSecrets>> => { + const configErrors = { + apiUrl: new Array<string>(), + appId: new Array<string>(), + connectorType: new Array<string>(), + mappings: new Array<Record<string, string>>(), + }; + const secretsErrors = { + apiToken: new Array<string>(), + }; + + const validationResult = { + config: { errors: configErrors }, + secrets: { errors: secretsErrors }, + }; + + if (!action.config.apiUrl) { + configErrors.apiUrl = [...configErrors.apiUrl, i18n.SW_API_URL_REQUIRED]; + } else if (action.config.apiUrl) { + if (!isValidUrl(action.config.apiUrl)) { + configErrors.apiUrl = [...configErrors.apiUrl, i18n.SW_API_URL_INVALID]; + } + } + + if (!action.secrets.apiToken) { + secretsErrors.apiToken = [...secretsErrors.apiToken, i18n.SW_REQUIRED_API_TOKEN_TEXT]; + } + + if (!action.config.appId) { + configErrors.appId = [...configErrors.appId, i18n.SW_REQUIRED_APP_ID_TEXT]; + } + + const mappingErrors = validateMappingForConnector( + action.config.connectorType, + action.config.mappings + ); + + if (!isEmpty(mappingErrors)) { + configErrors.mappings = [...configErrors.mappings, mappingErrors]; + } + + return validationResult; + }, + validateParams: async ( + actionParams: SwimlaneActionParams + ): Promise<GenericValidationResult<unknown>> => { + const errors = { + 'subActionParams.incident.ruleName': new Array<string>(), + 'subActionParams.incident.alertId': new Array<string>(), + }; + const validationResult = { + errors, + }; + + const hasIncident = actionParams.subActionParams && actionParams.subActionParams.incident; + + if (hasIncident && !actionParams.subActionParams.incident.ruleName?.length) { + errors['subActionParams.incident.ruleName'].push(i18n.SW_REQUIRED_RULE_NAME); + } + + if (hasIncident && !actionParams.subActionParams.incident.alertId?.length) { + errors['subActionParams.incident.alertId'].push(i18n.SW_REQUIRED_ALERT_ID); + } + + return validationResult; + }, + actionConnectorFields: lazy(() => import('./swimlane_connectors')), + actionParamsFields: lazy(() => import('./swimlane_params')), + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx new file mode 100644 index 0000000000000..6740179d786f2 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx @@ -0,0 +1,319 @@ +/* + * 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 { mountWithIntl, nextTick } from '@kbn/test/jest'; +import { act } from 'react-dom/test-utils'; +import { SwimlaneActionConnector } from './types'; +import SwimlaneActionConnectorFields from './swimlane_connectors'; +import { useGetApplication } from './use_get_application'; +import { applicationFields, mappings } from './mocks'; + +jest.mock('../../../../common/lib/kibana'); +jest.mock('./use_get_application'); + +const useGetApplicationMock = useGetApplication as jest.Mock; +const getApplication = jest.fn(); + +describe('SwimlaneActionConnectorFields renders', () => { + beforeAll(() => { + useGetApplicationMock.mockReturnValue({ + getApplication, + isLoading: false, + }); + }); + + test('all connector fields are rendered', async () => { + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'all', + mappings, + }, + } as SwimlaneActionConnector; + + const wrapper = mountWithIntl( + <SwimlaneActionConnectorFields + action={actionConnector} + errors={{ connectorType: [], appId: [], apiUrl: [], mappings: [], apiToken: [] }} + editActionConfig={() => {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="swimlaneApiUrlInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneAppIdInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneApiTokenInput"]').exists()).toBeTruthy(); + }); + + test('should display a message on create to remember credentials', () => { + const actionConnector = { + actionTypeId: '.swimlane', + secrets: {}, + config: {}, + } as SwimlaneActionConnector; + + const wrapper = mountWithIntl( + <SwimlaneActionConnectorFields + action={actionConnector} + errors={{ connectorType: [], appId: [], apiUrl: [], mappings: [], apiToken: [] }} + editActionConfig={() => {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); + }); + + test('should display a message on edit to re-enter credentials', () => { + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'all', + mappings, + }, + } as SwimlaneActionConnector; + + const wrapper = mountWithIntl( + <SwimlaneActionConnectorFields + action={actionConnector} + errors={{ connectorType: [], appId: [], apiUrl: [], mappings: [], apiToken: [] }} + editActionConfig={() => {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); + }); + + test('renders the mappings correctly - connector type all', async () => { + getApplication.mockResolvedValue({ + fields: applicationFields, + }); + + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'all', + mappings, + }, + } as SwimlaneActionConnector; + + const wrapper = mountWithIntl( + <SwimlaneActionConnectorFields + action={actionConnector} + errors={{ connectorType: [], appId: [], apiUrl: [], mappings: [], apiToken: [] }} + editActionConfig={() => {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + + await act(async () => { + wrapper.find('[data-test-subj="swimlaneConfigureMapping"]').first().simulate('click'); + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="swimlaneAlertIdInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneAlertNameInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneSeverityInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneCaseIdConfig"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneCaseNameConfig"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneCommentsConfig"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneDescriptionConfig"]').exists()).toBeTruthy(); + }); + + test('renders the mappings correctly - connector type cases', async () => { + getApplication.mockResolvedValue({ + fields: applicationFields, + }); + + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'cases', + mappings, + }, + } as SwimlaneActionConnector; + + const wrapper = mountWithIntl( + <SwimlaneActionConnectorFields + action={actionConnector} + errors={{ connectorType: [], appId: [], apiUrl: [], mappings: [], apiToken: [] }} + editActionConfig={() => {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + + await act(async () => { + wrapper.find('[data-test-subj="swimlaneConfigureMapping"]').first().simulate('click'); + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="swimlaneAlertIdInput"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="swimlaneAlertNameInput"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="swimlaneSeverityInput"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="swimlaneCaseIdConfig"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneCaseNameConfig"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneCommentsConfig"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneDescriptionConfig"]').exists()).toBeTruthy(); + }); + + test('renders the mappings correctly - connector type alerts', async () => { + getApplication.mockResolvedValue({ + fields: applicationFields, + }); + + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'alerts', + mappings, + }, + } as SwimlaneActionConnector; + + const wrapper = mountWithIntl( + <SwimlaneActionConnectorFields + action={actionConnector} + errors={{ connectorType: [], appId: [], apiUrl: [], mappings: [], apiToken: [] }} + editActionConfig={() => {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + + await act(async () => { + wrapper.find('[data-test-subj="swimlaneConfigureMapping"]').first().simulate('click'); + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="swimlaneAlertIdInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneAlertNameInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneSeverityInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneCaseIdConfig"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="swimlaneCaseNameConfig"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="swimlaneCommentsConfig"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneDescriptionConfig"]').exists()).toBeFalsy(); + }); + + test('renders the correct options per field', async () => { + getApplication.mockResolvedValue({ + fields: applicationFields, + }); + + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'all', + mappings, + }, + } as SwimlaneActionConnector; + + const textOptions = [ + { label: 'Alert Id (alert-id)', value: 'a6ide' }, + { label: 'Severity (severity)', value: 'adnlas' }, + { label: 'Rule Name (rule-name)', value: 'adnfls' }, + { label: 'Case Id (case-id-name)', value: 'a6sst' }, + { label: 'Case Name (case-name)', value: 'a6fst' }, + { label: 'Description (description)', value: 'a6fde' }, + ]; + + const commentOptions = [{ label: 'Comments (notes)', value: 'a6fdf' }]; + + const wrapper = mountWithIntl( + <SwimlaneActionConnectorFields + action={actionConnector} + errors={{ connectorType: [], appId: [], apiUrl: [], mappings: [], apiToken: [] }} + editActionConfig={() => {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + + await act(async () => { + wrapper.find('[data-test-subj="swimlaneConfigureMapping"]').first().simulate('click'); + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="swimlaneAlertIdInput"]').first().prop('options')).toEqual( + textOptions + ); + expect( + wrapper.find('[data-test-subj="swimlaneAlertNameInput"]').first().prop('options') + ).toEqual(textOptions); + expect( + wrapper.find('[data-test-subj="swimlaneSeverityInput"]').first().prop('options') + ).toEqual(textOptions); + expect(wrapper.find('[data-test-subj="swimlaneCaseIdConfig"]').first().prop('options')).toEqual( + textOptions + ); + expect( + wrapper.find('[data-test-subj="swimlaneCaseNameConfig"]').first().prop('options') + ).toEqual(textOptions); + expect( + wrapper.find('[data-test-subj="swimlaneCommentsConfig"]').first().prop('options') + ).toEqual(commentOptions); + expect( + wrapper.find('[data-test-subj="swimlaneDescriptionConfig"]').first().prop('options') + ).toEqual(textOptions); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx new file mode 100644 index 0000000000000..acf9f38e9ba48 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Fragment, useCallback, useMemo, useState } from 'react'; +import { EuiForm, EuiSpacer, EuiStepsHorizontal, EuiStepStatus } from '@elastic/eui'; +import * as i18n from './translations'; +import { ActionConnectorFieldsProps } from '../../../../types'; +import { SwimlaneActionConnector, SwimlaneFieldMappingConfig } from './types'; +import { SwimlaneConnection, SwimlaneFields } from './steps'; + +const SwimlaneActionConnectorFields: React.FunctionComponent< + ActionConnectorFieldsProps<SwimlaneActionConnector> +> = ({ errors, action, editActionConfig, editActionSecrets, readOnly }) => { + const [currentStep, setCurrentStep] = useState<number>(1); + const [stepsStatuses, setStepsStatuses] = useState<{ + connection: EuiStepStatus; + fields: EuiStepStatus; + }>({ connection: 'incomplete', fields: 'incomplete' }); + const [fields, setFields] = useState<SwimlaneFieldMappingConfig[]>([]); + + const updateCurrentStep = useCallback( + (step: number) => { + setCurrentStep(step); + if (step === 2) { + setStepsStatuses((statuses) => ({ ...statuses, connection: 'complete' })); + } else if (step === 1) { + setStepsStatuses({ + fields: 'incomplete', + connection: 'incomplete', + }); + editActionConfig('mappings', action.config.mappings); + } + }, + [action.config.mappings, editActionConfig] + ); + + const setupSteps = useMemo( + () => [ + { + title: i18n.SW_CONFIGURE_CONNECTION_LABEL, + status: stepsStatuses.connection, + onClick: () => updateCurrentStep(1), + }, + { + title: i18n.SW_MAPPING_TITLE_TEXT_FIELD_LABEL, + disabled: stepsStatuses.connection !== 'complete', + status: stepsStatuses.fields, + onClick: () => updateCurrentStep(2), + }, + ], + [stepsStatuses.connection, stepsStatuses.fields, updateCurrentStep] + ); + + const editActionConfigCb = useCallback( + (k: string, v: string) => { + editActionConfig(k, v); + if ( + Object.values(errors?.mappings ?? {}).every((mappingError) => mappingError.length === 0) + ) { + setStepsStatuses((statuses) => ({ ...statuses, fields: 'complete' })); + } else { + setStepsStatuses((statuses) => ({ ...statuses, fields: 'incomplete' })); + } + }, + [editActionConfig, errors?.mappings] + ); + + return ( + <Fragment> + <EuiStepsHorizontal steps={setupSteps} /> + <EuiSpacer size="l" /> + <EuiForm> + {currentStep === 1 && ( + <SwimlaneConnection + action={action} + editActionConfig={editActionConfigCb} + editActionSecrets={editActionSecrets} + readOnly={readOnly} + errors={errors} + updateCurrentStep={updateCurrentStep} + updateFields={setFields} + /> + )} + {currentStep === 2 && ( + <SwimlaneFields + action={action} + editActionConfig={editActionConfigCb} + updateCurrentStep={updateCurrentStep} + fields={fields} + errors={errors} + /> + )} + </EuiForm> + </Fragment> + ); +}; + +// eslint-disable-next-line import/no-default-export +export { SwimlaneActionConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx new file mode 100644 index 0000000000000..32cf2c3c786d3 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import SwimlaneParamsFields from './swimlane_params'; +import { SwimlaneConnectorType } from './types'; +import { mappings } from './mocks'; + +describe('SwimlaneParamsFields renders', () => { + const editAction = jest.fn(); + const actionParams = { + subAction: 'pushToService', + subActionParams: { + incident: { + alertId: '3456789', + ruleName: 'rule name', + severity: 'critical', + caseId: null, + caseName: null, + description: null, + externalId: null, + }, + comments: [], + }, + }; + + const connector = { + secrets: {}, + config: { mappings, connectorType: SwimlaneConnectorType.All }, + id: 'test', + actionTypeId: '.test', + name: 'Test', + isPreconfigured: false, + }; + + const defaultProps = { + actionParams, + errors: { + 'subActionParams.incident.ruleName': [], + 'subActionParams.incident.alertId': [], + }, + editAction, + index: 0, + messageVariables: [], + actionConnector: connector, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('all params fields are rendered', () => { + const wrapper = mountWithIntl(<SwimlaneParamsFields {...defaultProps} />); + + expect(wrapper.find('[data-test-subj="severity"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="comments"]').exists()).toBeTruthy(); + }); + + test('it set the correct default params', () => { + mountWithIntl(<SwimlaneParamsFields {...defaultProps} actionParams={{}} />); + expect(editAction).toHaveBeenCalledWith('subAction', 'pushToService', 0); + expect(editAction).toHaveBeenCalledWith( + 'subActionParams', + { + incident: { alertId: '{{alert.id}}', ruleName: '{{rule.name}}' }, + comments: [], + }, + 0 + ); + }); + + test('it reset the fields when connector changes', () => { + const wrapper = mountWithIntl(<SwimlaneParamsFields {...defaultProps} />); + expect(editAction).not.toHaveBeenCalled(); + + wrapper.setProps({ actionConnector: { ...connector, id: '1234' } }); + expect(editAction).toHaveBeenCalledWith( + 'subActionParams', + { + incident: { alertId: '{{alert.id}}', ruleName: '{{rule.name}}' }, + comments: [], + }, + 0 + ); + }); + + test('it set the severity', () => { + const wrapper = mountWithIntl(<SwimlaneParamsFields {...defaultProps} />); + expect(editAction).not.toHaveBeenCalled(); + + wrapper.setProps({ actionConnector: { ...connector, id: '1234' } }); + expect(editAction).toHaveBeenCalledWith( + 'subActionParams', + { + incident: { alertId: '{{alert.id}}', ruleName: '{{rule.name}}' }, + comments: [], + }, + 0 + ); + }); + + describe('UI updates', () => { + const changeEvent = { target: { value: 'Bug' } } as React.ChangeEvent<HTMLSelectElement>; + const simpleFields = [ + { dataTestSubj: 'input[data-test-subj="severityInput"]', key: 'severity' }, + ]; + + simpleFields.forEach((field) => + test(`${field.key} update triggers editAction`, () => { + const wrapper = mountWithIntl(<SwimlaneParamsFields {...defaultProps} />); + const theField = wrapper.find(field.dataTestSubj).first(); + theField.prop('onChange')!(changeEvent); + expect(editAction.mock.calls[0][1].incident[field.key]).toEqual(changeEvent.target.value); + }) + ); + + test('A comment triggers editAction', () => { + const wrapper = mountWithIntl(<SwimlaneParamsFields {...defaultProps} />); + const comments = wrapper.find('textarea[data-test-subj="commentsTextArea"]'); + expect(comments.simulate('change', changeEvent)); + expect(editAction.mock.calls[0][1].comments.length).toEqual(1); + }); + + test('An empty comment does not trigger editAction', () => { + const wrapper = mountWithIntl(<SwimlaneParamsFields {...defaultProps} />); + const emptyComment = { target: { value: '' } }; + const comments = wrapper.find('[data-test-subj="commentsTextArea"] textarea'); + expect(comments.simulate('change', emptyComment)); + expect(editAction.mock.calls.length).toEqual(0); + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx new file mode 100644 index 0000000000000..9bd14a06d657a --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useEffect, useRef, useMemo } from 'react'; +import { EuiCallOut, EuiFormRow, EuiSpacer } from '@elastic/eui'; +import * as i18n from './translations'; +import { ActionParamsProps } from '../../../../types'; +import { SwimlaneActionConnector, SwimlaneActionParams, SwimlaneConnectorType } from './types'; +import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; +import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; + +const SwimlaneParamsFields: React.FunctionComponent<ActionParamsProps<SwimlaneActionParams>> = ({ + actionParams, + editAction, + index, + messageVariables, + actionConnector, +}) => { + const { incident, comments } = useMemo( + () => + actionParams.subActionParams ?? + (({ + incident: {}, + comments: [], + } as unknown) as SwimlaneActionParams['subActionParams']), + [actionParams.subActionParams] + ); + + const actionConnectorRef = useRef(actionConnector?.id ?? ''); + + const { + mappings, + connectorType, + } = ((actionConnector as unknown) as SwimlaneActionConnector).config; + const { hasAlertId, hasRuleName, hasComments, hasSeverity } = useMemo( + () => ({ + hasAlertId: mappings.alertIdConfig != null, + hasRuleName: mappings.ruleNameConfig != null, + hasComments: mappings.commentsConfig != null, + hasSeverity: mappings.severityConfig != null, + }), + [ + mappings.alertIdConfig, + mappings.ruleNameConfig, + mappings.commentsConfig, + mappings.severityConfig, + ] + ); + + /** + * The user can use either a connector of type alerts or all. + * If the connector is of type all we should check if all + * required field have been configured. + */ + const showMappingWarning = + connectorType === SwimlaneConnectorType.Cases || !hasRuleName || !hasAlertId; + + const editSubActionProperty = useCallback( + (key: string, value: any) => { + if (key === 'comments') { + return editAction('subActionParams', { incident, comments: value }, index); + } + + return editAction( + 'subActionParams', + { + incident: { ...incident, [key]: value }, + comments, + }, + index + ); + }, + [editAction, incident, comments, index] + ); + + const editComment = useCallback( + (key, value) => { + if (value.length > 0) { + editSubActionProperty(key, [{ commentId: '1', comment: value }]); + } + }, + [editSubActionProperty] + ); + + useEffect(() => { + if (actionConnector != null && actionConnectorRef.current !== actionConnector.id) { + actionConnectorRef.current = actionConnector.id; + editAction( + 'subActionParams', + { + incident: { alertId: '{{alert.id}}', ruleName: '{{rule.name}}' }, + comments: [], + }, + index + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actionConnector]); + + useEffect(() => { + if (!actionParams.subAction) { + editAction('subAction', 'pushToService', index); + } + + if (!actionParams.subActionParams) { + editAction( + 'subActionParams', + { + incident: { alertId: '{{alert.id}}', ruleName: '{{rule.name}}' }, + comments: [], + }, + index + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actionParams]); + + return !showMappingWarning ? ( + <> + {hasSeverity && ( + <> + <EuiFormRow fullWidth label={i18n.SW_SEVERITY_FIELD_LABEL}> + <TextFieldWithMessageVariables + index={index} + data-test-subj="severity" + editAction={editSubActionProperty} + messageVariables={messageVariables} + paramsProperty={'severity'} + inputTargetValue={incident.severity ?? undefined} + /> + </EuiFormRow> + <EuiSpacer size="m" /> + </> + )} + {hasComments && ( + <TextAreaWithMessageVariables + data-test-subj="comments" + index={index} + editAction={editComment} + messageVariables={messageVariables} + paramsProperty={'comments'} + inputTargetValue={comments && comments.length > 0 ? comments[0].comment : undefined} + label={i18n.SW_COMMENTS_FIELD_LABEL} + /> + )} + </> + ) : ( + <EuiCallOut title={i18n.EMPTY_MAPPING_WARNING_TITLE} color="warning" iconType="help"> + {i18n.EMPTY_MAPPING_WARNING_DESC} + </EuiCallOut> + ); +}; + +// eslint-disable-next-line import/no-default-export +export { SwimlaneParamsFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts new file mode 100644 index 0000000000000..726997cb4456a --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts @@ -0,0 +1,282 @@ +/* + * 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 SW_SELECT_MESSAGE_TEXT = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.selectMessageText', + { + defaultMessage: 'Create record in Swimlane', + } +); + +export const SW_ACTION_TYPE_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.actionTypeTitle', + { + defaultMessage: 'Create Swimlane Record', + } +); + +export const SW_REQUIRED_RULE_NAME = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredRuleName', + { + defaultMessage: 'Rule name is required.', + } +); + +export const SW_REQUIRED_APP_ID_TEXT = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAppIdText', + { + defaultMessage: 'An App ID is required.', + } +); + +export const SW_REQUIRED_FIELD_MAPPINGS_TEXT = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredFieldMappingsText', + { + defaultMessage: 'Field mappings are required.', + } +); + +export const SW_REQUIRED_API_TOKEN_TEXT = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredApiTokenText', + { + defaultMessage: 'An API token is required.', + } +); + +export const SW_GET_APPLICATION_API_ERROR = (id: string | null) => + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlane.unableToGetApplicationMessage', + { + defaultMessage: 'Unable to get application with id {id}', + values: { id }, + } + ); + +export const SW_GET_APPLICATION_API_NO_FIELDS_ERROR = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlane.unableToGetApplicationFieldsMessage', + { + defaultMessage: 'Unable to get application fields', + } +); + +export const SW_API_URL_TEXT_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiUrlTextFieldLabel', + { + defaultMessage: 'API Url', + } +); + +export const SW_API_URL_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.requiredApiUrlTextField', + { + defaultMessage: 'URL is required.', + } +); + +export const SW_API_URL_INVALID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.invalidApiUrlTextField', + { + defaultMessage: 'URL is invalid.', + } +); + +export const SW_APP_ID_TEXT_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.appIdTextFieldLabel', + { + defaultMessage: 'Application ID', + } +); + +export const SW_API_TOKEN_TEXT_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiTokenTextFieldLabel', + { + defaultMessage: 'API Token', + } +); + +export const SW_MAPPING_TITLE_TEXT_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingTitleTextFieldLabel', + { + defaultMessage: 'Configure Field Mappings', + } +); + +export const SW_ALERT_SOURCE_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.alertSourceFieldLabel', + { + defaultMessage: 'Alert source', + } +); + +export const SW_SEVERITY_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.severityFieldLabel', + { + defaultMessage: 'Severity', + } +); + +export const SW_MAPPING_DESCRIPTION_TEXT_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingDescriptionTextFieldLabel', + { + defaultMessage: 'Used to specify the field names in the Swimlane Application', + } +); + +export const SW_RULE_NAME_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.ruleNameFieldLabel', + { + defaultMessage: 'Rule name', + } +); + +export const SW_ALERT_ID_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.alertIdFieldLabel', + { + defaultMessage: 'Alert ID', + } +); + +export const SW_CASE_ID_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.caseIdFieldLabel', + { + defaultMessage: 'Case ID', + } +); + +export const SW_CASE_NAME_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.caseNameFieldLabel', + { + defaultMessage: 'Case name', + } +); + +export const SW_COMMENTS_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.commentsFieldLabel', + { + defaultMessage: 'Comments', + } +); + +export const SW_DESCRIPTION_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.descriptionFieldLabel', + { + defaultMessage: 'Description', + } +); + +export const SW_REMEMBER_VALUE_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.rememberValueLabel', + { defaultMessage: 'Remember this value. You must reenter it each time you edit the connector.' } +); + +export const SW_REENTER_VALUE_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.reenterValueLabel', + { defaultMessage: 'This key is encrypted. Please reenter a value for this field.' } +); + +export const SW_CONFIGURE_CONNECTION_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.configureConnectionLabel', + { defaultMessage: 'Configure API Connection' } +); + +export const SW_RETRIEVE_CONFIGURATION_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.retrieveConfigurationLabel', + { defaultMessage: 'Configure Fields' } +); + +export const SW_CONFIGURE_API_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.configureAPILabel', + { defaultMessage: 'Configure API' } +); + +export const SW_CONNECTOR_TYPE_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.connectorType', + { + defaultMessage: 'Connector Type', + } +); + +export const SW_FIELD_MAPPING_IS_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingFieldRequired', + { + defaultMessage: 'Field mapping is required.', + } +); + +export const EMPTY_MAPPING_WARNING_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.emptyMappingWarningTitle', + { + defaultMessage: 'This connector has missing field mappings', + } +); + +export const EMPTY_MAPPING_WARNING_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.emptyMappingWarningDesc', + { + defaultMessage: + 'This connector cannot be selected because it is missing the required case field mappings. You can edit this connector to add required field mappings or select a connector of type Alerts.', + } +); + +export const SW_REQUIRED_ALERT_SOURCE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAlertSource', + { + defaultMessage: 'Alert source is required.', + } +); + +export const SW_REQUIRED_SEVERITY = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredSeverity', + { + defaultMessage: 'Severity is required.', + } +); + +export const SW_REQUIRED_CASE_NAME = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredCaseName', + { + defaultMessage: 'Case name is required.', + } +); + +export const SW_REQUIRED_CASE_ID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredCaseID', + { + defaultMessage: 'Case ID is required.', + } +); + +export const SW_REQUIRED_COMMENTS = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredComments', + { + defaultMessage: 'Comments are required.', + } +); + +export const SW_REQUIRED_DESCRIPTION = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredDescription', + { + defaultMessage: 'Description is required.', + } +); + +export const SW_REQUIRED_ALERT_ID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAlertID', + { + defaultMessage: 'Alert ID is required.', + } +); + +export const SW_ALERT_SOURCE_TOOLTIP = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.alertSourceTooltip', + { + defaultMessage: 'The index of the alert. Use {index} in Detections.', + values: { index: '{{context.rule.output_index}}' }, + } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts new file mode 100644 index 0000000000000..f0a54e8b6c3bf --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable @kbn/eslint/no-restricted-paths */ + +import { UserConfiguredActionConnector } from '../../../../types'; +import { + ExecutorSubActionPushParams, + MappingConfigType, +} from '../../../../../../actions/server/builtin_action_types/swimlane/types'; + +export type SwimlaneActionConnector = UserConfiguredActionConnector< + SwimlaneConfig, + SwimlaneSecrets +>; + +export interface SwimlaneConfig { + apiUrl: string; + appId: string; + connectorType: SwimlaneConnectorType; + mappings: SwimlaneMappingConfig; +} + +export type MappingConfigurationKeys = keyof MappingConfigType; +export type SwimlaneMappingConfig = Record<keyof MappingConfigType, SwimlaneFieldMappingConfig>; + +export interface SwimlaneFieldMappingConfig { + id: string; + key: string; + name: string; + fieldType: string; +} + +export interface SwimlaneSecrets { + apiToken: string; +} + +export interface SwimlaneActionParams { + subAction: string; + subActionParams: ExecutorSubActionPushParams; +} + +export interface SwimlaneFieldMap { + key: string; + name: string; +} + +export enum SwimlaneConnectorType { + All = 'all', + Alerts = 'alerts', + Cases = 'cases', +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.test.tsx new file mode 100644 index 0000000000000..4744c4d22fdc9 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.test.tsx @@ -0,0 +1,180 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; + +import { useKibana } from '../../../../common/lib/kibana'; +import { getApplication } from './api'; +import { SwimlaneActionConnector } from './types'; +import { useGetApplication, UseGetApplication } from './use_get_application'; + +jest.mock('./api'); +jest.mock('../../../../common/lib/kibana'); + +const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>; +const getApplicationMock = getApplication as jest.Mock; + +const action = { + secrets: { apiToken: 'token' }, + id: 'test', + actionTypeId: '.swimlane', + name: 'Swimlane', + isPreconfigured: false, + config: { + apiUrl: 'https://test.swimlane.com/', + appId: 'bcq16kdTbz5jlwM6h', + mappings: {}, + }, +} as SwimlaneActionConnector; + +describe('useGetApplication', () => { + const { services } = useKibanaMock(); + getApplicationMock.mockResolvedValue({ + data: { fields: [] }, + }); + const abortCtrl = new AbortController(); + + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetApplication>(() => + useGetApplication({ + appId: action.config.appId, + apiToken: action.secrets.apiToken, + apiUrl: action.config.apiUrl, + toastNotifications: services.notifications.toasts, + }) + ); + + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + getApplication: result.current.getApplication, + }); + }); + }); + + it('calls getApplication with correct arguments', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetApplication>(() => + useGetApplication({ + appId: action.config.appId, + apiToken: action.secrets.apiToken, + apiUrl: action.config.apiUrl, + toastNotifications: services.notifications.toasts, + }) + ); + + await waitForNextUpdate(); + + result.current.getApplication(); + await waitForNextUpdate(); + expect(getApplicationMock).toBeCalledWith({ + signal: abortCtrl.signal, + appId: action.config.appId, + apiToken: action.secrets.apiToken, + url: action.config.apiUrl, + }); + }); + }); + + it('get application', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetApplication>(() => + useGetApplication({ + appId: action.config.appId, + apiToken: action.secrets.apiToken, + apiUrl: action.config.apiUrl, + toastNotifications: services.notifications.toasts, + }) + ); + + await waitForNextUpdate(); + result.current.getApplication(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + isLoading: false, + getApplication: result.current.getApplication, + }); + }); + }); + + it('set isLoading to true when getting the application', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetApplication>(() => + useGetApplication({ + appId: action.config.appId, + apiToken: action.secrets.apiToken, + apiUrl: action.config.apiUrl, + toastNotifications: services.notifications.toasts, + }) + ); + + await waitForNextUpdate(); + result.current.getApplication(); + + expect(result.current.isLoading).toBe(true); + }); + }); + + it('it displays an error when http throws an error', async () => { + getApplicationMock.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetApplication>(() => + useGetApplication({ + appId: action.config.appId, + apiToken: action.secrets.apiToken, + apiUrl: action.config.apiUrl, + toastNotifications: services.notifications.toasts, + }) + ); + await waitForNextUpdate(); + result.current.getApplication(); + + expect(result.current).toEqual({ + isLoading: false, + getApplication: result.current.getApplication, + }); + + expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({ + title: 'Unable to get application with id bcq16kdTbz5jlwM6h', + text: 'Something went wrong', + }); + }); + }); + + it('it displays an error when the response does not contain the correct fields', async () => { + getApplicationMock.mockResolvedValue({}); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetApplication>(() => + useGetApplication({ + appId: action.config.appId, + apiToken: action.secrets.apiToken, + apiUrl: action.config.apiUrl, + toastNotifications: services.notifications.toasts, + }) + ); + await waitForNextUpdate(); + result.current.getApplication(); + await waitForNextUpdate(); + + expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({ + title: 'Unable to get application with id bcq16kdTbz5jlwM6h', + text: 'Unable to get application fields', + }); + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx new file mode 100644 index 0000000000000..f18770067b8a8 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useState, useCallback, useRef } from 'react'; +import { ToastsApi } from 'kibana/public'; +import { getApplication as getApplicationApi } from './api'; +import * as i18n from './translations'; +import { SwimlaneFieldMappingConfig } from './types'; + +interface Props { + toastNotifications: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; + appId: string; + apiToken: string; + apiUrl: string; +} + +export interface UseGetApplication { + getApplication: () => Promise<{ fields?: SwimlaneFieldMappingConfig[] } | undefined>; + isLoading: boolean; +} + +export const useGetApplication = ({ + toastNotifications, + appId, + apiToken, + apiUrl, +}: Props): UseGetApplication => { + const [isLoading, setIsLoading] = useState(false); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); + + const getApplication = useCallback(async () => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + setIsLoading(true); + + const data = await getApplicationApi({ + signal: abortCtrlRef.current.signal, + appId, + apiToken, + url: apiUrl, + }); + + if (!isCancelledRef.current) { + setIsLoading(false); + if (!data.fields) { + // If the response was malformed and fields doesn't exist, show an error toast + toastNotifications.addDanger({ + title: i18n.SW_GET_APPLICATION_API_ERROR(appId), + text: i18n.SW_GET_APPLICATION_API_NO_FIELDS_ERROR, + }); + return; + } + return data; + } + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { + toastNotifications.addDanger({ + title: i18n.SW_GET_APPLICATION_API_ERROR(appId), + text: error.message, + }); + } + setIsLoading(false); + } + } + }, [apiToken, apiUrl, appId, toastNotifications]); + + return { + isLoading, + getApplication, + }; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx index 4428d635c6493..cc7e08bc73d15 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx @@ -9,7 +9,7 @@ import React, { useEffect, useState } from 'react'; import { EuiFlexItem, EuiCard, EuiIcon, EuiFlexGrid, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EuiToolTip } from '@elastic/eui'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage } from '@kbn/i18n/react'; import { ActionType, ActionTypeIndex, ActionTypeRegistryContract } from '../../../types'; import { loadActionTypes } from '../../lib/action_connector_api'; import { actionTypeCompare } from '../../lib/action_type_compare'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx index 2d111d5405230..26f11fa3326ab 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx @@ -175,6 +175,7 @@ const AlertAdd = ({ aria-labelledby="flyoutAlertAddTitle" size="m" maxWidth={620} + ownFocus={false} > <EuiFlyoutHeader hasBorder> <EuiTitle size="s" data-test-subj="addAlertFlyoutTitle"> diff --git a/x-pack/plugins/uptime/common/constants/alerts.ts b/x-pack/plugins/uptime/common/constants/alerts.ts index 37258fca3bc4d..cb31d83839590 100644 --- a/x-pack/plugins/uptime/common/constants/alerts.ts +++ b/x-pack/plugins/uptime/common/constants/alerts.ts @@ -8,7 +8,8 @@ import { ActionGroup } from '../../../alerting/common'; export type MonitorStatusActionGroup = ActionGroup<'xpack.uptime.alerts.actionGroups.monitorStatus'>; -export type TLSActionGroup = ActionGroup<'xpack.uptime.alerts.actionGroups.tls'>; +export type TLSLegacyActionGroup = ActionGroup<'xpack.uptime.alerts.actionGroups.tls'>; +export type TLSActionGroup = ActionGroup<'xpack.uptime.alerts.actionGroups.tlsCertificate'>; export type DurationAnomalyActionGroup = ActionGroup<'xpack.uptime.alerts.actionGroups.durationAnomaly'>; export const MONITOR_STATUS: MonitorStatusActionGroup = { @@ -16,8 +17,13 @@ export const MONITOR_STATUS: MonitorStatusActionGroup = { name: 'Uptime Down Monitor', }; -export const TLS: TLSActionGroup = { +export const TLS_LEGACY: TLSLegacyActionGroup = { id: 'xpack.uptime.alerts.actionGroups.tls', + name: 'Uptime TLS Alert (Legacy)', +}; + +export const TLS: TLSActionGroup = { + id: 'xpack.uptime.alerts.actionGroups.tlsCertificate', name: 'Uptime TLS Alert', }; @@ -28,16 +34,19 @@ export const DURATION_ANOMALY: DurationAnomalyActionGroup = { export const ACTION_GROUP_DEFINITIONS: { MONITOR_STATUS: MonitorStatusActionGroup; + TLS_LEGACY: TLSLegacyActionGroup; TLS: TLSActionGroup; DURATION_ANOMALY: DurationAnomalyActionGroup; } = { MONITOR_STATUS, + TLS_LEGACY, TLS, DURATION_ANOMALY, }; export const CLIENT_ALERT_TYPES = { MONITOR_STATUS: 'xpack.uptime.alerts.monitorStatus', - TLS: 'xpack.uptime.alerts.tls', + TLS_LEGACY: 'xpack.uptime.alerts.tls', + TLS: 'xpack.uptime.alerts.tlsCertificate', DURATION_ANOMALY: 'xpack.uptime.alerts.durationAnomaly', }; diff --git a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx index 35161561a23fe..1a53a2c9b64a0 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx @@ -191,7 +191,7 @@ export const PingHistogramComponent: React.FC<PingHistogramComponentProps> = ({ { 'pings-over-time': { dataType: 'synthetics', - reportType: 'kpi', + reportType: 'kpi-over-time', time: { from: dateRangeStart, to: dateRangeEnd }, ...(monitorId ? { filters: [{ field: 'monitor.id', values: [monitorId] }] } : {}), }, diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx index da32ffd41853b..479a512b7238a 100644 --- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx @@ -40,10 +40,11 @@ export function ActionMenuContent(): React.ReactElement { const syntheticExploratoryViewLink = createExploratoryViewUrl( { - 'synthetics-series': { + 'synthetics-series': ({ dataType: 'synthetics', + isNew: true, time: { from: dateRangeStart, to: dateRangeEnd }, - } as SeriesUrl, + } as unknown) as SeriesUrl, }, basePath ); diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/license_info.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/license_info.test.tsx.snap index b1d2e53997e36..98414f82bf197 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/license_info.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/license_info.test.tsx.snap @@ -24,24 +24,28 @@ Array [ <div class="euiText euiText--small" > - <p> - In order to access duration anomaly detection, you have to be subscribed to an Elastic Platinum license. - </p> - <a - class="euiButton euiButton--primary" - href="/app/management/stack/license_management/home" - rel="noreferrer" + <div + class="euiTextColor euiTextColor--default" > - <span - class="euiButtonContent euiButton__content" + <p> + In order to access duration anomaly detection, you have to be subscribed to an Elastic Platinum license. + </p> + <a + class="euiButton euiButton--primary" + href="/app/management/stack/license_management/home" + rel="noreferrer" > <span - class="euiButton__text" + class="euiButtonContent euiButton__content" > - Start free 14-day trial + <span + class="euiButton__text" + > + Start free 14-day trial + </span> </span> - </span> - </a> + </a> + </div> </div> </div>, <div diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/ml_flyout.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/ml_flyout.test.tsx.snap index e41366fadef84..e4672338485fa 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/ml_flyout.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/ml_flyout.test.tsx.snap @@ -81,69 +81,60 @@ exports[`ML Flyout component renders without errors 1`] = ` exports[`ML Flyout component shows license info if no ml available 1`] = ` <div - data-eui="EuiFocusTrap" + data-eui="EuiFlyout" + data-test-subj="uptimeMLFlyout" + role="dialog" > + <button + data-test-subj="euiFlyoutCloseButton" + type="button" + /> <div - class="euiFlyout euiFlyout--small euiFlyout--paddingLarge" - data-test-subj="uptimeMLFlyout" - role="dialog" - tabindex="0" + class="euiFlyoutHeader" > - <button - aria-label="Close this dialog" - class="euiButtonIcon euiButtonIcon--text euiButtonIcon--empty euiButtonIcon--xSmall euiFlyout__closeButton" - data-test-subj="euiFlyoutCloseButton" - type="button" + <h2 + class="euiTitle euiTitle--medium" > - <span - aria-hidden="true" - class="euiButtonIcon__icon" - color="inherit" - data-euiicon-type="cross" - /> - </button> + Enable anomaly detection + </h2> <div - class="euiFlyoutHeader" - > - <h2 - class="euiTitle euiTitle--medium" - > - Enable anomaly detection - </h2> - <div - class="euiSpacer euiSpacer--s" - /> - </div> + class="euiSpacer euiSpacer--s" + /> + </div> + <div + class="euiFlyoutBody" + > <div - class="euiFlyoutBody" + class="euiFlyoutBody__overflow" + tabindex="0" > <div - class="euiFlyoutBody__overflow" + class="euiFlyoutBody__overflowContent" > <div - class="euiFlyoutBody__overflowContent" + class="euiCallOut euiCallOut--primary license-info-trial" + data-test-subj="uptimeMLLicenseInfo" > <div - class="euiCallOut euiCallOut--primary license-info-trial" - data-test-subj="uptimeMLLicenseInfo" + class="euiCallOutHeader" > - <div - class="euiCallOutHeader" + <span + aria-hidden="true" + class="euiCallOutHeader__icon" + color="inherit" + data-euiicon-type="help" + /> + <span + class="euiCallOutHeader__title" > - <span - aria-hidden="true" - class="euiCallOutHeader__icon" - color="inherit" - data-euiicon-type="help" - /> - <span - class="euiCallOutHeader__title" - > - Start free 14-day trial - </span> - </div> + Start free 14-day trial + </span> + </div> + <div + class="euiText euiText--small" + > <div - class="euiText euiText--small" + class="euiTextColor euiTextColor--default" > <p> In order to access duration anomaly detection, you have to be subscribed to an Elastic Platinum license. @@ -165,85 +156,85 @@ exports[`ML Flyout component shows license info if no ml available 1`] = ` </a> </div> </div> - <div - class="euiSpacer euiSpacer--l" - /> - <div - class="euiText euiText--medium" - > - <p> - Here you can create a machine learning job to calculate anomaly scores on + </div> + <div + class="euiSpacer euiSpacer--l" + /> + <div + class="euiText euiText--medium" + > + <p> + Here you can create a machine learning job to calculate anomaly scores on response durations for Uptime Monitor. Once enabled, the monitor duration chart on the details page will show the expected bounds and annotate the graph with anomalies. You can also potentially identify periods of increased latency across geographical regions. - </p> - <p> - Once a job is created, you can manage it and see more details in the - <a - class="euiLink euiLink--primary" - href="/app/ml" - rel="noreferrer" - > - Machine Learning jobs management page - </a> - . - </p> - <p> - <em> - Note: It might take a few minutes for the job to begin calculating results. - </em> - </p> - </div> - <div - class="euiSpacer euiSpacer--l" - /> + </p> + <p> + Once a job is created, you can manage it and see more details in the + <a + class="euiLink euiLink--primary" + href="/app/ml" + rel="noreferrer" + > + Machine Learning jobs management page + </a> + . + </p> + <p> + <em> + Note: It might take a few minutes for the job to begin calculating results. + </em> + </p> </div> + <div + class="euiSpacer euiSpacer--l" + /> </div> </div> + </div> + <div + class="euiFlyoutFooter" + > <div - class="euiFlyoutFooter" + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive" > <div - class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive" + class="euiFlexItem euiFlexItem--flexGrowZero" > - <div - class="euiFlexItem euiFlexItem--flexGrowZero" + <button + class="euiButtonEmpty euiButtonEmpty--primary" + type="button" > - <button - class="euiButtonEmpty euiButtonEmpty--primary" - type="button" + <span + class="euiButtonContent euiButtonEmpty__content" > <span - class="euiButtonContent euiButtonEmpty__content" + class="euiButtonEmpty__text" > - <span - class="euiButtonEmpty__text" - > - Cancel - </span> + Cancel </span> - </button> - </div> - <div - class="euiFlexItem euiFlexItem--flexGrowZero" + </span> + </button> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <button + class="euiButton euiButton--primary euiButton--fill euiButton-isDisabled" + data-test-subj="uptimeMLCreateJobBtn" + disabled="" + type="button" > - <button - class="euiButton euiButton--primary euiButton--fill euiButton-isDisabled" - data-test-subj="uptimeMLCreateJobBtn" - disabled="" - type="button" + <span + class="euiButtonContent euiButton__content" > <span - class="euiButtonContent euiButton__content" + class="euiButton__text" > - <span - class="euiButton__text" - > - Create new job - </span> + Create new job </span> - </button> - </div> + </span> + </button> </div> </div> </div> diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx index 377d7a8fa35d4..1590e225f9ca8 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx @@ -56,7 +56,7 @@ export const MonitorDuration: React.FC<MonitorIdParam> = ({ monitorId }) => { const exploratoryViewLink = createExploratoryViewUrl( { [`monitor-duration`]: { - reportType: 'kpi', + reportType: 'kpi-over-time', time: { from: dateRangeStart, to: dateRangeEnd }, reportDefinitions: { 'monitor.id': [monitorId] as string[], diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/__snapshots__/expanded_row.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ping_list/__snapshots__/expanded_row.test.tsx.snap index 377a2c9389bbd..51753d2ce8bb3 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/__snapshots__/expanded_row.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/__snapshots__/expanded_row.test.tsx.snap @@ -145,51 +145,55 @@ exports[`PingListExpandedRow renders link to docs if body is not recorded but it <div class="euiText euiText--small" > - <dl - class="euiDescriptionList euiDescriptionList--row" + <div + class="euiTextColor euiTextColor--default" > - <dt - class="euiDescriptionList__title" + <dl + class="euiDescriptionList euiDescriptionList--row" > - Response Body - </dt> - <dd - class="euiDescriptionList__description" - > - <div - class="euiText euiText--medium" + <dt + class="euiDescriptionList__title" > - Body size is 1MB. - </div> - <div - class="euiSpacer euiSpacer--s" - /> - <div - class="euiText euiText--medium" + Response Body + </dt> + <dd + class="euiDescriptionList__description" > - Body not recorded. Read our - <a - class="euiLink euiLink--primary" - href="https://www.elastic.co/guide/en/beats/heartbeat/current/configuration-heartbeat-options.html#monitor-http-response" - rel="noopener" - target="_blank" + <div + class="euiText euiText--medium" > - docs - <span - aria-label="External link" - class="euiLink__externalIcon" - data-euiicon-type="popout" - /> - <span - class="euiScreenReaderOnly" + Body size is 1MB. + </div> + <div + class="euiSpacer euiSpacer--s" + /> + <div + class="euiText euiText--medium" + > + Body not recorded. Read our + <a + class="euiLink euiLink--primary" + href="https://www.elastic.co/guide/en/beats/heartbeat/current/configuration-heartbeat-options.html#monitor-http-response" + rel="noopener" + target="_blank" > - (opens in a new tab or window) - </span> - </a> - for more information on recording response bodies. - </div> - </dd> - </dl> + docs + <span + aria-label="External link" + class="euiLink__externalIcon" + data-euiicon-type="popout" + /> + <span + class="euiScreenReaderOnly" + > + (opens in a new tab or window) + </span> + </a> + for more information on recording response bodies. + </div> + </dd> + </dl> + </div> </div> </div> </div> diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_flyout.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_flyout.tsx index 53e6583e00cc2..2242e692b4222 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_flyout.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_flyout.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, ReactNode } from 'react'; -import styled from 'styled-components'; +import styled, { StyledComponent } from 'styled-components'; import { EuiFlyout, @@ -49,7 +49,11 @@ export const RESPONSE_HEADERS = i18n.translate( } ); -const FlyoutContainer = styled(EuiFlyout)` +// TODO: EUI team follow up on complex types and styled-components `styled` +// https://github.com/elastic/eui/issues/4855 +const FlyoutContainer: StyledComponent<typeof EuiFlyout, {}, { children?: ReactNode }> = styled( + EuiFlyout +)` z-index: ${(props) => props.theme.eui.euiZLevel5}; `; diff --git a/x-pack/plugins/uptime/public/lib/alert_types/index.ts b/x-pack/plugins/uptime/public/lib/alert_types/index.ts index 36c84fe4c64cd..406b730fa1e6c 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/index.ts +++ b/x-pack/plugins/uptime/public/lib/alert_types/index.ts @@ -9,6 +9,7 @@ import { CoreStart } from 'kibana/public'; import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; import { initMonitorStatusAlertType } from './monitor_status'; import { initTlsAlertType } from './tls'; +import { initTlsLegacyAlertType } from './tls_legacy'; import { ClientPluginsStart } from '../../apps/plugin'; import { initDurationAnomalyAlertType } from './duration_anomaly'; @@ -20,5 +21,6 @@ export type AlertTypeInitializer = (dependenies: { export const alertTypeInitializers: AlertTypeInitializer[] = [ initMonitorStatusAlertType, initTlsAlertType, + initTlsLegacyAlertType, initDurationAnomalyAlertType, ]; diff --git a/x-pack/plugins/uptime/public/lib/alert_types/tls_legacy.tsx b/x-pack/plugins/uptime/public/lib/alert_types/tls_legacy.tsx new file mode 100644 index 0000000000000..1abcdb2c98662 --- /dev/null +++ b/x-pack/plugins/uptime/public/lib/alert_types/tls_legacy.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 { AlertTypeModel } from '../../../../triggers_actions_ui/public'; +import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts'; +import { TlsTranslationsLegacy } from './translations'; +import { AlertTypeInitializer } from '.'; + +const { defaultActionMessage, description } = TlsTranslationsLegacy; +const TLSAlert = React.lazy(() => import('./lazy_wrapper/tls_alert')); +export const initTlsLegacyAlertType: AlertTypeInitializer = ({ + core, + plugins, +}): AlertTypeModel => ({ + id: CLIENT_ALERT_TYPES.TLS_LEGACY, + iconClass: 'uptimeApp', + documentationUrl(docLinks) { + return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/uptime/${docLinks.DOC_LINK_VERSION}/uptime-alerting.html#_tls_alerts`; + }, + alertParamsExpression: (params: any) => ( + <TLSAlert core={core} plugins={plugins} params={params} /> + ), + description, + validate: () => ({ errors: {} }), + defaultActionMessage, + requiresAppContext: false, +}); diff --git a/x-pack/plugins/uptime/public/lib/alert_types/translations.ts b/x-pack/plugins/uptime/public/lib/alert_types/translations.ts index ea445e3d63c09..bb4af761d240d 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/translations.ts +++ b/x-pack/plugins/uptime/public/lib/alert_types/translations.ts @@ -8,14 +8,32 @@ import { i18n } from '@kbn/i18n'; export const TlsTranslations = { + defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.legacy.defaultActionMessage', { + defaultMessage: `Detected TLS certificate {commonName} from issuer {issuer} is {status}. Certificate {summary} +`, + values: { + commonName: '{{state.commonName}}', + issuer: '{{state.issuer}}', + summary: '{{state.summary}}', + status: '{{state.status}}', + }, + }), + name: i18n.translate('xpack.uptime.alerts.tls.legacy.clientName', { + defaultMessage: 'Uptime TLS (Legacy)', + }), + description: i18n.translate('xpack.uptime.alerts.tls.legacy.description', { + defaultMessage: + 'Alert when the TLS certificate of an Uptime monitor is about to expire. This alert will be deprecated in a future version.', + }), +}; + +export const TlsTranslationsLegacy = { defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.defaultActionMessage', { defaultMessage: `Detected {count} TLS certificates expiring or becoming too old. - {expiringConditionalOpen} Expiring cert count: {expiringCount} Expiring Certificates: {expiringCommonNameAndDate} {expiringConditionalClose} - {agingConditionalOpen} Aging cert count: {agingCount} Aging Certificates: {agingCommonNameAndDate} diff --git a/x-pack/plugins/uptime/server/lib/alerts/index.ts b/x-pack/plugins/uptime/server/lib/alerts/index.ts index 1559ceaae8bb6..c695a4b052cd9 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/index.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/index.ts @@ -8,6 +8,7 @@ import { UptimeAlertTypeFactory } from './types'; import { statusCheckAlertFactory, ActionGroupIds as statusCheckActionGroup } from './status_check'; import { tlsAlertFactory, ActionGroupIds as tlsActionGroup } from './tls'; +import { tlsLegacyAlertFactory, ActionGroupIds as tlsLegacyActionGroup } from './tls_legacy'; import { durationAnomalyAlertFactory, ActionGroupIds as durationAnomalyActionGroup, @@ -16,5 +17,6 @@ import { export const uptimeAlertTypeFactories: [ UptimeAlertTypeFactory<statusCheckActionGroup>, UptimeAlertTypeFactory<tlsActionGroup>, + UptimeAlertTypeFactory<tlsLegacyActionGroup>, UptimeAlertTypeFactory<durationAnomalyActionGroup> -] = [statusCheckAlertFactory, tlsAlertFactory, durationAnomalyAlertFactory]; +] = [statusCheckAlertFactory, tlsAlertFactory, tlsLegacyAlertFactory, durationAnomalyAlertFactory]; diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls.test.ts b/x-pack/plugins/uptime/server/lib/alerts/tls.test.ts index dde6ef8535365..a77fe10f0b9a4 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/tls.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/tls.test.ts @@ -23,6 +23,7 @@ describe('tls alert', () => { common_name: 'Common-One', monitors: [{ name: 'monitor-one', id: 'monitor1' }], sha256: 'abc', + issuer: 'Cloudflare Inc ECC CA-3', }, { not_after: '2020-07-18T03:15:39.000Z', @@ -30,6 +31,7 @@ describe('tls alert', () => { common_name: 'Common-Two', monitors: [{ name: 'monitor-two', id: 'monitor2' }], sha256: 'bcd', + issuer: 'Cloudflare Inc ECC CA-3', }, { not_after: '2020-07-19T03:15:39.000Z', @@ -37,6 +39,7 @@ describe('tls alert', () => { common_name: 'Common-Three', monitors: [{ name: 'monitor-three', id: 'monitor3' }], sha256: 'cde', + issuer: 'Cloudflare Inc ECC CA-3', }, { not_after: '2020-07-25T03:15:39.000Z', @@ -44,6 +47,7 @@ describe('tls alert', () => { common_name: 'Common-Four', monitors: [{ name: 'monitor-four', id: 'monitor4' }], sha256: 'def', + issuer: 'Cloudflare Inc ECC CA-3', }, ]; }); @@ -52,88 +56,66 @@ describe('tls alert', () => { jest.clearAllMocks(); }); - it('sorts expiring certs appropriately when creating summary', () => { - diffSpy.mockReturnValueOnce(900).mockReturnValueOnce(901).mockReturnValueOnce(902); + it('handles positive diffs for expired certs appropriately', () => { + diffSpy.mockReturnValueOnce(900); const result = getCertSummary( - mockCerts, + mockCerts[0], new Date('2020-07-20T05:00:00.000Z').valueOf(), new Date('2019-03-01T00:00:00.000Z').valueOf() ); - expect(result).toMatchInlineSnapshot(` - Object { - "agingCommonNameAndDate": "", - "agingCount": 0, - "count": 4, - "expiringCommonNameAndDate": "Common-One, expired on 2020-07-16T03:15:39.000Z 900 days ago.; Common-Two, expired on 2020-07-18T03:15:39.000Z 901 days ago.; Common-Three, expired on 2020-07-19T03:15:39.000Z 902 days ago.", - "expiringCount": 3, - "hasAging": null, - "hasExpired": true, - } - `); + expect(result).toEqual({ + commonName: mockCerts[0].common_name, + issuer: mockCerts[0].issuer, + summary: 'expired on Jul 15, 2020 EDT, 900 days ago.', + status: 'expired', + }); }); - it('sorts aging certs appropriate when creating summary', () => { - diffSpy.mockReturnValueOnce(702).mockReturnValueOnce(701).mockReturnValueOnce(700); + it('handles positive diffs for agining certs appropriately', () => { + diffSpy.mockReturnValueOnce(702); const result = getCertSummary( - mockCerts, + mockCerts[0], new Date('2020-07-01T12:00:00.000Z').valueOf(), new Date('2019-09-01T03:00:00.000Z').valueOf() ); - expect(result).toMatchInlineSnapshot(` - Object { - "agingCommonNameAndDate": "Common-Two, valid since 2019-07-20T03:15:39.000Z, 702 days ago.; Common-Three, valid since 2019-07-22T03:15:39.000Z, 701 days ago.; Common-One, valid since 2019-07-24T03:15:39.000Z, 700 days ago.", - "agingCount": 4, - "count": 4, - "expiringCommonNameAndDate": "", - "expiringCount": 0, - "hasAging": true, - "hasExpired": null, - } - `); + expect(result).toEqual({ + commonName: mockCerts[0].common_name, + issuer: mockCerts[0].issuer, + summary: 'valid since Jul 23, 2019 EDT, 702 days ago.', + status: 'becoming too old', + }); }); it('handles negative diff values appropriately for aging certs', () => { - diffSpy.mockReturnValueOnce(700).mockReturnValueOnce(-90).mockReturnValueOnce(-80); + diffSpy.mockReturnValueOnce(-90); const result = getCertSummary( - mockCerts, + mockCerts[0], new Date('2020-07-01T12:00:00.000Z').valueOf(), new Date('2019-09-01T03:00:00.000Z').valueOf() ); - expect(result).toMatchInlineSnapshot(` - Object { - "agingCommonNameAndDate": "Common-Two, valid since 2019-07-20T03:15:39.000Z, 700 days ago.; Common-Three, invalid until 2019-07-22T03:15:39.000Z, 90 days from now.; Common-One, invalid until 2019-07-24T03:15:39.000Z, 80 days from now.", - "agingCount": 4, - "count": 4, - "expiringCommonNameAndDate": "", - "expiringCount": 0, - "hasAging": true, - "hasExpired": null, - } - `); + expect(result).toEqual({ + commonName: mockCerts[0].common_name, + issuer: mockCerts[0].issuer, + summary: 'invalid until Jul 23, 2019 EDT, 90 days from now.', + status: 'invalid', + }); }); it('handles negative diff values appropriately for expiring certs', () => { diffSpy // negative days are in the future, positive days are in the past - .mockReturnValueOnce(-96) - .mockReturnValueOnce(-94) - .mockReturnValueOnce(2); + .mockReturnValueOnce(-96); const result = getCertSummary( - mockCerts, + mockCerts[0], new Date('2020-07-20T05:00:00.000Z').valueOf(), new Date('2019-03-01T00:00:00.000Z').valueOf() ); - expect(result).toMatchInlineSnapshot(` - Object { - "agingCommonNameAndDate": "", - "agingCount": 0, - "count": 4, - "expiringCommonNameAndDate": "Common-One, expires on 2020-07-16T03:15:39.000Z in 96 days.; Common-Two, expires on 2020-07-18T03:15:39.000Z in 94 days.; Common-Three, expired on 2020-07-19T03:15:39.000Z 2 days ago.", - "expiringCount": 3, - "hasAging": null, - "hasExpired": true, - } - `); + expect(result).toEqual({ + commonName: mockCerts[0].common_name, + issuer: mockCerts[0].issuer, + summary: 'expires on Jul 15, 2020 EDT in 96 days.', + status: 'expiring', + }); }); }); }); diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls.ts b/x-pack/plugins/uptime/server/lib/alerts/tls.ts index 2a2406a3629d0..f29744fdbb70f 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/tls.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/tls.ts @@ -22,71 +22,80 @@ export type ActionGroupIds = ActionGroupIdsOf<typeof TLS>; const DEFAULT_SIZE = 20; interface TlsAlertState { - count: number; - agingCount: number; - agingCommonNameAndDate: string; - expiringCount: number; - expiringCommonNameAndDate: string; - hasAging: true | null; - hasExpired: true | null; + commonName: string; + issuer: string; + summary: string; + status: string; } -const sortCerts = (a: string, b: string) => new Date(a).valueOf() - new Date(b).valueOf(); +interface TLSContent { + summary: string; + status?: string; +} const mapCertsToSummaryString = ( - certs: Cert[], - certLimitMessage: (cert: Cert) => string, - maxSummaryItems: number -): string => - certs - .slice(0, maxSummaryItems) - .map((cert) => `${cert.common_name}, ${certLimitMessage(cert)}`) - .reduce((prev, cur) => (prev === '' ? cur : prev.concat(`; ${cur}`)), ''); - -const getValidAfter = ({ not_after: date }: Cert) => { - if (!date) return 'Error, missing `certificate_not_valid_after` date.'; + cert: Cert, + certLimitMessage: (cert: Cert) => TLSContent +): TLSContent => certLimitMessage(cert); + +const getValidAfter = ({ not_after: date }: Cert): TLSContent => { + if (!date) return { summary: 'Error, missing `certificate_not_valid_after` date.' }; const relativeDate = moment().diff(date, 'days'); + const formattedDate = moment(date).format('MMM D, YYYY z'); return relativeDate >= 0 - ? tlsTranslations.validAfterExpiredString(date, relativeDate) - : tlsTranslations.validAfterExpiringString(date, Math.abs(relativeDate)); + ? { + summary: tlsTranslations.validAfterExpiredString(formattedDate, relativeDate), + status: tlsTranslations.expiredLabel, + } + : { + summary: tlsTranslations.validAfterExpiringString(formattedDate, Math.abs(relativeDate)), + status: tlsTranslations.expiringLabel, + }; }; -const getValidBefore = ({ not_before: date }: Cert): string => { - if (!date) return 'Error, missing `certificate_not_valid_before` date.'; +const getValidBefore = ({ not_before: date }: Cert): TLSContent => { + if (!date) return { summary: 'Error, missing `certificate_not_valid_before` date.' }; const relativeDate = moment().diff(date, 'days'); + const formattedDate = moment(date).format('MMM D, YYYY z'); return relativeDate >= 0 - ? tlsTranslations.validBeforeExpiredString(date, relativeDate) - : tlsTranslations.validBeforeExpiringString(date, Math.abs(relativeDate)); + ? { + summary: tlsTranslations.validBeforeExpiredString(formattedDate, relativeDate), + status: tlsTranslations.agingLabel, + } + : { + summary: tlsTranslations.validBeforeExpiringString(formattedDate, Math.abs(relativeDate)), + status: tlsTranslations.invalidLabel, + }; }; export const getCertSummary = ( - certs: Cert[], + cert: Cert, expirationThreshold: number, - ageThreshold: number, - maxSummaryItems: number = 3 + ageThreshold: number ): TlsAlertState => { - certs.sort((a, b) => sortCerts(a.not_after ?? '', b.not_after ?? '')); - const expiring = certs.filter( - (cert) => new Date(cert.not_after ?? '').valueOf() < expirationThreshold - ); + const isExpiring = new Date(cert.not_after ?? '').valueOf() < expirationThreshold; + const isAging = new Date(cert.not_before ?? '').valueOf() < ageThreshold; + let content: TLSContent | null = null; + + if (isExpiring) { + content = mapCertsToSummaryString(cert, getValidAfter); + } else if (isAging) { + content = mapCertsToSummaryString(cert, getValidBefore); + } - certs.sort((a, b) => sortCerts(a.not_before ?? '', b.not_before ?? '')); - const aging = certs.filter((cert) => new Date(cert.not_before ?? '').valueOf() < ageThreshold); + const { summary = '', status = '' } = content || {}; return { - count: certs.length, - agingCount: aging.length, - agingCommonNameAndDate: mapCertsToSummaryString(aging, getValidBefore, maxSummaryItems), - expiringCommonNameAndDate: mapCertsToSummaryString(expiring, getValidAfter, maxSummaryItems), - expiringCount: expiring.length, - hasAging: aging.length > 0 ? true : null, - hasExpired: expiring.length > 0 ? true : null, + commonName: cert.common_name ?? '', + issuer: cert.issuer ?? '', + summary, + status, }; }; export const tlsAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (_server, libs) => uptimeAlertWrapper<ActionGroupIds>({ - id: 'xpack.uptime.alerts.tls', + id: 'xpack.uptime.alerts.tlsCertificate', name: tlsTranslations.alertFactoryName, validate: { params: schema.object({}), @@ -129,26 +138,30 @@ export const tlsAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (_server, const foundCerts = total > 0; if (foundCerts) { - const absoluteExpirationThreshold = moment() - .add( - dynamicSettings.certExpirationThreshold ?? - DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold, - 'd' - ) - .valueOf(); - const absoluteAgeThreshold = moment() - .subtract( - dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold, - 'd' - ) - .valueOf(); - const alertInstance = alertInstanceFactory(TLS.id); - const summary = getCertSummary(certs, absoluteExpirationThreshold, absoluteAgeThreshold); - alertInstance.replaceState({ - ...updateState(state, foundCerts), - ...summary, + certs.forEach((cert) => { + const absoluteExpirationThreshold = moment() + .add( + dynamicSettings.certExpirationThreshold ?? + DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold, + 'd' + ) + .valueOf(); + const absoluteAgeThreshold = moment() + .subtract( + dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold, + 'd' + ) + .valueOf(); + const alertInstance = alertInstanceFactory( + `${cert.common_name}-${cert.issuer?.replace(/\s/g, '_')}-${cert.sha256}` + ); + const summary = getCertSummary(cert, absoluteExpirationThreshold, absoluteAgeThreshold); + alertInstance.replaceState({ + ...updateState(state, foundCerts), + ...summary, + }); + alertInstance.scheduleActions(TLS.id); }); - alertInstance.scheduleActions(TLS.id); } return updateState(state, foundCerts); diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.test.ts b/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.test.ts new file mode 100644 index 0000000000000..4c6a721e92159 --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.test.ts @@ -0,0 +1,139 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; +import { getCertSummary } from './tls_legacy'; +import { Cert } from '../../../common/runtime_types'; + +describe('tls alert', () => { + describe('getCertSummary', () => { + let mockCerts: Cert[]; + let diffSpy: jest.SpyInstance<any, unknown[]>; + + beforeEach(() => { + diffSpy = jest.spyOn(moment.prototype, 'diff'); + mockCerts = [ + { + not_after: '2020-07-16T03:15:39.000Z', + not_before: '2019-07-24T03:15:39.000Z', + common_name: 'Common-One', + monitors: [{ name: 'monitor-one', id: 'monitor1' }], + sha256: 'abc', + }, + { + not_after: '2020-07-18T03:15:39.000Z', + not_before: '2019-07-20T03:15:39.000Z', + common_name: 'Common-Two', + monitors: [{ name: 'monitor-two', id: 'monitor2' }], + sha256: 'bcd', + }, + { + not_after: '2020-07-19T03:15:39.000Z', + not_before: '2019-07-22T03:15:39.000Z', + common_name: 'Common-Three', + monitors: [{ name: 'monitor-three', id: 'monitor3' }], + sha256: 'cde', + }, + { + not_after: '2020-07-25T03:15:39.000Z', + not_before: '2019-07-25T03:15:39.000Z', + common_name: 'Common-Four', + monitors: [{ name: 'monitor-four', id: 'monitor4' }], + sha256: 'def', + }, + ]; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('sorts expiring certs appropriately when creating summary', () => { + diffSpy.mockReturnValueOnce(900).mockReturnValueOnce(901).mockReturnValueOnce(902); + const result = getCertSummary( + mockCerts, + new Date('2020-07-20T05:00:00.000Z').valueOf(), + new Date('2019-03-01T00:00:00.000Z').valueOf() + ); + expect(result).toMatchInlineSnapshot(` + Object { + "agingCommonNameAndDate": "", + "agingCount": 0, + "count": 4, + "expiringCommonNameAndDate": "Common-One, expired on 2020-07-16T03:15:39.000Z, 900 days ago.; Common-Two, expired on 2020-07-18T03:15:39.000Z, 901 days ago.; Common-Three, expired on 2020-07-19T03:15:39.000Z, 902 days ago.", + "expiringCount": 3, + "hasAging": null, + "hasExpired": true, + } + `); + }); + + it('sorts aging certs appropriate when creating summary', () => { + diffSpy.mockReturnValueOnce(702).mockReturnValueOnce(701).mockReturnValueOnce(700); + const result = getCertSummary( + mockCerts, + new Date('2020-07-01T12:00:00.000Z').valueOf(), + new Date('2019-09-01T03:00:00.000Z').valueOf() + ); + expect(result).toMatchInlineSnapshot(` + Object { + "agingCommonNameAndDate": "Common-Two, valid since 2019-07-20T03:15:39.000Z, 702 days ago.; Common-Three, valid since 2019-07-22T03:15:39.000Z, 701 days ago.; Common-One, valid since 2019-07-24T03:15:39.000Z, 700 days ago.", + "agingCount": 4, + "count": 4, + "expiringCommonNameAndDate": "", + "expiringCount": 0, + "hasAging": true, + "hasExpired": null, + } + `); + }); + + it('handles negative diff values appropriately for aging certs', () => { + diffSpy.mockReturnValueOnce(700).mockReturnValueOnce(-90).mockReturnValueOnce(-80); + const result = getCertSummary( + mockCerts, + new Date('2020-07-01T12:00:00.000Z').valueOf(), + new Date('2019-09-01T03:00:00.000Z').valueOf() + ); + expect(result).toMatchInlineSnapshot(` + Object { + "agingCommonNameAndDate": "Common-Two, valid since 2019-07-20T03:15:39.000Z, 700 days ago.; Common-Three, invalid until 2019-07-22T03:15:39.000Z, 90 days from now.; Common-One, invalid until 2019-07-24T03:15:39.000Z, 80 days from now.", + "agingCount": 4, + "count": 4, + "expiringCommonNameAndDate": "", + "expiringCount": 0, + "hasAging": true, + "hasExpired": null, + } + `); + }); + + it('handles negative diff values appropriately for expiring certs', () => { + diffSpy + // negative days are in the future, positive days are in the past + .mockReturnValueOnce(-96) + .mockReturnValueOnce(-94) + .mockReturnValueOnce(2); + const result = getCertSummary( + mockCerts, + new Date('2020-07-20T05:00:00.000Z').valueOf(), + new Date('2019-03-01T00:00:00.000Z').valueOf() + ); + expect(result).toMatchInlineSnapshot(` + Object { + "agingCommonNameAndDate": "", + "agingCount": 0, + "count": 4, + "expiringCommonNameAndDate": "Common-One, expires on 2020-07-16T03:15:39.000Z in 96 days.; Common-Two, expires on 2020-07-18T03:15:39.000Z in 94 days.; Common-Three, expired on 2020-07-19T03:15:39.000Z, 2 days ago.", + "expiringCount": 3, + "hasAging": null, + "hasExpired": true, + } + `); + }); + }); +}); diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts b/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts new file mode 100644 index 0000000000000..8f1c0093e60ac --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/alerts/tls_legacy.ts @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; +import { schema } from '@kbn/config-schema'; +import { UptimeAlertTypeFactory } from './types'; +import { updateState } from './common'; +import { TLS_LEGACY } from '../../../common/constants/alerts'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants'; +import { Cert, CertResult } from '../../../common/runtime_types'; +import { commonStateTranslations, tlsTranslations } from './translations'; +import { DEFAULT_FROM, DEFAULT_TO } from '../../rest_api/certs/certs'; +import { uptimeAlertWrapper } from './uptime_alert_wrapper'; +import { ActionGroupIdsOf } from '../../../../alerting/common'; + +export type ActionGroupIds = ActionGroupIdsOf<typeof TLS_LEGACY>; + +const DEFAULT_SIZE = 20; + +interface TlsAlertState { + count: number; + agingCount: number; + agingCommonNameAndDate: string; + expiringCount: number; + expiringCommonNameAndDate: string; + hasAging: true | null; + hasExpired: true | null; +} + +const sortCerts = (a: string, b: string) => new Date(a).valueOf() - new Date(b).valueOf(); + +const mapCertsToSummaryString = ( + certs: Cert[], + certLimitMessage: (cert: Cert) => string, + maxSummaryItems: number +): string => + certs + .slice(0, maxSummaryItems) + .map((cert) => `${cert.common_name}, ${certLimitMessage(cert)}`) + .reduce((prev, cur) => (prev === '' ? cur : prev.concat(`; ${cur}`)), ''); + +const getValidAfter = ({ not_after: date }: Cert) => { + if (!date) return 'Error, missing `certificate_not_valid_after` date.'; + const relativeDate = moment().diff(date, 'days'); + return relativeDate >= 0 + ? tlsTranslations.validAfterExpiredString(date, relativeDate) + : tlsTranslations.validAfterExpiringString(date, Math.abs(relativeDate)); +}; + +const getValidBefore = ({ not_before: date }: Cert): string => { + if (!date) return 'Error, missing `certificate_not_valid_before` date.'; + const relativeDate = moment().diff(date, 'days'); + return relativeDate >= 0 + ? tlsTranslations.validBeforeExpiredString(date, relativeDate) + : tlsTranslations.validBeforeExpiringString(date, Math.abs(relativeDate)); +}; + +export const getCertSummary = ( + certs: Cert[], + expirationThreshold: number, + ageThreshold: number, + maxSummaryItems: number = 3 +): TlsAlertState => { + certs.sort((a, b) => sortCerts(a.not_after ?? '', b.not_after ?? '')); + const expiring = certs.filter( + (cert) => new Date(cert.not_after ?? '').valueOf() < expirationThreshold + ); + + certs.sort((a, b) => sortCerts(a.not_before ?? '', b.not_before ?? '')); + const aging = certs.filter((cert) => new Date(cert.not_before ?? '').valueOf() < ageThreshold); + + return { + count: certs.length, + agingCount: aging.length, + agingCommonNameAndDate: mapCertsToSummaryString(aging, getValidBefore, maxSummaryItems), + expiringCommonNameAndDate: mapCertsToSummaryString(expiring, getValidAfter, maxSummaryItems), + expiringCount: expiring.length, + hasAging: aging.length > 0 ? true : null, + hasExpired: expiring.length > 0 ? true : null, + }; +}; + +export const tlsLegacyAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (_server, libs) => + uptimeAlertWrapper<ActionGroupIds>({ + id: 'xpack.uptime.alerts.tls', + name: tlsTranslations.legacyAlertFactoryName, + validate: { + params: schema.object({}), + }, + defaultActionGroupId: TLS_LEGACY.id, + actionGroups: [ + { + id: TLS_LEGACY.id, + name: TLS_LEGACY.name, + }, + ], + actionVariables: { + context: [], + state: [...tlsTranslations.actionVariables, ...commonStateTranslations], + }, + minimumLicenseRequired: 'basic', + async executor({ options, dynamicSettings, uptimeEsClient }) { + const { + services: { alertInstanceFactory }, + state, + } = options; + + const { certs, total }: CertResult = await libs.requests.getCerts({ + uptimeEsClient, + from: DEFAULT_FROM, + to: DEFAULT_TO, + index: 0, + size: DEFAULT_SIZE, + notValidAfter: `now+${ + dynamicSettings?.certExpirationThreshold ?? + DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold + }d`, + notValidBefore: `now-${ + dynamicSettings?.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold + }d`, + sortBy: 'common_name', + direction: 'desc', + }); + + const foundCerts = total > 0; + + if (foundCerts) { + const absoluteExpirationThreshold = moment() + .add( + dynamicSettings.certExpirationThreshold ?? + DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold, + 'd' + ) + .valueOf(); + const absoluteAgeThreshold = moment() + .subtract( + dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold, + 'd' + ) + .valueOf(); + const alertInstance = alertInstanceFactory(TLS_LEGACY.id); + const summary = getCertSummary(certs, absoluteExpirationThreshold, absoluteAgeThreshold); + alertInstance.replaceState({ + ...updateState(state, foundCerts), + ...summary, + }); + alertInstance.scheduleActions(TLS_LEGACY.id); + } + + return updateState(state, foundCerts); + }, + }); diff --git a/x-pack/plugins/uptime/server/lib/alerts/translations.ts b/x-pack/plugins/uptime/server/lib/alerts/translations.ts index 3630185e19ab0..ee356eb68a626 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/translations.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/translations.ts @@ -151,6 +151,9 @@ export const tlsTranslations = { alertFactoryName: i18n.translate('xpack.uptime.alerts.tls', { defaultMessage: 'Uptime TLS', }), + legacyAlertFactoryName: i18n.translate('xpack.uptime.alerts.tlsLegacy', { + defaultMessage: 'Uptime TLS (Legacy)', + }), actionVariables: [ { name: 'count', @@ -191,7 +194,7 @@ export const tlsTranslations = { ], validAfterExpiredString: (date: string, relativeDate: number) => i18n.translate('xpack.uptime.alerts.tls.validAfterExpiredString', { - defaultMessage: `expired on {date} {relativeDate} days ago.`, + defaultMessage: `expired on {date}, {relativeDate} days ago.`, values: { date, relativeDate, @@ -221,6 +224,18 @@ export const tlsTranslations = { relativeDate, }, }), + expiredLabel: i18n.translate('xpack.uptime.alerts.tls.expiredLabel', { + defaultMessage: 'expired', + }), + expiringLabel: i18n.translate('xpack.uptime.alerts.tls.expiringLabel', { + defaultMessage: 'expiring', + }), + agingLabel: i18n.translate('xpack.uptime.alerts.tls.agingLabel', { + defaultMessage: 'becoming too old', + }), + invalidLabel: i18n.translate('xpack.uptime.alerts.tls.invalidLabel', { + defaultMessage: 'invalid', + }), }; export const durationAnomalyTranslations = { diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts new file mode 100644 index 0000000000000..95e041bbeb03a --- /dev/null +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; + +// eslint-disable-next-line import/no-default-export +export default function swimlaneTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + const mockSwimlane = { + name: 'A swimlane action', + actionTypeId: '.swimlane', + config: { + apiUrl: 'http://swimlane.mynonexistent.co', + appId: '123456asdf', + connectorType: 'all', + mappings: { + severityConfig: { + id: 'adnlas', + name: 'Severity', + key: 'severity', + fieldType: 'text', + }, + ruleNameConfig: { + id: 'adnfls', + name: 'Rule Name', + key: 'rule-name', + fieldType: 'text', + }, + caseIdConfig: { + id: 'a6sst', + name: 'Case Id', + key: 'case-id-name', + fieldType: 'text', + }, + caseNameConfig: { + id: 'a6fst', + name: 'Case Name', + key: 'case-name', + fieldType: 'text', + }, + commentsConfig: { + id: 'a6fdf', + name: 'Comments', + key: 'comments', + fieldType: 'text', + }, + }, + }, + secrets: { + apiToken: 'swimlane-api-key', + }, + }; + + describe('swimlane', () => { + let swimlaneSimulatorURL: string = '<could not determine kibana url>'; + + // need to wait for kibanaServer to settle ... + before(() => { + swimlaneSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SWIMLANE) + ); + }); + it('should return 403 when creating a swimlane action', async () => { + await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + ...mockSwimlane, + config: { + ...mockSwimlane.config, + apiUrl: swimlaneSimulatorURL, + }, + }) + .expect(403, { + statusCode: 403, + error: 'Forbidden', + message: + 'Action type .swimlane is disabled because your basic license does not support it. Please upgrade your license.', + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts index 3f0524750d5f8..21cb0db3057bb 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts @@ -14,6 +14,7 @@ export default function actionsTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./builtin_action_types/es_index')); loadTestFile(require.resolve('./builtin_action_types/jira')); loadTestFile(require.resolve('./builtin_action_types/pagerduty')); + loadTestFile(require.resolve('./builtin_action_types/swimlane')); loadTestFile(require.resolve('./builtin_action_types/server_log')); loadTestFile(require.resolve('./builtin_action_types/servicenow')); loadTestFile(require.resolve('./builtin_action_types/slack')); diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 7ee6e146b2a50..3dcbde5f21149 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -22,7 +22,7 @@ interface CreateTestConfigOptions { verificationMode?: 'full' | 'none' | 'certificate'; publicBaseUrl?: boolean; preconfiguredAlertHistoryEsIndex?: boolean; - customizeLocalHostTls?: boolean; + customizeLocalHostSsl?: boolean; rejectUnauthorized?: boolean; // legacy } @@ -31,6 +31,7 @@ const enabledActionTypes = [ '.email', '.index', '.pagerduty', + '.swimlane', '.server-log', '.servicenow', '.jira', @@ -52,7 +53,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ssl = false, verificationMode = 'full', preconfiguredAlertHistoryEsIndex = false, - customizeLocalHostTls = false, + customizeLocalHostSsl = false, rejectUnauthorized = true, // legacy } = options; @@ -102,25 +103,25 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) const customHostSettingsValue = [ { url: tlsWebhookServers.rejectUnauthorizedFalse, - tls: { + ssl: { verificationMode: 'none', }, }, { url: tlsWebhookServers.rejectUnauthorizedTrue, - tls: { + ssl: { verificationMode: 'full', }, }, { url: tlsWebhookServers.caFile, - tls: { + ssl: { verificationMode: 'certificate', certificateAuthoritiesFiles: [CA_CERT_PATH], }, }, ]; - const customHostSettings = customizeLocalHostTls + const customHostSettings = customizeLocalHostSsl ? [`--xpack.actions.customHostSettings=${JSON.stringify(customHostSettingsValue)}`] : []; @@ -153,7 +154,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) '--xpack.alerting.healthCheck.interval="1s"', `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, `--xpack.actions.rejectUnauthorized=${rejectUnauthorized}`, - `--xpack.actions.tls.verificationMode=${verificationMode}`, + `--xpack.actions.ssl.verificationMode=${verificationMode}`, ...actionsProxyUrl, ...customHostSettings, '--xpack.eventLog.logEntries=true', @@ -198,28 +199,28 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) encrypted: 'this-is-also-ignored-and-also-required', }, }, - 'custom.tls.noCustom': { + 'custom.ssl.noCustom': { actionTypeId: '.webhook', name: `${tlsWebhookServers.noCustom}`, config: { url: tlsWebhookServers.noCustom, }, }, - 'custom.tls.rejectUnauthorizedFalse': { + 'custom.ssl.rejectUnauthorizedFalse': { actionTypeId: '.webhook', name: `${tlsWebhookServers.rejectUnauthorizedFalse}`, config: { url: tlsWebhookServers.rejectUnauthorizedFalse, }, }, - 'custom.tls.rejectUnauthorizedTrue': { + 'custom.ssl.rejectUnauthorizedTrue': { actionTypeId: '.webhook', name: `${tlsWebhookServers.rejectUnauthorizedTrue}`, config: { url: tlsWebhookServers.rejectUnauthorizedTrue, }, }, - 'custom.tls.caFile': { + 'custom.ssl.caFile': { actionTypeId: '.webhook', name: `${tlsWebhookServers.caFile}`, config: { diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts index 878507bcf4afc..a479070c824f2 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts @@ -13,6 +13,7 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../.. import { PluginSetupContract as ActionsPluginSetupContract } from '../../../../../../../plugins/actions/server/plugin'; import { ActionType } from '../../../../../../../plugins/actions/server'; import { initPlugin as initPagerduty } from './pagerduty_simulation'; +import { initPlugin as initSwimlane } from './swimlane_simulation'; import { initPlugin as initServiceNow } from './servicenow_simulation'; import { initPlugin as initJira } from './jira_simulation'; import { initPlugin as initResilient } from './resilient_simulation'; @@ -23,6 +24,7 @@ export const NAME = 'actions-FTS-external-service-simulators'; export enum ExternalServiceSimulator { PAGERDUTY = 'pagerduty', + SWIMLANE = 'swimlane', SERVICENOW = 'servicenow', SLACK = 'slack', JIRA = 'jira', @@ -66,6 +68,10 @@ export async function getSlackServer(): Promise<http.Server> { return await initSlack(); } +export async function getSwimlaneServer(): Promise<http.Server> { + return await initSwimlane(); +} + interface FixtureSetupDeps { actions: ActionsPluginSetupContract; features: FeaturesPluginSetup; diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/swimlane_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/swimlane_simulation.ts new file mode 100644 index 0000000000000..afba550908ddc --- /dev/null +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/swimlane_simulation.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 http from 'http'; + +export const initPlugin = async () => http.createServer(handler); + +const sendResponse = (response: http.ServerResponse, data: any) => { + response.statusCode = 200; + response.setHeader('Content-Type', 'application/json'); + response.end(JSON.stringify(data, null, 4)); +}; + +const handler = (request: http.IncomingMessage, response: http.ServerResponse) => { + if (request.method === 'POST') { + return sendResponse(response, { + id: 'wowzeronza', + name: 'ET-69', + createdDate: '2021-06-01T17:29:51.092Z', + }); + } + + if (request.method === 'PATCH') { + return sendResponse(response, { + id: 'wowzeronza', + name: 'ET-69', + modifiedDate: '2021-06-01T17:29:51.092Z', + }); + } + + // Return an 400 error if http method is not supported + response.statusCode = 400; + response.setHeader('Content-Type', 'application/json'); + response.end('Not supported http method to request slack simulator'); +}; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts new file mode 100644 index 0000000000000..92e99a9d504f3 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts @@ -0,0 +1,482 @@ +/* + * 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 httpProxy from 'http-proxy'; +import expect from '@kbn/expect'; +import getPort from 'get-port'; +import http from 'http'; + +import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { getSwimlaneServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; + +// eslint-disable-next-line import/no-default-export +export default function swimlaneTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const configService = getService('config'); + + const mockSwimlane = { + name: 'A swimlane action', + actionTypeId: '.swimlane', + config: { + apiUrl: 'http://swimlane.mynonexistent.com', + appId: '123456asdf', + connectorType: 'all', + mappings: { + alertIdConfig: { + id: 'ednjls', + name: 'Alert id', + key: 'alert-id', + fieldType: 'text', + }, + severityConfig: { + id: 'adnlas', + name: 'Severity', + key: 'severity', + fieldType: 'text', + }, + ruleNameConfig: { + id: 'adnfls', + name: 'Rule Name', + key: 'rule-name', + fieldType: 'text', + }, + caseIdConfig: { + id: 'a6sst', + name: 'Case Id', + key: 'case-id-name', + fieldType: 'text', + }, + caseNameConfig: { + id: 'a6fst', + name: 'Case Name', + key: 'case-name', + fieldType: 'text', + }, + commentsConfig: { + id: 'a6fdf', + name: 'Comments', + key: 'comments', + fieldType: 'notes', + }, + descriptionConfig: { + id: 'a6fdf', + name: 'Description', + key: 'description', + fieldType: 'text', + }, + }, + }, + secrets: { + apiToken: 'swimlane-api-key', + }, + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + alertId: 'fs345f78g', + ruleName: 'Rule Name', + severity: 'Critical', + caseName: 'Case Name', + caseId: 'es3456789', + description: 'This is a description', + externalId: null, + }, + comments: [ + { + comment: 'first comment', + commentId: '123', + }, + ], + }, + }, + }; + + describe('Swimlane', () => { + let simulatedActionId = ''; + let swimlaneSimulatorURL: string = ''; + let swimlaneServer: http.Server; + let proxyServer: httpProxy | undefined; + let proxyHaveBeenCalled = false; + + before(async () => { + swimlaneServer = await getSwimlaneServer(); + const availablePort = await getPort({ port: getPort.makeRange(9000, 9100) }); + if (!swimlaneServer.listening) { + swimlaneServer.listen(availablePort); + } + swimlaneSimulatorURL = `http://localhost:${availablePort}`; + proxyServer = await getHttpProxyServer( + swimlaneSimulatorURL, + configService.get('kbnTestServer.serverArgs'), + () => { + proxyHaveBeenCalled = true; + } + ); + }); + + after(() => { + swimlaneServer.close(); + if (proxyServer) { + proxyServer.close(); + } + }); + + describe('Swimlane - Action Creation', () => { + it('should return 200 when creating a swimlane action successfully', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A swimlane action', + connector_type_id: '.swimlane', + config: { + ...mockSwimlane.config, + apiUrl: swimlaneSimulatorURL, + }, + secrets: mockSwimlane.secrets, + }) + .expect(200); + + expect(createdAction).to.eql({ + config: { + ...mockSwimlane.config, + apiUrl: swimlaneSimulatorURL, + }, + connector_type_id: '.swimlane', + id: createdAction.id, + is_missing_secrets: false, + is_preconfigured: false, + name: 'A swimlane action', + }); + + expect(typeof createdAction.id).to.be('string'); + + const { body: fetchedAction } = await supertest + .get(`/api/actions/connector/${createdAction.id}`) + .expect(200); + + expect(fetchedAction).to.eql({ + id: fetchedAction.id, + is_preconfigured: false, + is_missing_secrets: false, + name: 'A swimlane action', + connector_type_id: '.swimlane', + config: { + ...mockSwimlane.config, + apiUrl: swimlaneSimulatorURL, + }, + }); + }); + + it('should respond with a 400 Bad Request when creating a swimlane action with no apiUrl', async () => { + await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A swimlane action', + connector_type_id: '.swimlane', + config: { + appId: mockSwimlane.config.appId, + mappings: mockSwimlane.config.mappings, + }, + secrets: mockSwimlane.secrets, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should respond with a 400 Bad Request when creating a swimlane action with no appId', async () => { + await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A swimlane action', + connector_type_id: '.swimlane', + config: { + mappings: mockSwimlane.config.mappings, + apiUrl: swimlaneSimulatorURL, + }, + secrets: mockSwimlane.secrets, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: [appId]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should respond with a 400 Bad Request when creating a swimlane action without secrets', async () => { + await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A swimlane action', + connector_type_id: '.swimlane', + config: { + ...mockSwimlane.config, + apiUrl: swimlaneSimulatorURL, + }, + secrets: {}, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type secrets: [apiToken]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should respond with a 400 Bad Request default swimlane url is not present in allowedHosts', async () => { + await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A swimlane action', + connector_type_id: '.swimlane', + config: mockSwimlane.config, + secrets: mockSwimlane.secrets, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: `error validating action type config: error configuring connector action: target url "${mockSwimlane.config.apiUrl}" is not added to the Kibana config xpack.actions.allowedHosts`, + }); + }); + }); + }); + + describe('Swimlane - Executor', () => { + before(async () => { + const { body } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A swimlane simulator', + connector_type_id: '.swimlane', + config: { + ...mockSwimlane.config, + apiUrl: swimlaneSimulatorURL, + }, + secrets: mockSwimlane.secrets, + }); + simulatedActionId = body.id; + }); + + describe('Validation', () => { + it('should handle failing with a simulated success without action', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: {}, + }) + .then((resp: any) => { + expect(Object.keys(resp.body)).to.eql(['status', 'message', 'retry', 'connector_id']); + expect(resp.body.connector_id).to.eql(simulatedActionId); + expect(resp.body.status).to.eql('error'); + expect(resp.body.retry).to.eql(false); + // Node.js 12 oddity: + // + // The first time after the server is booted, the error message will be: + // + // undefined is not iterable (cannot read property Symbol(Symbol.iterator)) + // + // After this, the error will be: + // + // Cannot destructure property 'value' of 'undefined' as it is undefined. + // + // The error seems to come from the exact same place in the code based on the + // exact same circomstances: + // + // https://github.com/elastic/kibana/blob/b0a223ebcbac7e404e8ae6da23b2cc6a4b509ff1/packages/kbn-config-schema/src/types/literal_type.ts#L28 + // + // What triggers the error is that the `handleError` function expects its 2nd + // argument to be an object containing a `valids` property of type array. + // + // In this test the object does not contain a `valids` property, so hence the + // error. + // + // Why the error message isn't the same in all scenarios is unknown to me and + // could be a bug in V8. + expect(resp.body.message).to.match( + /^error validating action params: (undefined is not iterable \(cannot read property Symbol\(Symbol.iterator\)\)|Cannot destructure property 'value' of 'undefined' as it is undefined\.)$/ + ); + }); + }); + + it('should handle failing with a simulated success without unsupported action', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { subAction: 'non-supported' }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [subAction]: expected value to equal [pushToService]', + }); + }); + }); + + /** + * All subActionParams are optional. + * If subActionParams is not provided all + * the subActionParams attributes will be set to null + * and the validation will succeed. For that reason, + * the subActionParams need to be set to null. + */ + it('should handle failing with a simulated success without subActionParams', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { subAction: 'pushToService', subActionParams: null }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [subActionParams]: expected a plain object value, but found [null] instead.', + }); + }); + }); + + it('should handle failing with a simulated success without commentId', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockSwimlane.params, + subActionParams: { + ...mockSwimlane.params.subActionParams, + comments: [{ comment: 'comment' }], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [subActionParams.comments]: types that failed validation:\n- [subActionParams.comments.0.0.commentId]: expected value of type [string] but got [undefined]\n- [subActionParams.comments.1]: expected value to equal [null]', + }); + }); + }); + + it('should handle failing with a simulated success without comment message', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockSwimlane.params, + subActionParams: { + ...mockSwimlane.params.subActionParams, + comments: [{ commentId: 'success' }], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [subActionParams.comments]: types that failed validation:\n- [subActionParams.comments.0.0.comment]: expected value of type [string] but got [undefined]\n- [subActionParams.comments.1]: expected value to equal [null]', + }); + }); + }); + }); + + describe('Execution', () => { + it('should handle creating an incident', async () => { + const { body } = await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockSwimlane.params, + subActionParams: { + ...mockSwimlane.params.subActionParams, + comments: [], + }, + }, + }) + .expect(200); + + expect(proxyHaveBeenCalled).to.equal(true); + expect(body).to.eql({ + status: 'ok', + connector_id: simulatedActionId, + data: { + id: 'wowzeronza', + title: 'ET-69', + pushedDate: '2021-06-01T17:29:51.092Z', + url: `${swimlaneSimulatorURL}/record/123456asdf/wowzeronza`, + }, + }); + }); + + it('should handle updating an incident', async () => { + const { body } = await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockSwimlane.params, + subActionParams: { + incident: { + ...mockSwimlane.params.subActionParams.incident, + externalId: 'wowzeronza', + }, + comments: [], + }, + }, + }) + .expect(200); + + expect(proxyHaveBeenCalled).to.equal(true); + expect(body).to.eql({ + status: 'ok', + connector_id: simulatedActionId, + data: { + id: 'wowzeronza', + title: 'ET-69', + pushedDate: '2021-06-01T17:29:51.092Z', + url: `${swimlaneSimulatorURL}/record/123456asdf/wowzeronza`, + }, + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts index 9a3a78342c5aa..a88a394863dbf 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts @@ -61,12 +61,12 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); - // the custom tls connectors have dynamic ports, so remove them before + // the custom ssl connectors have dynamic ports, so remove them before // comparing to what we expect - const nonCustomTlsConnectors = response.body.filter( - (conn: { id: string }) => !conn.id.startsWith('custom.tls.') + const nonCustomSslConnectors = response.body.filter( + (conn: { id: string }) => !conn.id.startsWith('custom.ssl.') ); - expect(nonCustomTlsConnectors).to.eql([ + expect(nonCustomSslConnectors).to.eql([ { id: createdAction.id, is_preconfigured: false, @@ -175,12 +175,12 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); - // the custom tls connectors have dynamic ports, so remove them before + // the custom ssl connectors have dynamic ports, so remove them before // comparing to what we expect - const nonCustomTlsConnectors = response.body.filter( - (conn: { id: string }) => !conn.id.startsWith('custom.tls.') + const nonCustomSslConnectors = response.body.filter( + (conn: { id: string }) => !conn.id.startsWith('custom.ssl.') ); - expect(nonCustomTlsConnectors).to.eql([ + expect(nonCustomSslConnectors).to.eql([ { id: createdAction.id, is_preconfigured: false, @@ -265,12 +265,12 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { case 'superuser at space1': expect(response.statusCode).to.eql(200); - // the custom tls connectors have dynamic ports, so remove them before + // the custom ssl connectors have dynamic ports, so remove them before // comparing to what we expect - const nonCustomTlsConnectors = response.body.filter( - (conn: { id: string }) => !conn.id.startsWith('custom.tls.') + const nonCustomSslConnectors = response.body.filter( + (conn: { id: string }) => !conn.id.startsWith('custom.ssl.') ); - expect(nonCustomTlsConnectors).to.eql([ + expect(nonCustomSslConnectors).to.eql([ { id: 'preconfigured-es-index-action', is_preconfigured: true, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts index b5ff287ac58f6..db57af0ba1a98 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts @@ -23,6 +23,7 @@ export default function actionsTests({ loadTestFile, getService }: FtrProviderCo loadTestFile(require.resolve('./builtin_action_types/es_index')); loadTestFile(require.resolve('./builtin_action_types/es_index_preconfigured')); loadTestFile(require.resolve('./builtin_action_types/pagerduty')); + loadTestFile(require.resolve('./builtin_action_types/swimlane')); loadTestFile(require.resolve('./builtin_action_types/server_log')); loadTestFile(require.resolve('./builtin_action_types/servicenow')); loadTestFile(require.resolve('./builtin_action_types/jira')); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts index e9ed14fbcddcd..b3d83ae22f330 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts @@ -1304,7 +1304,6 @@ instanceStateValue: true license: 'basic', category: ruleObject.alertInfo.ruleTypeId, ruleset: ruleObject.alertInfo.producer, - namespace: spaceId, name: ruleObject.alertInfo.name, }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts index 5d13d641367a4..940203a9b1f8c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts @@ -81,12 +81,12 @@ export default function eventLogTests({ getService }: FtrProviderContext) { errorMessage: 'Unable to decrypt attribute "apiKey"', status: 'error', reason: 'decrypt', + shouldHaveTask: true, rule: { id: alertId, category: response.body.rule_type_id, license: 'basic', ruleset: 'alertsFixture', - namespace: spaceId, }, }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/config.ts b/x-pack/test/alerting_api_integration/spaces_only/config.ts index 788d9d0698a19..204f5b27da9d5 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/config.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/config.ts @@ -13,6 +13,6 @@ export default createTestConfig('spaces_only', { license: 'trial', enableActionsProxy: false, verificationMode: 'none', - customizeLocalHostTls: true, + customizeLocalHostSsl: true, preconfiguredAlertHistoryEsIndex: true, }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts index 4af33136cd42c..9822254db444a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts @@ -123,9 +123,9 @@ export default function webhookTest({ getService }: FtrProviderContext) { }); }); - describe('tls customization', () => { + describe('ssl customization', () => { it('should handle the xpack.actions.rejectUnauthorized: false', async () => { - const connectorId = 'custom.tls.noCustom'; + const connectorId = 'custom.ssl.noCustom'; const port = await getPortOfConnector(connectorId); const server = await createTlsWebhookServer(port); const { status, body } = await supertest @@ -143,11 +143,11 @@ export default function webhookTest({ getService }: FtrProviderContext) { }); it('should handle the customized rejectUnauthorized: false', async () => { - const connectorId = 'custom.tls.rejectUnauthorizedFalse'; + const connectorId = 'custom.ssl.rejectUnauthorizedFalse'; const port = await getPortOfConnector(connectorId); const server = await createTlsWebhookServer(port); const { status, body } = await supertest - .post(`/api/actions/connector/custom.tls.rejectUnauthorizedFalse/_execute`) + .post(`/api/actions/connector/custom.ssl.rejectUnauthorizedFalse/_execute`) .set('kbn-xsrf', 'test') .send({ params: { @@ -161,11 +161,11 @@ export default function webhookTest({ getService }: FtrProviderContext) { }); it('should handle the customized rejectUnauthorized: true', async () => { - const connectorId = 'custom.tls.rejectUnauthorizedTrue'; + const connectorId = 'custom.ssl.rejectUnauthorizedTrue'; const port = await getPortOfConnector(connectorId); const server = await createTlsWebhookServer(port); const { status, body } = await supertest - .post(`/api/actions/connector/custom.tls.rejectUnauthorizedTrue/_execute`) + .post(`/api/actions/connector/custom.ssl.rejectUnauthorizedTrue/_execute`) .set('kbn-xsrf', 'test') .send({ params: { @@ -180,11 +180,11 @@ export default function webhookTest({ getService }: FtrProviderContext) { }); it('should handle the customized ca file', async () => { - const connectorId = 'custom.tls.caFile'; + const connectorId = 'custom.ssl.caFile'; const port = await getPortOfConnector(connectorId); const server = await createTlsWebhookServer(port); const { status, body } = await supertest - .post(`/api/actions/connector/custom.tls.caFile/_execute`) + .post(`/api/actions/connector/custom.ssl.caFile/_execute`) .set('kbn-xsrf', 'test') .send({ params: { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts index d494c99c80e8f..38f3a17f317c2 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts @@ -406,6 +406,8 @@ export default function ({ getService }: FtrProviderContext) { expect(startExecuteEvent?.message).to.eql(startMessage); } + expect(executeEvent?.kibana?.task).to.eql(undefined); + if (errorMessage) { expect(executeEvent?.error?.message).to.eql(errorMessage); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts index e7f500f2771e3..a965b1716a671 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts @@ -40,13 +40,13 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { .get(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connectors`) .expect(200); - // the custom tls connectors have dynamic ports, so remove them before + // the custom ssl connectors have dynamic ports, so remove them before // comparing to what we expect - const nonCustomTlsConnectors = connectors.filter( - (conn: { id: string }) => !conn.id.startsWith('custom.tls.') + const nonCustomSslConnectors = connectors.filter( + (conn: { id: string }) => !conn.id.startsWith('custom.ssl.') ); - expect(nonCustomTlsConnectors).to.eql([ + expect(nonCustomSslConnectors).to.eql([ { id: 'preconfigured-alert-history-es-index', name: 'Alert history Elasticsearch index', @@ -117,13 +117,13 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { .get(`${getUrlPrefix(Spaces.other.id)}/api/actions/connectors`) .expect(200); - // the custom tls connectors have dynamic ports, so remove them before + // the custom ssl connectors have dynamic ports, so remove them before // comparing to what we expect - const nonCustomTlsConnectors = connectors.filter( - (conn: { id: string }) => !conn.id.startsWith('custom.tls.') + const nonCustomSslConnectors = connectors.filter( + (conn: { id: string }) => !conn.id.startsWith('custom.ssl.') ); - expect(nonCustomTlsConnectors).to.eql([ + expect(nonCustomSslConnectors).to.eql([ { id: 'preconfigured-alert-history-es-index', name: 'Alert history Elasticsearch index', @@ -184,13 +184,13 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { .get(`${getUrlPrefix(Spaces.space1.id)}/api/actions`) .expect(200); - // the custom tls connectors have dynamic ports, so remove them before + // the custom ssl connectors have dynamic ports, so remove them before // comparing to what we expect - const nonCustomTlsConnectors = connectors.filter( - (conn: { id: string }) => !conn.id.startsWith('custom.tls.') + const nonCustomSslConnectors = connectors.filter( + (conn: { id: string }) => !conn.id.startsWith('custom.ssl.') ); - expect(nonCustomTlsConnectors).to.eql([ + expect(nonCustomSslConnectors).to.eql([ { id: 'preconfigured-alert-history-es-index', name: 'Alert history Elasticsearch index', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts index fae5958d7827a..9bf7baf95d8d2 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts @@ -24,476 +24,512 @@ export default function eventLogTests({ getService }: FtrProviderContext) { after(() => objectRemover.removeAll()); - it('should generate expected events for normal operation', async () => { - const { body: createdAction } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) - .set('kbn-xsrf', 'foo') - .send({ - name: 'MY action', - connector_type_id: 'test.noop', - config: {}, - secrets: {}, - }) - .expect(200); - - // pattern of when the alert should fire - const pattern = { - instance: [false, true, true], - }; - - const response = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestAlertData({ - rule_type_id: 'test.patternFiring', - schedule: { interval: '1s' }, - throttle: null, - params: { - pattern, - }, - actions: [ - { - id: createdAction.id, - group: 'default', - params: {}, - }, - ], - }) - ); - - expect(response.status).to.eql(200); - const alertId = response.body.id; - objectRemover.add(Spaces.space1.id, alertId, 'rule', 'alerting'); - - // get the events we're expecting - const events = await retry.try(async () => { - return await getEventLog({ - getService, - spaceId: Spaces.space1.id, - type: 'alert', - id: alertId, - provider: 'alerting', - actions: new Map([ - // make sure the counts of the # of events per type are as expected - ['execute-start', { gte: 4 }], - ['execute', { gte: 4 }], - ['execute-action', { equal: 2 }], - ['new-instance', { equal: 1 }], - ['active-instance', { gte: 1 }], - ['recovered-instance', { equal: 1 }], - ]), - }); - }); - - // get the filtered events only with action 'new-instance' - const filteredEvents = await retry.try(async () => { - return await getEventLog({ - getService, - spaceId: Spaces.space1.id, - type: 'alert', - id: alertId, - provider: 'alerting', - actions: new Map([['new-instance', { equal: 1 }]]), - filter: 'event.action:(new-instance)', - }); - }); + for (const space of [Spaces.default, Spaces.space1]) { + describe(`in space ${space.id}`, () => { + it('should generate expected events for normal operation', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + connector_type_id: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + + // pattern of when the alert should fire + const pattern = { + instance: [false, true, true], + }; + + const response = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + rule_type_id: 'test.patternFiring', + schedule: { interval: '1s' }, + throttle: null, + params: { + pattern, + }, + actions: [ + { + id: createdAction.id, + group: 'default', + params: {}, + }, + ], + }) + ); - expect(getEventsByAction(filteredEvents, 'execute').length).equal(0); - expect(getEventsByAction(filteredEvents, 'execute-action').length).equal(0); - expect(getEventsByAction(events, 'new-instance').length).equal(1); - - const executeEvents = getEventsByAction(events, 'execute'); - const executeStartEvents = getEventsByAction(events, 'execute-start'); - const executeActionEvents = getEventsByAction(events, 'execute-action'); - const newInstanceEvents = getEventsByAction(events, 'new-instance'); - const recoveredInstanceEvents = getEventsByAction(events, 'recovered-instance'); - - // make sure the events are in the right temporal order - const executeTimes = getTimestamps(executeEvents); - const executeStartTimes = getTimestamps(executeStartEvents); - const executeActionTimes = getTimestamps(executeActionEvents); - const newInstanceTimes = getTimestamps(newInstanceEvents); - const recoveredInstanceTimes = getTimestamps(recoveredInstanceEvents); - - expect(executeTimes[0] < newInstanceTimes[0]).to.be(true); - expect(executeTimes[1] <= newInstanceTimes[0]).to.be(true); - expect(executeTimes[2] > newInstanceTimes[0]).to.be(true); - expect(executeTimes[1] <= executeActionTimes[0]).to.be(true); - expect(executeTimes[2] > executeActionTimes[0]).to.be(true); - expect(executeStartTimes.length === executeTimes.length).to.be(true); - executeStartTimes.forEach((est, index) => expect(est === executeTimes[index]).to.be(true)); - expect(recoveredInstanceTimes[0] > newInstanceTimes[0]).to.be(true); - - // validate each event - let executeCount = 0; - const executeStatuses = ['ok', 'active', 'active']; - for (const event of events) { - switch (event?.event?.action) { - case 'execute-start': - validateEvent(event, { - spaceId: Spaces.space1.id, - savedObjects: [ - { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, - ], - message: `alert execution start: "${alertId}"`, - rule: { - id: alertId, - category: response.body.rule_type_id, - license: 'basic', - ruleset: 'alertsFixture', - namespace: Spaces.space1.id, - }, + expect(response.status).to.eql(200); + const alertId = response.body.id; + objectRemover.add(space.id, alertId, 'rule', 'alerting'); + + // get the events we're expecting + const events = await retry.try(async () => { + return await getEventLog({ + getService, + spaceId: space.id, + type: 'alert', + id: alertId, + provider: 'alerting', + actions: new Map([ + // make sure the counts of the # of events per type are as expected + ['execute-start', { gte: 4 }], + ['execute', { gte: 4 }], + ['execute-action', { equal: 2 }], + ['new-instance', { equal: 1 }], + ['active-instance', { gte: 1 }], + ['recovered-instance', { equal: 1 }], + ]), }); - break; - case 'execute': - validateEvent(event, { - spaceId: Spaces.space1.id, - savedObjects: [ - { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, - ], - outcome: 'success', - message: `alert executed: test.patternFiring:${alertId}: 'abc'`, - status: executeStatuses[executeCount++], - rule: { - id: alertId, - category: response.body.rule_type_id, - license: 'basic', - ruleset: 'alertsFixture', - namespace: Spaces.space1.id, - name: response.body.name, - }, + }); + + // get the filtered events only with action 'new-instance' + const filteredEvents = await retry.try(async () => { + return await getEventLog({ + getService, + spaceId: space.id, + type: 'alert', + id: alertId, + provider: 'alerting', + actions: new Map([['new-instance', { equal: 1 }]]), + filter: 'event.action:(new-instance)', }); - break; - case 'execute-action': + }); + + expect(getEventsByAction(filteredEvents, 'execute').length).equal(0); + expect(getEventsByAction(filteredEvents, 'execute-action').length).equal(0); + expect(getEventsByAction(events, 'new-instance').length).equal(1); + + const executeEvents = getEventsByAction(events, 'execute'); + const executeStartEvents = getEventsByAction(events, 'execute-start'); + const executeActionEvents = getEventsByAction(events, 'execute-action'); + const newInstanceEvents = getEventsByAction(events, 'new-instance'); + const recoveredInstanceEvents = getEventsByAction(events, 'recovered-instance'); + + // make sure the events are in the right temporal order + const executeTimes = getTimestamps(executeEvents); + const executeStartTimes = getTimestamps(executeStartEvents); + const executeActionTimes = getTimestamps(executeActionEvents); + const newInstanceTimes = getTimestamps(newInstanceEvents); + const recoveredInstanceTimes = getTimestamps(recoveredInstanceEvents); + + expect(executeTimes[0] < newInstanceTimes[0]).to.be(true); + expect(executeTimes[1] <= newInstanceTimes[0]).to.be(true); + expect(executeTimes[2] > newInstanceTimes[0]).to.be(true); + expect(executeTimes[1] <= executeActionTimes[0]).to.be(true); + expect(executeTimes[2] > executeActionTimes[0]).to.be(true); + expect(executeStartTimes.length === executeTimes.length).to.be(true); + executeStartTimes.forEach((est, index) => + expect(est === executeTimes[index]).to.be(true) + ); + expect(recoveredInstanceTimes[0] > newInstanceTimes[0]).to.be(true); + + // validate each event + let executeCount = 0; + const executeStatuses = ['ok', 'active', 'active']; + for (const event of events) { + switch (event?.event?.action) { + case 'execute-start': + validateEvent(event, { + spaceId: space.id, + savedObjects: [ + { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, + ], + message: `alert execution start: "${alertId}"`, + shouldHaveTask: true, + rule: { + id: alertId, + category: response.body.rule_type_id, + license: 'basic', + ruleset: 'alertsFixture', + }, + }); + break; + case 'execute': + validateEvent(event, { + spaceId: space.id, + savedObjects: [ + { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, + ], + outcome: 'success', + message: `alert executed: test.patternFiring:${alertId}: 'abc'`, + status: executeStatuses[executeCount++], + shouldHaveTask: true, + rule: { + id: alertId, + category: response.body.rule_type_id, + license: 'basic', + ruleset: 'alertsFixture', + name: response.body.name, + }, + }); + break; + case 'execute-action': + validateEvent(event, { + spaceId: space.id, + savedObjects: [ + { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, + { type: 'action', id: createdAction.id, type_id: 'test.noop' }, + ], + message: `alert: test.patternFiring:${alertId}: 'abc' instanceId: 'instance' scheduled actionGroup: 'default' action: test.noop:${createdAction.id}`, + instanceId: 'instance', + actionGroupId: 'default', + rule: { + id: alertId, + category: response.body.rule_type_id, + license: 'basic', + ruleset: 'alertsFixture', + name: response.body.name, + }, + }); + break; + case 'new-instance': + validateInstanceEvent(event, `created new instance: 'instance'`, false); + break; + case 'recovered-instance': + validateInstanceEvent(event, `instance 'instance' has recovered`, true); + break; + case 'active-instance': + validateInstanceEvent( + event, + `active instance: 'instance' in actionGroup: 'default'`, + false + ); + break; + // this will get triggered as we add new event actions + default: + throw new Error(`unexpected event action "${event?.event?.action}"`); + } + } + + const actionEvents = await retry.try(async () => { + return await getEventLog({ + getService, + spaceId: space.id, + type: 'action', + id: createdAction.id, + provider: 'actions', + actions: new Map([['execute', { gte: 1 }]]), + }); + }); + + for (const event of actionEvents) { + switch (event?.event?.action) { + case 'execute': + validateEvent(event, { + spaceId: space.id, + savedObjects: [ + { type: 'action', id: createdAction.id, rel: 'primary', type_id: 'test.noop' }, + ], + message: `action executed: test.noop:${createdAction.id}: MY action`, + outcome: 'success', + shouldHaveTask: true, + rule: undefined, + }); + break; + } + } + + function validateInstanceEvent( + event: IValidatedEvent, + subMessage: string, + shouldHaveEventEnd: boolean + ) { validateEvent(event, { - spaceId: Spaces.space1.id, + spaceId: space.id, savedObjects: [ { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, - { type: 'action', id: createdAction.id, type_id: 'test.noop' }, ], - message: `alert: test.patternFiring:${alertId}: 'abc' instanceId: 'instance' scheduled actionGroup: 'default' action: test.noop:${createdAction.id}`, + message: `test.patternFiring:${alertId}: 'abc' ${subMessage}`, instanceId: 'instance', actionGroupId: 'default', + shouldHaveEventEnd, rule: { id: alertId, category: response.body.rule_type_id, license: 'basic', ruleset: 'alertsFixture', - namespace: Spaces.space1.id, name: response.body.name, }, }); - break; - case 'new-instance': - validateInstanceEvent(event, `created new instance: 'instance'`, false); - break; - case 'recovered-instance': - validateInstanceEvent(event, `instance 'instance' has recovered`, true); - break; - case 'active-instance': - validateInstanceEvent( - event, - `active instance: 'instance' in actionGroup: 'default'`, - false - ); - break; - // this will get triggered as we add new event actions - default: - throw new Error(`unexpected event action "${event?.event?.action}"`); - } - } - - function validateInstanceEvent( - event: IValidatedEvent, - subMessage: string, - shouldHaveEventEnd: boolean - ) { - validateEvent(event, { - spaceId: Spaces.space1.id, - savedObjects: [ - { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, - ], - message: `test.patternFiring:${alertId}: 'abc' ${subMessage}`, - instanceId: 'instance', - actionGroupId: 'default', - shouldHaveEventEnd, - rule: { - id: alertId, - category: response.body.rule_type_id, - license: 'basic', - ruleset: 'alertsFixture', - namespace: Spaces.space1.id, - name: response.body.name, - }, + } }); - } - }); - - it('should generate expected events for normal operation with subgroups', async () => { - const { body: createdAction } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) - .set('kbn-xsrf', 'foo') - .send({ - name: 'MY action', - connector_type_id: 'test.noop', - config: {}, - secrets: {}, - }) - .expect(200); - - // pattern of when the alert should fire - const [firstSubgroup, secondSubgroup] = [uuid.v4(), uuid.v4()]; - const pattern = { - instance: [false, firstSubgroup, secondSubgroup], - }; - - const response = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestAlertData({ - rule_type_id: 'test.patternFiring', - schedule: { interval: '1s' }, - throttle: null, - params: { - pattern, - }, - actions: [ - { - id: createdAction.id, - group: 'default', - params: {}, - }, - ], - }) - ); - - expect(response.status).to.eql(200); - const alertId = response.body.id; - objectRemover.add(Spaces.space1.id, alertId, 'rule', 'alerting'); - - // get the events we're expecting - const events = await retry.try(async () => { - return await getEventLog({ - getService, - spaceId: Spaces.space1.id, - type: 'alert', - id: alertId, - provider: 'alerting', - actions: new Map([ - // make sure the counts of the # of events per type are as expected - ['execute-start', { gte: 4 }], - ['execute', { gte: 4 }], - ['execute-action', { equal: 2 }], - ['new-instance', { equal: 1 }], - ['active-instance', { gte: 2 }], - ['recovered-instance', { equal: 1 }], - ]), - }); - }); - const executeEvents = getEventsByAction(events, 'execute'); - const executeStartEvents = getEventsByAction(events, 'execute-start'); - const executeActionEvents = getEventsByAction(events, 'execute-action'); - const newInstanceEvents = getEventsByAction(events, 'new-instance'); - const recoveredInstanceEvents = getEventsByAction(events, 'recovered-instance'); - - // make sure the events are in the right temporal order - const executeTimes = getTimestamps(executeEvents); - const executeStartTimes = getTimestamps(executeStartEvents); - const executeActionTimes = getTimestamps(executeActionEvents); - const newInstanceTimes = getTimestamps(newInstanceEvents); - const recoveredInstanceTimes = getTimestamps(recoveredInstanceEvents); - - expect(executeTimes[0] < newInstanceTimes[0]).to.be(true); - expect(executeTimes[1] <= newInstanceTimes[0]).to.be(true); - expect(executeTimes[2] > newInstanceTimes[0]).to.be(true); - expect(executeTimes[1] <= executeActionTimes[0]).to.be(true); - expect(executeTimes[2] > executeActionTimes[0]).to.be(true); - expect(executeStartTimes.length === executeTimes.length).to.be(true); - executeStartTimes.forEach((est, index) => expect(est === executeTimes[index]).to.be(true)); - expect(recoveredInstanceTimes[0] > newInstanceTimes[0]).to.be(true); - - // validate each event - let executeCount = 0; - const executeStatuses = ['ok', 'active', 'active']; - for (const event of events) { - switch (event?.event?.action) { - case 'execute-start': - validateEvent(event, { - spaceId: Spaces.space1.id, - savedObjects: [ - { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, - ], - message: `alert execution start: "${alertId}"`, - rule: { - id: alertId, - category: response.body.rule_type_id, - license: 'basic', - ruleset: 'alertsFixture', - namespace: Spaces.space1.id, - }, - }); - break; - case 'execute': - validateEvent(event, { - spaceId: Spaces.space1.id, - savedObjects: [ - { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, - ], - outcome: 'success', - message: `alert executed: test.patternFiring:${alertId}: 'abc'`, - status: executeStatuses[executeCount++], - rule: { - id: alertId, - category: response.body.rule_type_id, - license: 'basic', - ruleset: 'alertsFixture', - namespace: Spaces.space1.id, - name: response.body.name, - }, + it('should generate expected events for normal operation with subgroups', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + connector_type_id: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + + // pattern of when the alert should fire + const [firstSubgroup, secondSubgroup] = [uuid.v4(), uuid.v4()]; + const pattern = { + instance: [false, firstSubgroup, secondSubgroup], + }; + + const response = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + rule_type_id: 'test.patternFiring', + schedule: { interval: '1s' }, + throttle: null, + params: { + pattern, + }, + actions: [ + { + id: createdAction.id, + group: 'default', + params: {}, + }, + ], + }) + ); + + expect(response.status).to.eql(200); + const alertId = response.body.id; + objectRemover.add(space.id, alertId, 'rule', 'alerting'); + + // get the events we're expecting + const events = await retry.try(async () => { + return await getEventLog({ + getService, + spaceId: space.id, + type: 'alert', + id: alertId, + provider: 'alerting', + actions: new Map([ + // make sure the counts of the # of events per type are as expected + ['execute-start', { gte: 4 }], + ['execute', { gte: 4 }], + ['execute-action', { equal: 2 }], + ['new-instance', { equal: 1 }], + ['active-instance', { gte: 2 }], + ['recovered-instance', { equal: 1 }], + ]), }); - break; - case 'execute-action': - expect( - [firstSubgroup, secondSubgroup].includes(event?.kibana?.alerting?.action_subgroup!) - ).to.be(true); + }); + + const executeEvents = getEventsByAction(events, 'execute'); + const executeStartEvents = getEventsByAction(events, 'execute-start'); + const executeActionEvents = getEventsByAction(events, 'execute-action'); + const newInstanceEvents = getEventsByAction(events, 'new-instance'); + const recoveredInstanceEvents = getEventsByAction(events, 'recovered-instance'); + + // make sure the events are in the right temporal order + const executeTimes = getTimestamps(executeEvents); + const executeStartTimes = getTimestamps(executeStartEvents); + const executeActionTimes = getTimestamps(executeActionEvents); + const newInstanceTimes = getTimestamps(newInstanceEvents); + const recoveredInstanceTimes = getTimestamps(recoveredInstanceEvents); + + expect(executeTimes[0] < newInstanceTimes[0]).to.be(true); + expect(executeTimes[1] <= newInstanceTimes[0]).to.be(true); + expect(executeTimes[2] > newInstanceTimes[0]).to.be(true); + expect(executeTimes[1] <= executeActionTimes[0]).to.be(true); + expect(executeTimes[2] > executeActionTimes[0]).to.be(true); + expect(executeStartTimes.length === executeTimes.length).to.be(true); + executeStartTimes.forEach((est, index) => + expect(est === executeTimes[index]).to.be(true) + ); + expect(recoveredInstanceTimes[0] > newInstanceTimes[0]).to.be(true); + + // validate each event + let executeCount = 0; + const executeStatuses = ['ok', 'active', 'active']; + for (const event of events) { + switch (event?.event?.action) { + case 'execute-start': + validateEvent(event, { + spaceId: space.id, + savedObjects: [ + { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, + ], + message: `alert execution start: "${alertId}"`, + shouldHaveTask: true, + rule: { + id: alertId, + category: response.body.rule_type_id, + license: 'basic', + ruleset: 'alertsFixture', + }, + }); + break; + case 'execute': + validateEvent(event, { + spaceId: space.id, + savedObjects: [ + { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, + ], + outcome: 'success', + message: `alert executed: test.patternFiring:${alertId}: 'abc'`, + status: executeStatuses[executeCount++], + shouldHaveTask: true, + rule: { + id: alertId, + category: response.body.rule_type_id, + license: 'basic', + ruleset: 'alertsFixture', + name: response.body.name, + }, + }); + break; + case 'execute-action': + expect( + [firstSubgroup, secondSubgroup].includes( + event?.kibana?.alerting?.action_subgroup! + ) + ).to.be(true); + validateEvent(event, { + spaceId: space.id, + savedObjects: [ + { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, + { type: 'action', id: createdAction.id, type_id: 'test.noop' }, + ], + message: `alert: test.patternFiring:${alertId}: 'abc' instanceId: 'instance' scheduled actionGroup(subgroup): 'default(${event?.kibana?.alerting?.action_subgroup})' action: test.noop:${createdAction.id}`, + instanceId: 'instance', + actionGroupId: 'default', + rule: { + id: alertId, + category: response.body.rule_type_id, + license: 'basic', + ruleset: 'alertsFixture', + name: response.body.name, + }, + }); + break; + case 'new-instance': + validateInstanceEvent(event, `created new instance: 'instance'`, false); + break; + case 'recovered-instance': + validateInstanceEvent(event, `instance 'instance' has recovered`, true); + break; + case 'active-instance': + expect( + [firstSubgroup, secondSubgroup].includes( + event?.kibana?.alerting?.action_subgroup! + ) + ).to.be(true); + validateInstanceEvent( + event, + `active instance: 'instance' in actionGroup(subgroup): 'default(${event?.kibana?.alerting?.action_subgroup})'`, + false + ); + break; + // this will get triggered as we add new event actions + default: + throw new Error(`unexpected event action "${event?.event?.action}"`); + } + } + + function validateInstanceEvent( + event: IValidatedEvent, + subMessage: string, + shouldHaveEventEnd: boolean + ) { validateEvent(event, { - spaceId: Spaces.space1.id, + spaceId: space.id, savedObjects: [ { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, - { type: 'action', id: createdAction.id, type_id: 'test.noop' }, ], - message: `alert: test.patternFiring:${alertId}: 'abc' instanceId: 'instance' scheduled actionGroup(subgroup): 'default(${event?.kibana?.alerting?.action_subgroup})' action: test.noop:${createdAction.id}`, + message: `test.patternFiring:${alertId}: 'abc' ${subMessage}`, instanceId: 'instance', actionGroupId: 'default', + shouldHaveEventEnd, rule: { id: alertId, category: response.body.rule_type_id, license: 'basic', ruleset: 'alertsFixture', - namespace: Spaces.space1.id, name: response.body.name, }, }); - break; - case 'new-instance': - validateInstanceEvent(event, `created new instance: 'instance'`, false); - break; - case 'recovered-instance': - validateInstanceEvent(event, `instance 'instance' has recovered`, true); - break; - case 'active-instance': - expect( - [firstSubgroup, secondSubgroup].includes(event?.kibana?.alerting?.action_subgroup!) - ).to.be(true); - validateInstanceEvent( - event, - `active instance: 'instance' in actionGroup(subgroup): 'default(${event?.kibana?.alerting?.action_subgroup})'`, - false - ); - break; - // this will get triggered as we add new event actions - default: - throw new Error(`unexpected event action "${event?.event?.action}"`); - } - } - - function validateInstanceEvent( - event: IValidatedEvent, - subMessage: string, - shouldHaveEventEnd: boolean - ) { - validateEvent(event, { - spaceId: Spaces.space1.id, - savedObjects: [ - { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, - ], - message: `test.patternFiring:${alertId}: 'abc' ${subMessage}`, - instanceId: 'instance', - actionGroupId: 'default', - shouldHaveEventEnd, - rule: { - id: alertId, - category: response.body.rule_type_id, - license: 'basic', - ruleset: 'alertsFixture', - namespace: Spaces.space1.id, - name: response.body.name, - }, - }); - } - }); - - it('should generate events for execution errors', async () => { - const response = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestAlertData({ - rule_type_id: 'test.throw', - schedule: { interval: '1s' }, - throttle: null, - }) - ); - - expect(response.status).to.eql(200); - const alertId = response.body.id; - objectRemover.add(Spaces.space1.id, alertId, 'rule', 'alerting'); - - const events = await retry.try(async () => { - return await getEventLog({ - getService, - spaceId: Spaces.space1.id, - type: 'alert', - id: alertId, - provider: 'alerting', - actions: new Map([ - ['execute-start', { gte: 1 }], - ['execute', { gte: 1 }], - ]), + } }); - }); - const startEvent = events[0]; - const executeEvent = events[1]; - - expect(startEvent).to.be.ok(); - expect(executeEvent).to.be.ok(); - - validateEvent(startEvent, { - spaceId: Spaces.space1.id, - savedObjects: [ - { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, - ], - message: `alert execution start: "${alertId}"`, - rule: { - id: alertId, - category: response.body.rule_type_id, - license: 'basic', - ruleset: 'alertsFixture', - namespace: Spaces.space1.id, - }, - }); + it('should generate events for execution errors', async () => { + const response = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + rule_type_id: 'test.throw', + schedule: { interval: '1s' }, + throttle: null, + }) + ); + + expect(response.status).to.eql(200); + const alertId = response.body.id; + objectRemover.add(space.id, alertId, 'rule', 'alerting'); + + const events = await retry.try(async () => { + return await getEventLog({ + getService, + spaceId: space.id, + type: 'alert', + id: alertId, + provider: 'alerting', + actions: new Map([ + ['execute-start', { gte: 1 }], + ['execute', { gte: 1 }], + ]), + }); + }); - validateEvent(executeEvent, { - spaceId: Spaces.space1.id, - savedObjects: [{ type: 'alert', id: alertId, rel: 'primary', type_id: 'test.throw' }], - outcome: 'failure', - message: `alert execution failure: test.throw:${alertId}: 'abc'`, - errorMessage: 'this alert is intended to fail', - status: 'error', - reason: 'execute', - rule: { - id: alertId, - category: response.body.rule_type_id, - license: 'basic', - ruleset: 'alertsFixture', - namespace: Spaces.space1.id, - }, + const startEvent = events[0]; + const executeEvent = events[1]; + + expect(startEvent).to.be.ok(); + expect(executeEvent).to.be.ok(); + + validateEvent(startEvent, { + spaceId: space.id, + savedObjects: [ + { type: 'alert', id: alertId, rel: 'primary', type_id: 'test.patternFiring' }, + ], + message: `alert execution start: "${alertId}"`, + shouldHaveTask: true, + rule: { + id: alertId, + category: response.body.rule_type_id, + license: 'basic', + ruleset: 'alertsFixture', + }, + }); + + validateEvent(executeEvent, { + spaceId: space.id, + savedObjects: [{ type: 'alert', id: alertId, rel: 'primary', type_id: 'test.throw' }], + outcome: 'failure', + message: `alert execution failure: test.throw:${alertId}: 'abc'`, + errorMessage: 'this alert is intended to fail', + status: 'error', + reason: 'execute', + shouldHaveTask: true, + rule: { + id: alertId, + category: response.body.rule_type_id, + license: 'basic', + ruleset: 'alertsFixture', + }, + }); + }); }); - }); + } }); } @@ -510,12 +546,13 @@ interface ValidateEventLogParams { outcome?: string; message: string; shouldHaveEventEnd?: boolean; + shouldHaveTask?: boolean; errorMessage?: string; status?: string; actionGroupId?: string; instanceId?: string; reason?: string; - rule: { + rule?: { id: string; name?: string; version?: string; @@ -529,7 +566,7 @@ interface ValidateEventLogParams { } export function validateEvent(event: IValidatedEvent, params: ValidateEventLogParams): void { - const { spaceId, savedObjects, outcome, message, errorMessage, rule } = params; + const { spaceId, savedObjects, outcome, message, errorMessage, rule, shouldHaveTask } = params; const { status, actionGroupId, instanceId, reason, shouldHaveEventEnd } = params; if (status) { @@ -587,6 +624,16 @@ export function validateEvent(event: IValidatedEvent, params: ValidateEventLogPa expect(event?.rule).to.eql(rule); + if (shouldHaveTask) { + const task = event?.kibana?.task; + expect(task).to.be.ok(); + expect(typeof Date.parse(typeof task?.scheduled)).to.be('number'); + expect(typeof task?.schedule_delay).to.be('number'); + expect(task?.schedule_delay).to.be.greaterThan(-1); + } else { + expect(event?.kibana?.task).to.be(undefined); + } + if (errorMessage) { expect(event?.error?.message).to.eql(errorMessage); } @@ -602,12 +649,13 @@ function getTimestamps(events: IValidatedEvent[]) { function isSavedObjectInEvent( event: IValidatedEvent, - namespace: string, + spaceId: string, type: string, id: string, rel?: string ): boolean { const savedObjects = event?.kibana?.saved_objects ?? []; + const namespace = spaceId === 'default' ? undefined : spaceId; for (const savedObject of savedObjects) { if ( diff --git a/x-pack/test/alerting_api_integration/spaces_only_legacy/config.ts b/x-pack/test/alerting_api_integration/spaces_only_legacy/config.ts index 511e97b96e35d..b322b8dffbf95 100644 --- a/x-pack/test/alerting_api_integration/spaces_only_legacy/config.ts +++ b/x-pack/test/alerting_api_integration/spaces_only_legacy/config.ts @@ -14,6 +14,6 @@ export default createTestConfig('spaces_only', { enableActionsProxy: false, rejectUnauthorized: false, verificationMode: undefined, - customizeLocalHostTls: true, + customizeLocalHostSsl: true, preconfiguredAlertHistoryEsIndex: true, }); diff --git a/x-pack/test/alerting_api_integration/spaces_only_legacy/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/spaces_only_legacy/tests/actions/builtin_action_types/webhook.ts index 4af33136cd42c..9822254db444a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only_legacy/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/spaces_only_legacy/tests/actions/builtin_action_types/webhook.ts @@ -123,9 +123,9 @@ export default function webhookTest({ getService }: FtrProviderContext) { }); }); - describe('tls customization', () => { + describe('ssl customization', () => { it('should handle the xpack.actions.rejectUnauthorized: false', async () => { - const connectorId = 'custom.tls.noCustom'; + const connectorId = 'custom.ssl.noCustom'; const port = await getPortOfConnector(connectorId); const server = await createTlsWebhookServer(port); const { status, body } = await supertest @@ -143,11 +143,11 @@ export default function webhookTest({ getService }: FtrProviderContext) { }); it('should handle the customized rejectUnauthorized: false', async () => { - const connectorId = 'custom.tls.rejectUnauthorizedFalse'; + const connectorId = 'custom.ssl.rejectUnauthorizedFalse'; const port = await getPortOfConnector(connectorId); const server = await createTlsWebhookServer(port); const { status, body } = await supertest - .post(`/api/actions/connector/custom.tls.rejectUnauthorizedFalse/_execute`) + .post(`/api/actions/connector/custom.ssl.rejectUnauthorizedFalse/_execute`) .set('kbn-xsrf', 'test') .send({ params: { @@ -161,11 +161,11 @@ export default function webhookTest({ getService }: FtrProviderContext) { }); it('should handle the customized rejectUnauthorized: true', async () => { - const connectorId = 'custom.tls.rejectUnauthorizedTrue'; + const connectorId = 'custom.ssl.rejectUnauthorizedTrue'; const port = await getPortOfConnector(connectorId); const server = await createTlsWebhookServer(port); const { status, body } = await supertest - .post(`/api/actions/connector/custom.tls.rejectUnauthorizedTrue/_execute`) + .post(`/api/actions/connector/custom.ssl.rejectUnauthorizedTrue/_execute`) .set('kbn-xsrf', 'test') .send({ params: { @@ -180,11 +180,11 @@ export default function webhookTest({ getService }: FtrProviderContext) { }); it('should handle the customized ca file', async () => { - const connectorId = 'custom.tls.caFile'; + const connectorId = 'custom.ssl.caFile'; const port = await getPortOfConnector(connectorId); const server = await createTlsWebhookServer(port); const { status, body } = await supertest - .post(`/api/actions/connector/custom.tls.caFile/_execute`) + .post(`/api/actions/connector/custom.ssl.caFile/_execute`) .set('kbn-xsrf', 'test') .send({ params: { diff --git a/x-pack/test/api_integration/apis/ml/jobs/close_jobs.ts b/x-pack/test/api_integration/apis/ml/jobs/close_jobs.ts index 0d64008a49688..40485205f9fb5 100644 --- a/x-pack/test/api_integration/apis/ml/jobs/close_jobs.ts +++ b/x-pack/test/api_integration/apis/ml/jobs/close_jobs.ts @@ -20,68 +20,6 @@ export default ({ getService }: FtrProviderContext) => { const testSetupJobConfigs = [SINGLE_METRIC_JOB_CONFIG, MULTI_METRIC_JOB_CONFIG]; - const testDataList = [ - { - testTitle: 'as ML Poweruser', - user: USER.ML_POWERUSER, - requestBody: { - jobIds: [SINGLE_METRIC_JOB_CONFIG.job_id, MULTI_METRIC_JOB_CONFIG.job_id], - }, - expected: { - responseCode: 200, - responseBody: { - [SINGLE_METRIC_JOB_CONFIG.job_id]: { closed: true }, - [MULTI_METRIC_JOB_CONFIG.job_id]: { closed: true }, - }, - }, - }, - ]; - - const testDataListFailed = [ - { - testTitle: 'as ML Poweruser', - user: USER.ML_POWERUSER, - requestBody: { - jobIds: [SINGLE_METRIC_JOB_CONFIG.job_id, MULTI_METRIC_JOB_CONFIG.job_id], - }, - expected: { - responseCode: 200, - - responseBody: { - [SINGLE_METRIC_JOB_CONFIG.job_id]: { closed: false, error: { status: 409 } }, - [MULTI_METRIC_JOB_CONFIG.job_id]: { closed: false, error: { status: 409 } }, - }, - }, - }, - ]; - - const testDataListUnauthorized = [ - { - testTitle: 'as ML Unauthorized user', - user: USER.ML_UNAUTHORIZED, - requestBody: { - jobIds: [SINGLE_METRIC_JOB_CONFIG.job_id, MULTI_METRIC_JOB_CONFIG.job_id], - }, - // Note that the jobs and datafeeds are loaded async so the actual error message is not deterministic. - expected: { - responseCode: 403, - error: 'Forbidden', - }, - }, - { - testTitle: 'as ML Viewer', - user: USER.ML_VIEWER, - requestBody: { - jobIds: [SINGLE_METRIC_JOB_CONFIG.job_id, MULTI_METRIC_JOB_CONFIG.job_id], - }, - // Note that the jobs and datafeeds are loaded async so the actual error message is not deterministic. - expected: { - responseCode: 403, - error: 'Forbidden', - }, - }, - ]; - async function runCloseJobsRequest( user: USER, requestBody: object, @@ -97,6 +35,14 @@ export default ({ getService }: FtrProviderContext) => { return body; } + async function startDatafeedsInRealtime() { + for (const job of testSetupJobConfigs) { + const datafeedId = `datafeed-${job.job_id}`; + await ml.api.startDatafeed(datafeedId, { start: '0' }); + await ml.api.waitForDatafeedState(datafeedId, DATAFEED_STATE.STARTED); + } + } + describe('close_jobs', function () { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); @@ -104,11 +50,7 @@ export default ({ getService }: FtrProviderContext) => { await ml.testResources.setKibanaTimeZoneToUTC(); }); - after(async () => { - await ml.api.cleanMlIndices(); - }); - - it('sets up jobs', async () => { + beforeEach(async () => { for (const job of testSetupJobConfigs) { const datafeedId = `datafeed-${job.job_id}`; await ml.api.createAnomalyDetectionJob(job); @@ -118,98 +60,132 @@ export default ({ getService }: FtrProviderContext) => { datafeed_id: datafeedId, job_id: job.job_id, }); - await ml.api.startDatafeed(datafeedId, { start: '0' }); - await ml.api.waitForDatafeedState(datafeedId, DATAFEED_STATE.STARTED); } }); - describe('rejects request', function () { - for (const testData of testDataListUnauthorized) { - describe('fails to close job ID supplied', function () { - it(`${testData.testTitle}`, async () => { - const body = await runCloseJobsRequest( - testData.user, - testData.requestBody, - testData.expected.responseCode - ); - - expect(body).to.have.property('error').eql(testData.expected.error); - - // ensure jobs are still open - for (const id of testData.requestBody.jobIds) { - await ml.api.waitForJobState(id, JOB_STATE.OPENED); - } - }); - }); + afterEach(async () => { + for (const job of testSetupJobConfigs) { + await ml.api.deleteAnomalyDetectionJobES(job.job_id); } + await ml.api.cleanMlIndices(); }); - describe('close jobs fail because they are running', function () { - for (const testData of testDataListFailed) { - it(`${testData.testTitle}`, async () => { - const body = await runCloseJobsRequest( - testData.user, - testData.requestBody, - testData.expected.responseCode - ); - const expectedResponse = testData.expected.responseBody; - const expectedRspJobIds = Object.keys(expectedResponse).sort((a, b) => - a.localeCompare(b) - ); - const actualRspJobIds = Object.keys(body).sort((a, b) => a.localeCompare(b)); - - expect(actualRspJobIds).to.have.length(expectedRspJobIds.length); - expect(actualRspJobIds).to.eql(expectedRspJobIds); - - expectedRspJobIds.forEach((id) => { - expect(body[id].closed).to.eql(testData.expected.responseBody[id].closed); - expect(body[id].error.status).to.eql(testData.expected.responseBody[id].error.status); - }); - - // ensure jobs are still open - for (const id of testData.requestBody.jobIds) { - await ml.api.waitForJobState(id, JOB_STATE.OPENED); - } - }); + it('rejects request for ML Unauthorized user', async () => { + await startDatafeedsInRealtime(); + + const jobIds = [SINGLE_METRIC_JOB_CONFIG.job_id, MULTI_METRIC_JOB_CONFIG.job_id]; + const body = await runCloseJobsRequest(USER.ML_UNAUTHORIZED, { jobIds }, 403); + + expect(body).to.have.property('error').eql('Forbidden'); + + // ensure jobs are still open + for (const id of jobIds) { + await ml.api.waitForJobState(id, JOB_STATE.OPENED); + } + }); + + it('rejects request for ML Viewer user', async () => { + await startDatafeedsInRealtime(); + + const jobIds = [SINGLE_METRIC_JOB_CONFIG.job_id, MULTI_METRIC_JOB_CONFIG.job_id]; + const body = await runCloseJobsRequest(USER.ML_VIEWER, { jobIds }, 403); + + expect(body).to.have.property('error').eql('Forbidden'); + + // ensure jobs are still open + for (const id of jobIds) { + await ml.api.waitForJobState(id, JOB_STATE.OPENED); + } + }); + + it('succeeds for ML Poweruser with datafeed started', async () => { + await startDatafeedsInRealtime(); + + const jobIds = [SINGLE_METRIC_JOB_CONFIG.job_id, MULTI_METRIC_JOB_CONFIG.job_id]; + const body = await runCloseJobsRequest(USER.ML_POWERUSER, { jobIds }, 200); + + const expectedRspBody = { + [SINGLE_METRIC_JOB_CONFIG.job_id]: { closed: true }, + [MULTI_METRIC_JOB_CONFIG.job_id]: { closed: true }, + }; + const expectedRspJobIds = Object.keys(expectedRspBody).sort((a, b) => a.localeCompare(b)); + const actualRspJobIds = Object.keys(body).sort((a, b) => a.localeCompare(b)); + + expect(actualRspJobIds).to.have.length(expectedRspJobIds.length); + expect(actualRspJobIds).to.eql(expectedRspJobIds); + + expectedRspJobIds.forEach((id) => { + expect(body[id].closed).to.eql(expectedRspBody[id].closed); + }); + + // datafeeds should be stopped automatically + for (const id of jobIds) { + await ml.api.waitForDatafeedState(`datafeed-${id}`, DATAFEED_STATE.STOPPED); + } + + // ensure jobs are actually closed + for (const id of jobIds) { + await ml.api.waitForJobState(id, JOB_STATE.CLOSED); } }); - describe('stops datafeeds', function () { - it('stops datafeeds', async () => { - for (const job of testSetupJobConfigs) { - const datafeedId = `datafeed-${job.job_id}`; - await ml.api.stopDatafeed(datafeedId); - await ml.api.waitForDatafeedState(datafeedId, DATAFEED_STATE.STOPPED); - } + it('succeeds for ML Poweruser with datafeed stopped', async () => { + const jobIds = [SINGLE_METRIC_JOB_CONFIG.job_id, MULTI_METRIC_JOB_CONFIG.job_id]; + const body = await runCloseJobsRequest(USER.ML_POWERUSER, { jobIds }, 200); + + const expectedRspBody = { + [SINGLE_METRIC_JOB_CONFIG.job_id]: { closed: true }, + [MULTI_METRIC_JOB_CONFIG.job_id]: { closed: true }, + }; + const expectedRspJobIds = Object.keys(expectedRspBody).sort((a, b) => a.localeCompare(b)); + const actualRspJobIds = Object.keys(body).sort((a, b) => a.localeCompare(b)); + + expect(actualRspJobIds).to.have.length(expectedRspJobIds.length); + expect(actualRspJobIds).to.eql(expectedRspJobIds); + + expectedRspJobIds.forEach((id) => { + expect(body[id].closed).to.eql(expectedRspBody[id].closed); }); + + // datafeeds should still be stopped + for (const id of jobIds) { + await ml.api.waitForDatafeedState(`datafeed-${id}`, DATAFEED_STATE.STOPPED); + } + + // ensure jobs are actually closed + for (const id of jobIds) { + await ml.api.waitForJobState(id, JOB_STATE.CLOSED); + } }); - describe('close jobs succeed', function () { - for (const testData of testDataList) { - it(`${testData.testTitle}`, async () => { - const body = await runCloseJobsRequest( - testData.user, - testData.requestBody, - testData.expected.responseCode - ); - const expectedResponse = testData.expected.responseBody; - const expectedRspJobIds = Object.keys(expectedResponse).sort((a, b) => - a.localeCompare(b) - ); - const actualRspJobIds = Object.keys(body).sort((a, b) => a.localeCompare(b)); - - expect(actualRspJobIds).to.have.length(expectedRspJobIds.length); - expect(actualRspJobIds).to.eql(expectedRspJobIds); - - expectedRspJobIds.forEach((id) => { - expect(body[id].closed).to.eql(testData.expected.responseBody[id].closed); - }); - - // ensure jobs are now closed - for (const id of testData.requestBody.jobIds) { - await ml.api.waitForJobState(id, JOB_STATE.CLOSED); - } - }); + it('succeeds for ML Poweruser with job already closed', async () => { + const jobIds = [SINGLE_METRIC_JOB_CONFIG.job_id, MULTI_METRIC_JOB_CONFIG.job_id]; + await runCloseJobsRequest(USER.ML_POWERUSER, { jobIds }, 200); + + const body = await runCloseJobsRequest(USER.ML_POWERUSER, { jobIds }, 200); + + const expectedRspBody = { + [SINGLE_METRIC_JOB_CONFIG.job_id]: { closed: true }, + [MULTI_METRIC_JOB_CONFIG.job_id]: { closed: true }, + }; + const expectedRspJobIds = Object.keys(expectedRspBody).sort((a, b) => a.localeCompare(b)); + const actualRspJobIds = Object.keys(body).sort((a, b) => a.localeCompare(b)); + + expect(actualRspJobIds).to.have.length(expectedRspJobIds.length); + expect(actualRspJobIds).to.eql(expectedRspJobIds); + + expectedRspJobIds.forEach((id) => { + expect(body[id].closed).to.eql(expectedRspBody[id].closed); + }); + + // datafeeds should still be stopped + for (const id of jobIds) { + await ml.api.waitForDatafeedState(`datafeed-${id}`, DATAFEED_STATE.STOPPED); + } + + // jobs should still be closed + for (const id of jobIds) { + await ml.api.waitForJobState(id, JOB_STATE.CLOSED); } }); }); diff --git a/x-pack/test/api_integration/apis/ml/modules/index.ts b/x-pack/test/api_integration/apis/ml/modules/index.ts index 1a0c532dc36fa..3cf1c7f787840 100644 --- a/x-pack/test/api_integration/apis/ml/modules/index.ts +++ b/x-pack/test/api_integration/apis/ml/modules/index.ts @@ -9,11 +9,14 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, loadTestFile }: FtrProviderContext) { const ml = getService('ml'); + const supertest = getService('supertest'); const fleetPackages = ['apache', 'nginx']; describe('modules', function () { before(async () => { + // Fleet need to be setup to be able to setup packages + await supertest.post(`/api/fleet/setup`).set({ 'kbn-xsrf': 'some-xsrf-token' }).expect(200); for (const fleetPackage of fleetPackages) { await ml.testResources.installFleetPackage(fleetPackage); } diff --git a/x-pack/test/api_integration/apis/search/session.ts b/x-pack/test/api_integration/apis/search/session.ts index d47199a0f1c1e..06be7c6759bc0 100644 --- a/x-pack/test/api_integration/apis/search/session.ts +++ b/x-pack/test/api_integration/apis/search/session.ts @@ -403,7 +403,12 @@ export default function ({ getService }: FtrProviderContext) { const { id: id1 } = searchRes1.body; // it might take the session a moment to be created - await new Promise((resolve) => setTimeout(resolve, 2500)); + await retry.waitFor('search session created', async () => { + const response = await supertest + .get(`/internal/session/${sessionId}`) + .set('kbn-xsrf', 'foo'); + return response.body.statusCode === undefined; + }); const getSessionFirstTime = await supertest .get(`/internal/session/${sessionId}`) diff --git a/x-pack/test/api_integration/apis/security_solution/events.ts b/x-pack/test/api_integration/apis/security_solution/events.ts index 2135bdafd70ec..ff4256f1a1adf 100644 --- a/x-pack/test/api_integration/apis/security_solution/events.ts +++ b/x-pack/test/api_integration/apis/security_solution/events.ts @@ -415,7 +415,7 @@ export default function ({ getService }: FtrProviderContext) { it('Make sure that we get Timeline data', async () => { await retry.try(async () => { const resp = await supertest - .post('/internal/search/securitySolutionTimelineSearchStrategy/') + .post('/internal/search/timelineSearchStrategy/') .set('kbn-xsrf', 'true') .set('Content-Type', 'application/json') .send({ @@ -457,7 +457,7 @@ export default function ({ getService }: FtrProviderContext) { it('Make sure that pagination is working in Timeline query', async () => { await retry.try(async () => { const resp = await supertest - .post('/internal/search/securitySolutionTimelineSearchStrategy/') + .post('/internal/search/timelineSearchStrategy/') .set('kbn-xsrf', 'true') .set('Content-Type', 'application/json') .send({ diff --git a/x-pack/test/api_integration/apis/security_solution/sources.ts b/x-pack/test/api_integration/apis/security_solution/sources.ts index db9156a53048b..7f5c46610d607 100644 --- a/x-pack/test/api_integration/apis/security_solution/sources.ts +++ b/x-pack/test/api_integration/apis/security_solution/sources.ts @@ -19,7 +19,7 @@ export default function ({ getService }: FtrProviderContext) { it('Make sure that we get source information when auditbeat indices is there', async () => { const { body: sourceStatus } = await supertest - .post('/internal/search/securitySolutionIndexFields/') + .post('/internal/search/indexFields/') .set('kbn-xsrf', 'true') .send({ indices: ['auditbeat-*'], @@ -34,7 +34,7 @@ export default function ({ getService }: FtrProviderContext) { it('should find indexes as being available when they exist', async () => { const { body: sourceStatus } = await supertest - .post('/internal/search/securitySolutionIndexFields/') + .post('/internal/search/indexFields/') .set('kbn-xsrf', 'true') .send({ indices: ['auditbeat-*', 'filebeat-*'], @@ -48,7 +48,7 @@ export default function ({ getService }: FtrProviderContext) { it('should not find indexes as existing when there is an empty array of them', async () => { const { body: sourceStatus } = await supertest - .post('/internal/search/securitySolutionIndexFields/') + .post('/internal/search/indexFields/') .set('kbn-xsrf', 'true') .send({ indices: [], @@ -62,7 +62,7 @@ export default function ({ getService }: FtrProviderContext) { it('should not find indexes as existing when there is a _all within it', async () => { const { body: sourceStatus } = await supertest - .post('/internal/search/securitySolutionIndexFields/') + .post('/internal/search/indexFields/') .set('kbn-xsrf', 'true') .send({ indices: ['_all'], @@ -76,7 +76,7 @@ export default function ({ getService }: FtrProviderContext) { it('should not find indexes as existing when there are empty strings within it', async () => { const { body: sourceStatus } = await supertest - .post('/internal/search/securitySolutionIndexFields/') + .post('/internal/search/indexFields/') .set('kbn-xsrf', 'true') .send({ indices: [''], @@ -90,7 +90,7 @@ export default function ({ getService }: FtrProviderContext) { it('should not find indexes as existing when there are blank spaces within it', async () => { const { body: sourceStatus } = await supertest - .post('/internal/search/securitySolutionIndexFields/') + .post('/internal/search/indexFields/') .set('kbn-xsrf', 'true') .send({ indices: [' '], @@ -104,7 +104,7 @@ export default function ({ getService }: FtrProviderContext) { it('should find indexes when one is an empty index but the others are valid', async () => { const { body: sourceStatus } = await supertest - .post('/internal/search/securitySolutionIndexFields/') + .post('/internal/search/indexFields/') .set('kbn-xsrf', 'true') .send({ indices: ['', 'auditbeat-*'], diff --git a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts index e1eaef823d2e0..3aefd9f8b597a 100644 --- a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts +++ b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts @@ -681,7 +681,7 @@ export default function ({ getService }: FtrProviderContext) { const { body: { data: detailsData }, } = await supertest - .post('/internal/search/securitySolutionTimelineSearchStrategy/') + .post('/internal/search/timelineSearchStrategy/') .set('kbn-xsrf', 'true') .send({ factoryQueryType: TimelineEventsQueries.details, @@ -701,7 +701,7 @@ export default function ({ getService }: FtrProviderContext) { const { body: { destinationIpCount, hostCount, processCount, sourceIpCount, userCount }, } = await supertest - .post('/internal/search/securitySolutionTimelineSearchStrategy/') + .post('/internal/search/timelineSearchStrategy/') .set('kbn-xsrf', 'true') .send({ factoryQueryType: TimelineEventsQueries.kpi, diff --git a/x-pack/test/api_integration/config.ts b/x-pack/test/api_integration/config.ts index 6708a6d55f402..550148531e2ec 100644 --- a/x-pack/test/api_integration/config.ts +++ b/x-pack/test/api_integration/config.ts @@ -33,6 +33,7 @@ export async function getApiIntegrationConfig({ readConfigFile }: FtrConfigProvi '--xpack.data_enhanced.search.sessions.enabled=true', // enable WIP send to background UI '--xpack.data_enhanced.search.sessions.notTouchedTimeout=15s', // shorten notTouchedTimeout for quicker testing '--xpack.data_enhanced.search.sessions.trackingInterval=5s', // shorten trackingInterval for quicker testing + '--xpack.data_enhanced.search.sessions.cleanupInterval=5s', // shorten cleanupInterval for quicker testing ], }, esTestCluster: { diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts index 6c81f1fcfa264..887e6e7894f98 100644 --- a/x-pack/test/case_api_integration/common/config.ts +++ b/x-pack/test/case_api_integration/common/config.ts @@ -26,6 +26,7 @@ const enabledActionTypes = [ '.index', '.jira', '.pagerduty', + '.swimlane', '.resilient', '.server-log', '.servicenow', diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index 921589b2341dd..6b59d9780a513 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -46,6 +46,7 @@ import { CasesConfigurationsResponse, CaseUserActionsResponse, AlertResponse, + ConnectorMappings, CasesByAlertId, } from '../../../../plugins/cases/common/api'; import { getPostCaseRequest, postCollectionReq, postCommentGenAlertReq } from './mock'; @@ -578,6 +579,32 @@ export const ensureSavedObjectIsAuthorized = ( entities.forEach((entity) => expect(owners.includes(entity.owner)).to.be(true)); }; +interface ConnectorMappingsSavedObject { + 'cases-connector-mappings': ConnectorMappings; +} + +/** + * Returns connector mappings saved objects from Elasticsearch directly. + */ +export const getConnectorMappingsFromES = async ({ es }: { es: KibanaClient }) => { + const mappings: ApiResponse< + estypes.SearchResponse<ConnectorMappingsSavedObject> + > = await es.search({ + index: '.kibana', + body: { + query: { + term: { + type: { + value: 'cases-connector-mappings', + }, + }, + }, + }, + }); + + return mappings; +}; + export const createCaseWithConnector = async ({ supertest, configureReq = {}, diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts index 8a58c59718feb..374053dd3b8b7 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts @@ -28,12 +28,19 @@ import { deleteAllCaseItems, superUserSpace1Auth, createCaseWithConnector, + createConnector, + getServiceNowConnector, + getConnectorMappingsFromES, } from '../../../../common/lib/utils'; import { ExternalServiceSimulator, getExternalServiceSimulatorPath, } from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; -import { CaseStatuses, CaseUserActionResponse } from '../../../../../../plugins/cases/common/api'; +import { + CaseConnector, + CaseStatuses, + CaseUserActionResponse, +} from '../../../../../../plugins/cases/common/api'; import { globalRead, noKibanaPrivileges, @@ -95,6 +102,56 @@ export default ({ getService }: FtrProviderContext): void => { ).to.equal(true); }); + it('should create the mappings when pushing a case', async () => { + // create a connector but not a configuration so that the mapping will not be present + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, + }); + + actionsRemover.add('default', connector.id, 'action', 'actions'); + + const postedCase = await createCase( + supertest, + { + ...getPostCaseRequest(), + connector: { + id: connector.id, + name: connector.name, + type: connector.connector_type_id, + fields: { + urgency: '2', + impact: '2', + severity: '2', + category: 'software', + subcategory: 'os', + }, + } as CaseConnector, + }, + 200 + ); + + // there should be no mappings initially + let mappings = await getConnectorMappingsFromES({ es }); + expect(mappings.body.hits.hits.length).to.eql(0); + + await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + }); + + // the mappings should now be created after the push + mappings = await getConnectorMappingsFromES({ es }); + expect(mappings.body.hits.hits.length).to.be(1); + expect( + mappings.body.hits.hits[0]._source?.['cases-connector-mappings'].mappings.length + ).to.be.above(0); + }); + it('pushes a comment appropriately', async () => { const { postedCase, connector } = await createCaseWithConnector({ supertest, diff --git a/x-pack/test/detection_engine_api_integration/common/config.ts b/x-pack/test/detection_engine_api_integration/common/config.ts index 5cbf9598dc4a1..ef822b0af2a29 100644 --- a/x-pack/test/detection_engine_api_integration/common/config.ts +++ b/x-pack/test/detection_engine_api_integration/common/config.ts @@ -20,6 +20,7 @@ const enabledActionTypes = [ '.email', '.index', '.pagerduty', + '.swimlane', '.server-log', '.servicenow', '.slack', diff --git a/x-pack/test/fleet_api_integration/apis/epm/__snapshots__/install_by_upload.snap b/x-pack/test/fleet_api_integration/apis/epm/__snapshots__/install_by_upload.snap index 7584dfcc8a6c0..13c2dd24f9103 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/__snapshots__/install_by_upload.snap +++ b/x-pack/test/fleet_api_integration/apis/epm/__snapshots__/install_by_upload.snap @@ -341,14 +341,26 @@ Object { "id": "logs-apache.access", "type": "index_template", }, + Object { + "id": "logs-apache.access@custom", + "type": "component_template", + }, Object { "id": "metrics-apache.status", "type": "index_template", }, + Object { + "id": "metrics-apache.status@custom", + "type": "component_template", + }, Object { "id": "logs-apache.error", "type": "index_template", }, + Object { + "id": "logs-apache.error@custom", + "type": "component_template", + }, ], "installed_kibana": Array [ Object { diff --git a/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts b/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts index 81f712e095c78..68a78dd842c4b 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts @@ -12,7 +12,7 @@ import { skipIfNoDockerRegistry } from '../../helpers'; const TEST_INDEX = 'logs-log.log-test'; -const FINAL_PIPELINE_ID = '.fleet_final_pipeline'; +const FINAL_PIPELINE_ID = '.fleet_final_pipeline-1'; let pkgKey: string; @@ -43,7 +43,6 @@ export default function (providerContext: FtrProviderContext) { const { body: getPackagesRes } = await supertest.get( `/api/fleet/epm/packages?experimental=true` ); - const logPackage = getPackagesRes.response.find((p: any) => p.name === 'log'); if (!logPackage) { throw new Error('No log package'); @@ -85,12 +84,11 @@ export default function (providerContext: FtrProviderContext) { it('should correctly setup the final pipeline and apply to fleet managed index template', async () => { const pipelineRes = await es.ingest.getPipeline({ id: FINAL_PIPELINE_ID }); expect(pipelineRes.body).to.have.property(FINAL_PIPELINE_ID); - const res = await es.indices.getIndexTemplate({ name: 'logs-log.log' }); expect(res.body.index_templates.length).to.be(1); - expect( - res.body.index_templates[0]?.index_template?.template?.settings?.index?.final_pipeline - ).to.be(FINAL_PIPELINE_ID); + expect(res.body.index_templates[0]?.index_template?.composed_of).to.contain( + '.fleet_component_template-1' + ); }); it('For a doc written without api key should write the correct api key status', async () => { diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts b/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts index 71cf7ed79fa2b..182838f21dbda 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts @@ -70,7 +70,7 @@ export default function (providerContext: FtrProviderContext) { .type('application/gzip') .send(buf) .expect(200); - expect(res.body.response.length).to.be(23); + expect(res.body.response.length).to.be(26); }); it('should install a zip archive correctly and package info should return correctly after validation', async function () { @@ -81,7 +81,7 @@ export default function (providerContext: FtrProviderContext) { .type('application/zip') .send(buf) .expect(200); - expect(res.body.response.length).to.be(23); + expect(res.body.response.length).to.be(26); const packageInfoRes = await supertest .get(`/api/fleet/epm/packages/${testPkgKey}`) diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_overrides.ts b/x-pack/test/fleet_api_integration/apis/epm/install_overrides.ts index 1b916dff573af..770502db49dae 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_overrides.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_overrides.ts @@ -7,22 +7,22 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; -import { warnAndSkipTest } from '../../helpers'; +import { skipIfNoDockerRegistry } from '../../helpers'; -export default function ({ getService }: FtrProviderContext) { +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; const supertest = getService('supertest'); const es = getService('es'); const dockerServers = getService('dockerServers'); - const log = getService('log'); const mappingsPackage = 'overrides-0.1.0'; const server = dockerServers.get('registry'); - const deletePackage = async (pkgkey: string) => { - await supertest.delete(`/api/fleet/epm/packages/${pkgkey}`).set('kbn-xsrf', 'xxxx'); - }; + const deletePackage = async (pkgkey: string) => + supertest.delete(`/api/fleet/epm/packages/${pkgkey}`).set('kbn-xsrf', 'xxxx'); describe('installs packages that include settings and mappings overrides', async () => { + skipIfNoDockerRegistry(providerContext); after(async () => { if (server.enabled) { // remove the package just in case it being installed will affect other tests @@ -31,50 +31,108 @@ export default function ({ getService }: FtrProviderContext) { }); it('should install the overrides package correctly', async function () { - if (server.enabled) { - let { body } = await supertest - .post(`/api/fleet/epm/packages/${mappingsPackage}`) - .set('kbn-xsrf', 'xxxx') - .expect(200); - - const templateName = body.response[0].id; - - ({ body } = await es.transport.request({ - method: 'GET', - path: `/_index_template/${templateName}`, - })); - - // make sure it has the right composed_of array, the contents should be the component templates - // that were installed - expect(body.index_templates[0].index_template.composed_of).to.contain( - `${templateName}-mappings` - ); - expect(body.index_templates[0].index_template.composed_of).to.contain( - `${templateName}-settings` - ); - - ({ body } = await es.transport.request({ - method: 'GET', - path: `/_component_template/${templateName}-mappings`, - })); - - // Make sure that the `dynamic` field exists and is set to false (as it is in the package) - expect(body.component_templates[0].component_template.template.mappings.dynamic).to.be( - false - ); - - ({ body } = await es.transport.request({ - method: 'GET', - path: `/_component_template/${templateName}-settings`, - })); - - // Make sure that the lifecycle name gets set correct in the settings - expect( - body.component_templates[0].component_template.template.settings.index.lifecycle.name - ).to.be('reference'); - } else { - warnAndSkipTest(this, log); - } + let { body } = await supertest + .post(`/api/fleet/epm/packages/${mappingsPackage}`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + + const templateName = body.response[0].id; + + const { body: indexTemplateResponse } = await es.transport.request({ + method: 'GET', + path: `/_index_template/${templateName}`, + }); + + // the index template composed_of has the correct component templates in the correct order + const indexTemplate = indexTemplateResponse.index_templates[0].index_template; + expect(indexTemplate.composed_of).to.eql([ + `${templateName}@mappings`, + `${templateName}@settings`, + `${templateName}@custom`, + '.fleet_component_template-1', + ]); + + ({ body } = await es.transport.request({ + method: 'GET', + path: `/_component_template/${templateName}@mappings`, + })); + + // The mappings override provided in the package is set in the mappings component template + expect(body.component_templates[0].component_template.template.mappings.dynamic).to.be(false); + + ({ body } = await es.transport.request({ + method: 'GET', + path: `/_component_template/${templateName}@settings`, + })); + + // The settings override provided in the package is set in the settings component template + expect( + body.component_templates[0].component_template.template.settings.index.lifecycle.name + ).to.be('reference'); + + ({ body } = await es.transport.request({ + method: 'GET', + path: `/_component_template/${templateName}@custom`, + })); + + // The user_settings component template is an empty/stub template at first + const storedTemplate = body.component_templates[0].component_template.template.settings; + expect(storedTemplate).to.eql({}); + + // Update the user_settings component template + ({ body } = await es.transport.request({ + method: 'PUT', + path: `/_component_template/${templateName}@custom`, + body: { + template: { + settings: { + number_of_shards: 3, + index: { + lifecycle: { name: 'overridden by user' }, + number_of_shards: 123, + }, + }, + }, + }, + })); + + // simulate the result + ({ body } = await es.transport.request({ + method: 'POST', + path: `/_index_template/_simulate/${templateName}`, + // body: indexTemplate, // I *think* this should work, but it doesn't + body: { + index_patterns: [`${templateName}-*`], + composed_of: [ + `${templateName}@mappings`, + `${templateName}@settings`, + `${templateName}@custom`, + ], + }, + })); + + expect(body).to.eql({ + template: { + settings: { + index: { + lifecycle: { + name: 'overridden by user', + }, + number_of_shards: '3', + }, + }, + mappings: { + dynamic: 'false', + }, + aliases: {}, + }, + overlapping: [ + { + name: 'logs', + index_patterns: ['logs-*-*'], + }, + ], + }); }); }); } diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts index 8e09e331bf867..85573560177ee 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts @@ -87,6 +87,40 @@ export default function (providerContext: FtrProviderContext) { ); expect(resMetricsTemplate.statusCode).equal(404); }); + it('should have uninstalled the component templates', async function () { + const resMappings = await es.transport.request( + { + method: 'GET', + path: `/_component_template/${logsTemplateName}@mappings`, + }, + { + ignore: [404], + } + ); + expect(resMappings.statusCode).equal(404); + + const resSettings = await es.transport.request( + { + method: 'GET', + path: `/_component_template/${logsTemplateName}@settings`, + }, + { + ignore: [404], + } + ); + expect(resSettings.statusCode).equal(404); + + const resUserSettings = await es.transport.request( + { + method: 'GET', + path: `/_component_template/${logsTemplateName}@custom`, + }, + { + ignore: [404], + } + ); + expect(resUserSettings.statusCode).equal(404); + }); it('should have uninstalled the pipelines', async function () { const res = await es.transport.request( { @@ -328,17 +362,22 @@ const expectAssetsInstalled = ({ }); expect(resPipeline2.statusCode).equal(200); }); - it('should have installed the template components', async function () { - const res = await es.transport.request({ + it('should have installed the component templates', async function () { + const resMappings = await es.transport.request({ method: 'GET', - path: `/_component_template/${logsTemplateName}-mappings`, + path: `/_component_template/${logsTemplateName}@mappings`, }); - expect(res.statusCode).equal(200); + expect(resMappings.statusCode).equal(200); const resSettings = await es.transport.request({ method: 'GET', - path: `/_component_template/${logsTemplateName}-settings`, + path: `/_component_template/${logsTemplateName}@settings`, }); expect(resSettings.statusCode).equal(200); + const resUserSettings = await es.transport.request({ + method: 'GET', + path: `/_component_template/${logsTemplateName}@custom`, + }); + expect(resUserSettings.statusCode).equal(200); }); it('should have installed the transform components', async function () { const res = await es.transport.request({ @@ -487,6 +526,22 @@ const expectAssetsInstalled = ({ }, ], installed_es: [ + { + id: 'logs-all_assets.test_logs@mappings', + type: 'component_template', + }, + { + id: 'logs-all_assets.test_logs@settings', + type: 'component_template', + }, + { + id: 'logs-all_assets.test_logs@custom', + type: 'component_template', + }, + { + id: 'metrics-all_assets.test_metrics@custom', + type: 'component_template', + }, { id: 'logs-all_assets.test_logs-all_assets', type: 'data_stream_ilm_policy', diff --git a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts index a6f79414ab8c0..6b4d104423144 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts @@ -199,23 +199,45 @@ export default function (providerContext: FtrProviderContext) { ); expect(resPipeline2.statusCode).equal(404); }); - it('should have updated the template components', async function () { - const res = await es.transport.request({ + it('should have updated the component templates', async function () { + const resMappings = await es.transport.request({ method: 'GET', - path: `/_component_template/${logsTemplateName}-mappings`, + path: `/_component_template/${logsTemplateName}@mappings`, }); - expect(res.statusCode).equal(200); - expect(res.body.component_templates[0].component_template.template.mappings).eql({ + expect(resMappings.statusCode).equal(200); + expect(resMappings.body.component_templates[0].component_template.template.mappings).eql({ dynamic: true, }); const resSettings = await es.transport.request({ method: 'GET', - path: `/_component_template/${logsTemplateName}-settings`, + path: `/_component_template/${logsTemplateName}@settings`, }); - expect(res.statusCode).equal(200); + expect(resSettings.statusCode).equal(200); expect(resSettings.body.component_templates[0].component_template.template.settings).eql({ index: { lifecycle: { name: 'reference2' } }, }); + const resUserSettings = await es.transport.request({ + method: 'GET', + path: `/_component_template/${logsTemplateName}@custom`, + }); + expect(resUserSettings.statusCode).equal(200); + expect(resUserSettings.body).eql({ + component_templates: [ + { + name: 'logs-all_assets.test_logs@custom', + component_template: { + _meta: { + package: { + name: 'all_assets', + }, + }, + template: { + settings: {}, + }, + }, + }, + ], + }); }); it('should have updated the index patterns', async function () { const resIndexPatternLogs = await kibanaServer.savedObjects.get({ @@ -321,14 +343,34 @@ export default function (providerContext: FtrProviderContext) { id: 'logs-all_assets.test_logs', type: 'index_template', }, + { + id: 'logs-all_assets.test_logs@mappings', + type: 'component_template', + }, + { + id: 'logs-all_assets.test_logs@settings', + type: 'component_template', + }, + { + id: 'logs-all_assets.test_logs@custom', + type: 'component_template', + }, { id: 'logs-all_assets.test_logs2', type: 'index_template', }, + { + id: 'logs-all_assets.test_logs2@custom', + type: 'component_template', + }, { id: 'metrics-all_assets.test_metrics', type: 'index_template', }, + { + id: 'metrics-all_assets.test_metrics@custom', + type: 'component_template', + }, ], es_index_patterns: { test_logs: 'logs-all_assets.test_logs-*', diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.1.0/img/logo_overrides_64_color.svg b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.1.0/img/logo_overrides_64_color.svg new file mode 100644 index 0000000000000..b03007a76ffcc --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.1.0/img/logo_overrides_64_color.svg @@ -0,0 +1,7 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"> + <g fill="none" fill-rule="evenodd"> + <path fill="#F04E98" d="M29,32.0001 L15.935,9.4321 C13.48,5.1941 7,6.9351 7,11.8321 L7,52.1681 C7,57.0651 13.48,58.8061 15.935,54.5671 L29,32.0001 Z"/> + <path fill="#FA744E" d="M34.7773,32.0001 L33.3273,34.5051 L20.2613,57.0731 C19.8473,57.7871 19.3533,58.4271 18.8023,59.0001 L34.9273,59.0001 C38.7073,59.0001 42.2213,57.0601 44.2363,53.8611 L58.0003,32.0001 L34.7773,32.0001 Z"/> + <path fill="#343741" d="M44.2363,10.1392 C42.2213,6.9402 38.7073,5.0002 34.9273,5.0002 L18.8023,5.0002 C19.3533,5.5732 19.8473,6.2122 20.2613,6.9272 L33.3273,29.4942 L34.7773,32.0002 L58.0003,32.0002 L44.2363,10.1392 Z"/> + </g> +</svg> diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.1.0/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.1.0/manifest.yml index bba1a6a4c347d..312cd2874804c 100644 --- a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.1.0/manifest.yml +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.1.0/manifest.yml @@ -1,7 +1,7 @@ format_version: 1.0.0 -name: error_handling +name: error_handling title: Error handling -description: tests error handling and rollback +description: tests error handling and rollback version: 0.1.0 categories: [] release: beta @@ -17,4 +17,4 @@ requirement: icons: - src: '/img/logo_overrides_64_color.svg' size: '16x16' - type: 'image/svg+xml' \ No newline at end of file + type: 'image/svg+xml' diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/img/logo_overrides_64_color.svg b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/img/logo_overrides_64_color.svg new file mode 100644 index 0000000000000..b03007a76ffcc --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/img/logo_overrides_64_color.svg @@ -0,0 +1,7 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"> + <g fill="none" fill-rule="evenodd"> + <path fill="#F04E98" d="M29,32.0001 L15.935,9.4321 C13.48,5.1941 7,6.9351 7,11.8321 L7,52.1681 C7,57.0651 13.48,58.8061 15.935,54.5671 L29,32.0001 Z"/> + <path fill="#FA744E" d="M34.7773,32.0001 L33.3273,34.5051 L20.2613,57.0731 C19.8473,57.7871 19.3533,58.4271 18.8023,59.0001 L34.9273,59.0001 C38.7073,59.0001 42.2213,57.0601 44.2363,53.8611 L58.0003,32.0001 L34.7773,32.0001 Z"/> + <path fill="#343741" d="M44.2363,10.1392 C42.2213,6.9402 38.7073,5.0002 34.9273,5.0002 L18.8023,5.0002 C19.3533,5.5732 19.8473,6.2122 20.2613,6.9272 L33.3273,29.4942 L34.7773,32.0002 L58.0003,32.0002 L44.2363,10.1392 Z"/> + </g> +</svg> diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/manifest.yml index 2eb6a41a77ede..c92f0ab5ae7f3 100644 --- a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/manifest.yml +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/manifest.yml @@ -1,7 +1,7 @@ format_version: 1.0.0 -name: error_handling +name: error_handling title: Error handling -description: tests error handling and rollback +description: tests error handling and rollback version: 0.2.0 categories: [] release: beta @@ -16,4 +16,4 @@ requirement: icons: - src: '/img/logo_overrides_64_color.svg' - size: '16x16' \ No newline at end of file + size: '16x16' diff --git a/x-pack/test/fleet_api_integration/apis/preconfiguration/preconfiguration.ts b/x-pack/test/fleet_api_integration/apis/preconfiguration/preconfiguration.ts index 7fc784ee11af1..7c5c7d7f3f804 100644 --- a/x-pack/test/fleet_api_integration/apis/preconfiguration/preconfiguration.ts +++ b/x-pack/test/fleet_api_integration/apis/preconfiguration/preconfiguration.ts @@ -37,7 +37,7 @@ export default function (providerContext: FtrProviderContext) { // Basic health check for the API; functionality is covered by the unit tests it('should succeed with an empty payload', async () => { const { body } = await supertest - .put(PRECONFIGURATION_API_ROUTES.PUT_PRECONFIG) + .put(PRECONFIGURATION_API_ROUTES.UPDATE_PATTERN) .set('kbn-xsrf', 'xxxx') .send({}) .expect(200); diff --git a/x-pack/test/fleet_api_integration/config.ts b/x-pack/test/fleet_api_integration/config.ts index 52c9760d66c19..d18ba9c55ca96 100644 --- a/x-pack/test/fleet_api_integration/config.ts +++ b/x-pack/test/fleet_api_integration/config.ts @@ -51,17 +51,11 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { waitForLogLine: 'package manifests loaded', }, }), - services: { - ...xPackAPITestsConfig.get('services'), - }, + services: xPackAPITestsConfig.get('services'), junit: { reportName: 'X-Pack EPM API Integration Tests', }, - - esTestCluster: { - ...xPackAPITestsConfig.get('esTestCluster'), - }, - + esTestCluster: xPackAPITestsConfig.get('esTestCluster'), kbnTestServer: { ...xPackAPITestsConfig.get('kbnTestServer'), serverArgs: [ diff --git a/x-pack/test/functional/apps/dashboard/index.ts b/x-pack/test/functional/apps/dashboard/index.ts index 1d046c7c18218..99f8c6ffedefc 100644 --- a/x-pack/test/functional/apps/dashboard/index.ts +++ b/x-pack/test/functional/apps/dashboard/index.ts @@ -19,5 +19,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_async_dashboard')); loadTestFile(require.resolve('./dashboard_lens_by_value')); loadTestFile(require.resolve('./dashboard_maps_by_value')); + + loadTestFile(require.resolve('./migration_smoke_tests/lens_migration_smoke_test')); }); } diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/lens_dashboard_migration_test_7_12_1.ndjson b/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/lens_dashboard_migration_test_7_12_1.ndjson new file mode 100644 index 0000000000000..cdf6e94537ae6 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/lens_dashboard_migration_test_7_12_1.ndjson @@ -0,0 +1,7 @@ +{"attributes":{"fieldAttrs":"{}","fields":"[]","runtimeFieldMap":"{}","title":"shakespeare"},"coreMigrationVersion":"7.12.2","id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","migrationVersion":{"index-pattern":"7.11.0"},"references":[],"type":"index-pattern","updated_at":"2021-06-17T22:28:02.495Z","version":"WzEyLDJd"} +{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"},"optionsJSON":"{\"useMargins\":true,\"hidePanelTitles\":false}","panelsJSON":"[]","timeRestore":false,"title":"Blank Destination Dashboard","version":1},"coreMigrationVersion":"7.12.2","id":"759faf20-cfbd-11eb-984d-af3b44ed60a7","migrationVersion":{"dashboard":"7.11.0"},"references":[],"type":"dashboard","updated_at":"2021-06-17T22:43:39.414Z","version":"WzI1MiwyXQ=="} +{"attributes":{"state":{"datasourceStates":{"indexpattern":{"layers":{"8faa1a43-2c03-4277-b19b-575da8b59561":{"columnOrder":["20d61a13-4000-4df2-9d83-d9ec0c87b32a","6f1df118-ab3f-4f7a-b5ea-c38e0c11aefe"],"columns":{"20d61a13-4000-4df2-9d83-d9ec0c87b32a":{"dataType":"string","isBucketed":true,"label":"Top values of speaker","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"6f1df118-ab3f-4f7a-b5ea-c38e0c11aefe","type":"column"},"orderDirection":"desc","otherBucket":true,"size":20},"scale":"ordinal","sourceField":"speaker"},"6f1df118-ab3f-4f7a-b5ea-c38e0c11aefe":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"}},"incompleteColumns":{}}}}},"filters":[{"$state":{"store":"appState"},"meta":{"alias":null,"disabled":false,"indexRefName":"filter-index-pattern-0","key":"play_name","negate":false,"params":{"query":"Hamlet"},"type":"phrase"},"query":{"match_phrase":{"play_name":"Hamlet"}}},{"$state":{"store":"appState"},"meta":{"alias":null,"disabled":false,"indexRefName":"filter-index-pattern-1","key":"speaker","negate":true,"params":{"query":"HAMLET"},"type":"phrase"},"query":{"match_phrase":{"speaker":"HAMLET"}}}],"query":{"language":"kuery","query":""},"visualization":{"layers":[{"categoryDisplay":"default","groups":["20d61a13-4000-4df2-9d83-d9ec0c87b32a","20d61a13-4000-4df2-9d83-d9ec0c87b32a","20d61a13-4000-4df2-9d83-d9ec0c87b32a","20d61a13-4000-4df2-9d83-d9ec0c87b32a","20d61a13-4000-4df2-9d83-d9ec0c87b32a","20d61a13-4000-4df2-9d83-d9ec0c87b32a","20d61a13-4000-4df2-9d83-d9ec0c87b32a"],"layerId":"8faa1a43-2c03-4277-b19b-575da8b59561","legendDisplay":"default","metric":"6f1df118-ab3f-4f7a-b5ea-c38e0c11aefe","nestedLegend":false,"numberDisplay":"percent"}],"shape":"donut"}},"title":"Lens by Reference With Various Filters","visualizationType":"lnsPie"},"coreMigrationVersion":"7.12.2","id":"bf5d7860-cfbb-11eb-984d-af3b44ed60a7","migrationVersion":{"lens":"7.12.0"},"references":[{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-layer-8faa1a43-2c03-4277-b19b-575da8b59561","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"filter-index-pattern-0","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"filter-index-pattern-1","type":"index-pattern"}],"type":"lens","updated_at":"2021-06-17T22:31:24.138Z","version":"WzgzLDJd"} +{"attributes":{"state":{"datasourceStates":{"indexpattern":{"layers":{"b349d4ba-df44-415f-b0be-999b12f52213":{"columnOrder":["73bc446b-31e8-47a1-b7a1-9549bc81570a","45d911a5-6178-4d9a-a8b4-702a8377c859","89abee74-0f49-4e13-b0e1-2698af72c6f6"],"columns":{"45d911a5-6178-4d9a-a8b4-702a8377c859":{"dataType":"string","isBucketed":true,"label":"Top values of speaker","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"89abee74-0f49-4e13-b0e1-2698af72c6f6","type":"column"},"orderDirection":"desc","otherBucket":true,"size":3},"scale":"ordinal","sourceField":"speaker"},"73bc446b-31e8-47a1-b7a1-9549bc81570a":{"dataType":"string","isBucketed":true,"label":"Top values of play_name","operationType":"terms","params":{"missingBucket":false,"orderBy":{"type":"alphabetical"},"orderDirection":"asc","otherBucket":true,"size":5},"scale":"ordinal","sourceField":"play_name"},"89abee74-0f49-4e13-b0e1-2698af72c6f6":{"dataType":"number","isBucketed":false,"label":"Average of speech_number","operationType":"avg","scale":"ratio","sourceField":"speech_number"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["89abee74-0f49-4e13-b0e1-2698af72c6f6"],"layerId":"b349d4ba-df44-415f-b0be-999b12f52213","position":"top","seriesType":"bar_stacked","showGridlines":false,"splitAccessor":"45d911a5-6178-4d9a-a8b4-702a8377c859","xAccessor":"73bc446b-31e8-47a1-b7a1-9549bc81570a"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"[7.12.1] Lens By Reference with Average","visualizationType":"lnsXY"},"coreMigrationVersion":"7.12.2","id":"09ae9610-cfbc-11eb-984d-af3b44ed60a7","migrationVersion":{"lens":"7.12.0"},"references":[{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-layer-b349d4ba-df44-415f-b0be-999b12f52213","type":"index-pattern"}],"type":"lens","updated_at":"2021-06-17T22:33:28.820Z","version":"WzEyMywyXQ=="} +{"attributes":{"state":{"datasourceStates":{"indexpattern":{"layers":{"7d197461-9572-4437-b565-f3d7ec731753":{"columnOrder":["de0485bc-e55f-45d1-bf14-2a252ff718d0","a8fced94-076e-44ac-9e94-c0e3847e51b5"],"columns":{"a8fced94-076e-44ac-9e94-c0e3847e51b5":{"dataType":"number","isBucketed":false,"label":"Average of speech_number","operationType":"avg","scale":"ratio","sourceField":"speech_number"},"de0485bc-e55f-45d1-bf14-2a252ff718d0":{"dataType":"string","isBucketed":true,"label":"Top values of type.keyword","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"a8fced94-076e-44ac-9e94-c0e3847e51b5","type":"column"},"orderDirection":"desc","otherBucket":true,"size":5},"scale":"ordinal","sourceField":"type.keyword"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["a8fced94-076e-44ac-9e94-c0e3847e51b5"],"layerId":"7d197461-9572-4437-b565-f3d7ec731753","seriesType":"bar_stacked","xAccessor":"de0485bc-e55f-45d1-bf14-2a252ff718d0"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"[7.12.1] Lens By Reference with Drilldown","visualizationType":"lnsXY"},"coreMigrationVersion":"7.12.2","id":"8ac83fc0-cfbd-11eb-984d-af3b44ed60a7","migrationVersion":{"lens":"7.12.0"},"references":[{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-layer-7d197461-9572-4437-b565-f3d7ec731753","type":"index-pattern"}],"type":"lens","updated_at":"2021-06-17T22:44:14.911Z","version":"WzI2OSwyXQ=="} +{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.12.2\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":9,\"h\":15,\"i\":\"64bf6149-4bba-423e-91b4-9ff160f520e0\"},\"panelIndex\":\"64bf6149-4bba-423e-91b4-9ff160f520e0\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsPie\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"8faa1a43-2c03-4277-b19b-575da8b59561\":{\"columns\":{\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\":{\"label\":\"Top values of speaker\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"speaker\",\"isBucketed\":true,\"params\":{\"size\":20,\"orderBy\":{\"type\":\"column\",\"columnId\":\"6f1df118-ab3f-4f7a-b5ea-c38e0c11aefe\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"6f1df118-ab3f-4f7a-b5ea-c38e0c11aefe\":{\"label\":\"Count of records\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"Records\"}},\"columnOrder\":[\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\",\"6f1df118-ab3f-4f7a-b5ea-c38e0c11aefe\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"shape\":\"donut\",\"layers\":[{\"layerId\":\"8faa1a43-2c03-4277-b19b-575da8b59561\",\"groups\":[\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\",\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\",\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\",\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\",\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\",\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\",\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\"],\"metric\":\"6f1df118-ab3f-4f7a-b5ea-c38e0c11aefe\",\"numberDisplay\":\"percent\",\"categoryDisplay\":\"default\",\"legendDisplay\":\"default\",\"nestedLegend\":false}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[{\"meta\":{\"alias\":null,\"negate\":false,\"disabled\":false,\"type\":\"phrase\",\"key\":\"play_name\",\"params\":{\"query\":\"Hamlet\"},\"indexRefName\":\"filter-index-pattern-0\"},\"query\":{\"match_phrase\":{\"play_name\":\"Hamlet\"}},\"$state\":{\"store\":\"appState\"}},{\"meta\":{\"alias\":null,\"negate\":true,\"disabled\":false,\"type\":\"phrase\",\"key\":\"speaker\",\"params\":{\"query\":\"HAMLET\"},\"indexRefName\":\"filter-index-pattern-1\"},\"query\":{\"match_phrase\":{\"speaker\":\"HAMLET\"}},\"$state\":{\"store\":\"appState\"}}]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"472d30b0-cfbb-11eb-984d-af3b44ed60a7\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"472d30b0-cfbb-11eb-984d-af3b44ed60a7\",\"name\":\"indexpattern-datasource-layer-8faa1a43-2c03-4277-b19b-575da8b59561\"},{\"name\":\"filter-index-pattern-0\",\"type\":\"index-pattern\",\"id\":\"472d30b0-cfbb-11eb-984d-af3b44ed60a7\"},{\"name\":\"filter-index-pattern-1\",\"type\":\"index-pattern\",\"id\":\"472d30b0-cfbb-11eb-984d-af3b44ed60a7\"}]},\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"[7.12.1] Lens by Value With Various Filters\"},{\"version\":\"7.12.2\",\"type\":\"lens\",\"gridData\":{\"x\":9,\"y\":0,\"w\":24,\"h\":15,\"i\":\"6ecd96ef-70cc-4d80-a5e5-a2d5b43a2236\"},\"panelIndex\":\"6ecd96ef-70cc-4d80-a5e5-a2d5b43a2236\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsXY\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"b349d4ba-df44-415f-b0be-999b12f52213\":{\"columns\":{\"73bc446b-31e8-47a1-b7a1-9549bc81570a\":{\"label\":\"Top values of play_name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"play_name\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"alphabetical\"},\"orderDirection\":\"asc\",\"otherBucket\":true,\"missingBucket\":false}},\"89abee74-0f49-4e13-b0e1-2698af72c6f6\":{\"label\":\"Average of speech_number\",\"dataType\":\"number\",\"operationType\":\"avg\",\"sourceField\":\"speech_number\",\"isBucketed\":false,\"scale\":\"ratio\"},\"45d911a5-6178-4d9a-a8b4-702a8377c859\":{\"label\":\"Top values of speaker\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"speaker\",\"isBucketed\":true,\"params\":{\"size\":3,\"orderBy\":{\"type\":\"column\",\"columnId\":\"89abee74-0f49-4e13-b0e1-2698af72c6f6\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}}},\"columnOrder\":[\"73bc446b-31e8-47a1-b7a1-9549bc81570a\",\"45d911a5-6178-4d9a-a8b4-702a8377c859\",\"89abee74-0f49-4e13-b0e1-2698af72c6f6\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_stacked\",\"layers\":[{\"layerId\":\"b349d4ba-df44-415f-b0be-999b12f52213\",\"accessors\":[\"89abee74-0f49-4e13-b0e1-2698af72c6f6\"],\"position\":\"top\",\"seriesType\":\"bar_stacked\",\"showGridlines\":false,\"xAccessor\":\"73bc446b-31e8-47a1-b7a1-9549bc81570a\",\"splitAccessor\":\"45d911a5-6178-4d9a-a8b4-702a8377c859\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"472d30b0-cfbb-11eb-984d-af3b44ed60a7\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"472d30b0-cfbb-11eb-984d-af3b44ed60a7\",\"name\":\"indexpattern-datasource-layer-b349d4ba-df44-415f-b0be-999b12f52213\"}]},\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"[7.12.1] Lens By Value Lens with Average\"},{\"version\":\"7.12.2\",\"type\":\"lens\",\"gridData\":{\"x\":33,\"y\":0,\"w\":15,\"h\":15,\"i\":\"fed39777-b755-45f8-9efb-1203b4b3d7cf\"},\"panelIndex\":\"fed39777-b755-45f8-9efb-1203b4b3d7cf\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsXY\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"7d197461-9572-4437-b565-f3d7ec731753\":{\"columns\":{\"de0485bc-e55f-45d1-bf14-2a252ff718d0\":{\"label\":\"Top values of type.keyword\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"type.keyword\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"a8fced94-076e-44ac-9e94-c0e3847e51b5\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"a8fced94-076e-44ac-9e94-c0e3847e51b5\":{\"label\":\"Average of speech_number\",\"dataType\":\"number\",\"operationType\":\"avg\",\"sourceField\":\"speech_number\",\"isBucketed\":false,\"scale\":\"ratio\"}},\"columnOrder\":[\"de0485bc-e55f-45d1-bf14-2a252ff718d0\",\"a8fced94-076e-44ac-9e94-c0e3847e51b5\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_stacked\",\"layers\":[{\"layerId\":\"7d197461-9572-4437-b565-f3d7ec731753\",\"seriesType\":\"bar_stacked\",\"accessors\":[\"a8fced94-076e-44ac-9e94-c0e3847e51b5\"],\"xAccessor\":\"de0485bc-e55f-45d1-bf14-2a252ff718d0\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"472d30b0-cfbb-11eb-984d-af3b44ed60a7\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"472d30b0-cfbb-11eb-984d-af3b44ed60a7\",\"name\":\"indexpattern-datasource-layer-7d197461-9572-4437-b565-f3d7ec731753\"}]},\"hidePanelTitles\":false,\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"8934b2f2-b989-4b8c-8339-c95e387f4372\",\"triggers\":[\"FILTER_TRIGGER\"],\"action\":{\"name\":\"Test Drilldown\",\"config\":{\"useCurrentFilters\":true,\"useCurrentDateRange\":true},\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\"}}]}}},\"title\":\"[7.12.1] Lens By Value with Drilldown\"},{\"version\":\"7.12.2\",\"gridData\":{\"x\":0,\"y\":15,\"w\":9,\"h\":15,\"i\":\"eb826c7a-0ead-4c8d-99cf-d823388bb91d\"},\"panelIndex\":\"eb826c7a-0ead-4c8d-99cf-d823388bb91d\",\"embeddableConfig\":{\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"[7.12.1] Lens by Reference With Various Filters\",\"panelRefName\":\"panel_3\"},{\"version\":\"7.12.2\",\"gridData\":{\"x\":9,\"y\":15,\"w\":24,\"h\":15,\"i\":\"80a4927b-aa69-4c80-ad38-482c141d0b93\"},\"panelIndex\":\"80a4927b-aa69-4c80-ad38-482c141d0b93\",\"embeddableConfig\":{\"hidePanelTitles\":false,\"enhancements\":{}},\"panelRefName\":\"panel_4\"},{\"version\":\"7.12.2\",\"gridData\":{\"x\":33,\"y\":15,\"w\":15,\"h\":15,\"i\":\"57a79145-1314-49f3-87e3-7c494cf55f64\"},\"panelIndex\":\"57a79145-1314-49f3-87e3-7c494cf55f64\",\"embeddableConfig\":{\"hidePanelTitles\":false,\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"8934b2f2-b989-4b8c-8339-c95e387f4372\",\"triggers\":[\"FILTER_TRIGGER\"],\"action\":{\"name\":\"Test Drilldown\",\"config\":{\"useCurrentFilters\":true,\"useCurrentDateRange\":true},\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\"}}]}}},\"panelRefName\":\"panel_5\"}]","timeRestore":false,"title":"[7.12.1] Lens By Value Test Dashboard","version":1},"coreMigrationVersion":"7.12.2","id":"60a5cfa0-cfbd-11eb-984d-af3b44ed60a7","migrationVersion":{"dashboard":"7.11.0"},"references":[{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-layer-8faa1a43-2c03-4277-b19b-575da8b59561","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"filter-index-pattern-0","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"filter-index-pattern-1","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-layer-b349d4ba-df44-415f-b0be-999b12f52213","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-layer-7d197461-9572-4437-b565-f3d7ec731753","type":"index-pattern"},{"id":"759faf20-cfbd-11eb-984d-af3b44ed60a7","name":"drilldown:DASHBOARD_TO_DASHBOARD_DRILLDOWN:8934b2f2-b989-4b8c-8339-c95e387f4372:dashboardId","type":"dashboard"},{"id":"759faf20-cfbd-11eb-984d-af3b44ed60a7","name":"drilldown:DASHBOARD_TO_DASHBOARD_DRILLDOWN:8934b2f2-b989-4b8c-8339-c95e387f4372:dashboardId","type":"dashboard"},{"id":"bf5d7860-cfbb-11eb-984d-af3b44ed60a7","name":"panel_3","type":"lens"},{"id":"09ae9610-cfbc-11eb-984d-af3b44ed60a7","name":"panel_4","type":"lens"},{"id":"8ac83fc0-cfbd-11eb-984d-af3b44ed60a7","name":"panel_5","type":"lens"}],"type":"dashboard","updated_at":"2021-06-17T22:44:36.881Z","version":"WzI3NSwyXQ=="} +{"exportedCount":6,"missingRefCount":0,"missingReferences":[]} \ No newline at end of file diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/lens_migration_smoke_test.ts b/x-pack/test/functional/apps/dashboard/migration_smoke_tests/lens_migration_smoke_test.ts new file mode 100644 index 0000000000000..78b7ccfe7df08 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/migration_smoke_tests/lens_migration_smoke_test.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* This test is importing saved objects from 7.13.0 to 8.0 and the backported version + * will import from 6.8.x to 7.x.x + */ + +import expect from '@kbn/expect'; +import path from 'path'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); + const dashboardPanelActions = getService('dashboardPanelActions'); + + const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects', 'dashboard']); + + describe('Export import saved objects between versions', () => { + before(async () => { + await esArchiver.loadIfNeeded( + 'x-pack/test/functional/es_archives/getting_started/shakespeare' + ); + await kibanaServer.uiSettings.replace({}); + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSavedObjects(); + await PageObjects.savedObjects.importFile( + path.join(__dirname, 'exports', 'lens_dashboard_migration_test_7_12_1.ndjson') + ); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/getting_started/shakespeare'); + await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); + }); + + it('should be able to import dashboard with various Lens panels from 7.12.1', async () => { + // this will catch cases where there is an error in the migrations. + await PageObjects.savedObjects.checkImportSucceeded(); + await PageObjects.savedObjects.clickImportDone(); + }); + + it('should render all panels on the dashboard', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('[7.12.1] Lens By Value Test Dashboard'); + + // dashboard should load properly + await PageObjects.dashboard.expectOnDashboard('[7.12.1] Lens By Value Test Dashboard'); + await PageObjects.dashboard.waitForRenderComplete(); + + // There should be 0 error embeddables on the dashboard + const errorEmbeddables = await testSubjects.findAll('embeddableStackError'); + expect(errorEmbeddables.length).to.be(0); + }); + + it('should show the edit action for all panels', async () => { + await PageObjects.dashboard.switchToEditMode(); + + // All panels should be editable. This will catch cases where an error does not create an error embeddable. + const panelTitles = await PageObjects.dashboard.getPanelTitles(); + for (const title of panelTitles) { + await dashboardPanelActions.expectExistsEditPanelAction(title); + } + }); + + it('should retain all panel drilldowns from 7.12.1', async () => { + // Both panels configured with drilldowns in 7.12.1 should still have drilldowns. + const totalPanels = await PageObjects.dashboard.getPanelCount(); + let panelsWithDrilldowns = 0; + for (let panelIndex = 0; panelIndex < totalPanels; panelIndex++) { + if ((await PageObjects.dashboard.getPanelDrilldownCount(panelIndex)) === 1) { + panelsWithDrilldowns++; + } + } + expect(panelsWithDrilldowns).to.be(2); + }); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts b/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts index e736fe08eba99..94540aa8b4c46 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts +++ b/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts @@ -17,10 +17,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardPanelActions = getService('dashboardPanelActions'); const log = getService('log'); const testSubjects = getService('testSubjects'); + const kibanaServer = getService('kibanaServer'); const filterBar = getService('filterBar'); const find = getService('find'); const retry = getService('retry'); const PageObjects = getPageObjects(['reporting', 'common', 'dashboard', 'timePicker']); + const ecommerceSOPath = 'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json'; const getCsvPath = (name: string) => path.resolve(REPO_ROOT, `target/functional-tests/downloads/${name}.csv`); @@ -67,11 +69,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('E-Commerce Data', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.load(ecommerceSOPath); }); after(async () => { await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.unload(ecommerceSOPath); }); it('Download CSV export of a saved search panel', async function () { diff --git a/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts b/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts index 7c5e4b2d12baa..7eb2ef74000e0 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts +++ b/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts @@ -27,13 +27,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const es = getService('es'); const testSubjects = getService('testSubjects'); const kibanaServer = getService('kibanaServer'); + const ecommerceSOPath = 'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json'; - describe('Dashboard Reporting Screenshots', () => { + // https://github.com/elastic/kibana/issues/102911 + describe.skip('Dashboard Reporting Screenshots', () => { before('initialize tests', async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.loadIfNeeded( - 'x-pack/test/functional/es_archives/reporting/ecommerce_kibana' - ); + await kibanaServer.importExport.load(ecommerceSOPath); await browser.setWindowSize(1600, 850); await security.role.create('test_reporting_user', { @@ -61,7 +61,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); after('clean up archives', async () => { await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.unload(ecommerceSOPath); await es.deleteByQuery({ index: '.reporting-*', refresh: true, diff --git a/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap b/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap index baa49cb6f9d81..c7666bf00dd53 100644 --- a/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap +++ b/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap @@ -65,7 +65,7 @@ exports[`discover Discover CSV Export Generate CSV: archived search generates a exports[`discover Discover CSV Export Generate CSV: new search generates a report from a new search with data: default 1`] = ` "\\"_id\\",\\"_index\\",\\"_score\\",\\"_type\\",category,\\"category.keyword\\",currency,\\"customer_first_name\\",\\"customer_first_name.keyword\\",\\"customer_full_name\\",\\"customer_full_name.keyword\\",\\"customer_gender\\",\\"customer_id\\",\\"customer_last_name\\",\\"customer_last_name.keyword\\",\\"customer_phone\\",\\"day_of_week\\",\\"day_of_week_i\\",email,\\"geoip.city_name\\",\\"geoip.continent_name\\",\\"geoip.country_iso_code\\",\\"geoip.location\\",\\"geoip.region_name\\",manufacturer,\\"manufacturer.keyword\\",\\"order_date\\",\\"order_id\\",\\"products._id\\",\\"products._id.keyword\\",\\"products.base_price\\",\\"products.base_unit_price\\",\\"products.category\\",\\"products.category.keyword\\",\\"products.created_on\\",\\"products.discount_amount\\",\\"products.discount_percentage\\",\\"products.manufacturer\\",\\"products.manufacturer.keyword\\",\\"products.min_price\\",\\"products.price\\",\\"products.product_id\\",\\"products.product_name\\",\\"products.product_name.keyword\\",\\"products.quantity\\",\\"products.sku\\",\\"products.tax_amount\\",\\"products.taxful_price\\",\\"products.taxless_price\\",\\"products.unit_discount_amount\\",sku,\\"taxful_total_price\\",\\"taxless_total_price\\",\\"total_quantity\\",\\"total_unique_products\\",type,user -3AMtOW0BH63Xcmy432DJ,ecommerce,\\"-\\",\\"-\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",EUR,\\"Sultan Al\\",\\"Sultan Al\\",\\"Sultan Al Boone\\",\\"Sultan Al Boone\\",MALE,19,Boone,Boone,,Saturday,5,\\"sultan al@boone-family.zzz\\",\\"Abu Dhabi\\",Asia,AE,\\"{ +3AMtOW0BH63Xcmy432DJ,ecommerce,\\"-\\",\\"-\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",EUR,\\"Sultan Al\\",\\"Sultan Al\\",\\"Sultan Al Boone\\",\\"Sultan Al Boone\\",MALE,19,Boone,Boone,\\"(empty)\\",Saturday,5,\\"sultan al@boone-family.zzz\\",\\"Abu Dhabi\\",Asia,AE,\\"{ \\"\\"coordinates\\"\\": [ 54.4, 24.5 @@ -77,7 +77,7 @@ exports[`discover Discover CSV Export Generate CSV: new search generates a repor exports[`discover Discover CSV Export Generate CSV: new search generates a report from a new search with data: discover:searchFieldsFromSource 1`] = ` "\\"_id\\",\\"_index\\",\\"_score\\",\\"_type\\",category,\\"category.keyword\\",currency,\\"customer_first_name\\",\\"customer_first_name.keyword\\",\\"customer_full_name\\",\\"customer_full_name.keyword\\",\\"customer_gender\\",\\"customer_id\\",\\"customer_last_name\\",\\"customer_last_name.keyword\\",\\"customer_phone\\",\\"day_of_week\\",\\"day_of_week_i\\",email,\\"geoip.city_name\\",\\"geoip.continent_name\\",\\"geoip.country_iso_code\\",\\"geoip.location\\",\\"geoip.region_name\\",manufacturer,\\"manufacturer.keyword\\",\\"order_date\\",\\"order_id\\",\\"products._id\\",\\"products._id.keyword\\",\\"products.base_price\\",\\"products.base_unit_price\\",\\"products.category\\",\\"products.category.keyword\\",\\"products.created_on\\",\\"products.discount_amount\\",\\"products.discount_percentage\\",\\"products.manufacturer\\",\\"products.manufacturer.keyword\\",\\"products.min_price\\",\\"products.price\\",\\"products.product_id\\",\\"products.product_name\\",\\"products.product_name.keyword\\",\\"products.quantity\\",\\"products.sku\\",\\"products.tax_amount\\",\\"products.taxful_price\\",\\"products.taxless_price\\",\\"products.unit_discount_amount\\",sku,\\"taxful_total_price\\",\\"taxless_total_price\\",\\"total_quantity\\",\\"total_unique_products\\",type,user -3AMtOW0BH63Xcmy432DJ,ecommerce,\\"-\\",\\"-\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",EUR,\\"Sultan Al\\",\\"Sultan Al\\",\\"Sultan Al Boone\\",\\"Sultan Al Boone\\",MALE,19,Boone,Boone,,Saturday,5,\\"sultan al@boone-family.zzz\\",\\"Abu Dhabi\\",Asia,AE,\\"{ +3AMtOW0BH63Xcmy432DJ,ecommerce,\\"-\\",\\"-\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",EUR,\\"Sultan Al\\",\\"Sultan Al\\",\\"Sultan Al Boone\\",\\"Sultan Al Boone\\",MALE,19,Boone,Boone,\\"(empty)\\",Saturday,5,\\"sultan al@boone-family.zzz\\",\\"Abu Dhabi\\",Asia,AE,\\"{ \\"\\"coordinates\\"\\": [ 54.4, 24.5 diff --git a/x-pack/test/functional/apps/discover/reporting.ts b/x-pack/test/functional/apps/discover/reporting.ts index 2b424b94b7236..3eb66204df564 100644 --- a/x-pack/test/functional/apps/discover/reporting.ts +++ b/x-pack/test/functional/apps/discover/reporting.ts @@ -16,6 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const PageObjects = getPageObjects(['reporting', 'common', 'discover', 'timePicker']); const filterBar = getService('filterBar'); + const ecommerceSOPath = 'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json'; const setFieldsFromSource = async (setValue: boolean) => { await kibanaServer.uiSettings.update({ 'discover:searchFieldsFromSource': setValue }); @@ -25,12 +26,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before('initialize tests', async () => { log.debug('ReportingPage:initTests'); await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.load(ecommerceSOPath); await browser.setWindowSize(1600, 850); }); after('clean up archives', async () => { await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.unload(ecommerceSOPath); await es.deleteByQuery({ index: '.reporting-*', refresh: true, @@ -74,7 +75,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Generate CSV: new search', () => { beforeEach(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); // reload the archive to wipe out changes made by each test + await kibanaServer.importExport.load(ecommerceSOPath); await PageObjects.common.navigateToApp('discover'); }); @@ -151,12 +152,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.load(ecommerceSOPath); }); after(async () => { await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.unload(ecommerceSOPath); }); beforeEach(() => PageObjects.common.navigateToApp('discover')); diff --git a/x-pack/test/functional/apps/discover/saved_searches.ts b/x-pack/test/functional/apps/discover/saved_searches.ts index 5df9bf9949128..1d8de9fe9fb6d 100644 --- a/x-pack/test/functional/apps/discover/saved_searches.ts +++ b/x-pack/test/functional/apps/discover/saved_searches.ts @@ -16,16 +16,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dataGrid = getService('dataGrid'); const panelActions = getService('dashboardPanelActions'); const panelActionsTimeRange = getService('dashboardPanelTimeRange'); + const ecommerceSOPath = 'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json'; describe('Discover Saved Searches', () => { before('initialize tests', async () => { await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.load(ecommerceSOPath); await kibanaServer.uiSettings.update({ 'doc_table:legacy': false }); }); after('clean up archives', async () => { await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.unload(ecommerceSOPath); await kibanaServer.uiSettings.unset('doc_table:legacy'); }); diff --git a/x-pack/test/functional/apps/grok_debugger/grok_debugger.js b/x-pack/test/functional/apps/grok_debugger/grok_debugger.js index 0162b660a1408..68cd5820e2a32 100644 --- a/x-pack/test/functional/apps/grok_debugger/grok_debugger.js +++ b/x-pack/test/functional/apps/grok_debugger/grok_debugger.js @@ -11,7 +11,8 @@ export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['grokDebugger']); - describe('grok debugger app', function () { + // FLAKY: https://github.com/elastic/kibana/issues/84440 + describe.skip('grok debugger app', function () { this.tags('includeFirefox'); before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); diff --git a/x-pack/test/functional/apps/lens/formula.ts b/x-pack/test/functional/apps/lens/formula.ts index e9e5051c006f0..38d1f63e946d4 100644 --- a/x-pack/test/functional/apps/lens/formula.ts +++ b/x-pack/test/functional/apps/lens/formula.ts @@ -14,6 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const listingTable = getService('listingTable'); const browser = getService('browser'); const testSubjects = getService('testSubjects'); + const fieldEditor = getService('fieldEditor'); describe('lens formula', () => { it('should transition from count to formula', async () => { @@ -88,6 +89,41 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await element.getVisibleText()).to.equal(`count(kql='Men\\'s Clothing')`); }); + it('should insert single quotes and escape when needed to create valid field name', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.lens.switchToVisualization('lnsDatatable'); + await PageObjects.lens.clickAddField(); + await fieldEditor.setName(`*' "'`); + await fieldEditor.enableValue(); + await fieldEditor.typeScript("emit('abc')"); + await fieldEditor.save(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsDatatable_metrics > lns-empty-dimension', + operation: 'unique_count', + field: `*`, + keepOpen: true, + }); + + await PageObjects.lens.switchToFormula(); + let element = await find.byCssSelector('.monaco-editor'); + expect(await element.getVisibleText()).to.equal(`unique_count('*\\' "\\'')`); + + const input = await find.activeElement(); + await input.clearValueWithKeyboard({ charByChar: true }); + await input.type('unique_count('); + await PageObjects.common.sleep(100); + await input.type('*'); + await input.pressKeys(browser.keys.ENTER); + + await PageObjects.common.sleep(100); + + element = await find.byCssSelector('.monaco-editor'); + expect(await element.getVisibleText()).to.equal(`unique_count('*\\' "\\'')`); + }); + it('should persist a broken formula on close', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); diff --git a/x-pack/test/functional/apps/lens/lens_tagging.ts b/x-pack/test/functional/apps/lens/lens_tagging.ts index b659515a6031c..cbe04b26830d6 100644 --- a/x-pack/test/functional/apps/lens/lens_tagging.ts +++ b/x-pack/test/functional/apps/lens/lens_tagging.ts @@ -107,7 +107,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { `tag-searchbar-option-${PageObjects.tagManagement.testSubjFriendly(lensTag)}` ); // click elsewhere to close the filter dropdown - const searchFilter = await find.byCssSelector('main .euiFieldSearch'); + const searchFilter = await find.byCssSelector('.euiPageBody .euiFieldSearch'); await searchFilter.click(); // wait until the table refreshes await listingTable.waitUntilTableIsLoaded(); diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index ec32d7620fcf9..78900e6fabca4 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -604,7 +604,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); }); - it('should not leave an incomplete column in the visualization config with reference-based operations', async () => { + it('should revert to previous configuration and not leave an incomplete column in the visualization config with reference-based operations', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); await PageObjects.lens.goToTimeRange(); @@ -636,7 +636,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.closeDimensionEditor(); expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql( - undefined + 'Moving average of Count of records' ); }); diff --git a/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js b/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js index fb0fdcf333cf2..3479f292374d2 100644 --- a/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js +++ b/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js @@ -21,12 +21,12 @@ export default function ({ getPageObjects, getService }) { await security.testUser.restoreDefaults(); }); - it('should only fetch geo_point field and nothing else when source does not have data driven styling', async () => { + it('should only fetch geo_point field and time field and nothing else when source does not have data driven styling', async () => { await PageObjects.maps.loadSavedMap('document example'); const { rawResponse: response } = await PageObjects.maps.getResponse(); const firstHit = response.hits.hits[0]; expect(firstHit).to.only.have.keys(['_id', '_index', '_score', 'fields']); - expect(firstHit.fields).to.only.have.keys(['geo.coordinates']); + expect(firstHit.fields).to.only.have.keys(['@timestamp', 'geo.coordinates']); }); it('should only fetch geo_point field and data driven styling fields', async () => { @@ -34,7 +34,12 @@ export default function ({ getPageObjects, getService }) { const { rawResponse: response } = await PageObjects.maps.getResponse(); const firstHit = response.hits.hits[0]; expect(firstHit).to.only.have.keys(['_id', '_index', '_score', 'fields']); - expect(firstHit.fields).to.only.have.keys(['bytes', 'geo.coordinates', 'hour_of_day']); + expect(firstHit.fields).to.only.have.keys([ + '@timestamp', + 'bytes', + 'geo.coordinates', + 'hour_of_day', + ]); }); it('should format date fields as epoch_millis when data driven styling is applied to a date field', async () => { diff --git a/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts index a8fed205a9e56..3867ed6f7dfea 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/file_data_visualizer.ts @@ -223,6 +223,7 @@ export default function ({ getService }: FtrProviderContext) { fieldRow.docCountFormatted, fieldRow.topValuesCount, false, + false, false ); } diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index.ts b/x-pack/test/functional/apps/ml/data_visualizer/index.ts index 65f7033b5bd66..3e6b644a0b494 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index.ts @@ -13,6 +13,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./index_data_visualizer')); loadTestFile(require.resolve('./index_data_visualizer_actions_panel')); + loadTestFile(require.resolve('./index_data_visualizer_index_pattern_management')); loadTestFile(require.resolve('./file_data_visualizer')); }); } diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_index_pattern_management.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_index_pattern_management.ts new file mode 100644 index 0000000000000..0d9163a872043 --- /dev/null +++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_index_pattern_management.ts @@ -0,0 +1,274 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; +import { ML_JOB_FIELD_TYPES } from '../../../../../plugins/ml/common/constants/field_types'; +import { FieldVisConfig } from '../../../../../plugins/data_visualizer/public/application/common/components/stats_table/types'; + +interface MetricFieldVisConfig extends FieldVisConfig { + statsMaxDecimalPlaces: number; + docCountFormatted: string; + topValuesCount: number; + viewableInLens: boolean; + hasActionMenu: boolean; +} + +interface NonMetricFieldVisConfig extends FieldVisConfig { + docCountFormatted: string; + exampleCount: number; + viewableInLens: boolean; + hasActionMenu: boolean; +} + +interface TestData { + suiteTitle: string; + sourceIndexOrSavedSearch: string; + rowsPerPage?: 10 | 25 | 50; + newFields?: Array<{ fieldName: string; type: string; script: string }>; + fieldsToRename?: Array<{ originalName: string; newName: string }>; + expected: { + totalDocCountFormatted: string; + metricFields?: MetricFieldVisConfig[]; + nonMetricFields?: NonMetricFieldVisConfig[]; + visibleMetricFieldsCount: number; + totalMetricFieldsCount: number; + populatedFieldsCount: number; + totalFieldsCount: number; + }; +} + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + const originalTestData: TestData = { + suiteTitle: 'original index pattern', + sourceIndexOrSavedSearch: 'ft_farequote', + expected: { + totalDocCountFormatted: '86,274', + metricFields: [], + nonMetricFields: [], + visibleMetricFieldsCount: 1, + totalMetricFieldsCount: 1, + populatedFieldsCount: 7, + totalFieldsCount: 8, + }, + }; + const addDeleteFieldTestData: TestData = { + suiteTitle: 'add field', + sourceIndexOrSavedSearch: 'ft_farequote', + newFields: [ + { + fieldName: 'rt_airline_lowercase', + type: 'Keyword', + script: 'emit(params._source.airline.toLowerCase())', + }, + ], + expected: { + totalDocCountFormatted: '86,274', + metricFields: [], + nonMetricFields: [ + { + fieldName: 'rt_airline_lowercase', + type: ML_JOB_FIELD_TYPES.KEYWORD, + existsInDocs: true, + aggregatable: true, + loading: false, + exampleCount: 10, + docCountFormatted: '5000 (100%)', + viewableInLens: true, + hasActionMenu: true, + }, + ], + visibleMetricFieldsCount: 2, + totalMetricFieldsCount: 2, + populatedFieldsCount: 9, + totalFieldsCount: 10, + }, + }; + const customLabelTestData: TestData = { + suiteTitle: 'custom label', + sourceIndexOrSavedSearch: 'ft_farequote', + fieldsToRename: [ + { + originalName: 'responsetime', + newName: 'new_responsetime', + }, + ], + expected: { + totalDocCountFormatted: '86,274', + metricFields: [ + { + fieldName: 'new_responsetime', + type: ML_JOB_FIELD_TYPES.NUMBER, + existsInDocs: true, + aggregatable: true, + loading: false, + docCountFormatted: '5000 (100%)', + statsMaxDecimalPlaces: 3, + topValuesCount: 10, + viewableInLens: true, + hasActionMenu: false, + }, + ], + nonMetricFields: [], + visibleMetricFieldsCount: 1, + totalMetricFieldsCount: 1, + populatedFieldsCount: 7, + totalFieldsCount: 8, + }, + }; + + async function navigateToIndexDataVisualizer(testData: TestData) { + // Start navigation from the base of the ML app. + await ml.testExecution.logTestStep( + `${testData.suiteTitle} loads the data visualizer selector page` + ); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToDataVisualizer(); + + await ml.testExecution.logTestStep( + `${testData.suiteTitle} loads the saved search selection page` + ); + await ml.dataVisualizer.navigateToIndexPatternSelection(); + + await ml.testExecution.logTestStep( + `${testData.suiteTitle} loads the index data visualizer page` + ); + await ml.jobSourceSelection.selectSourceForIndexBasedDataVisualizer( + testData.sourceIndexOrSavedSearch + ); + + await ml.testExecution.logTestStep(`${testData.suiteTitle} displays the time range step`); + await ml.dataVisualizerIndexBased.assertTimeRangeSelectorSectionExists(); + + await ml.testExecution.logTestStep(`${testData.suiteTitle} loads data for full time range`); + await ml.dataVisualizerIndexBased.clickUseFullDataButton( + testData.expected.totalDocCountFormatted + ); + } + + async function checkPageDetails(testData: TestData) { + await ml.testExecution.logTestStep( + `${testData.suiteTitle} displays elements in the doc count panel correctly` + ); + await ml.dataVisualizerIndexBased.assertTotalDocCountHeaderExist(); + await ml.dataVisualizerIndexBased.assertTotalDocCountChartExist(); + + await ml.testExecution.logTestStep( + `${testData.suiteTitle} displays elements in the data visualizer table correctly` + ); + await ml.dataVisualizerIndexBased.assertDataVisualizerTableExist(); + + if (testData.rowsPerPage) { + await ml.dataVisualizerTable.ensureNumRowsPerPage(testData.rowsPerPage); + } + + await ml.dataVisualizerTable.assertSearchPanelExist(); + await ml.dataVisualizerTable.assertSampleSizeInputExists(); + await ml.dataVisualizerTable.assertFieldTypeInputExists(); + await ml.dataVisualizerTable.assertFieldNameInputExists(); + + await ml.dataVisualizerIndexBased.assertFieldCountPanelExist(); + await ml.dataVisualizerIndexBased.assertMetricFieldsSummaryExist(); + await ml.dataVisualizerIndexBased.assertFieldsSummaryExist(); + await ml.dataVisualizerIndexBased.assertVisibleMetricFieldsCount( + testData.expected.visibleMetricFieldsCount + ); + await ml.dataVisualizerIndexBased.assertTotalMetricFieldsCount( + testData.expected.totalMetricFieldsCount + ); + await ml.dataVisualizerIndexBased.assertVisibleFieldsCount( + testData.expected.populatedFieldsCount + ); + await ml.dataVisualizerIndexBased.assertTotalFieldsCount(testData.expected.totalFieldsCount); + } + + describe('index pattern management', function () { + this.tags(['mlqa']); + const indexPatternTitle = 'ft_farequote'; + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); + }); + + beforeEach(async () => { + await ml.testResources.createIndexPatternIfNeeded(indexPatternTitle, '@timestamp'); + await navigateToIndexDataVisualizer(originalTestData); + }); + + afterEach(async () => { + await ml.testResources.deleteIndexPatternByTitle(indexPatternTitle); + }); + + it(`adds new field`, async () => { + await ml.testExecution.logTestStep('adds new runtime fields'); + for (const newField of addDeleteFieldTestData.newFields!) { + await ml.dataVisualizerIndexPatternManagement.addRuntimeField( + newField.fieldName, + newField.script, + newField.type + ); + } + + await ml.testExecution.logTestStep('displays details for added runtime metric fields'); + for (const fieldRow of addDeleteFieldTestData.expected.metricFields as Array< + Required<MetricFieldVisConfig> + >) { + await ml.dataVisualizerTable.assertNumberFieldContents( + fieldRow.fieldName, + fieldRow.docCountFormatted, + fieldRow.topValuesCount, + fieldRow.viewableInLens, + fieldRow.hasActionMenu + ); + } + await ml.testExecution.logTestStep('displays details for added runtime non metric fields'); + for (const fieldRow of addDeleteFieldTestData.expected.nonMetricFields!) { + await ml.dataVisualizerTable.assertNonMetricFieldContents( + fieldRow.type, + fieldRow.fieldName!, + fieldRow.docCountFormatted, + fieldRow.exampleCount, + fieldRow.viewableInLens, + fieldRow.hasActionMenu + ); + } + await checkPageDetails(addDeleteFieldTestData); + }); + + it(`sets custom label for existing field`, async () => { + for (const field of customLabelTestData.fieldsToRename!) { + await ml.dataVisualizerIndexPatternManagement.renameField( + field.originalName, + field.newName + ); + await ml.dataVisualizerTable.assertDisplayName(field.originalName, field.newName); + } + }); + + it(`deletes existing field`, async () => { + await ml.testExecution.logTestStep('adds new runtime fields'); + for (const newField of addDeleteFieldTestData.newFields!) { + await ml.dataVisualizerIndexPatternManagement.addRuntimeField( + newField.fieldName, + newField.script, + newField.type + ); + } + await ml.testExecution.logTestStep('deletes newly added runtime fields'); + for (const fieldToDelete of addDeleteFieldTestData.newFields!) { + await ml.dataVisualizerIndexPatternManagement.deleteField(fieldToDelete.fieldName); + } + + await ml.testExecution.logTestStep('displays page details without the deleted fields'); + await checkPageDetails(originalTestData); + }); + }); +} diff --git a/x-pack/test/functional/apps/visualize/reporting.ts b/x-pack/test/functional/apps/visualize/reporting.ts index 799006337300f..c43747c346ca7 100644 --- a/x-pack/test/functional/apps/visualize/reporting.ts +++ b/x-pack/test/functional/apps/visualize/reporting.ts @@ -13,6 +13,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); const log = getService('log'); + const kibanaServer = getService('kibanaServer'); + const ecommerceSOPath = 'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json'; + const PageObjects = getPageObjects([ 'reporting', 'common', @@ -25,14 +28,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { before('initialize tests', async () => { log.debug('ReportingPage:initTests'); await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.loadIfNeeded( - 'x-pack/test/functional/es_archives/reporting/ecommerce_kibana' - ); + await kibanaServer.importExport.load(ecommerceSOPath); await browser.setWindowSize(1600, 850); }); after('clean up archives', async () => { await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.unload(ecommerceSOPath); await es.deleteByQuery({ index: '.reporting-*', refresh: true, diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index cf05bd6e15898..2c3a3c93e2a0a 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -103,6 +103,7 @@ export default async function ({ readConfigFile }) { 'accessibility:disableAnimations': true, 'dateFormat:tz': 'UTC', 'visualization:visualize:legacyChartsLibrary': true, + 'visualization:visualize:legacyPieChartsLibrary': true, }, }, // the apps section defines the urls that diff --git a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/data.json b/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/data.json deleted file mode 100644 index f0e7d7ae6d1d5..0000000000000 --- a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/data.json +++ /dev/null @@ -1,788 +0,0 @@ -{ - "type": "doc", - "value": { - "id": "config:7.0.0", - "index": ".kibana_1", - "source": { - "config": { - "buildNum": 9007199254740991, - "dateFormat:tz": "UTC", - "defaultIndex": "5193f870-d861-11e9-a311-0fa548c5f953" - }, - "migrationVersion": { - "config": "7.13.0" - }, - "references": [], - "type": "config", - "updated_at": "2019-09-16T09:06:51.201Z" - } - } -} - -{ - "type": "doc", - "value": { - "id": "config:8.0.0", - "index": ".kibana_1", - "source": { - "config": { - "accessibility:disableAnimations": true, - "visualization:visualize:legacyChartsLibrary": true, - "buildNum": 9007199254740991, - "dateFormat:tz": "UTC", - "defaultIndex": "5193f870-d861-11e9-a311-0fa548c5f953" - }, - "migrationVersion": { - "config": "7.13.0" - }, - "references": [], - "type": "config", - "updated_at": "2021-05-03T18:23:19.891Z" - } - } -} - -{ - "type": "doc", - "value": { - "id": "index-pattern:5193f870-d861-11e9-a311-0fa548c5f953", - "index": ".kibana_1", - "source": { - "index-pattern": { - "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"category\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"category.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"category\"}}},{\"name\":\"currency\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"customer_birth_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"customer_first_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"customer_first_name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"customer_first_name\"}}},{\"name\":\"customer_full_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"customer_full_name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"customer_full_name\"}}},{\"name\":\"customer_gender\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"customer_id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"customer_last_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"customer_last_name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"customer_last_name\"}}},{\"name\":\"customer_phone\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"day_of_week\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"day_of_week_i\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"email\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.city_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.continent_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.country_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.location\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.region_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"manufacturer\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"manufacturer.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"manufacturer\"}}},{\"name\":\"order_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"order_id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products._id\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"products._id.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"products._id\"}}},{\"name\":\"products.base_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.base_unit_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.category\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"products.category.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"products.category\"}}},{\"name\":\"products.created_on\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.discount_amount\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.discount_percentage\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.manufacturer\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"products.manufacturer.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"products.manufacturer\"}}},{\"name\":\"products.min_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.product_id\",\"type\":\"number\",\"esTypes\":[\"long\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.product_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"products.product_name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"products.product_name\"}}},{\"name\":\"products.quantity\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.sku\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.tax_amount\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.taxful_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.taxless_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.unit_discount_amount\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"sku\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"taxful_total_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"taxless_total_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"total_quantity\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"total_unique_products\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", - "timeFieldName": "order_date", - "title": "ecommerce" - }, - "migrationVersion": { - "index-pattern": "7.11.0" - }, - "references": [], - "type": "index-pattern", - "updated_at": "2019-12-11T23:24:13.381Z" - } - } -} - -{ - "type": "doc", - "value": { - "id": "search:6091ead0-1c6d-11ea-a100-8589bb9d7c6b", - "index": ".kibana_1", - "source": { - "migrationVersion": { - "search": "7.9.3" - }, - "references": [ - { - "id": "5193f870-d861-11e9-a311-0fa548c5f953", - "name": "kibanaSavedObjectMeta.searchSourceJSON.index", - "type": "index-pattern" - } - ], - "search": { - "columns": [ - "order_date", - "category", - "currency", - "customer_id", - "order_id", - "day_of_week_i", - "products.created_on", - "sku" - ], - "description": "", - "hits": 0, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" - }, - "sort": [ - [ - "order_date", - "desc" - ] - ], - "title": "Ecommerce Data", - "version": 1 - }, - "type": "search", - "updated_at": "2019-12-11T23:24:28.540Z" - } - } -} - -{ - "type": "doc", - "value": { - "id": "dashboard:constructed-sample-saved-object-id", - "index": ".kibana_1", - "source": { - "dashboard": { - "description": "", - "hits": 0, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}" - }, - "optionsJSON": "{\"hidePanelTitles\":true,\"useMargins\":true}", - "panelsJSON": "[{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1c12c2f2-80c2-4d5c-b722-55b2415006e1\"},\"panelIndex\":\"1c12c2f2-80c2-4d5c-b722-55b2415006e1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1c4b99e1-7785-444f-a1c5-f592893b1a96\"},\"panelIndex\":\"1c4b99e1-7785-444f-a1c5-f592893b1a96\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":15,\"w\":48,\"h\":18,\"i\":\"94eab06f-60ac-4a85-b771-3a8ed475c9bb\"},\"panelIndex\":\"94eab06f-60ac-4a85-b771-3a8ed475c9bb\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":33,\"w\":48,\"h\":8,\"i\":\"52c19b6b-7117-42ac-a74e-c507a1c3ffc0\"},\"panelIndex\":\"52c19b6b-7117-42ac-a74e-c507a1c3ffc0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":41,\"w\":11,\"h\":10,\"i\":\"a1e889dc-b80e-4937-a576-979f34d1859b\"},\"panelIndex\":\"a1e889dc-b80e-4937-a576-979f34d1859b\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":11,\"y\":41,\"w\":5,\"h\":10,\"i\":\"4930b035-d756-4cc5-9a18-1af9e67d6f31\"},\"panelIndex\":\"4930b035-d756-4cc5-9a18-1af9e67d6f31\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"}]", - "refreshInterval": { - "pause": true, - "value": 0 - }, - "timeFrom": "2019-06-26T06:20:28.066Z", - "timeRestore": true, - "timeTo": "2019-06-26T07:27:58.573Z", - "title": "Ecom Dashboard Hidden Panel Titles", - "version": 1 - }, - "migrationVersion": { - "dashboard": "7.11.0" - }, - "references": [ - { - "id": "0a464230-79f0-11ea-ae7f-13c5d6e410a0", - "name": "panel_0", - "type": "visualization" - }, - { - "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", - "name": "panel_1", - "type": "visualization" - }, - { - "id": "6091ead0-1c6d-11ea-a100-8589bb9d7c6b", - "name": "panel_2", - "type": "search" - }, - { - "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "panel_3", - "type": "visualization" - }, - { - "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", - "name": "panel_4", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "panel_5", - "type": "visualization" - } - ], - "type": "dashboard", - "updated_at": "2020-04-10T00:37:48.462Z" - } - } -} - -{ - "type": "doc", - "value": { - "id": "visualization:0a464230-79f0-11ea-ae7f-13c5d6e410a0", - "index": ".kibana_1", - "source": { - "migrationVersion": { - "visualization": "7.13.0" - }, - "references": [ - { - "id": "5193f870-d861-11e9-a311-0fa548c5f953", - "name": "kibanaSavedObjectMeta.searchSourceJSON.index", - "type": "index-pattern" - } - ], - "type": "visualization", - "updated_at": "2020-04-08T23:24:05.971Z", - "visualization": { - "description": "", - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" - }, - "title": "e-commerce area chart", - "uiStateJSON": "{}", - "version": 1, - "visState": "{\"type\":\"area\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"order_date\",\"timeRange\":{\"from\":\"2019-06-26T06:20:28.066Z\",\"to\":\"2019-06-26T07:27:58.573Z\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}],\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"title\":\"e-commerce area chart\"}" - } - } - } -} - -{ - "type": "doc", - "value": { - "id": "visualization:200609c0-79f0-11ea-ae7f-13c5d6e410a0", - "index": ".kibana_1", - "source": { - "migrationVersion": { - "visualization": "7.13.0" - }, - "references": [ - { - "id": "5193f870-d861-11e9-a311-0fa548c5f953", - "name": "kibanaSavedObjectMeta.searchSourceJSON.index", - "type": "index-pattern" - } - ], - "type": "visualization", - "updated_at": "2020-04-08T23:24:42.460Z", - "visualization": { - "description": "", - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" - }, - "title": "e-commerce pie chart", - "uiStateJSON": "{}", - "version": 1, - "visState": "{\"type\":\"pie\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"order_date\",\"timeRange\":{\"from\":\"2019-06-26T06:20:28.066Z\",\"to\":\"2019-06-26T07:27:58.573Z\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}],\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100}},\"title\":\"e-commerce pie chart\"}" - } - } - } -} - -{ - "type": "doc", - "value": { - "id": "visualization:ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", - "index": ".kibana_1", - "source": { - "migrationVersion": { - "visualization": "7.13.0" - }, - "references": [ - { - "id": "5193f870-d861-11e9-a311-0fa548c5f953", - "name": "kibanaSavedObjectMeta.searchSourceJSON.index", - "type": "index-pattern" - } - ], - "type": "visualization", - "updated_at": "2020-04-10T00:33:44.909Z", - "visualization": { - "description": "", - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\",\"filter\":[]}" - }, - "title": "게이지", - "uiStateJSON": "{}", - "version": 1, - "visState": "{\"type\":\"gauge\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}],\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"alignment\":\"automatic\",\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":50},{\"from\":50,\"to\":75},{\"from\":75,\"to\":100}],\"invertColors\":false,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":true,\"labels\":false,\"color\":\"rgba(105,112,125,0.2)\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"rgba(105,112,125,0.2)\",\"bgColor\":true,\"subText\":\"\",\"fontSize\":60}}},\"title\":\"게이지\"}" - } - } - } -} - -{ - "type": "doc", - "value": { - "id": "visualization:132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "index": ".kibana_1", - "source": { - "migrationVersion": { - "visualization": "7.13.0" - }, - "references": [ - { - "id": "5193f870-d861-11e9-a311-0fa548c5f953", - "name": "kibanaSavedObjectMeta.searchSourceJSON.index", - "type": "index-pattern" - } - ], - "type": "visualization", - "updated_at": "2020-04-10T00:34:44.700Z", - "visualization": { - "description": "", - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\",\"filter\":[]}" - }, - "title": "Українська", - "uiStateJSON": "{}", - "version": 1, - "visState": "{\"type\":\"metric\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}],\"params\":{\"addTooltip\":true,\"addLegend\":false,\"type\":\"metric\",\"metric\":{\"percentageMode\":false,\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"metricColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"labels\":{\"show\":true},\"invertColors\":false,\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"title\":\"Українська\"}" - } - } - } -} - -{ - "type": "doc", - "value": { - "id": "visualization:4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", - "index": ".kibana_1", - "source": { - "migrationVersion": { - "visualization": "7.13.0" - }, - "references": [], - "type": "visualization", - "updated_at": "2020-04-10T00:36:17.053Z", - "visualization": { - "description": "", - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" - }, - "title": "Tiểu thuyết", - "uiStateJSON": "{}", - "version": 1, - "visState": "{\"type\":\"markdown\",\"aggs\":[],\"params\":{\"fontSize\":12,\"openLinksInNewTab\":false,\"markdown\":\"Tiểu thuyết là một thể loại văn xuôi có hư cấu, thông qua nhân vật, hoàn cảnh, sự việc để phản ánh bức tranh xã hội rộng lớn và những vấn đề của cuộc sống con người, biểu hiện tính chất tường thuật, tính chất kể chuyện bằng ngôn ngữ văn xuôi theo những chủ đề xác định.\\n\\nTrong một cách hiểu khác, nhận định của Belinski: \\\"tiểu thuyết là sử thi của đời tư\\\" chỉ ra khái quát nhất về một dạng thức tự sự, trong đó sự trần thuật tập trung vào số phận của một cá nhân trong quá trình hình thành và phát triển của nó. Sự trần thuật ở đây được khai triển trong không gian và thời gian nghệ thuật đến mức đủ để truyền đạt cơ cấu của nhân cách[1].\\n\\n\\n[1]^ Mục từ Tiểu thuyết trong cuốn 150 thuật ngữ văn học, Lại Nguyên Ân biên soạn, Nhà xuất bản Đại học Quốc gia Hà Nội, in lần thứ 2 có sửa đổi bổ sung. H. 2003. Trang 326.\"},\"title\":\"Tiểu thuyết\"}" - } - } - } -} - -{ - "type": "doc", - "value": { - "id": "space:default", - "index": ".kibana_1", - "source": { - "space": { - "_reserved": true, - "description": "This is the default space", - "disabledFeatures": [], - "name": "Default Space" - }, - "type": "space", - "updated_at": "2021-01-07T00:17:12.785Z" - } - } -} - -{ - "type": "doc", - "value": { - "id": "dashboard:6c263e00-1c6d-11ea-a100-8589bb9d7c6b", - "index": ".kibana_1", - "source": { - "dashboard": { - "description": "", - "hits": 0, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}" - }, - "optionsJSON": "{\"hidePanelTitles\":false,\"useMargins\":true}", - "panelsJSON": "[{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1c12c2f2-80c2-4d5c-b722-55b2415006e1\"},\"panelIndex\":\"1c12c2f2-80c2-4d5c-b722-55b2415006e1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1c4b99e1-7785-444f-a1c5-f592893b1a96\"},\"panelIndex\":\"1c4b99e1-7785-444f-a1c5-f592893b1a96\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":35,\"w\":48,\"h\":18,\"i\":\"94eab06f-60ac-4a85-b771-3a8ed475c9bb\"},\"panelIndex\":\"94eab06f-60ac-4a85-b771-3a8ed475c9bb\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":15,\"w\":48,\"h\":8,\"i\":\"52c19b6b-7117-42ac-a74e-c507a1c3ffc0\"},\"panelIndex\":\"52c19b6b-7117-42ac-a74e-c507a1c3ffc0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":23,\"w\":16,\"h\":12,\"i\":\"a1e889dc-b80e-4937-a576-979f34d1859b\"},\"panelIndex\":\"a1e889dc-b80e-4937-a576-979f34d1859b\",\"embeddableConfig\":{\"enhancements\":{},\"vis\":null},\"panelRefName\":\"panel_4\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":16,\"y\":23,\"w\":12,\"h\":12,\"i\":\"4930b035-d756-4cc5-9a18-1af9e67d6f31\"},\"panelIndex\":\"4930b035-d756-4cc5-9a18-1af9e67d6f31\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":28,\"y\":23,\"w\":20,\"h\":12,\"i\":\"55112375-d6f0-44f7-a8fb-867c8f7d464d\"},\"panelIndex\":\"55112375-d6f0-44f7-a8fb-867c8f7d464d\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6\"}]", - "refreshInterval": { - "pause": true, - "value": 0 - }, - "timeFrom": "2019-03-23T03:06:17.785Z", - "timeRestore": true, - "timeTo": "2019-10-04T02:33:16.708Z", - "title": "Ecom Dashboard", - "version": 1 - }, - "migrationVersion": { - "dashboard": "7.11.0" - }, - "references": [ - { - "id": "0a464230-79f0-11ea-ae7f-13c5d6e410a0", - "name": "panel_0", - "type": "visualization" - }, - { - "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", - "name": "panel_1", - "type": "visualization" - }, - { - "id": "6091ead0-1c6d-11ea-a100-8589bb9d7c6b", - "name": "panel_2", - "type": "search" - }, - { - "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "panel_3", - "type": "visualization" - }, - { - "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", - "name": "panel_4", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "panel_5", - "type": "visualization" - }, - { - "id": "1bba55f0-507e-11eb-9c0d-97106882b997", - "name": "panel_6", - "type": "visualization" - } - ], - "type": "dashboard", - "updated_at": "2021-01-07T00:22:16.102Z" - } - } -} - -{ - "type": "doc", - "value": { - "id": "visualization:1bba55f0-507e-11eb-9c0d-97106882b997", - "index": ".kibana_1", - "source": { - "migrationVersion": { - "visualization": "7.13.0" - }, - "references": [ - { - "id": "6091ead0-1c6d-11ea-a100-8589bb9d7c6b", - "name": "search_0", - "type": "search" - } - ], - "type": "visualization", - "updated_at": "2021-01-07T00:23:04.624Z", - "visualization": { - "description": "", - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" - }, - "savedSearchRefName": "search_0", - "title": "Tag Cloud of Names", - "uiStateJSON": "{}", - "version": 1, - "visState": "{\"title\":\"Tag Cloud of Names\",\"type\":\"tagcloud\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"customer_first_name.keyword\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":10,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"segment\"}],\"params\":{\"scale\":\"linear\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":72,\"showLabel\":true}}" - } - } - } -} - -{ - "type": "doc", - "value": { - "id": "search:e5bfe380-ac3e-11eb-8f24-bffe9ba4af2b", - "index": ".kibana_1", - "source": { - "migrationVersion": { - "search": "7.9.3" - }, - "references": [ - { - "id": "5193f870-d861-11e9-a311-0fa548c5f953", - "name": "kibanaSavedObjectMeta.searchSourceJSON.index", - "type": "index-pattern" - } - ], - "search": { - "columns": [ - "order_date", - "category", - "currency", - "customer_id", - "order_id", - "day_of_week_i", - "products.created_on", - "sku" - ], - "description": "", - "hits": 0, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" - }, - "sort": [ - [ - "order_date", - "desc" - ] - ], - "title": "Ecommerce Data (copy)", - "version": 1 - }, - "type": "search", - "updated_at": "2021-05-03T18:39:30.751Z" - } - } -} - -{ - "type": "doc", - "value": { - "id": "dashboard:f7192e90-ac3c-11eb-8f24-bffe9ba4af2b", - "index": ".kibana_1", - "source": { - "dashboard": { - "description": "", - "hits": 0, - "kibanaSavedObjectMeta": { - "searchSourceJSON": "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}" - }, - "optionsJSON": "{\"hidePanelTitles\":false,\"useMargins\":true}", - "panelsJSON": "[{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":9,\"y\":0,\"w\":24,\"h\":15,\"i\":\"914ac161-94d4-4d93-a287-c21fca46a974\"},\"panelIndex\":\"914ac161-94d4-4d93-a287-c21fca46a974\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_914ac161-94d4-4d93-a287-c21fca46a974\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":22,\"y\":15,\"w\":24,\"h\":15,\"i\":\"c4cec7d1-97e3-4101-adc4-c3f15102511c\"},\"panelIndex\":\"c4cec7d1-97e3-4101-adc4-c3f15102511c\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_c4cec7d1-97e3-4101-adc4-c3f15102511c\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":30,\"w\":24,\"h\":15,\"i\":\"09f7de68-0d07-4661-8fda-73ea8b577ac7\"},\"panelIndex\":\"09f7de68-0d07-4661-8fda-73ea8b577ac7\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_09f7de68-0d07-4661-8fda-73ea8b577ac7\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":45,\"w\":24,\"h\":15,\"i\":\"6c25ca6e-6aa1-4f06-9a96-e83ffd9f52e8\"},\"panelIndex\":\"6c25ca6e-6aa1-4f06-9a96-e83ffd9f52e8\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_6c25ca6e-6aa1-4f06-9a96-e83ffd9f52e8\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":60,\"w\":24,\"h\":15,\"i\":\"37764cf9-3c89-454a-bd7e-ae4c242dc624\"},\"panelIndex\":\"37764cf9-3c89-454a-bd7e-ae4c242dc624\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_37764cf9-3c89-454a-bd7e-ae4c242dc624\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":15,\"y\":75,\"w\":24,\"h\":15,\"i\":\"990422fd-a9cf-446f-ba2f-ea9178a7b2e0\"},\"panelIndex\":\"990422fd-a9cf-446f-ba2f-ea9178a7b2e0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_990422fd-a9cf-446f-ba2f-ea9178a7b2e0\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":90,\"w\":24,\"h\":15,\"i\":\"0cdc13ec-2775-4da9-9a47-1e833bb807eb\"},\"panelIndex\":\"0cdc13ec-2775-4da9-9a47-1e833bb807eb\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0cdc13ec-2775-4da9-9a47-1e833bb807eb\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":18,\"y\":105,\"w\":24,\"h\":15,\"i\":\"eee160de-5777-40c8-9c2c-e75f64bf208a\"},\"panelIndex\":\"eee160de-5777-40c8-9c2c-e75f64bf208a\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_eee160de-5777-40c8-9c2c-e75f64bf208a\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":120,\"w\":24,\"h\":15,\"i\":\"b36f6a97-5d3d-4fc4-b076-b3e514f8f7bb\"},\"panelIndex\":\"b36f6a97-5d3d-4fc4-b076-b3e514f8f7bb\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_b36f6a97-5d3d-4fc4-b076-b3e514f8f7bb\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":135,\"w\":24,\"h\":15,\"i\":\"2e72acbf-7ade-451e-a5e4-7414f12facf2\"},\"panelIndex\":\"2e72acbf-7ade-451e-a5e4-7414f12facf2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2e72acbf-7ade-451e-a5e4-7414f12facf2\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":150,\"w\":24,\"h\":15,\"i\":\"4119e9b0-5d03-482d-9356-89bb62b6a851\"},\"panelIndex\":\"4119e9b0-5d03-482d-9356-89bb62b6a851\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4119e9b0-5d03-482d-9356-89bb62b6a851\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":165,\"w\":24,\"h\":15,\"i\":\"42b4a37c-8b04-4510-9f27-831355221b65\"},\"panelIndex\":\"42b4a37c-8b04-4510-9f27-831355221b65\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_42b4a37c-8b04-4510-9f27-831355221b65\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":14,\"y\":180,\"w\":24,\"h\":15,\"i\":\"dc676050-d752-4c3e-a1ae-73ef2f1bcdc6\"},\"panelIndex\":\"dc676050-d752-4c3e-a1ae-73ef2f1bcdc6\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_dc676050-d752-4c3e-a1ae-73ef2f1bcdc6\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":17,\"y\":195,\"w\":24,\"h\":15,\"i\":\"6602e0e0-9e66-4e0e-90c1-f66b9c3d2340\"},\"panelIndex\":\"6602e0e0-9e66-4e0e-90c1-f66b9c3d2340\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_6602e0e0-9e66-4e0e-90c1-f66b9c3d2340\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":210,\"w\":24,\"h\":15,\"i\":\"c9c65725-9b4d-4343-93db-7efa4a7a2d60\"},\"panelIndex\":\"c9c65725-9b4d-4343-93db-7efa4a7a2d60\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_c9c65725-9b4d-4343-93db-7efa4a7a2d60\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":225,\"w\":24,\"h\":15,\"i\":\"69141f9b-5c23-409d-9c96-7f94c243f79e\"},\"panelIndex\":\"69141f9b-5c23-409d-9c96-7f94c243f79e\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_69141f9b-5c23-409d-9c96-7f94c243f79e\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":17,\"y\":240,\"w\":24,\"h\":15,\"i\":\"6feeec2c-34ab-4844-8445-e417c8e0595b\"},\"panelIndex\":\"6feeec2c-34ab-4844-8445-e417c8e0595b\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6feeec2c-34ab-4844-8445-e417c8e0595b\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":15,\"y\":255,\"w\":24,\"h\":15,\"i\":\"985d9dc1-de44-4803-afad-f1d497d050a1\"},\"panelIndex\":\"985d9dc1-de44-4803-afad-f1d497d050a1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_985d9dc1-de44-4803-afad-f1d497d050a1\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":270,\"w\":24,\"h\":15,\"i\":\"d7ef9e23-d0dd-4c7c-90b3-f611bbfcd1b0\"},\"panelIndex\":\"d7ef9e23-d0dd-4c7c-90b3-f611bbfcd1b0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_d7ef9e23-d0dd-4c7c-90b3-f611bbfcd1b0\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":20,\"y\":285,\"w\":24,\"h\":15,\"i\":\"6b0768b1-0cd2-47f0-a639-b369e7318d44\"},\"panelIndex\":\"6b0768b1-0cd2-47f0-a639-b369e7318d44\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6b0768b1-0cd2-47f0-a639-b369e7318d44\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":20,\"y\":300,\"w\":24,\"h\":15,\"i\":\"c9cc2835-06a8-4448-b703-2d41a6692feb\"},\"panelIndex\":\"c9cc2835-06a8-4448-b703-2d41a6692feb\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_c9cc2835-06a8-4448-b703-2d41a6692feb\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":315,\"w\":24,\"h\":15,\"i\":\"af2a55b1-8b3d-478a-96b1-72e4f12585e4\"},\"panelIndex\":\"af2a55b1-8b3d-478a-96b1-72e4f12585e4\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_af2a55b1-8b3d-478a-96b1-72e4f12585e4\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":330,\"w\":24,\"h\":15,\"i\":\"ee92986a-adab-4d66-ad4e-a43a608f52f7\"},\"panelIndex\":\"ee92986a-adab-4d66-ad4e-a43a608f52f7\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_ee92986a-adab-4d66-ad4e-a43a608f52f7\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":22,\"y\":345,\"w\":24,\"h\":15,\"i\":\"3b4e1fd0-2acb-444a-b478-42d7bd10b96c\"},\"panelIndex\":\"3b4e1fd0-2acb-444a-b478-42d7bd10b96c\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3b4e1fd0-2acb-444a-b478-42d7bd10b96c\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":360,\"w\":24,\"h\":15,\"i\":\"04d7056d-88a4-4b00-b8f4-33f79f1b6f7a\"},\"panelIndex\":\"04d7056d-88a4-4b00-b8f4-33f79f1b6f7a\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_04d7056d-88a4-4b00-b8f4-33f79f1b6f7a\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":21,\"y\":375,\"w\":24,\"h\":15,\"i\":\"51122bae-427e-45a6-904e-6c821447cc46\"},\"panelIndex\":\"51122bae-427e-45a6-904e-6c821447cc46\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_51122bae-427e-45a6-904e-6c821447cc46\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":20,\"y\":390,\"w\":24,\"h\":15,\"i\":\"4efab22c-1892-4013-8406-5e5d924a8a21\"},\"panelIndex\":\"4efab22c-1892-4013-8406-5e5d924a8a21\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4efab22c-1892-4013-8406-5e5d924a8a21\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":405,\"w\":24,\"h\":15,\"i\":\"4c3c1b29-100e-474c-8290-9470684ae407\"},\"panelIndex\":\"4c3c1b29-100e-474c-8290-9470684ae407\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4c3c1b29-100e-474c-8290-9470684ae407\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":18,\"y\":420,\"w\":24,\"h\":15,\"i\":\"b4501df0-d759-4513-9e87-5dd8eefe4a4f\"},\"panelIndex\":\"b4501df0-d759-4513-9e87-5dd8eefe4a4f\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_b4501df0-d759-4513-9e87-5dd8eefe4a4f\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":20,\"y\":435,\"w\":24,\"h\":15,\"i\":\"4fbff0ec-b3a6-4ee7-8734-9b177c3e51c6\"},\"panelIndex\":\"4fbff0ec-b3a6-4ee7-8734-9b177c3e51c6\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_4fbff0ec-b3a6-4ee7-8734-9b177c3e51c6\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":450,\"w\":24,\"h\":15,\"i\":\"13d9982e-2745-44b1-af94-fa4b9f6761a9\"},\"panelIndex\":\"13d9982e-2745-44b1-af94-fa4b9f6761a9\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_13d9982e-2745-44b1-af94-fa4b9f6761a9\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":17,\"y\":465,\"w\":24,\"h\":15,\"i\":\"efa18320-9650-4bfe-9418-ac29b7979f70\"},\"panelIndex\":\"efa18320-9650-4bfe-9418-ac29b7979f70\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_efa18320-9650-4bfe-9418-ac29b7979f70\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":21,\"y\":480,\"w\":24,\"h\":15,\"i\":\"1f03bc70-0545-4a3a-bebc-ad477674b841\"},\"panelIndex\":\"1f03bc70-0545-4a3a-bebc-ad477674b841\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1f03bc70-0545-4a3a-bebc-ad477674b841\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":495,\"w\":24,\"h\":15,\"i\":\"d766ce3a-9ec5-4ead-8698-6a2e66e729bb\"},\"panelIndex\":\"d766ce3a-9ec5-4ead-8698-6a2e66e729bb\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_d766ce3a-9ec5-4ead-8698-6a2e66e729bb\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":510,\"w\":24,\"h\":15,\"i\":\"de93deb0-6c16-45ae-8fae-de0b2e1c4ae0\"},\"panelIndex\":\"de93deb0-6c16-45ae-8fae-de0b2e1c4ae0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_de93deb0-6c16-45ae-8fae-de0b2e1c4ae0\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":525,\"w\":24,\"h\":15,\"i\":\"b93cc5e1-084a-42d9-9958-a3f569573d43\"},\"panelIndex\":\"b93cc5e1-084a-42d9-9958-a3f569573d43\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_b93cc5e1-084a-42d9-9958-a3f569573d43\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":20,\"y\":540,\"w\":24,\"h\":15,\"i\":\"0b6c380f-3536-4f03-8dbd-95c53be69263\"},\"panelIndex\":\"0b6c380f-3536-4f03-8dbd-95c53be69263\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0b6c380f-3536-4f03-8dbd-95c53be69263\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":23,\"y\":555,\"w\":24,\"h\":15,\"i\":\"5c68b67a-ac42-48b8-85de-2409aaa0cdc6\"},\"panelIndex\":\"5c68b67a-ac42-48b8-85de-2409aaa0cdc6\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5c68b67a-ac42-48b8-85de-2409aaa0cdc6\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":570,\"w\":24,\"h\":15,\"i\":\"098a69b8-c9a0-40c8-8703-62838e0ec4a9\"},\"panelIndex\":\"098a69b8-c9a0-40c8-8703-62838e0ec4a9\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_098a69b8-c9a0-40c8-8703-62838e0ec4a9\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":1,\"y\":585,\"w\":24,\"h\":15,\"i\":\"a0f4b9ce-2e36-4d22-8dd9-8988f1a3b883\"},\"panelIndex\":\"a0f4b9ce-2e36-4d22-8dd9-8988f1a3b883\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_a0f4b9ce-2e36-4d22-8dd9-8988f1a3b883\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":15,\"y\":600,\"w\":24,\"h\":15,\"i\":\"eb651411-ea02-4506-a674-f0125d0b2a4a\"},\"panelIndex\":\"eb651411-ea02-4506-a674-f0125d0b2a4a\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_eb651411-ea02-4506-a674-f0125d0b2a4a\"},{\"version\":\"8.0.0\",\"type\":\"search\",\"gridData\":{\"x\":0,\"y\":615,\"w\":48,\"h\":111,\"i\":\"8ec9b67a-5d08-4006-bccc-a7341b88bb63\"},\"panelIndex\":\"8ec9b67a-5d08-4006-bccc-a7341b88bb63\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_8ec9b67a-5d08-4006-bccc-a7341b88bb63\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":4,\"y\":852,\"w\":24,\"h\":15,\"i\":\"1201144d-5c9c-4015-89a3-0cb803405986\"},\"panelIndex\":\"1201144d-5c9c-4015-89a3-0cb803405986\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1201144d-5c9c-4015-89a3-0cb803405986\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":837,\"w\":24,\"h\":15,\"i\":\"913c1c46-ded4-4e04-81ff-e683f725d3a5\"},\"panelIndex\":\"913c1c46-ded4-4e04-81ff-e683f725d3a5\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_913c1c46-ded4-4e04-81ff-e683f725d3a5\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":867,\"w\":24,\"h\":15,\"i\":\"f49dfd93-ce95-4a65-b9ec-531f340da083\"},\"panelIndex\":\"f49dfd93-ce95-4a65-b9ec-531f340da083\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_f49dfd93-ce95-4a65-b9ec-531f340da083\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":18,\"y\":882,\"w\":24,\"h\":15,\"i\":\"0705993c-492c-4ce0-83e0-a481c90bd432\"},\"panelIndex\":\"0705993c-492c-4ce0-83e0-a481c90bd432\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0705993c-492c-4ce0-83e0-a481c90bd432\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":897,\"w\":24,\"h\":15,\"i\":\"02de39d3-6839-4198-94e3-cc91f61d0c6e\"},\"panelIndex\":\"02de39d3-6839-4198-94e3-cc91f61d0c6e\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_02de39d3-6839-4198-94e3-cc91f61d0c6e\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":5,\"y\":912,\"w\":24,\"h\":15,\"i\":\"e6b958fa-931f-4358-94fc-07934419066d\"},\"panelIndex\":\"e6b958fa-931f-4358-94fc-07934419066d\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_e6b958fa-931f-4358-94fc-07934419066d\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":927,\"w\":24,\"h\":15,\"i\":\"e6d70fc7-1bdc-4743-9a15-615dff91a5c1\"},\"panelIndex\":\"e6d70fc7-1bdc-4743-9a15-615dff91a5c1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_e6d70fc7-1bdc-4743-9a15-615dff91a5c1\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":22,\"y\":942,\"w\":24,\"h\":15,\"i\":\"9db5f35e-ab94-4a5a-8c0f-70bf2aa095aa\"},\"panelIndex\":\"9db5f35e-ab94-4a5a-8c0f-70bf2aa095aa\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_9db5f35e-ab94-4a5a-8c0f-70bf2aa095aa\"},{\"version\":\"8.0.0\",\"type\":\"search\",\"gridData\":{\"x\":0,\"y\":726,\"w\":48,\"h\":111,\"i\":\"e985d8b0-4a76-46d0-af01-3edab5995b97\"},\"panelIndex\":\"e985d8b0-4a76-46d0-af01-3edab5995b97\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_e985d8b0-4a76-46d0-af01-3edab5995b97\"}]", - "refreshInterval": { - "pause": true, - "value": 0 - }, - "timeFrom": "2019-06-01T03:59:54.350Z", - "timeRestore": true, - "timeTo": "2019-08-01T14:52:40.436Z", - "title": "Large Dashboard", - "version": 1 - }, - "migrationVersion": { - "dashboard": "7.11.0" - }, - "references": [ - { - "id": "0a464230-79f0-11ea-ae7f-13c5d6e410a0", - "name": "914ac161-94d4-4d93-a287-c21fca46a974:panel_914ac161-94d4-4d93-a287-c21fca46a974", - "type": "visualization" - }, - { - "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", - "name": "c4cec7d1-97e3-4101-adc4-c3f15102511c:panel_c4cec7d1-97e3-4101-adc4-c3f15102511c", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "09f7de68-0d07-4661-8fda-73ea8b577ac7:panel_09f7de68-0d07-4661-8fda-73ea8b577ac7", - "type": "visualization" - }, - { - "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", - "name": "6c25ca6e-6aa1-4f06-9a96-e83ffd9f52e8:panel_6c25ca6e-6aa1-4f06-9a96-e83ffd9f52e8", - "type": "visualization" - }, - { - "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "37764cf9-3c89-454a-bd7e-ae4c242dc624:panel_37764cf9-3c89-454a-bd7e-ae4c242dc624", - "type": "visualization" - }, - { - "id": "1bba55f0-507e-11eb-9c0d-97106882b997", - "name": "990422fd-a9cf-446f-ba2f-ea9178a7b2e0:panel_990422fd-a9cf-446f-ba2f-ea9178a7b2e0", - "type": "visualization" - }, - { - "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "0cdc13ec-2775-4da9-9a47-1e833bb807eb:panel_0cdc13ec-2775-4da9-9a47-1e833bb807eb", - "type": "visualization" - }, - { - "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", - "name": "eee160de-5777-40c8-9c2c-e75f64bf208a:panel_eee160de-5777-40c8-9c2c-e75f64bf208a", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "b36f6a97-5d3d-4fc4-b076-b3e514f8f7bb:panel_b36f6a97-5d3d-4fc4-b076-b3e514f8f7bb", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "2e72acbf-7ade-451e-a5e4-7414f12facf2:panel_2e72acbf-7ade-451e-a5e4-7414f12facf2", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "4119e9b0-5d03-482d-9356-89bb62b6a851:panel_4119e9b0-5d03-482d-9356-89bb62b6a851", - "type": "visualization" - }, - { - "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", - "name": "42b4a37c-8b04-4510-9f27-831355221b65:panel_42b4a37c-8b04-4510-9f27-831355221b65", - "type": "visualization" - }, - { - "id": "1bba55f0-507e-11eb-9c0d-97106882b997", - "name": "dc676050-d752-4c3e-a1ae-73ef2f1bcdc6:panel_dc676050-d752-4c3e-a1ae-73ef2f1bcdc6", - "type": "visualization" - }, - { - "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", - "name": "6602e0e0-9e66-4e0e-90c1-f66b9c3d2340:panel_6602e0e0-9e66-4e0e-90c1-f66b9c3d2340", - "type": "visualization" - }, - { - "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "c9c65725-9b4d-4343-93db-7efa4a7a2d60:panel_c9c65725-9b4d-4343-93db-7efa4a7a2d60", - "type": "visualization" - }, - { - "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", - "name": "69141f9b-5c23-409d-9c96-7f94c243f79e:panel_69141f9b-5c23-409d-9c96-7f94c243f79e", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "6feeec2c-34ab-4844-8445-e417c8e0595b:panel_6feeec2c-34ab-4844-8445-e417c8e0595b", - "type": "visualization" - }, - { - "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "985d9dc1-de44-4803-afad-f1d497d050a1:panel_985d9dc1-de44-4803-afad-f1d497d050a1", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "d7ef9e23-d0dd-4c7c-90b3-f611bbfcd1b0:panel_d7ef9e23-d0dd-4c7c-90b3-f611bbfcd1b0", - "type": "visualization" - }, - { - "id": "1bba55f0-507e-11eb-9c0d-97106882b997", - "name": "6b0768b1-0cd2-47f0-a639-b369e7318d44:panel_6b0768b1-0cd2-47f0-a639-b369e7318d44", - "type": "visualization" - }, - { - "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", - "name": "c9cc2835-06a8-4448-b703-2d41a6692feb:panel_c9cc2835-06a8-4448-b703-2d41a6692feb", - "type": "visualization" - }, - { - "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", - "name": "af2a55b1-8b3d-478a-96b1-72e4f12585e4:panel_af2a55b1-8b3d-478a-96b1-72e4f12585e4", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "ee92986a-adab-4d66-ad4e-a43a608f52f7:panel_ee92986a-adab-4d66-ad4e-a43a608f52f7", - "type": "visualization" - }, - { - "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "3b4e1fd0-2acb-444a-b478-42d7bd10b96c:panel_3b4e1fd0-2acb-444a-b478-42d7bd10b96c", - "type": "visualization" - }, - { - "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "04d7056d-88a4-4b00-b8f4-33f79f1b6f7a:panel_04d7056d-88a4-4b00-b8f4-33f79f1b6f7a", - "type": "visualization" - }, - { - "id": "1bba55f0-507e-11eb-9c0d-97106882b997", - "name": "51122bae-427e-45a6-904e-6c821447cc46:panel_51122bae-427e-45a6-904e-6c821447cc46", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "4efab22c-1892-4013-8406-5e5d924a8a21:panel_4efab22c-1892-4013-8406-5e5d924a8a21", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "4c3c1b29-100e-474c-8290-9470684ae407:panel_4c3c1b29-100e-474c-8290-9470684ae407", - "type": "visualization" - }, - { - "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", - "name": "b4501df0-d759-4513-9e87-5dd8eefe4a4f:panel_b4501df0-d759-4513-9e87-5dd8eefe4a4f", - "type": "visualization" - }, - { - "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", - "name": "4fbff0ec-b3a6-4ee7-8734-9b177c3e51c6:panel_4fbff0ec-b3a6-4ee7-8734-9b177c3e51c6", - "type": "visualization" - }, - { - "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", - "name": "13d9982e-2745-44b1-af94-fa4b9f6761a9:panel_13d9982e-2745-44b1-af94-fa4b9f6761a9", - "type": "visualization" - }, - { - "id": "1bba55f0-507e-11eb-9c0d-97106882b997", - "name": "efa18320-9650-4bfe-9418-ac29b7979f70:panel_efa18320-9650-4bfe-9418-ac29b7979f70", - "type": "visualization" - }, - { - "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "1f03bc70-0545-4a3a-bebc-ad477674b841:panel_1f03bc70-0545-4a3a-bebc-ad477674b841", - "type": "visualization" - }, - { - "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "d766ce3a-9ec5-4ead-8698-6a2e66e729bb:panel_d766ce3a-9ec5-4ead-8698-6a2e66e729bb", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "de93deb0-6c16-45ae-8fae-de0b2e1c4ae0:panel_de93deb0-6c16-45ae-8fae-de0b2e1c4ae0", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "b93cc5e1-084a-42d9-9958-a3f569573d43:panel_b93cc5e1-084a-42d9-9958-a3f569573d43", - "type": "visualization" - }, - { - "id": "1bba55f0-507e-11eb-9c0d-97106882b997", - "name": "0b6c380f-3536-4f03-8dbd-95c53be69263:panel_0b6c380f-3536-4f03-8dbd-95c53be69263", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "5c68b67a-ac42-48b8-85de-2409aaa0cdc6:panel_5c68b67a-ac42-48b8-85de-2409aaa0cdc6", - "type": "visualization" - }, - { - "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", - "name": "098a69b8-c9a0-40c8-8703-62838e0ec4a9:panel_098a69b8-c9a0-40c8-8703-62838e0ec4a9", - "type": "visualization" - }, - { - "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", - "name": "a0f4b9ce-2e36-4d22-8dd9-8988f1a3b883:panel_a0f4b9ce-2e36-4d22-8dd9-8988f1a3b883", - "type": "visualization" - }, - { - "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "eb651411-ea02-4506-a674-f0125d0b2a4a:panel_eb651411-ea02-4506-a674-f0125d0b2a4a", - "type": "visualization" - }, - { - "id": "6091ead0-1c6d-11ea-a100-8589bb9d7c6b", - "name": "8ec9b67a-5d08-4006-bccc-a7341b88bb63:panel_8ec9b67a-5d08-4006-bccc-a7341b88bb63", - "type": "search" - }, - { - "id": "1bba55f0-507e-11eb-9c0d-97106882b997", - "name": "1201144d-5c9c-4015-89a3-0cb803405986:panel_1201144d-5c9c-4015-89a3-0cb803405986", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "913c1c46-ded4-4e04-81ff-e683f725d3a5:panel_913c1c46-ded4-4e04-81ff-e683f725d3a5", - "type": "visualization" - }, - { - "id": "1bba55f0-507e-11eb-9c0d-97106882b997", - "name": "f49dfd93-ce95-4a65-b9ec-531f340da083:panel_f49dfd93-ce95-4a65-b9ec-531f340da083", - "type": "visualization" - }, - { - "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", - "name": "0705993c-492c-4ce0-83e0-a481c90bd432:panel_0705993c-492c-4ce0-83e0-a481c90bd432", - "type": "visualization" - }, - { - "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", - "name": "02de39d3-6839-4198-94e3-cc91f61d0c6e:panel_02de39d3-6839-4198-94e3-cc91f61d0c6e", - "type": "visualization" - }, - { - "id": "1bba55f0-507e-11eb-9c0d-97106882b997", - "name": "e6b958fa-931f-4358-94fc-07934419066d:panel_e6b958fa-931f-4358-94fc-07934419066d", - "type": "visualization" - }, - { - "id": "1bba55f0-507e-11eb-9c0d-97106882b997", - "name": "e6d70fc7-1bdc-4743-9a15-615dff91a5c1:panel_e6d70fc7-1bdc-4743-9a15-615dff91a5c1", - "type": "visualization" - }, - { - "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", - "name": "9db5f35e-ab94-4a5a-8c0f-70bf2aa095aa:panel_9db5f35e-ab94-4a5a-8c0f-70bf2aa095aa", - "type": "visualization" - }, - { - "id": "e5bfe380-ac3e-11eb-8f24-bffe9ba4af2b", - "name": "e985d8b0-4a76-46d0-af01-3edab5995b97:panel_e985d8b0-4a76-46d0-af01-3edab5995b97", - "type": "search" - } - ], - "type": "dashboard", - "updated_at": "2021-05-03T18:39:45.983Z" - } - } -} diff --git a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/mappings.json b/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/mappings.json deleted file mode 100644 index 8ee08f968d072..0000000000000 --- a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/mappings.json +++ /dev/null @@ -1,2730 +0,0 @@ -{ - "type": "index", - "value": { - "aliases": { - ".kibana": { - } - }, - "index": ".kibana_1", - "mappings": { - "_meta": { - "migrationMappingPropertyHashes": { - "action": "6e96ac5e648f57523879661ea72525b7", - "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", - "alert": "49eb3350984bd2a162914d3776e70cfb", - "api_key_pending_invalidation": "16f515278a295f6245149ad7c5ddedb7", - "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", - "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", - "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", - "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", - "background-session": "dfd06597e582fdbbbc09f1a3615e6ce0", - "canvas-element": "7390014e1091044523666d97247392fc", - "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", - "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", - "cases": "0b7746a97518ec67b787d141886ad3c1", - "cases-comments": "8a50736330e953bca91747723a319593", - "cases-configure": "387c5f3a3bda7e0ae0dd4e106f914a69", - "cases-connector-mappings": "6bc7e49411d38be4969dc6aa8bd43776", - "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", - "config": "c63748b75f39d0c54de12d12c1ccbc20", - "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", - "dashboard": "40554caf09725935e2c02e02563a2d07", - "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", - "endpoint:user-artifact-manifest": "a0d7b04ad405eed54d76e279c3727862", - "enterprise_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "epm-packages": "0cbbb16506734d341a96aaed65ec6413", - "epm-packages-assets": "44621b2f6052ef966da47b7c3a00f33b", - "exception-list": "67f055ab8c10abd7b2ebfd969b836788", - "exception-list-agnostic": "67f055ab8c10abd7b2ebfd969b836788", - "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", - "fleet-agent-actions": "9511b565b1cc6441a42033db3d5de8e9", - "fleet-agent-events": "e20a508b6e805189356be381dbfac8db", - "fleet-agents": "cb661e8ede2b640c42c8e5ef99db0683", - "fleet-enrollment-api-keys": "a69ef7ae661dab31561d6c6f052ef2a7", - "graph-workspace": "27a94b2edcb0610c6aea54a7c56d7752", - "index-pattern": "45915a1ad866812242df474eb0479052", - "infrastructure-ui-source": "3d1b76c39bfb2cc8296b024d73854724", - "ingest-agent-policies": "8b0733cce189659593659dad8db426f0", - "ingest-outputs": "8854f34453a47e26f86a29f8f3b80b4e", - "ingest-package-policies": "c91ca97b1ff700f0fc64dc6b13d65a85", - "ingest_manager_settings": "02a03095f0e05b7a538fa801b88a217f", - "inventory-view": "3d1b76c39bfb2cc8296b024d73854724", - "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", - "lens": "52346cfec69ff7b47d5f0c12361a2797", - "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", - "map": "9134b47593116d7953f6adba096fc463", - "maps-telemetry": "5ef305b18111b77789afefbd36b66171", - "metrics-explorer-view": "3d1b76c39bfb2cc8296b024d73854724", - "migrationVersion": "4a1746014a75ade3a714e1db5763276f", - "ml-job": "3bb64c31915acf93fc724af137a0891b", - "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", - "monitoring-telemetry": "2669d5ec15e82391cf58df4294ee9c68", - "namespace": "2f4316de49999235636386fe51dc06c1", - "namespaces": "2f4316de49999235636386fe51dc06c1", - "originId": "2f4316de49999235636386fe51dc06c1", - "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", - "references": "7997cf5a56cc02bdc9c93361bde732b0", - "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", - "search": "959dde12a55b3118eab009d8b2b72ad6", - "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", - "security-solution-signals-migration": "72761fd374ca11122ac8025a92b84fca", - "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", - "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", - "siem-ui-timeline": "d12c5474364d737d17252acf1dc4585c", - "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", - "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", - "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", - "spaces-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", - "tag": "83d55da58f6530f7055415717ec06474", - "telemetry": "36a616f7026dfa617d6655df850fe16d", - "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", - "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", - "type": "2f4316de49999235636386fe51dc06c1", - "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3", - "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", - "updated_at": "00da57df13e94e9d98437d13ace4bfe0", - "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", - "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", - "uptime-dynamic-settings": "3d1b76c39bfb2cc8296b024d73854724", - "url": "c7f66a0df8b1b52f17c28c4adb111105", - "visualization": "f819cf6636b75c9e76ba733a0c6ef355", - "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" - } - }, - "dynamic": "strict", - "properties": { - "action": { - "properties": { - "actionTypeId": { - "type": "keyword" - }, - "config": { - "enabled": false, - "type": "object" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "secrets": { - "type": "binary" - } - } - }, - "action_task_params": { - "properties": { - "actionId": { - "type": "keyword" - }, - "apiKey": { - "type": "binary" - }, - "params": { - "enabled": false, - "type": "object" - } - } - }, - "agent_actions": { - "dynamic": "false", - "type": "object" - }, - "agent_configs": { - "dynamic": "false", - "type": "object" - }, - "agent_events": { - "dynamic": "false", - "type": "object" - }, - "agents": { - "dynamic": "false", - "type": "object" - }, - "alert": { - "properties": { - "actions": { - "properties": { - "actionRef": { - "type": "keyword" - }, - "actionTypeId": { - "type": "keyword" - }, - "group": { - "type": "keyword" - }, - "params": { - "enabled": false, - "type": "object" - } - }, - "type": "nested" - }, - "alertTypeId": { - "type": "keyword" - }, - "apiKey": { - "type": "binary" - }, - "apiKeyOwner": { - "type": "keyword" - }, - "consumer": { - "type": "keyword" - }, - "createdAt": { - "type": "date" - }, - "createdBy": { - "type": "keyword" - }, - "enabled": { - "type": "boolean" - }, - "executionStatus": { - "properties": { - "error": { - "properties": { - "message": { - "type": "keyword" - }, - "reason": { - "type": "keyword" - } - } - }, - "lastExecutionDate": { - "type": "date" - }, - "status": { - "type": "keyword" - } - } - }, - "meta": { - "properties": { - "versionApiKeyLastmodified": { - "type": "keyword" - } - } - }, - "muteAll": { - "type": "boolean" - }, - "mutedInstanceIds": { - "type": "keyword" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "notifyWhen": { - "type": "keyword" - }, - "params": { - "enabled": false, - "type": "object" - }, - "schedule": { - "properties": { - "interval": { - "type": "keyword" - } - } - }, - "scheduledTaskId": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "throttle": { - "type": "keyword" - }, - "updatedAt": { - "type": "date" - }, - "updatedBy": { - "type": "keyword" - } - } - }, - "api_key_pending_invalidation": { - "properties": { - "apiKeyId": { - "type": "keyword" - }, - "createdAt": { - "type": "date" - } - } - }, - "apm-indices": { - "properties": { - "apm_oss": { - "properties": { - "errorIndices": { - "type": "keyword" - }, - "metricsIndices": { - "type": "keyword" - }, - "onboardingIndices": { - "type": "keyword" - }, - "sourcemapIndices": { - "type": "keyword" - }, - "spanIndices": { - "type": "keyword" - }, - "transactionIndices": { - "type": "keyword" - } - } - } - } - }, - "apm-services-telemetry": { - "dynamic": "false", - "type": "object" - }, - "apm-telemetry": { - "dynamic": "false", - "type": "object" - }, - "app_search_telemetry": { - "dynamic": "false", - "type": "object" - }, - "application_usage_daily": { - "dynamic": "false", - "properties": { - "timestamp": { - "type": "date" - } - } - }, - "application_usage_totals": { - "dynamic": "false", - "type": "object" - }, - "application_usage_transactional": { - "dynamic": "false", - "type": "object" - }, - "background-session": { - "properties": { - "appId": { - "type": "keyword" - }, - "created": { - "type": "date" - }, - "expires": { - "type": "date" - }, - "idMapping": { - "enabled": false, - "type": "object" - }, - "initialState": { - "enabled": false, - "type": "object" - }, - "name": { - "type": "keyword" - }, - "restoreState": { - "enabled": false, - "type": "object" - }, - "sessionId": { - "type": "keyword" - }, - "status": { - "type": "keyword" - }, - "urlGeneratorId": { - "type": "keyword" - } - } - }, - "canvas-element": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "content": { - "type": "text" - }, - "help": { - "type": "text" - }, - "image": { - "type": "text" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "canvas-workpad": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "canvas-workpad-template": { - "dynamic": "false", - "properties": { - "help": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "tags": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "template_key": { - "type": "keyword" - } - } - }, - "cases": { - "properties": { - "closed_at": { - "type": "date" - }, - "closed_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "connector": { - "properties": { - "fields": { - "properties": { - "key": { - "type": "text" - }, - "value": { - "type": "text" - } - } - }, - "id": { - "type": "keyword" - }, - "name": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "created_at": { - "type": "date" - }, - "created_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "description": { - "type": "text" - }, - "external_service": { - "properties": { - "connector_id": { - "type": "keyword" - }, - "connector_name": { - "type": "keyword" - }, - "external_id": { - "type": "keyword" - }, - "external_title": { - "type": "text" - }, - "external_url": { - "type": "text" - }, - "pushed_at": { - "type": "date" - }, - "pushed_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - } - } - }, - "settings": { - "properties": { - "syncAlerts": { - "type": "boolean" - } - } - }, - "status": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "title": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - } - } - }, - "cases-comments": { - "properties": { - "alertId": { - "type": "keyword" - }, - "comment": { - "type": "text" - }, - "created_at": { - "type": "date" - }, - "created_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "index": { - "type": "keyword" - }, - "pushed_at": { - "type": "date" - }, - "pushed_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "type": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - } - } - }, - "cases-configure": { - "properties": { - "closure_type": { - "type": "keyword" - }, - "connector": { - "properties": { - "fields": { - "properties": { - "key": { - "type": "text" - }, - "value": { - "type": "text" - } - } - }, - "id": { - "type": "keyword" - }, - "name": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "created_at": { - "type": "date" - }, - "created_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - } - } - }, - "cases-connector-mappings": { - "properties": { - "mappings": { - "properties": { - "action_type": { - "type": "keyword" - }, - "source": { - "type": "keyword" - }, - "target": { - "type": "keyword" - } - } - } - } - }, - "cases-user-actions": { - "properties": { - "action": { - "type": "keyword" - }, - "action_at": { - "type": "date" - }, - "action_by": { - "properties": { - "email": { - "type": "keyword" - }, - "full_name": { - "type": "keyword" - }, - "username": { - "type": "keyword" - } - } - }, - "action_field": { - "type": "keyword" - }, - "new_value": { - "type": "text" - }, - "old_value": { - "type": "text" - } - } - }, - "config": { - "dynamic": "false", - "properties": { - "buildNum": { - "type": "keyword" - } - } - }, - "core-usage-stats": { - "dynamic": "false", - "type": "object" - }, - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "doc_values": false, - "index": false, - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "index": false, - "type": "text" - } - } - }, - "optionsJSON": { - "index": false, - "type": "text" - }, - "panelsJSON": { - "index": false, - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "pause": { - "doc_values": false, - "index": false, - "type": "boolean" - }, - "section": { - "doc_values": false, - "index": false, - "type": "integer" - }, - "value": { - "doc_values": false, - "index": false, - "type": "integer" - } - } - }, - "timeFrom": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "timeRestore": { - "doc_values": false, - "index": false, - "type": "boolean" - }, - "timeTo": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "datasources": { - "dynamic": "false", - "type": "object" - }, - "endpoint:user-artifact": { - "properties": { - "body": { - "type": "binary" - }, - "compressionAlgorithm": { - "index": false, - "type": "keyword" - }, - "created": { - "index": false, - "type": "date" - }, - "decodedSha256": { - "index": false, - "type": "keyword" - }, - "decodedSize": { - "index": false, - "type": "long" - }, - "encodedSha256": { - "type": "keyword" - }, - "encodedSize": { - "index": false, - "type": "long" - }, - "encryptionAlgorithm": { - "index": false, - "type": "keyword" - }, - "identifier": { - "type": "keyword" - } - } - }, - "endpoint:user-artifact-manifest": { - "properties": { - "created": { - "index": false, - "type": "date" - }, - "schemaVersion": { - "type": "keyword" - }, - "semanticVersion": { - "index": false, - "type": "keyword" - }, - "artifacts": { - "type": "nested", - "properties": { - "policyId": { - "type": "keyword", - "index": false - }, - "artifactId": { - "type": "keyword", - "index": false - } - } - } - } - }, - "enrollment_api_keys": { - "dynamic": "false", - "type": "object" - }, - "enterprise_search_telemetry": { - "dynamic": "false", - "type": "object" - }, - "epm-package": { - "dynamic": "false", - "type": "object" - }, - "epm-packages": { - "properties": { - "es_index_patterns": { - "enabled": false, - "type": "object" - }, - "install_source": { - "type": "keyword" - }, - "install_started_at": { - "type": "date" - }, - "install_status": { - "type": "keyword" - }, - "install_version": { - "type": "keyword" - }, - "installed_es": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "installed_kibana": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "internal": { - "type": "boolean" - }, - "name": { - "type": "keyword" - }, - "package_assets": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "removable": { - "type": "boolean" - }, - "version": { - "type": "keyword" - } - } - }, - "epm-packages-assets": { - "properties": { - "asset_path": { - "type": "keyword" - }, - "data_base64": { - "type": "binary" - }, - "data_utf8": { - "index": false, - "type": "text" - }, - "install_source": { - "type": "keyword" - }, - "media_type": { - "type": "keyword" - }, - "package_name": { - "type": "keyword" - }, - "package_version": { - "type": "keyword" - } - } - }, - "exception-list": { - "properties": { - "_tags": { - "type": "keyword" - }, - "comments": { - "properties": { - "comment": { - "type": "keyword" - }, - "created_at": { - "type": "keyword" - }, - "created_by": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "updated_at": { - "type": "keyword" - }, - "updated_by": { - "type": "keyword" - } - } - }, - "created_at": { - "type": "keyword" - }, - "created_by": { - "type": "keyword" - }, - "description": { - "type": "keyword" - }, - "entries": { - "properties": { - "entries": { - "properties": { - "field": { - "type": "keyword" - }, - "operator": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "value": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" - } - } - }, - "field": { - "type": "keyword" - }, - "list": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "operator": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "value": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" - } - } - }, - "immutable": { - "type": "boolean" - }, - "item_id": { - "type": "keyword" - }, - "list_id": { - "type": "keyword" - }, - "list_type": { - "type": "keyword" - }, - "meta": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "os_types": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "tie_breaker_id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "updated_by": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "exception-list-agnostic": { - "properties": { - "_tags": { - "type": "keyword" - }, - "comments": { - "properties": { - "comment": { - "type": "keyword" - }, - "created_at": { - "type": "keyword" - }, - "created_by": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "updated_at": { - "type": "keyword" - }, - "updated_by": { - "type": "keyword" - } - } - }, - "created_at": { - "type": "keyword" - }, - "created_by": { - "type": "keyword" - }, - "description": { - "type": "keyword" - }, - "entries": { - "properties": { - "entries": { - "properties": { - "field": { - "type": "keyword" - }, - "operator": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "value": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" - } - } - }, - "field": { - "type": "keyword" - }, - "list": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "operator": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "value": { - "fields": { - "text": { - "type": "text" - } - }, - "type": "keyword" - } - } - }, - "immutable": { - "type": "boolean" - }, - "item_id": { - "type": "keyword" - }, - "list_id": { - "type": "keyword" - }, - "list_type": { - "type": "keyword" - }, - "meta": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "os_types": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "tie_breaker_id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "updated_by": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "file-upload-telemetry": { - "properties": { - "filesUploadedTotalCount": { - "type": "long" - } - } - }, - "fleet-agent-actions": { - "properties": { - "ack_data": { - "type": "text" - }, - "agent_id": { - "type": "keyword" - }, - "created_at": { - "type": "date" - }, - "data": { - "type": "binary" - }, - "policy_id": { - "type": "keyword" - }, - "policy_revision": { - "type": "integer" - }, - "sent_at": { - "type": "date" - }, - "type": { - "type": "keyword" - } - } - }, - "fleet-agent-events": { - "properties": { - "action_id": { - "type": "keyword" - }, - "agent_id": { - "type": "keyword" - }, - "data": { - "type": "text" - }, - "message": { - "type": "text" - }, - "payload": { - "type": "text" - }, - "policy_id": { - "type": "keyword" - }, - "stream_id": { - "type": "keyword" - }, - "subtype": { - "type": "keyword" - }, - "timestamp": { - "type": "date" - }, - "type": { - "type": "keyword" - } - } - }, - "fleet-agents": { - "properties": { - "access_api_key_id": { - "type": "keyword" - }, - "active": { - "type": "boolean" - }, - "current_error_events": { - "index": false, - "type": "text" - }, - "default_api_key": { - "type": "binary" - }, - "default_api_key_id": { - "type": "keyword" - }, - "enrolled_at": { - "type": "date" - }, - "last_checkin": { - "type": "date" - }, - "last_checkin_status": { - "type": "keyword" - }, - "last_updated": { - "type": "date" - }, - "local_metadata": { - "type": "flattened" - }, - "packages": { - "type": "keyword" - }, - "policy_id": { - "type": "keyword" - }, - "policy_revision": { - "type": "integer" - }, - "shared_id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "unenrolled_at": { - "type": "date" - }, - "unenrollment_started_at": { - "type": "date" - }, - "updated_at": { - "type": "date" - }, - "upgrade_started_at": { - "type": "date" - }, - "upgraded_at": { - "type": "date" - }, - "user_provided_metadata": { - "type": "flattened" - }, - "version": { - "type": "keyword" - } - } - }, - "fleet-enrollment-api-keys": { - "properties": { - "active": { - "type": "boolean" - }, - "api_key": { - "type": "binary" - }, - "api_key_id": { - "type": "keyword" - }, - "created_at": { - "type": "date" - }, - "expire_at": { - "type": "date" - }, - "name": { - "type": "keyword" - }, - "policy_id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - } - } - }, - "graph-workspace": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "legacyIndexPatternRef": { - "index": false, - "type": "text" - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "index-pattern": { - "dynamic": "false", - "properties": { - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "infrastructure-ui-source": { - "dynamic": "false", - "type": "object" - }, - "ingest-agent-policies": { - "properties": { - "description": { - "type": "text" - }, - "is_default": { - "type": "boolean" - }, - "monitoring_enabled": { - "index": false, - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "namespace": { - "type": "keyword" - }, - "package_policies": { - "type": "keyword" - }, - "revision": { - "type": "integer" - }, - "status": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "type": "keyword" - } - } - }, - "ingest-outputs": { - "properties": { - "ca_sha256": { - "index": false, - "type": "keyword" - }, - "config": { - "type": "flattened" - }, - "config_yaml": { - "type": "text" - }, - "fleet_enroll_password": { - "type": "binary" - }, - "fleet_enroll_username": { - "type": "binary" - }, - "hosts": { - "type": "keyword" - }, - "is_default": { - "type": "boolean" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "ingest-package-policies": { - "properties": { - "created_at": { - "type": "date" - }, - "created_by": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "enabled": { - "type": "boolean" - }, - "inputs": { - "enabled": false, - "properties": { - "compiled_input": { - "type": "flattened" - }, - "config": { - "type": "flattened" - }, - "enabled": { - "type": "boolean" - }, - "streams": { - "properties": { - "compiled_stream": { - "type": "flattened" - }, - "config": { - "type": "flattened" - }, - "data_stream": { - "properties": { - "dataset": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "enabled": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "vars": { - "type": "flattened" - } - }, - "type": "nested" - }, - "type": { - "type": "keyword" - }, - "vars": { - "type": "flattened" - } - }, - "type": "nested" - }, - "name": { - "type": "keyword" - }, - "namespace": { - "type": "keyword" - }, - "output_id": { - "type": "keyword" - }, - "package": { - "properties": { - "name": { - "type": "keyword" - }, - "title": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "policy_id": { - "type": "keyword" - }, - "revision": { - "type": "integer" - }, - "updated_at": { - "type": "date" - }, - "updated_by": { - "type": "keyword" - } - } - }, - "ingest_manager_settings": { - "properties": { - "agent_auto_upgrade": { - "type": "keyword" - }, - "has_seen_add_data_notice": { - "index": false, - "type": "boolean" - }, - "kibana_ca_sha256": { - "type": "keyword" - }, - "kibana_urls": { - "type": "keyword" - }, - "package_auto_upgrade": { - "type": "keyword" - } - } - }, - "inventory-view": { - "dynamic": "false", - "type": "object" - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } - }, - "lens": { - "properties": { - "description": { - "type": "text" - }, - "expression": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "state": { - "type": "flattened" - }, - "title": { - "type": "text" - }, - "visualizationType": { - "type": "keyword" - } - } - }, - "lens-ui-telemetry": { - "properties": { - "count": { - "type": "integer" - }, - "date": { - "type": "date" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "map": { - "properties": { - "bounds": { - "dynamic": "false", - "type": "object" - }, - "description": { - "type": "text" - }, - "layerListJSON": { - "type": "text" - }, - "mapStateJSON": { - "type": "text" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "maps-telemetry": { - "enabled": false, - "type": "object" - }, - "metrics-explorer-view": { - "dynamic": "false", - "type": "object" - }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "config": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "dashboard": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "index-pattern": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "search": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "visualization": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "ml-job": { - "properties": { - "datafeed_id": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "job_id": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - }, - "type": { - "type": "keyword" - } - } - }, - "ml-telemetry": { - "properties": { - "file_data_visualizer": { - "properties": { - "index_creation_count": { - "type": "long" - } - } - } - } - }, - "monitoring-telemetry": { - "properties": { - "reportedClusterUuids": { - "type": "keyword" - } - } - }, - "namespace": { - "type": "keyword" - }, - "namespaces": { - "type": "keyword" - }, - "originId": { - "type": "keyword" - }, - "outputs": { - "dynamic": "false", - "type": "object" - }, - "query": { - "properties": { - "description": { - "type": "text" - }, - "filters": { - "enabled": false, - "type": "object" - }, - "query": { - "properties": { - "language": { - "type": "keyword" - }, - "query": { - "index": false, - "type": "keyword" - } - } - }, - "timefilter": { - "enabled": false, - "type": "object" - }, - "title": { - "type": "text" - } - } - }, - "references": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "sample-data-telemetry": { - "properties": { - "installCount": { - "type": "long" - }, - "unInstallCount": { - "type": "long" - } - } - }, - "search": { - "properties": { - "columns": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "description": { - "type": "text" - }, - "grid": { - "enabled": false, - "type": "object" - }, - "hits": { - "doc_values": false, - "index": false, - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "index": false, - "type": "text" - } - } - }, - "sort": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "search-telemetry": { - "dynamic": "false", - "type": "object" - }, - "security-solution-signals-migration": { - "properties": { - "created": { - "index": false, - "type": "date" - }, - "createdBy": { - "index": false, - "type": "text" - }, - "destinationIndex": { - "index": false, - "type": "keyword" - }, - "error": { - "index": false, - "type": "text" - }, - "sourceIndex": { - "type": "keyword" - }, - "status": { - "index": false, - "type": "keyword" - }, - "taskId": { - "index": false, - "type": "keyword" - }, - "updated": { - "index": false, - "type": "date" - }, - "updatedBy": { - "index": false, - "type": "text" - }, - "version": { - "type": "long" - } - } - }, - "server": { - "dynamic": "false", - "type": "object" - }, - "siem-detection-engine-rule-actions": { - "properties": { - "actions": { - "properties": { - "action_type_id": { - "type": "keyword" - }, - "group": { - "type": "keyword" - }, - "id": { - "type": "keyword" - }, - "params": { - "enabled": false, - "type": "object" - } - } - }, - "alertThrottle": { - "type": "keyword" - }, - "ruleAlertId": { - "type": "keyword" - }, - "ruleThrottle": { - "type": "keyword" - } - } - }, - "siem-detection-engine-rule-status": { - "properties": { - "alertId": { - "type": "keyword" - }, - "bulkCreateTimeDurations": { - "type": "float" - }, - "gap": { - "type": "text" - }, - "lastFailureAt": { - "type": "date" - }, - "lastFailureMessage": { - "type": "text" - }, - "lastLookBackDate": { - "type": "date" - }, - "lastSuccessAt": { - "type": "date" - }, - "lastSuccessMessage": { - "type": "text" - }, - "searchAfterTimeDurations": { - "type": "float" - }, - "status": { - "type": "keyword" - }, - "statusDate": { - "type": "date" - } - } - }, - "siem-ui-timeline": { - "properties": { - "columns": { - "properties": { - "aggregatable": { - "type": "boolean" - }, - "category": { - "type": "keyword" - }, - "columnHeaderType": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "example": { - "type": "text" - }, - "id": { - "type": "keyword" - }, - "indexes": { - "type": "keyword" - }, - "name": { - "type": "text" - }, - "placeholder": { - "type": "text" - }, - "searchable": { - "type": "boolean" - }, - "type": { - "type": "keyword" - } - } - }, - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "dataProviders": { - "properties": { - "and": { - "properties": { - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "kqlQuery": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queryMatch": { - "properties": { - "displayField": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "field": { - "type": "text" - }, - "operator": { - "type": "text" - }, - "value": { - "type": "text" - } - } - }, - "type": { - "type": "text" - } - } - }, - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "kqlQuery": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queryMatch": { - "properties": { - "displayField": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "field": { - "type": "text" - }, - "operator": { - "type": "text" - }, - "value": { - "type": "text" - } - } - }, - "type": { - "type": "text" - } - } - }, - "dateRange": { - "properties": { - "end": { - "type": "date" - }, - "start": { - "type": "date" - } - } - }, - "description": { - "type": "text" - }, - "eventType": { - "type": "keyword" - }, - "excludedRowRendererIds": { - "type": "text" - }, - "favorite": { - "properties": { - "favoriteDate": { - "type": "date" - }, - "fullName": { - "type": "text" - }, - "keySearch": { - "type": "text" - }, - "userName": { - "type": "text" - } - } - }, - "filters": { - "properties": { - "exists": { - "type": "text" - }, - "match_all": { - "type": "text" - }, - "meta": { - "properties": { - "alias": { - "type": "text" - }, - "controlledBy": { - "type": "text" - }, - "disabled": { - "type": "boolean" - }, - "field": { - "type": "text" - }, - "formattedValue": { - "type": "text" - }, - "index": { - "type": "keyword" - }, - "key": { - "type": "keyword" - }, - "negate": { - "type": "boolean" - }, - "params": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "value": { - "type": "text" - } - } - }, - "missing": { - "type": "text" - }, - "query": { - "type": "text" - }, - "range": { - "type": "text" - }, - "script": { - "type": "text" - } - } - }, - "indexNames": { - "type": "text" - }, - "kqlMode": { - "type": "keyword" - }, - "kqlQuery": { - "properties": { - "filterQuery": { - "properties": { - "kuery": { - "properties": { - "expression": { - "type": "text" - }, - "kind": { - "type": "keyword" - } - } - }, - "serializedQuery": { - "type": "text" - } - } - } - } - }, - "savedQueryId": { - "type": "keyword" - }, - "sort": { - "properties": { - "columnId": { - "type": "keyword" - }, - "sortDirection": { - "type": "keyword" - } - } - }, - "status": { - "type": "keyword" - }, - "templateTimelineId": { - "type": "text" - }, - "templateTimelineVersion": { - "type": "integer" - }, - "timelineType": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "siem-ui-timeline-note": { - "properties": { - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "eventId": { - "type": "keyword" - }, - "note": { - "type": "text" - }, - "timelineId": { - "type": "keyword" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "siem-ui-timeline-pinned-event": { - "properties": { - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "eventId": { - "type": "keyword" - }, - "timelineId": { - "type": "keyword" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "disabledFeatures": { - "type": "keyword" - }, - "imageUrl": { - "index": false, - "type": "text" - }, - "initials": { - "type": "keyword" - }, - "name": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "spaces-usage-stats": { - "dynamic": "false", - "type": "object" - }, - "tag": { - "properties": { - "color": { - "type": "text" - }, - "description": { - "type": "text" - }, - "name": { - "type": "text" - } - } - }, - "telemetry": { - "properties": { - "allowChangingOptInStatus": { - "type": "boolean" - }, - "enabled": { - "type": "boolean" - }, - "lastReported": { - "type": "date" - }, - "lastVersionChecked": { - "type": "keyword" - }, - "reportFailureCount": { - "type": "integer" - }, - "reportFailureVersion": { - "type": "keyword" - }, - "sendUsageFrom": { - "type": "keyword" - }, - "userHasSeenNotice": { - "type": "boolean" - } - } - }, - "timelion-sheet": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "timelion_chart_height": { - "type": "integer" - }, - "timelion_columns": { - "type": "integer" - }, - "timelion_interval": { - "type": "keyword" - }, - "timelion_other_interval": { - "type": "keyword" - }, - "timelion_rows": { - "type": "integer" - }, - "timelion_sheet": { - "type": "text" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "tsvb-validation-telemetry": { - "properties": { - "failedRequests": { - "type": "long" - } - } - }, - "type": { - "type": "keyword" - }, - "ui-counter": { - "properties": { - "count": { - "type": "integer" - } - } - }, - "ui-metric": { - "properties": { - "count": { - "type": "integer" - } - } - }, - "updated_at": { - "type": "date" - }, - "upgrade-assistant-reindex-operation": { - "properties": { - "errorMessage": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "indexName": { - "type": "keyword" - }, - "lastCompletedStep": { - "type": "long" - }, - "locked": { - "type": "date" - }, - "newIndexName": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "reindexOptions": { - "properties": { - "openAndClose": { - "type": "boolean" - }, - "queueSettings": { - "properties": { - "queuedAt": { - "type": "long" - }, - "startedAt": { - "type": "long" - } - } - } - } - }, - "reindexTaskId": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "reindexTaskPercComplete": { - "type": "float" - }, - "runningReindexCount": { - "type": "integer" - }, - "status": { - "type": "integer" - } - } - }, - "upgrade-assistant-telemetry": { - "properties": { - "features": { - "properties": { - "deprecation_logging": { - "properties": { - "enabled": { - "null_value": true, - "type": "boolean" - } - } - } - } - }, - "ui_open": { - "properties": { - "cluster": { - "null_value": 0, - "type": "long" - }, - "indices": { - "null_value": 0, - "type": "long" - }, - "overview": { - "null_value": 0, - "type": "long" - } - } - }, - "ui_reindex": { - "properties": { - "close": { - "null_value": 0, - "type": "long" - }, - "open": { - "null_value": 0, - "type": "long" - }, - "start": { - "null_value": 0, - "type": "long" - }, - "stop": { - "null_value": 0, - "type": "long" - } - } - } - } - }, - "uptime-dynamic-settings": { - "dynamic": "false", - "type": "object" - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "index": false, - "type": "text" - } - } - }, - "savedSearchRefName": { - "doc_values": false, - "index": false, - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "index": false, - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "index": false, - "type": "text" - } - } - }, - "workplace_search_telemetry": { - "dynamic": "false", - "type": "object" - } - } - }, - "settings": { - "index": { - "auto_expand_replicas": "0-1", - "number_of_replicas": "0", - "number_of_shards": "1" - } - } - } -} diff --git a/x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json b/x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json new file mode 100644 index 0000000000000..568b2e17a9332 --- /dev/null +++ b/x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json @@ -0,0 +1,678 @@ +{ + "attributes": { + "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"category\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"category.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"category\"}}},{\"name\":\"currency\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"customer_birth_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"customer_first_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"customer_first_name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"customer_first_name\"}}},{\"name\":\"customer_full_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"customer_full_name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"customer_full_name\"}}},{\"name\":\"customer_gender\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"customer_id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"customer_last_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"customer_last_name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"customer_last_name\"}}},{\"name\":\"customer_phone\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"day_of_week\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"day_of_week_i\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"email\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.city_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.continent_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.country_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.location\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.region_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"manufacturer\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"manufacturer.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"manufacturer\"}}},{\"name\":\"order_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"order_id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products._id\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"products._id.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"products._id\"}}},{\"name\":\"products.base_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.base_unit_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.category\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"products.category.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"products.category\"}}},{\"name\":\"products.created_on\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.discount_amount\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.discount_percentage\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.manufacturer\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"products.manufacturer.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"products.manufacturer\"}}},{\"name\":\"products.min_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.product_id\",\"type\":\"number\",\"esTypes\":[\"long\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.product_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"products.product_name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"products.product_name\"}}},{\"name\":\"products.quantity\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.sku\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.tax_amount\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.taxful_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.taxless_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.unit_discount_amount\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"sku\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"taxful_total_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"taxless_total_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"total_quantity\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"total_unique_products\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "timeFieldName": "order_date", + "title": "ecommerce" + }, + "coreMigrationVersion": "8.0.0", + "id": "5193f870-d861-11e9-a311-0fa548c5f953", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2019-12-11T23:24:13.381Z", + "version": "WzE3LDJd" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "e-commerce area chart", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"type\":\"area\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"order_date\",\"timeRange\":{\"from\":\"2019-06-26T06:20:28.066Z\",\"to\":\"2019-06-26T07:27:58.573Z\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}],\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"title\":\"e-commerce area chart\"}" + }, + "coreMigrationVersion": "8.0.0", + "id": "0a464230-79f0-11ea-ae7f-13c5d6e410a0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "5193f870-d861-11e9-a311-0fa548c5f953", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2020-04-08T23:24:05.971Z", + "version": "WzIwLDJd" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\",\"filter\":[]}" + }, + "title": "Українська", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"type\":\"metric\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}],\"params\":{\"addTooltip\":true,\"addLegend\":false,\"type\":\"metric\",\"metric\":{\"percentageMode\":false,\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"metricColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"labels\":{\"show\":true},\"invertColors\":false,\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"title\":\"Українська\"}" + }, + "coreMigrationVersion": "8.0.0", + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "5193f870-d861-11e9-a311-0fa548c5f953", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2020-04-10T00:34:44.700Z", + "version": "WzIzLDJd" +} + +{ + "attributes": { + "columns": [ + "order_date", + "category", + "currency", + "customer_id", + "order_id", + "day_of_week_i", + "products.created_on", + "sku" + ], + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "sort": [ + [ + "order_date", + "desc" + ] + ], + "title": "Ecommerce Data", + "version": 1 + }, + "coreMigrationVersion": "8.0.0", + "id": "6091ead0-1c6d-11ea-a100-8589bb9d7c6b", + "migrationVersion": { + "search": "7.9.3" + }, + "references": [ + { + "id": "5193f870-d861-11e9-a311-0fa548c5f953", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "search", + "updated_at": "2019-12-11T23:24:28.540Z", + "version": "WzE4LDJd" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "savedSearchRefName": "search_0", + "title": "Tag Cloud of Names", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Tag Cloud of Names\",\"type\":\"tagcloud\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"customer_first_name.keyword\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":10,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"segment\"}],\"params\":{\"scale\":\"linear\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":72,\"showLabel\":true,\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"}}}" + }, + "coreMigrationVersion": "8.0.0", + "id": "1bba55f0-507e-11eb-9c0d-97106882b997", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "6091ead0-1c6d-11ea-a100-8589bb9d7c6b", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization", + "updated_at": "2021-01-07T00:23:04.624Z", + "version": "WzI3LDJd" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "e-commerce pie chart", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"type\":\"pie\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"order_date\",\"timeRange\":{\"from\":\"2019-06-26T06:20:28.066Z\",\"to\":\"2019-06-26T07:27:58.573Z\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}],\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"palette\":{\"type\":\"palette\",\"name\":\"kibana_palette\"},\"distinctColors\":true},\"title\":\"e-commerce pie chart\"}" + }, + "coreMigrationVersion": "8.0.0", + "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "5193f870-d861-11e9-a311-0fa548c5f953", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2020-04-08T23:24:42.460Z", + "version": "WzIxLDJd" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "title": "Tiểu thuyết", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"type\":\"markdown\",\"aggs\":[],\"params\":{\"fontSize\":12,\"openLinksInNewTab\":false,\"markdown\":\"Tiểu thuyết là một thể loại văn xuôi có hư cấu, thông qua nhân vật, hoàn cảnh, sự việc để phản ánh bức tranh xã hội rộng lớn và những vấn đề của cuộc sống con người, biểu hiện tính chất tường thuật, tính chất kể chuyện bằng ngôn ngữ văn xuôi theo những chủ đề xác định.\\n\\nTrong một cách hiểu khác, nhận định của Belinski: \\\"tiểu thuyết là sử thi của đời tư\\\" chỉ ra khái quát nhất về một dạng thức tự sự, trong đó sự trần thuật tập trung vào số phận của một cá nhân trong quá trình hình thành và phát triển của nó. Sự trần thuật ở đây được khai triển trong không gian và thời gian nghệ thuật đến mức đủ để truyền đạt cơ cấu của nhân cách[1].\\n\\n\\n[1]^ Mục từ Tiểu thuyết trong cuốn 150 thuật ngữ văn học, Lại Nguyên Ân biên soạn, Nhà xuất bản Đại học Quốc gia Hà Nội, in lần thứ 2 có sửa đổi bổ sung. H. 2003. Trang 326.\"},\"title\":\"Tiểu thuyết\"}" + }, + "coreMigrationVersion": "8.0.0", + "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [], + "type": "visualization", + "updated_at": "2020-04-10T00:36:17.053Z", + "version": "WzI0LDJd" +} + +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\",\"filter\":[]}" + }, + "title": "게이지", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"type\":\"gauge\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}],\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"alignment\":\"automatic\",\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":50},{\"from\":50,\"to\":75},{\"from\":75,\"to\":100}],\"invertColors\":false,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":true,\"labels\":false,\"color\":\"rgba(105,112,125,0.2)\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"rgba(105,112,125,0.2)\",\"bgColor\":true,\"subText\":\"\",\"fontSize\":60}}},\"title\":\"게이지\"}" + }, + "coreMigrationVersion": "8.0.0", + "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "migrationVersion": { + "visualization": "7.14.0" + }, + "references": [ + { + "id": "5193f870-d861-11e9-a311-0fa548c5f953", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2020-04-10T00:33:44.909Z", + "version": "WzIyLDJd" +} + +{ + "attributes": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}" + }, + "optionsJSON": "{\"hidePanelTitles\":false,\"useMargins\":true}", + "panelsJSON": "[{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1c12c2f2-80c2-4d5c-b722-55b2415006e1\"},\"panelIndex\":\"1c12c2f2-80c2-4d5c-b722-55b2415006e1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1c4b99e1-7785-444f-a1c5-f592893b1a96\"},\"panelIndex\":\"1c4b99e1-7785-444f-a1c5-f592893b1a96\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":35,\"w\":48,\"h\":18,\"i\":\"94eab06f-60ac-4a85-b771-3a8ed475c9bb\"},\"panelIndex\":\"94eab06f-60ac-4a85-b771-3a8ed475c9bb\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":15,\"w\":48,\"h\":8,\"i\":\"52c19b6b-7117-42ac-a74e-c507a1c3ffc0\"},\"panelIndex\":\"52c19b6b-7117-42ac-a74e-c507a1c3ffc0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":23,\"w\":16,\"h\":12,\"i\":\"a1e889dc-b80e-4937-a576-979f34d1859b\"},\"panelIndex\":\"a1e889dc-b80e-4937-a576-979f34d1859b\",\"embeddableConfig\":{\"enhancements\":{},\"vis\":null},\"panelRefName\":\"panel_4\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":16,\"y\":23,\"w\":12,\"h\":12,\"i\":\"4930b035-d756-4cc5-9a18-1af9e67d6f31\"},\"panelIndex\":\"4930b035-d756-4cc5-9a18-1af9e67d6f31\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":28,\"y\":23,\"w\":20,\"h\":12,\"i\":\"55112375-d6f0-44f7-a8fb-867c8f7d464d\"},\"panelIndex\":\"55112375-d6f0-44f7-a8fb-867c8f7d464d\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6\"}]", + "refreshInterval": { + "pause": true, + "value": 0 + }, + "timeFrom": "2019-03-23T03:06:17.785Z", + "timeRestore": true, + "timeTo": "2019-10-04T02:33:16.708Z", + "title": "Ecom Dashboard", + "version": 1 + }, + "coreMigrationVersion": "8.0.0", + "id": "6c263e00-1c6d-11ea-a100-8589bb9d7c6b", + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + { + "id": "0a464230-79f0-11ea-ae7f-13c5d6e410a0", + "name": "panel_0", + "type": "visualization" + }, + { + "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", + "name": "panel_1", + "type": "visualization" + }, + { + "id": "6091ead0-1c6d-11ea-a100-8589bb9d7c6b", + "name": "panel_2", + "type": "search" + }, + { + "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "panel_3", + "type": "visualization" + }, + { + "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "name": "panel_4", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "panel_5", + "type": "visualization" + }, + { + "id": "1bba55f0-507e-11eb-9c0d-97106882b997", + "name": "panel_6", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2021-01-07T00:22:16.102Z", + "version": "WzI2LDJd" +} + +{ + "attributes": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}" + }, + "optionsJSON": "{\"hidePanelTitles\":true,\"useMargins\":true}", + "panelsJSON": "[{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1c12c2f2-80c2-4d5c-b722-55b2415006e1\"},\"panelIndex\":\"1c12c2f2-80c2-4d5c-b722-55b2415006e1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1c4b99e1-7785-444f-a1c5-f592893b1a96\"},\"panelIndex\":\"1c4b99e1-7785-444f-a1c5-f592893b1a96\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":15,\"w\":48,\"h\":18,\"i\":\"94eab06f-60ac-4a85-b771-3a8ed475c9bb\"},\"panelIndex\":\"94eab06f-60ac-4a85-b771-3a8ed475c9bb\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":33,\"w\":48,\"h\":8,\"i\":\"52c19b6b-7117-42ac-a74e-c507a1c3ffc0\"},\"panelIndex\":\"52c19b6b-7117-42ac-a74e-c507a1c3ffc0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":41,\"w\":11,\"h\":10,\"i\":\"a1e889dc-b80e-4937-a576-979f34d1859b\"},\"panelIndex\":\"a1e889dc-b80e-4937-a576-979f34d1859b\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":11,\"y\":41,\"w\":5,\"h\":10,\"i\":\"4930b035-d756-4cc5-9a18-1af9e67d6f31\"},\"panelIndex\":\"4930b035-d756-4cc5-9a18-1af9e67d6f31\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"}]", + "refreshInterval": { + "pause": true, + "value": 0 + }, + "timeFrom": "2019-06-26T06:20:28.066Z", + "timeRestore": true, + "timeTo": "2019-06-26T07:27:58.573Z", + "title": "Ecom Dashboard Hidden Panel Titles", + "version": 1 + }, + "coreMigrationVersion": "8.0.0", + "id": "constructed-sample-saved-object-id", + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + { + "id": "0a464230-79f0-11ea-ae7f-13c5d6e410a0", + "name": "panel_0", + "type": "visualization" + }, + { + "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", + "name": "panel_1", + "type": "visualization" + }, + { + "id": "6091ead0-1c6d-11ea-a100-8589bb9d7c6b", + "name": "panel_2", + "type": "search" + }, + { + "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "panel_3", + "type": "visualization" + }, + { + "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "name": "panel_4", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "panel_5", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2020-04-10T00:37:48.462Z", + "version": "WzE5LDJd" +} + +{ + "attributes": { + "columns": [ + "order_date", + "category", + "currency", + "customer_id", + "order_id", + "day_of_week_i", + "products.created_on", + "sku" + ], + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "sort": [ + [ + "order_date", + "desc" + ] + ], + "title": "Ecommerce Data (copy)", + "version": 1 + }, + "coreMigrationVersion": "8.0.0", + "id": "e5bfe380-ac3e-11eb-8f24-bffe9ba4af2b", + "migrationVersion": { + "search": "7.9.3" + }, + "references": [ + { + "id": "5193f870-d861-11e9-a311-0fa548c5f953", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "search", + "updated_at": "2021-05-03T18:39:30.751Z", + "version": "WzI4LDJd" +} + +{ + "attributes": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}" + }, + "optionsJSON": "{\"hidePanelTitles\":false,\"useMargins\":true}", + "panelsJSON": "[{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":9,\"y\":0,\"w\":24,\"h\":15,\"i\":\"914ac161-94d4-4d93-a287-c21fca46a974\"},\"panelIndex\":\"914ac161-94d4-4d93-a287-c21fca46a974\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_914ac161-94d4-4d93-a287-c21fca46a974\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":22,\"y\":15,\"w\":24,\"h\":15,\"i\":\"c4cec7d1-97e3-4101-adc4-c3f15102511c\"},\"panelIndex\":\"c4cec7d1-97e3-4101-adc4-c3f15102511c\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_c4cec7d1-97e3-4101-adc4-c3f15102511c\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":30,\"w\":24,\"h\":15,\"i\":\"09f7de68-0d07-4661-8fda-73ea8b577ac7\"},\"panelIndex\":\"09f7de68-0d07-4661-8fda-73ea8b577ac7\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_09f7de68-0d07-4661-8fda-73ea8b577ac7\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":45,\"w\":24,\"h\":15,\"i\":\"6c25ca6e-6aa1-4f06-9a96-e83ffd9f52e8\"},\"panelIndex\":\"6c25ca6e-6aa1-4f06-9a96-e83ffd9f52e8\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_6c25ca6e-6aa1-4f06-9a96-e83ffd9f52e8\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":60,\"w\":24,\"h\":15,\"i\":\"37764cf9-3c89-454a-bd7e-ae4c242dc624\"},\"panelIndex\":\"37764cf9-3c89-454a-bd7e-ae4c242dc624\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_37764cf9-3c89-454a-bd7e-ae4c242dc624\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":15,\"y\":75,\"w\":24,\"h\":15,\"i\":\"990422fd-a9cf-446f-ba2f-ea9178a7b2e0\"},\"panelIndex\":\"990422fd-a9cf-446f-ba2f-ea9178a7b2e0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_990422fd-a9cf-446f-ba2f-ea9178a7b2e0\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":90,\"w\":24,\"h\":15,\"i\":\"0cdc13ec-2775-4da9-9a47-1e833bb807eb\"},\"panelIndex\":\"0cdc13ec-2775-4da9-9a47-1e833bb807eb\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0cdc13ec-2775-4da9-9a47-1e833bb807eb\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":18,\"y\":105,\"w\":24,\"h\":15,\"i\":\"eee160de-5777-40c8-9c2c-e75f64bf208a\"},\"panelIndex\":\"eee160de-5777-40c8-9c2c-e75f64bf208a\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_eee160de-5777-40c8-9c2c-e75f64bf208a\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":120,\"w\":24,\"h\":15,\"i\":\"b36f6a97-5d3d-4fc4-b076-b3e514f8f7bb\"},\"panelIndex\":\"b36f6a97-5d3d-4fc4-b076-b3e514f8f7bb\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_b36f6a97-5d3d-4fc4-b076-b3e514f8f7bb\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":135,\"w\":24,\"h\":15,\"i\":\"2e72acbf-7ade-451e-a5e4-7414f12facf2\"},\"panelIndex\":\"2e72acbf-7ade-451e-a5e4-7414f12facf2\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2e72acbf-7ade-451e-a5e4-7414f12facf2\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":150,\"w\":24,\"h\":15,\"i\":\"4119e9b0-5d03-482d-9356-89bb62b6a851\"},\"panelIndex\":\"4119e9b0-5d03-482d-9356-89bb62b6a851\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4119e9b0-5d03-482d-9356-89bb62b6a851\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":165,\"w\":24,\"h\":15,\"i\":\"42b4a37c-8b04-4510-9f27-831355221b65\"},\"panelIndex\":\"42b4a37c-8b04-4510-9f27-831355221b65\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_42b4a37c-8b04-4510-9f27-831355221b65\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":14,\"y\":180,\"w\":24,\"h\":15,\"i\":\"dc676050-d752-4c3e-a1ae-73ef2f1bcdc6\"},\"panelIndex\":\"dc676050-d752-4c3e-a1ae-73ef2f1bcdc6\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_dc676050-d752-4c3e-a1ae-73ef2f1bcdc6\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":17,\"y\":195,\"w\":24,\"h\":15,\"i\":\"6602e0e0-9e66-4e0e-90c1-f66b9c3d2340\"},\"panelIndex\":\"6602e0e0-9e66-4e0e-90c1-f66b9c3d2340\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_6602e0e0-9e66-4e0e-90c1-f66b9c3d2340\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":210,\"w\":24,\"h\":15,\"i\":\"c9c65725-9b4d-4343-93db-7efa4a7a2d60\"},\"panelIndex\":\"c9c65725-9b4d-4343-93db-7efa4a7a2d60\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_c9c65725-9b4d-4343-93db-7efa4a7a2d60\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":225,\"w\":24,\"h\":15,\"i\":\"69141f9b-5c23-409d-9c96-7f94c243f79e\"},\"panelIndex\":\"69141f9b-5c23-409d-9c96-7f94c243f79e\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_69141f9b-5c23-409d-9c96-7f94c243f79e\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":17,\"y\":240,\"w\":24,\"h\":15,\"i\":\"6feeec2c-34ab-4844-8445-e417c8e0595b\"},\"panelIndex\":\"6feeec2c-34ab-4844-8445-e417c8e0595b\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6feeec2c-34ab-4844-8445-e417c8e0595b\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":15,\"y\":255,\"w\":24,\"h\":15,\"i\":\"985d9dc1-de44-4803-afad-f1d497d050a1\"},\"panelIndex\":\"985d9dc1-de44-4803-afad-f1d497d050a1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_985d9dc1-de44-4803-afad-f1d497d050a1\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":270,\"w\":24,\"h\":15,\"i\":\"d7ef9e23-d0dd-4c7c-90b3-f611bbfcd1b0\"},\"panelIndex\":\"d7ef9e23-d0dd-4c7c-90b3-f611bbfcd1b0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_d7ef9e23-d0dd-4c7c-90b3-f611bbfcd1b0\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":20,\"y\":285,\"w\":24,\"h\":15,\"i\":\"6b0768b1-0cd2-47f0-a639-b369e7318d44\"},\"panelIndex\":\"6b0768b1-0cd2-47f0-a639-b369e7318d44\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6b0768b1-0cd2-47f0-a639-b369e7318d44\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":20,\"y\":300,\"w\":24,\"h\":15,\"i\":\"c9cc2835-06a8-4448-b703-2d41a6692feb\"},\"panelIndex\":\"c9cc2835-06a8-4448-b703-2d41a6692feb\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_c9cc2835-06a8-4448-b703-2d41a6692feb\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":315,\"w\":24,\"h\":15,\"i\":\"af2a55b1-8b3d-478a-96b1-72e4f12585e4\"},\"panelIndex\":\"af2a55b1-8b3d-478a-96b1-72e4f12585e4\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_af2a55b1-8b3d-478a-96b1-72e4f12585e4\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":330,\"w\":24,\"h\":15,\"i\":\"ee92986a-adab-4d66-ad4e-a43a608f52f7\"},\"panelIndex\":\"ee92986a-adab-4d66-ad4e-a43a608f52f7\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_ee92986a-adab-4d66-ad4e-a43a608f52f7\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":22,\"y\":345,\"w\":24,\"h\":15,\"i\":\"3b4e1fd0-2acb-444a-b478-42d7bd10b96c\"},\"panelIndex\":\"3b4e1fd0-2acb-444a-b478-42d7bd10b96c\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3b4e1fd0-2acb-444a-b478-42d7bd10b96c\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":360,\"w\":24,\"h\":15,\"i\":\"04d7056d-88a4-4b00-b8f4-33f79f1b6f7a\"},\"panelIndex\":\"04d7056d-88a4-4b00-b8f4-33f79f1b6f7a\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_04d7056d-88a4-4b00-b8f4-33f79f1b6f7a\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":21,\"y\":375,\"w\":24,\"h\":15,\"i\":\"51122bae-427e-45a6-904e-6c821447cc46\"},\"panelIndex\":\"51122bae-427e-45a6-904e-6c821447cc46\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_51122bae-427e-45a6-904e-6c821447cc46\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":20,\"y\":390,\"w\":24,\"h\":15,\"i\":\"4efab22c-1892-4013-8406-5e5d924a8a21\"},\"panelIndex\":\"4efab22c-1892-4013-8406-5e5d924a8a21\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4efab22c-1892-4013-8406-5e5d924a8a21\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":405,\"w\":24,\"h\":15,\"i\":\"4c3c1b29-100e-474c-8290-9470684ae407\"},\"panelIndex\":\"4c3c1b29-100e-474c-8290-9470684ae407\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4c3c1b29-100e-474c-8290-9470684ae407\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":18,\"y\":420,\"w\":24,\"h\":15,\"i\":\"b4501df0-d759-4513-9e87-5dd8eefe4a4f\"},\"panelIndex\":\"b4501df0-d759-4513-9e87-5dd8eefe4a4f\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_b4501df0-d759-4513-9e87-5dd8eefe4a4f\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":20,\"y\":435,\"w\":24,\"h\":15,\"i\":\"4fbff0ec-b3a6-4ee7-8734-9b177c3e51c6\"},\"panelIndex\":\"4fbff0ec-b3a6-4ee7-8734-9b177c3e51c6\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_4fbff0ec-b3a6-4ee7-8734-9b177c3e51c6\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":450,\"w\":24,\"h\":15,\"i\":\"13d9982e-2745-44b1-af94-fa4b9f6761a9\"},\"panelIndex\":\"13d9982e-2745-44b1-af94-fa4b9f6761a9\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_13d9982e-2745-44b1-af94-fa4b9f6761a9\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":17,\"y\":465,\"w\":24,\"h\":15,\"i\":\"efa18320-9650-4bfe-9418-ac29b7979f70\"},\"panelIndex\":\"efa18320-9650-4bfe-9418-ac29b7979f70\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_efa18320-9650-4bfe-9418-ac29b7979f70\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":21,\"y\":480,\"w\":24,\"h\":15,\"i\":\"1f03bc70-0545-4a3a-bebc-ad477674b841\"},\"panelIndex\":\"1f03bc70-0545-4a3a-bebc-ad477674b841\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1f03bc70-0545-4a3a-bebc-ad477674b841\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":495,\"w\":24,\"h\":15,\"i\":\"d766ce3a-9ec5-4ead-8698-6a2e66e729bb\"},\"panelIndex\":\"d766ce3a-9ec5-4ead-8698-6a2e66e729bb\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_d766ce3a-9ec5-4ead-8698-6a2e66e729bb\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":19,\"y\":510,\"w\":24,\"h\":15,\"i\":\"de93deb0-6c16-45ae-8fae-de0b2e1c4ae0\"},\"panelIndex\":\"de93deb0-6c16-45ae-8fae-de0b2e1c4ae0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_de93deb0-6c16-45ae-8fae-de0b2e1c4ae0\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":525,\"w\":24,\"h\":15,\"i\":\"b93cc5e1-084a-42d9-9958-a3f569573d43\"},\"panelIndex\":\"b93cc5e1-084a-42d9-9958-a3f569573d43\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_b93cc5e1-084a-42d9-9958-a3f569573d43\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":20,\"y\":540,\"w\":24,\"h\":15,\"i\":\"0b6c380f-3536-4f03-8dbd-95c53be69263\"},\"panelIndex\":\"0b6c380f-3536-4f03-8dbd-95c53be69263\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0b6c380f-3536-4f03-8dbd-95c53be69263\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":23,\"y\":555,\"w\":24,\"h\":15,\"i\":\"5c68b67a-ac42-48b8-85de-2409aaa0cdc6\"},\"panelIndex\":\"5c68b67a-ac42-48b8-85de-2409aaa0cdc6\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5c68b67a-ac42-48b8-85de-2409aaa0cdc6\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":570,\"w\":24,\"h\":15,\"i\":\"098a69b8-c9a0-40c8-8703-62838e0ec4a9\"},\"panelIndex\":\"098a69b8-c9a0-40c8-8703-62838e0ec4a9\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_098a69b8-c9a0-40c8-8703-62838e0ec4a9\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":1,\"y\":585,\"w\":24,\"h\":15,\"i\":\"a0f4b9ce-2e36-4d22-8dd9-8988f1a3b883\"},\"panelIndex\":\"a0f4b9ce-2e36-4d22-8dd9-8988f1a3b883\",\"embeddableConfig\":{\"vis\":null,\"enhancements\":{}},\"panelRefName\":\"panel_a0f4b9ce-2e36-4d22-8dd9-8988f1a3b883\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":15,\"y\":600,\"w\":24,\"h\":15,\"i\":\"eb651411-ea02-4506-a674-f0125d0b2a4a\"},\"panelIndex\":\"eb651411-ea02-4506-a674-f0125d0b2a4a\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_eb651411-ea02-4506-a674-f0125d0b2a4a\"},{\"version\":\"8.0.0\",\"type\":\"search\",\"gridData\":{\"x\":0,\"y\":615,\"w\":48,\"h\":111,\"i\":\"8ec9b67a-5d08-4006-bccc-a7341b88bb63\"},\"panelIndex\":\"8ec9b67a-5d08-4006-bccc-a7341b88bb63\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_8ec9b67a-5d08-4006-bccc-a7341b88bb63\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":4,\"y\":852,\"w\":24,\"h\":15,\"i\":\"1201144d-5c9c-4015-89a3-0cb803405986\"},\"panelIndex\":\"1201144d-5c9c-4015-89a3-0cb803405986\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1201144d-5c9c-4015-89a3-0cb803405986\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":837,\"w\":24,\"h\":15,\"i\":\"913c1c46-ded4-4e04-81ff-e683f725d3a5\"},\"panelIndex\":\"913c1c46-ded4-4e04-81ff-e683f725d3a5\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_913c1c46-ded4-4e04-81ff-e683f725d3a5\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":867,\"w\":24,\"h\":15,\"i\":\"f49dfd93-ce95-4a65-b9ec-531f340da083\"},\"panelIndex\":\"f49dfd93-ce95-4a65-b9ec-531f340da083\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_f49dfd93-ce95-4a65-b9ec-531f340da083\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":18,\"y\":882,\"w\":24,\"h\":15,\"i\":\"0705993c-492c-4ce0-83e0-a481c90bd432\"},\"panelIndex\":\"0705993c-492c-4ce0-83e0-a481c90bd432\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0705993c-492c-4ce0-83e0-a481c90bd432\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":24,\"y\":897,\"w\":24,\"h\":15,\"i\":\"02de39d3-6839-4198-94e3-cc91f61d0c6e\"},\"panelIndex\":\"02de39d3-6839-4198-94e3-cc91f61d0c6e\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_02de39d3-6839-4198-94e3-cc91f61d0c6e\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":5,\"y\":912,\"w\":24,\"h\":15,\"i\":\"e6b958fa-931f-4358-94fc-07934419066d\"},\"panelIndex\":\"e6b958fa-931f-4358-94fc-07934419066d\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_e6b958fa-931f-4358-94fc-07934419066d\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":0,\"y\":927,\"w\":24,\"h\":15,\"i\":\"e6d70fc7-1bdc-4743-9a15-615dff91a5c1\"},\"panelIndex\":\"e6d70fc7-1bdc-4743-9a15-615dff91a5c1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_e6d70fc7-1bdc-4743-9a15-615dff91a5c1\"},{\"version\":\"8.0.0\",\"type\":\"visualization\",\"gridData\":{\"x\":22,\"y\":942,\"w\":24,\"h\":15,\"i\":\"9db5f35e-ab94-4a5a-8c0f-70bf2aa095aa\"},\"panelIndex\":\"9db5f35e-ab94-4a5a-8c0f-70bf2aa095aa\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_9db5f35e-ab94-4a5a-8c0f-70bf2aa095aa\"},{\"version\":\"8.0.0\",\"type\":\"search\",\"gridData\":{\"x\":0,\"y\":726,\"w\":48,\"h\":111,\"i\":\"e985d8b0-4a76-46d0-af01-3edab5995b97\"},\"panelIndex\":\"e985d8b0-4a76-46d0-af01-3edab5995b97\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_e985d8b0-4a76-46d0-af01-3edab5995b97\"}]", + "refreshInterval": { + "pause": true, + "value": 0 + }, + "timeFrom": "2019-06-01T03:59:54.350Z", + "timeRestore": true, + "timeTo": "2019-08-01T14:52:40.436Z", + "title": "Large Dashboard", + "version": 1 + }, + "coreMigrationVersion": "8.0.0", + "id": "f7192e90-ac3c-11eb-8f24-bffe9ba4af2b", + "migrationVersion": { + "dashboard": "7.14.0" + }, + "references": [ + { + "id": "0a464230-79f0-11ea-ae7f-13c5d6e410a0", + "name": "914ac161-94d4-4d93-a287-c21fca46a974:panel_914ac161-94d4-4d93-a287-c21fca46a974", + "type": "visualization" + }, + { + "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", + "name": "c4cec7d1-97e3-4101-adc4-c3f15102511c:panel_c4cec7d1-97e3-4101-adc4-c3f15102511c", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "09f7de68-0d07-4661-8fda-73ea8b577ac7:panel_09f7de68-0d07-4661-8fda-73ea8b577ac7", + "type": "visualization" + }, + { + "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "name": "6c25ca6e-6aa1-4f06-9a96-e83ffd9f52e8:panel_6c25ca6e-6aa1-4f06-9a96-e83ffd9f52e8", + "type": "visualization" + }, + { + "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "37764cf9-3c89-454a-bd7e-ae4c242dc624:panel_37764cf9-3c89-454a-bd7e-ae4c242dc624", + "type": "visualization" + }, + { + "id": "1bba55f0-507e-11eb-9c0d-97106882b997", + "name": "990422fd-a9cf-446f-ba2f-ea9178a7b2e0:panel_990422fd-a9cf-446f-ba2f-ea9178a7b2e0", + "type": "visualization" + }, + { + "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "0cdc13ec-2775-4da9-9a47-1e833bb807eb:panel_0cdc13ec-2775-4da9-9a47-1e833bb807eb", + "type": "visualization" + }, + { + "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "name": "eee160de-5777-40c8-9c2c-e75f64bf208a:panel_eee160de-5777-40c8-9c2c-e75f64bf208a", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "b36f6a97-5d3d-4fc4-b076-b3e514f8f7bb:panel_b36f6a97-5d3d-4fc4-b076-b3e514f8f7bb", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "2e72acbf-7ade-451e-a5e4-7414f12facf2:panel_2e72acbf-7ade-451e-a5e4-7414f12facf2", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "4119e9b0-5d03-482d-9356-89bb62b6a851:panel_4119e9b0-5d03-482d-9356-89bb62b6a851", + "type": "visualization" + }, + { + "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "name": "42b4a37c-8b04-4510-9f27-831355221b65:panel_42b4a37c-8b04-4510-9f27-831355221b65", + "type": "visualization" + }, + { + "id": "1bba55f0-507e-11eb-9c0d-97106882b997", + "name": "dc676050-d752-4c3e-a1ae-73ef2f1bcdc6:panel_dc676050-d752-4c3e-a1ae-73ef2f1bcdc6", + "type": "visualization" + }, + { + "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "name": "6602e0e0-9e66-4e0e-90c1-f66b9c3d2340:panel_6602e0e0-9e66-4e0e-90c1-f66b9c3d2340", + "type": "visualization" + }, + { + "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "c9c65725-9b4d-4343-93db-7efa4a7a2d60:panel_c9c65725-9b4d-4343-93db-7efa4a7a2d60", + "type": "visualization" + }, + { + "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", + "name": "69141f9b-5c23-409d-9c96-7f94c243f79e:panel_69141f9b-5c23-409d-9c96-7f94c243f79e", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "6feeec2c-34ab-4844-8445-e417c8e0595b:panel_6feeec2c-34ab-4844-8445-e417c8e0595b", + "type": "visualization" + }, + { + "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "985d9dc1-de44-4803-afad-f1d497d050a1:panel_985d9dc1-de44-4803-afad-f1d497d050a1", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "d7ef9e23-d0dd-4c7c-90b3-f611bbfcd1b0:panel_d7ef9e23-d0dd-4c7c-90b3-f611bbfcd1b0", + "type": "visualization" + }, + { + "id": "1bba55f0-507e-11eb-9c0d-97106882b997", + "name": "6b0768b1-0cd2-47f0-a639-b369e7318d44:panel_6b0768b1-0cd2-47f0-a639-b369e7318d44", + "type": "visualization" + }, + { + "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "name": "c9cc2835-06a8-4448-b703-2d41a6692feb:panel_c9cc2835-06a8-4448-b703-2d41a6692feb", + "type": "visualization" + }, + { + "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "name": "af2a55b1-8b3d-478a-96b1-72e4f12585e4:panel_af2a55b1-8b3d-478a-96b1-72e4f12585e4", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "ee92986a-adab-4d66-ad4e-a43a608f52f7:panel_ee92986a-adab-4d66-ad4e-a43a608f52f7", + "type": "visualization" + }, + { + "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "3b4e1fd0-2acb-444a-b478-42d7bd10b96c:panel_3b4e1fd0-2acb-444a-b478-42d7bd10b96c", + "type": "visualization" + }, + { + "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "04d7056d-88a4-4b00-b8f4-33f79f1b6f7a:panel_04d7056d-88a4-4b00-b8f4-33f79f1b6f7a", + "type": "visualization" + }, + { + "id": "1bba55f0-507e-11eb-9c0d-97106882b997", + "name": "51122bae-427e-45a6-904e-6c821447cc46:panel_51122bae-427e-45a6-904e-6c821447cc46", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "4efab22c-1892-4013-8406-5e5d924a8a21:panel_4efab22c-1892-4013-8406-5e5d924a8a21", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "4c3c1b29-100e-474c-8290-9470684ae407:panel_4c3c1b29-100e-474c-8290-9470684ae407", + "type": "visualization" + }, + { + "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", + "name": "b4501df0-d759-4513-9e87-5dd8eefe4a4f:panel_b4501df0-d759-4513-9e87-5dd8eefe4a4f", + "type": "visualization" + }, + { + "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "name": "4fbff0ec-b3a6-4ee7-8734-9b177c3e51c6:panel_4fbff0ec-b3a6-4ee7-8734-9b177c3e51c6", + "type": "visualization" + }, + { + "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "name": "13d9982e-2745-44b1-af94-fa4b9f6761a9:panel_13d9982e-2745-44b1-af94-fa4b9f6761a9", + "type": "visualization" + }, + { + "id": "1bba55f0-507e-11eb-9c0d-97106882b997", + "name": "efa18320-9650-4bfe-9418-ac29b7979f70:panel_efa18320-9650-4bfe-9418-ac29b7979f70", + "type": "visualization" + }, + { + "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "1f03bc70-0545-4a3a-bebc-ad477674b841:panel_1f03bc70-0545-4a3a-bebc-ad477674b841", + "type": "visualization" + }, + { + "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "d766ce3a-9ec5-4ead-8698-6a2e66e729bb:panel_d766ce3a-9ec5-4ead-8698-6a2e66e729bb", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "de93deb0-6c16-45ae-8fae-de0b2e1c4ae0:panel_de93deb0-6c16-45ae-8fae-de0b2e1c4ae0", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "b93cc5e1-084a-42d9-9958-a3f569573d43:panel_b93cc5e1-084a-42d9-9958-a3f569573d43", + "type": "visualization" + }, + { + "id": "1bba55f0-507e-11eb-9c0d-97106882b997", + "name": "0b6c380f-3536-4f03-8dbd-95c53be69263:panel_0b6c380f-3536-4f03-8dbd-95c53be69263", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "5c68b67a-ac42-48b8-85de-2409aaa0cdc6:panel_5c68b67a-ac42-48b8-85de-2409aaa0cdc6", + "type": "visualization" + }, + { + "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", + "name": "098a69b8-c9a0-40c8-8703-62838e0ec4a9:panel_098a69b8-c9a0-40c8-8703-62838e0ec4a9", + "type": "visualization" + }, + { + "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "name": "a0f4b9ce-2e36-4d22-8dd9-8988f1a3b883:panel_a0f4b9ce-2e36-4d22-8dd9-8988f1a3b883", + "type": "visualization" + }, + { + "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "eb651411-ea02-4506-a674-f0125d0b2a4a:panel_eb651411-ea02-4506-a674-f0125d0b2a4a", + "type": "visualization" + }, + { + "id": "6091ead0-1c6d-11ea-a100-8589bb9d7c6b", + "name": "8ec9b67a-5d08-4006-bccc-a7341b88bb63:panel_8ec9b67a-5d08-4006-bccc-a7341b88bb63", + "type": "search" + }, + { + "id": "1bba55f0-507e-11eb-9c0d-97106882b997", + "name": "1201144d-5c9c-4015-89a3-0cb803405986:panel_1201144d-5c9c-4015-89a3-0cb803405986", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "913c1c46-ded4-4e04-81ff-e683f725d3a5:panel_913c1c46-ded4-4e04-81ff-e683f725d3a5", + "type": "visualization" + }, + { + "id": "1bba55f0-507e-11eb-9c0d-97106882b997", + "name": "f49dfd93-ce95-4a65-b9ec-531f340da083:panel_f49dfd93-ce95-4a65-b9ec-531f340da083", + "type": "visualization" + }, + { + "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", + "name": "0705993c-492c-4ce0-83e0-a481c90bd432:panel_0705993c-492c-4ce0-83e0-a481c90bd432", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "02de39d3-6839-4198-94e3-cc91f61d0c6e:panel_02de39d3-6839-4198-94e3-cc91f61d0c6e", + "type": "visualization" + }, + { + "id": "1bba55f0-507e-11eb-9c0d-97106882b997", + "name": "e6b958fa-931f-4358-94fc-07934419066d:panel_e6b958fa-931f-4358-94fc-07934419066d", + "type": "visualization" + }, + { + "id": "1bba55f0-507e-11eb-9c0d-97106882b997", + "name": "e6d70fc7-1bdc-4743-9a15-615dff91a5c1:panel_e6d70fc7-1bdc-4743-9a15-615dff91a5c1", + "type": "visualization" + }, + { + "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", + "name": "9db5f35e-ab94-4a5a-8c0f-70bf2aa095aa:panel_9db5f35e-ab94-4a5a-8c0f-70bf2aa095aa", + "type": "visualization" + }, + { + "id": "e5bfe380-ac3e-11eb-8f24-bffe9ba4af2b", + "name": "e985d8b0-4a76-46d0-af01-3edab5995b97:panel_e985d8b0-4a76-46d0-af01-3edab5995b97", + "type": "search" + } + ], + "type": "dashboard", + "updated_at": "2021-05-03T18:39:45.983Z", + "version": "WzI5LDJd" +} \ No newline at end of file diff --git a/x-pack/test/functional/page_objects/graph_page.ts b/x-pack/test/functional/page_objects/graph_page.ts index bd9e5100e0c57..b0389510e5ef5 100644 --- a/x-pack/test/functional/page_objects/graph_page.ts +++ b/x-pack/test/functional/page_objects/graph_page.ts @@ -201,7 +201,9 @@ export class GraphPageObject extends FtrService { } async getSearchFilter() { - const searchFilter = await this.find.allByCssSelector('main .euiFieldSearch'); + const searchFilter = await this.find.allByCssSelector( + '[data-test-subj="graphLandingPage"] .euiFieldSearch' + ); return searchFilter[0]; } diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 9953ab3dfcead..0fc85f78ac90b 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -500,7 +500,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await comboBox.setElement(formatInput, format); }, async editDimensionColor(color: string) { - const colorPickerInput = await testSubjects.find('colorPickerAnchor'); + const colorPickerInput = await testSubjects.find('~indexPattern-dimension-colorPicker'); await colorPickerInput.type(color); await PageObjects.common.sleep(1000); // give time for debounced components to rerender }, @@ -873,7 +873,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }, async assertColor(color: string) { // TODO: target dimensionTrigger color element after merging https://github.com/elastic/kibana/pull/76871 - await testSubjects.getAttribute('colorPickerAnchor', color); + await testSubjects.getAttribute('~indexPattern-dimension-colorPicker', color); }, /** diff --git a/x-pack/test/functional/page_objects/space_selector_page.ts b/x-pack/test/functional/page_objects/space_selector_page.ts index 9e05ae1f2c42b..313916fd3b07e 100644 --- a/x-pack/test/functional/page_objects/space_selector_page.ts +++ b/x-pack/test/functional/page_objects/space_selector_page.ts @@ -79,11 +79,11 @@ export class SpaceSelectorPageObject extends FtrService { } async clickColorPicker() { - await this.testSubjects.click('colorPickerAnchor'); + await this.testSubjects.click('euiColorPickerAnchor'); } async setColorinPicker(hexValue: string) { - await this.testSubjects.setValue('colorPickerAnchor', hexValue); + await this.testSubjects.setValue('euiColorPickerAnchor', hexValue); } async clickShowFeatures() { diff --git a/x-pack/test/functional/page_objects/tag_management_page.ts b/x-pack/test/functional/page_objects/tag_management_page.ts index b3f6622b04a57..253f91ff0496d 100644 --- a/x-pack/test/functional/page_objects/tag_management_page.ts +++ b/x-pack/test/functional/page_objects/tag_management_page.ts @@ -56,8 +56,7 @@ class TagModal extends FtrService { await this.testSubjects.setValue('createModalField-name', fields.name); } if (fields.color !== undefined) { - // EuiColorPicker does not allow to specify data-test-subj for the colorpicker input - await this.testSubjects.setValue('colorPickerAnchor', fields.color); + await this.testSubjects.setValue('~createModalField-color', fields.color); } if (fields.description !== undefined) { await this.testSubjects.click('createModalField-description'); @@ -75,7 +74,7 @@ class TagModal extends FtrService { async getFormValues(): Promise<Required<FillTagFormFields>> { return { name: await this.testSubjects.getAttribute('createModalField-name', 'value'), - color: await this.testSubjects.getAttribute('colorPickerAnchor', 'value'), + color: await this.testSubjects.getAttribute('~createModalField-color', 'value'), description: await this.testSubjects.getAttribute('createModalField-description', 'value'), }; } diff --git a/x-pack/test/functional/services/ml/data_visualizer_index_pattern_management.ts b/x-pack/test/functional/services/ml/data_visualizer_index_pattern_management.ts new file mode 100644 index 0000000000000..e5d884b22514b --- /dev/null +++ b/x-pack/test/functional/services/ml/data_visualizer_index_pattern_management.ts @@ -0,0 +1,118 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { MlDataVisualizerTable } from './data_visualizer_table'; + +export function MachineLearningDataVisualizerIndexPatternManagementProvider( + { getService }: FtrProviderContext, + dataVisualizerTable: MlDataVisualizerTable +) { + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const fieldEditor = getService('fieldEditor'); + const comboBox = getService('comboBox'); + + return { + async assertIndexPatternManagementButtonExists() { + await testSubjects.existOrFail('dataVisualizerIndexPatternManagementButton'); + }, + async assertIndexPatternManagementMenuExists() { + await testSubjects.existOrFail('dataVisualizerIndexPatternManagementMenu'); + }, + async assertIndexPatternFieldEditorExists() { + await testSubjects.existOrFail('indexPatternFieldEditorForm'); + }, + + async assertIndexPatternFieldEditorNotExist() { + await testSubjects.missingOrFail('indexPatternFieldEditorForm'); + }, + + async clickIndexPatternManagementButton() { + await retry.tryForTime(5000, async () => { + await testSubjects.clickWhenNotDisabled('dataVisualizerIndexPatternManagementButton'); + await this.assertIndexPatternManagementMenuExists(); + }); + }, + async clickAddIndexPatternFieldAction() { + await retry.tryForTime(5000, async () => { + await this.assertIndexPatternManagementMenuExists(); + await testSubjects.clickWhenNotDisabled('dataVisualizerAddIndexPatternFieldAction'); + await this.assertIndexPatternFieldEditorExists(); + }); + }, + + async clickManageIndexPatternAction() { + await retry.tryForTime(5000, async () => { + await this.assertIndexPatternManagementMenuExists(); + await testSubjects.clickWhenNotDisabled('dataVisualizerManageIndexPatternAction'); + await testSubjects.existOrFail('editIndexPattern'); + }); + }, + + async assertIndexPatternFieldEditorFieldType(expectedIdentifier: string) { + await retry.tryForTime(2000, async () => { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'typeField > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier === '' ? [] : [expectedIdentifier], + `Expected type field to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); + }); + }, + + async setIndexPatternFieldEditorFieldType(type: string) { + await comboBox.set('typeField > comboBoxInput', type); + + await this.assertIndexPatternFieldEditorFieldType(type); + }, + + async addRuntimeField(name: string, script: string, fieldType: string) { + await retry.tryForTime(5000, async () => { + await this.clickIndexPatternManagementButton(); + await this.clickAddIndexPatternFieldAction(); + + await this.assertIndexPatternFieldEditorExists(); + await fieldEditor.setName(name); + await fieldEditor.enableValue(); + await fieldEditor.typeScript(script); + await this.setIndexPatternFieldEditorFieldType(fieldType); + + await fieldEditor.save(); + await this.assertIndexPatternFieldEditorNotExist(); + }); + }, + + async renameField(originalName: string, newName: string) { + await retry.tryForTime(5000, async () => { + await dataVisualizerTable.clickEditIndexPatternFieldButton(originalName); + await this.assertIndexPatternFieldEditorExists(); + await fieldEditor.enableCustomLabel(); + await fieldEditor.setCustomLabel(newName); + await fieldEditor.save(); + await this.assertIndexPatternFieldEditorNotExist(); + }); + }, + + async confirmDeleteField() { + await testSubjects.existOrFail('deleteModalConfirmText'); + await testSubjects.setValue('deleteModalConfirmText', 'remove'); + await testSubjects.click('confirmModalConfirmButton'); + await testSubjects.missingOrFail('deleteModalConfirmText'); + }, + + async deleteField(fieldName: string) { + await retry.tryForTime(5000, async () => { + await dataVisualizerTable.clickActionMenuDeleteIndexPatternFieldButton(fieldName); + await this.confirmDeleteField(); + await dataVisualizerTable.assertRowNotExists(fieldName); + }); + }, + }; +} diff --git a/x-pack/test/functional/services/ml/data_visualizer_table.ts b/x-pack/test/functional/services/ml/data_visualizer_table.ts index 1eb0edbe01c8e..2f67a9b75e3d6 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_table.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_table.ts @@ -18,6 +18,8 @@ export function MachineLearningDataVisualizerTableProvider( ) { const retry = getService('retry'); const testSubjects = getService('testSubjects'); + const find = getService('find'); + const browser = getService('browser'); return new (class DataVisualizerTable { public async parseDataVisualizerTable() { @@ -79,6 +81,25 @@ export function MachineLearningDataVisualizerTableProvider( await testSubjects.existOrFail(this.rowSelector(fieldName)); } + public async assertRowNotExists(fieldName: string) { + await retry.tryForTime(1000, async () => { + await testSubjects.missingOrFail(this.rowSelector(fieldName)); + }); + } + + public async assertDisplayName(fieldName: string, expectedDisplayName: string) { + await retry.tryForTime(10000, async () => { + const subj = await testSubjects.find( + this.rowSelector(fieldName, `dataVisualizerDisplayName-${fieldName}`) + ); + const displayName = await subj.getVisibleText(); + expect(displayName).to.eql( + expectedDisplayName, + `Expected display name of ${fieldName} to be '${expectedDisplayName}' (got '${displayName}')` + ); + }); + } + public detailsSelector(fieldName: string, subSelector?: string) { const row = `~dataVisualizerTable > ~dataVisualizerFieldExpandedRow-${fieldName}`; return !subSelector ? row : `${row} > ${subSelector}`; @@ -133,10 +154,85 @@ export function MachineLearningDataVisualizerTableProvider( ); } - public async assertViewInLensActionEnabled(fieldName: string) { + public async ensureAllMenuPopoversClosed() { + await retry.tryForTime(5000, async () => { + await browser.pressKeys(browser.keys.ESCAPE); + const popoverExists = await find.existsByCssSelector('euiContextMenuPanel'); + expect(popoverExists).to.eql(false, 'All popovers should be closed'); + }); + } + + public async ensureActionsMenuOpen(fieldName: string) { + await retry.tryForTime(30 * 1000, async () => { + await this.ensureAllMenuPopoversClosed(); + await testSubjects.click(this.rowSelector(fieldName, 'euiCollapsedItemActionsButton')); + await find.existsByCssSelector('euiContextMenuPanel'); + }); + } + + public async assertActionsMenuClosed(fieldName: string, action: string) { + await retry.tryForTime(30 * 1000, async () => { + await testSubjects.missingOrFail(action, { timeout: 5000 }); + }); + } + + public async assertActionMenuViewInLensEnabled(fieldName: string, expectedValue: boolean) { + await retry.tryForTime(30 * 1000, async () => { + await this.ensureActionsMenuOpen(fieldName); + const actionMenuViewInLensButton = await find.byCssSelector( + '[data-test-subj="dataVisualizerActionViewInLensButton"][class="euiContextMenuItem"]' + ); + const isEnabled = await actionMenuViewInLensButton.isEnabled(); + expect(isEnabled).to.eql( + expectedValue, + `Expected "Explore in lens" action menu button for '${fieldName}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }); + } + + public async assertActionMenuDeleteIndexPatternFieldButtonEnabled( + fieldName: string, + expectedValue: boolean + ) { + await this.ensureActionsMenuOpen(fieldName); + const actionMenuViewInLensButton = await find.byCssSelector( + '[data-test-subj="dataVisualizerActionDeleteIndexPatternFieldButton"][class="euiContextMenuItem"]' + ); + const isEnabled = await actionMenuViewInLensButton.isEnabled(); + expect(isEnabled).to.eql( + expectedValue, + `Expected "Delete index pattern field" action menu button for '${fieldName}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async clickActionMenuDeleteIndexPatternFieldButton(fieldName: string) { + const testSubj = 'dataVisualizerActionDeleteIndexPatternFieldButton'; + await retry.tryForTime(5000, async () => { + await this.ensureActionsMenuOpen(fieldName); + + const button = await find.byCssSelector( + `[data-test-subj="${testSubj}"][class="euiContextMenuItem"]` + ); + await button.click(); + await this.assertActionsMenuClosed(fieldName, testSubj); + await testSubjects.existOrFail('runtimeFieldDeleteConfirmModal'); + }); + } + + public async assertViewInLensActionEnabled(fieldName: string, expectedValue: boolean) { const actionButton = this.rowSelector(fieldName, 'dataVisualizerActionViewInLensButton'); await testSubjects.existOrFail(actionButton); - await testSubjects.isEnabled(actionButton); + const isEnabled = await testSubjects.isEnabled(actionButton); + expect(isEnabled).to.eql( + expectedValue, + `Expected "Explore in lens" button for '${fieldName}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); } public async assertViewInLensActionNotExists(fieldName: string) { @@ -144,6 +240,34 @@ export function MachineLearningDataVisualizerTableProvider( await testSubjects.missingOrFail(actionButton); } + public async assertEditIndexPatternFieldButtonEnabled( + fieldName: string, + expectedValue: boolean + ) { + const selector = this.rowSelector( + fieldName, + 'dataVisualizerActionEditIndexPatternFieldButton' + ); + await testSubjects.existOrFail(selector); + const isEnabled = await testSubjects.isEnabled(selector); + expect(isEnabled).to.eql( + expectedValue, + `Expected "Edit index pattern" button for '${fieldName}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async clickEditIndexPatternFieldButton(fieldName: string) { + await retry.tryForTime(5000, async () => { + await this.assertEditIndexPatternFieldButtonEnabled(fieldName, true); + await testSubjects.click( + this.rowSelector(fieldName, 'dataVisualizerActionEditIndexPatternFieldButton') + ); + await testSubjects.existOrFail('indexPatternFieldEditorForm'); + }); + } + public async assertFieldDistinctValuesExist(fieldName: string) { const selector = this.rowSelector(fieldName, 'dataVisualizerTableColumnDistinctValues'); await testSubjects.existOrFail(selector); @@ -263,6 +387,7 @@ export function MachineLearningDataVisualizerTableProvider( docCountFormatted: string, topValuesCount: number, viewableInLens: boolean, + hasActionMenu = false, checkDistributionPreviewExist = true ) { await this.assertRowExists(fieldName); @@ -282,7 +407,11 @@ export function MachineLearningDataVisualizerTableProvider( await this.assertDistributionPreviewExist(fieldName); } if (viewableInLens) { - await this.assertViewInLensActionEnabled(fieldName); + if (hasActionMenu) { + await this.assertActionMenuViewInLensEnabled(fieldName, true); + } else { + await this.assertViewInLensActionEnabled(fieldName, true); + } } else { await this.assertViewInLensActionNotExists(fieldName); } @@ -378,7 +507,8 @@ export function MachineLearningDataVisualizerTableProvider( fieldName: string, docCountFormatted: string, exampleCount: number, - viewableInLens: boolean + viewableInLens: boolean, + hasActionMenu?: boolean ) { // Currently the data used in the data visualizer tests only contains these field types. if (fieldType === ML_JOB_FIELD_TYPES.DATE) { @@ -394,7 +524,11 @@ export function MachineLearningDataVisualizerTableProvider( } if (viewableInLens) { - await this.assertViewInLensActionEnabled(fieldName); + if (hasActionMenu) { + await this.assertActionMenuViewInLensEnabled(fieldName, true); + } else { + await this.assertViewInLensActionEnabled(fieldName, true); + } } else { await this.assertViewInLensActionNotExists(fieldName); } diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts index 64298bbdedd63..2cc9a3afa442b 100644 --- a/x-pack/test/functional/services/ml/index.ts +++ b/x-pack/test/functional/services/ml/index.ts @@ -23,6 +23,7 @@ import { MachineLearningDataFrameAnalyticsTableProvider } from './data_frame_ana import { MachineLearningDataVisualizerProvider } from './data_visualizer'; import { MachineLearningDataVisualizerFileBasedProvider } from './data_visualizer_file_based'; import { MachineLearningDataVisualizerIndexBasedProvider } from './data_visualizer_index_based'; +import { MachineLearningDataVisualizerIndexPatternManagementProvider } from './data_visualizer_index_pattern_management'; import { MachineLearningJobManagementProvider } from './job_management'; import { MachineLearningJobSelectionProvider } from './job_selection'; import { MachineLearningJobSourceSelectionProvider } from './job_source_selection'; @@ -86,6 +87,10 @@ export function MachineLearningProvider(context: FtrProviderContext) { const dataVisualizerFileBased = MachineLearningDataVisualizerFileBasedProvider(context, commonUI); const dataVisualizerIndexBased = MachineLearningDataVisualizerIndexBasedProvider(context); + const dataVisualizerIndexPatternManagement = MachineLearningDataVisualizerIndexPatternManagementProvider( + context, + dataVisualizerTable + ); const jobManagement = MachineLearningJobManagementProvider(context, api); const jobSelection = MachineLearningJobSelectionProvider(context); @@ -131,6 +136,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { dataVisualizer, dataVisualizerFileBased, dataVisualizerIndexBased, + dataVisualizerIndexPatternManagement, dataVisualizerTable, jobManagement, jobSelection, diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts index bbd212b61e439..afc6dca936bbf 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts @@ -201,7 +201,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { } = alert; try { expect(actions).to.eql([]); - expect(alertTypeId).to.eql('xpack.uptime.alerts.tls'); + expect(alertTypeId).to.eql('xpack.uptime.alerts.tlsCertificate'); expect(consumer).to.eql('uptime'); expect(tags).to.eql(['uptime', 'certs']); expect(params).to.eql({}); diff --git a/x-pack/test/functional_with_es_ssl/config.ts b/x-pack/test/functional_with_es_ssl/config.ts index 3ed382053f561..b8010c089ad03 100644 --- a/x-pack/test/functional_with_es_ssl/config.ts +++ b/x-pack/test/functional_with_es_ssl/config.ts @@ -16,6 +16,7 @@ const enabledActionTypes = [ '.email', '.index', '.pagerduty', + '.swimlane', '.servicenow', '.slack', '.webhook', diff --git a/x-pack/test/load/config.ts b/x-pack/test/load/config.ts index 514440fd73f46..8f8708d155fb1 100644 --- a/x-pack/test/load/config.ts +++ b/x-pack/test/load/config.ts @@ -30,6 +30,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { esTestCluster: { ...xpackFunctionalTestsConfig.get('esTestCluster'), serverArgs: [...xpackFunctionalTestsConfig.get('esTestCluster.serverArgs')], + esJavaOpts: '-Xms8g -Xmx8g', }, kbnTestServer: { diff --git a/x-pack/test/load/runner.ts b/x-pack/test/load/runner.ts index 3e7a4817eeef1..2d379391b2089 100644 --- a/x-pack/test/load/runner.ts +++ b/x-pack/test/load/runner.ts @@ -18,7 +18,7 @@ const simulationPackage = 'org.kibanaLoadTest.simulation'; const simulationFIleExtension = '.scala'; const gatlingProjectRootPath: string = process.env.GATLING_PROJECT_PATH || resolve(REPO_ROOT, '../kibana-load-testing'); -const simulationEntry: string = process.env.GATLING_SIMULATIONS || 'DemoJourney'; +const simulationEntry: string = process.env.GATLING_SIMULATIONS || 'branch.DemoJourney'; if (!Fs.existsSync(gatlingProjectRootPath)) { throw createFlagError( diff --git a/x-pack/test/osquery_cypress/cli_config.ts b/x-pack/test/osquery_cypress/cli_config.ts new file mode 100644 index 0000000000000..d0de73151952d --- /dev/null +++ b/x-pack/test/osquery_cypress/cli_config.ts @@ -0,0 +1,19 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +import { OsqueryCypressCliTestRunner } from './runner'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const osqueryCypressConfig = await readConfigFile(require.resolve('./config.ts')); + return { + ...osqueryCypressConfig.getAll(), + + testRunner: OsqueryCypressCliTestRunner, + }; +} diff --git a/x-pack/test/osquery_cypress/config.ts b/x-pack/test/osquery_cypress/config.ts new file mode 100644 index 0000000000000..18b4605fb9d8b --- /dev/null +++ b/x-pack/test/osquery_cypress/config.ts @@ -0,0 +1,43 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +import { CA_CERT_PATH } from '@kbn/dev-utils'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const kibanaCommonTestsConfig = await readConfigFile( + require.resolve('../../../test/common/config.js') + ); + const xpackFunctionalTestsConfig = await readConfigFile( + require.resolve('../functional/config.js') + ); + + return { + ...kibanaCommonTestsConfig.getAll(), + + esTestCluster: { + ...xpackFunctionalTestsConfig.get('esTestCluster'), + serverArgs: [ + ...xpackFunctionalTestsConfig.get('esTestCluster.serverArgs'), + // define custom es server here + // API Keys is enabled at the top level + 'xpack.security.enabled=true', + ], + }, + + kbnTestServer: { + ...xpackFunctionalTestsConfig.get('kbnTestServer'), + serverArgs: [ + ...xpackFunctionalTestsConfig.get('kbnTestServer.serverArgs'), + '--csp.strict=false', + // define custom kibana server args here + `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, + ], + }, + }; +} diff --git a/x-pack/test/osquery_cypress/ftr_provider_context.d.ts b/x-pack/test/osquery_cypress/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..aa56557c09df8 --- /dev/null +++ b/x-pack/test/osquery_cypress/ftr_provider_context.d.ts @@ -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. + */ + +import { GenericFtrProviderContext } from '@kbn/test'; + +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext<typeof services, {}>; diff --git a/x-pack/test/osquery_cypress/runner.ts b/x-pack/test/osquery_cypress/runner.ts new file mode 100644 index 0000000000000..32c84af5faf76 --- /dev/null +++ b/x-pack/test/osquery_cypress/runner.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { resolve } from 'path'; +import Url from 'url'; + +import { withProcRunner } from '@kbn/dev-utils'; + +import { FtrProviderContext } from './ftr_provider_context'; + +export async function OsqueryCypressCliTestRunner({ getService }: FtrProviderContext) { + const log = getService('log'); + const config = getService('config'); + + await withProcRunner(log, async (procs) => { + await procs.run('cypress', { + cmd: 'yarn', + args: ['cypress:run'], + cwd: resolve(__dirname, '../../plugins/osquery'), + env: { + FORCE_COLOR: '1', + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_baseUrl: Url.format(config.get('servers.kibana')), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_protocol: config.get('servers.kibana.protocol'), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_hostname: config.get('servers.kibana.hostname'), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_configport: config.get('servers.kibana.port'), + CYPRESS_ELASTICSEARCH_URL: Url.format(config.get('servers.elasticsearch')), + CYPRESS_ELASTICSEARCH_USERNAME: config.get('servers.elasticsearch.username'), + CYPRESS_ELASTICSEARCH_PASSWORD: config.get('servers.elasticsearch.password'), + CYPRESS_KIBANA_URL: Url.format({ + protocol: config.get('servers.kibana.protocol'), + hostname: config.get('servers.kibana.hostname'), + port: config.get('servers.kibana.port'), + }), + ...process.env, + }, + wait: true, + }); + }); +} + +export async function OsqueryCypressVisualTestRunner({ getService }: FtrProviderContext) { + const log = getService('log'); + const config = getService('config'); + + await withProcRunner(log, async (procs) => { + await procs.run('cypress', { + cmd: 'yarn', + args: ['cypress:open'], + cwd: resolve(__dirname, '../../plugins/osquery'), + env: { + FORCE_COLOR: '1', + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_baseUrl: Url.format(config.get('servers.kibana')), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_protocol: config.get('servers.kibana.protocol'), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_hostname: config.get('servers.kibana.hostname'), + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_configport: config.get('servers.kibana.port'), + CYPRESS_ELASTICSEARCH_URL: Url.format(config.get('servers.elasticsearch')), + CYPRESS_ELASTICSEARCH_USERNAME: config.get('servers.elasticsearch.username'), + CYPRESS_ELASTICSEARCH_PASSWORD: config.get('servers.elasticsearch.password'), + CYPRESS_KIBANA_URL: Url.format({ + protocol: config.get('servers.kibana.protocol'), + hostname: config.get('servers.kibana.hostname'), + port: config.get('servers.kibana.port'), + }), + ...process.env, + }, + wait: true, + }); + }); +} diff --git a/x-pack/test/osquery_cypress/services.ts b/x-pack/test/osquery_cypress/services.ts new file mode 100644 index 0000000000000..5e063134081ad --- /dev/null +++ b/x-pack/test/osquery_cypress/services.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 '../../../test/common/services'; diff --git a/x-pack/test/osquery_cypress/visual_config.ts b/x-pack/test/osquery_cypress/visual_config.ts new file mode 100644 index 0000000000000..35ffe311fdc27 --- /dev/null +++ b/x-pack/test/osquery_cypress/visual_config.ts @@ -0,0 +1,19 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +import { OsqueryCypressVisualTestRunner } from './runner'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const osqueryCypressConfig = await readConfigFile(require.resolve('./config.ts')); + return { + ...osqueryCypressConfig.getAll(), + + testRunner: OsqueryCypressVisualTestRunner, + }; +} diff --git a/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx b/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx index a6772c3b0bb5b..084b79d6a32b3 100644 --- a/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx +++ b/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx @@ -11,7 +11,7 @@ import ReactDOM from 'react-dom'; import { AppMountParameters, CoreStart } from 'kibana/public'; import { I18nProvider } from '@kbn/i18n/react'; import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; -import { TimelinesPluginSetup } from '../../../../../../../plugins/timelines/public'; +import { TimelinesUIStart } from '../../../../../../../plugins/timelines/public'; /** * Render the Timeline Test app. Returns a cleanup function. @@ -19,7 +19,7 @@ import { TimelinesPluginSetup } from '../../../../../../../plugins/timelines/pub export function renderApp( coreStart: CoreStart, parameters: AppMountParameters, - timelinesPluginSetup: TimelinesPluginSetup + timelinesPluginSetup: TimelinesUIStart | null ) { ReactDOM.render( <AppRoot @@ -43,14 +43,34 @@ const AppRoot = React.memo( }: { coreStart: CoreStart; parameters: AppMountParameters; - timelinesPluginSetup: TimelinesPluginSetup; + timelinesPluginSetup: TimelinesUIStart | null; }) => { return ( <I18nProvider> <Router history={parameters.history}> <KibanaContextProvider services={coreStart}> - {(timelinesPluginSetup.getTimeline && - timelinesPluginSetup.getTimeline({ timelineId: 'test' })) ?? + {(timelinesPluginSetup && + timelinesPluginSetup.getTGrid && + timelinesPluginSetup.getTGrid<'standalone'>({ + type: 'standalone', + columns: [], + indexNames: [], + deletedEventIds: [], + filters: [], + itemsPerPage: 50, + itemsPerPageOptions: [1, 2, 3], + end: '', + renderCellValue: () => <div data-test-subj="timeline-wrapper">test</div>, + sort: [], + leadingControlColumns: [], + trailingControlColumns: [], + query: { + query: '', + language: 'kuery', + }, + start: '', + rowRenderers: [], + })) ?? null} </KibanaContextProvider> </Router> diff --git a/x-pack/test/plugin_functional/plugins/timelines_test/public/plugin.ts b/x-pack/test/plugin_functional/plugins/timelines_test/public/plugin.ts index 5cf900e194d0c..b3b9c7ecbf6e0 100644 --- a/x-pack/test/plugin_functional/plugins/timelines_test/public/plugin.ts +++ b/x-pack/test/plugin_functional/plugins/timelines_test/public/plugin.ts @@ -5,19 +5,19 @@ * 2.0. */ -import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; +import { Plugin, CoreStart, CoreSetup, AppMountParameters } from 'kibana/public'; import { i18n } from '@kbn/i18n'; -import { TimelinesPluginSetup } from '../../../../../plugins/timelines/public'; +import { TimelinesUIStart } from '../../../../../plugins/timelines/public'; import { renderApp } from './applications/timelines_test'; export type TimelinesTestPluginSetup = void; export type TimelinesTestPluginStart = void; -export interface TimelinesTestPluginSetupDependencies { - timelines: TimelinesPluginSetup; -} - // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface TimelinesTestPluginStartDependencies {} +export interface TimelinesTestPluginSetupDependencies {} + +export interface TimelinesTestPluginStartDependencies { + timelines: TimelinesUIStart; +} export class TimelinesTestPlugin implements @@ -27,6 +27,7 @@ export class TimelinesTestPlugin TimelinesTestPluginSetupDependencies, TimelinesTestPluginStartDependencies > { + private timelinesPlugin: TimelinesUIStart | null = null; public setup( core: CoreSetup<TimelinesTestPluginStartDependencies, TimelinesTestPluginStart>, setupDependencies: TimelinesTestPluginSetupDependencies @@ -39,12 +40,12 @@ export class TimelinesTestPlugin mount: async (params: AppMountParameters<unknown>) => { const startServices = await core.getStartServices(); const [coreStart] = startServices; - const { timelines } = setupDependencies; - - return renderApp(coreStart, params, timelines); + return renderApp(coreStart, params, this.timelinesPlugin); }, }); } - public start() {} + public start(core: CoreStart, { timelines }: TimelinesTestPluginStartDependencies) { + this.timelinesPlugin = timelines; + } } diff --git a/x-pack/test/plugin_functional/test_suites/timelines/index.ts b/x-pack/test/plugin_functional/test_suites/timelines/index.ts index 655ed9dc3898a..2ca8d81132ab3 100644 --- a/x-pack/test/plugin_functional/test_suites/timelines/index.ts +++ b/x-pack/test/plugin_functional/test_suites/timelines/index.ts @@ -18,7 +18,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await pageObjects.common.navigateToApp('timelineTest'); }); it('shows the timeline component on navigation', async () => { - await testSubjects.existOrFail('timeline-wrapper'); + await testSubjects.existOrFail('events-viewer-panel'); }); }); }); diff --git a/x-pack/test/reporting_api_integration/reporting_without_security/job_apis.ts b/x-pack/test/reporting_api_integration/reporting_without_security/job_apis.ts index 3b34e17cd3cb1..4c64176dacc8b 100644 --- a/x-pack/test/reporting_api_integration/reporting_without_security/job_apis.ts +++ b/x-pack/test/reporting_api_integration/reporting_without_security/job_apis.ts @@ -45,7 +45,6 @@ export default function ({ getService }: FtrProviderContext) { created_by: false, jobtype: 'csv', status: 'pending', - // TODO: remove the payload field from the api respones }; forOwn(expectedResJob, (value: any, key: string) => { expect(resJob[key]).to.eql(value, key); diff --git a/x-pack/test/reporting_api_integration/services/scenarios.ts b/x-pack/test/reporting_api_integration/services/scenarios.ts index bfbf030b0887a..e45af4bd140b0 100644 --- a/x-pack/test/reporting_api_integration/services/scenarios.ts +++ b/x-pack/test/reporting_api_integration/services/scenarios.ts @@ -22,8 +22,10 @@ export function createScenarios({ getService }: Pick<FtrProviderContext, 'getSer const log = getService('log'); const supertest = getService('supertest'); const esSupertest = getService('esSupertest'); + const kibanaServer = getService('kibanaServer'); const supertestWithoutAuth = getService('supertestWithoutAuth'); const retry = getService('retry'); + const ecommerceSOPath = 'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json'; const DATA_ANALYST_USERNAME = 'data_analyst'; const DATA_ANALYST_PASSWORD = 'data_analyst-password'; @@ -32,11 +34,11 @@ export function createScenarios({ getService }: Pick<FtrProviderContext, 'getSer const initEcommerce = async () => { await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.load(ecommerceSOPath); }; const teardownEcommerce = async () => { await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce'); - await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.unload(ecommerceSOPath); await deleteAllReports(); }; diff --git a/x-pack/test/reporting_functional/reporting_without_security/management.ts b/x-pack/test/reporting_functional/reporting_without_security/management.ts index 030c890c963b1..a97cb211b7c0e 100644 --- a/x-pack/test/reporting_functional/reporting_without_security/management.ts +++ b/x-pack/test/reporting_functional/reporting_without_security/management.ts @@ -14,10 +14,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const PageObjects = getPageObjects(['common', 'reporting']); const log = getService('log'); const supertest = getService('supertestWithoutAuth'); - + const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); const esArchiver = getService('esArchiver'); const reportingApi = getService('reportingAPI'); + const ecommerceSOPath = 'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json'; const postJobJSON = async ( apiPath: string, @@ -31,12 +32,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Polling for jobs', () => { beforeEach(async () => { await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); - await esArchiver.load('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.load(ecommerceSOPath); }); afterEach(async () => { await esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana'); - await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce_kibana'); + await kibanaServer.importExport.unload(ecommerceSOPath); await reportingApi.deleteAllReports(); }); diff --git a/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts b/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts index ffaa595c16bec..1bd95ca9f16e4 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts @@ -30,7 +30,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ); } // click elsewhere to close the filter dropdown - const searchFilter = await find.byCssSelector('main .euiFieldSearch'); + const searchFilter = await find.byCssSelector('.euiPageBody .euiFieldSearch'); await searchFilter.click(); // wait until the table refreshes await listingTable.waitUntilTableIsLoaded(); diff --git a/x-pack/test/saved_object_tagging/functional/tests/maps_integration.ts b/x-pack/test/saved_object_tagging/functional/tests/maps_integration.ts index 632610fee0f53..6494eba66a437 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/maps_integration.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/maps_integration.ts @@ -30,7 +30,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ); } // click elsewhere to close the filter dropdown - const searchFilter = await find.byCssSelector('main .euiFieldSearch'); + const searchFilter = await find.byCssSelector('.euiPageBody .euiFieldSearch'); await searchFilter.click(); }; diff --git a/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts b/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts index 9789b4146c05d..4bd95cd9a7f42 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts @@ -30,7 +30,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ); } // click elsewhere to close the filter dropdown - const searchFilter = await find.byCssSelector('main .euiFieldSearch'); + const searchFilter = await find.byCssSelector('.euiPageBody .euiFieldSearch'); await searchFilter.click(); // wait until the table refreshes await listingTable.waitUntilTableIsLoaded(); diff --git a/x-pack/yarn.lock b/x-pack/yarn.lock new file mode 100644 index 0000000000000..def2ba279bfff --- /dev/null +++ b/x-pack/yarn.lock @@ -0,0 +1,31 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@kbn/interpreter@link:../packages/kbn-interpreter": + version "0.0.0" + uid "" + +"@kbn/optimizer@link:../packages/kbn-optimizer": + version "0.0.0" + uid "" + +"@kbn/plugin-helpers@link:../packages/kbn-plugin-helpers": + version "0.0.0" + uid "" + +"@kbn/storybook@link:../packages/kbn-storybook": + version "0.0.0" + uid "" + +"@kbn/test@link:../packages/kbn-test": + version "0.0.0" + uid "" + +"@kbn/ui-framework@link:../packages/kbn-ui-framework": + version "0.0.0" + uid "" + +"@kbn/ui-shared-deps@link:../packages/kbn-ui-shared-deps": + version "0.0.0" + uid "" diff --git a/yarn.lock b/yarn.lock index 953e7907590e7..448c97ff82469 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1147,7 +1147,7 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.5.tgz#665450911c6031af38f81db530f387ec04cd9a98" integrity sha512-121rumjddw9c3NCQ55KGkyE1h/nzWhU/owjhw0l4mQrkzz4x9SGS1X8gFLraHwX7td3Yo4QTL+qj0NcIzN87BA== @@ -1352,10 +1352,10 @@ dependencies: "@elastic/apm-rum-core" "^5.11.0" -"@elastic/app-search-javascript@^7.3.0": - version "7.8.0" - resolved "https://registry.yarnpkg.com/@elastic/app-search-javascript/-/app-search-javascript-7.8.0.tgz#cbc7af6bcdd224518f7f595145d6ec744e0b165d" - integrity sha512-EsAa/E/dQwBO72nrQ9YrXudP9KVY0sVUOvqPKZ3hBj9Mr3+MtWMyIKcyMf09bzdayk4qE+moetYDe5ahVbiA+Q== +"@elastic/app-search-javascript@^7.13.1": + version "7.13.1" + resolved "https://registry.yarnpkg.com/@elastic/app-search-javascript/-/app-search-javascript-7.13.1.tgz#07d84daa27e856ad14f3f840683288eab06577f4" + integrity sha512-ShzZtGWykLQ0+wXzfk6lJztv68fRcGa8rsLDxJLH/O/2CGY+PJDnj8Qu5lJPmsAPZlZgaB8u7l26YGVPOoaqSA== dependencies: object-hash "^1.3.0" @@ -1436,10 +1436,10 @@ resolved "https://registry.yarnpkg.com/@elastic/eslint-plugin-eui/-/eslint-plugin-eui-0.0.2.tgz#56b9ef03984a05cc213772ae3713ea8ef47b0314" integrity sha512-IoxURM5zraoQ7C8f+mJb9HYSENiZGgRVcG4tLQxE61yHNNRDXtGDWTZh8N1KIHcsqN1CEPETjuzBXkJYF/fDiQ== -"@elastic/eui@33.0.0": - version "33.0.0" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-33.0.0.tgz#525cf5ea6e6ab16c32bb09766e9c23806640240f" - integrity sha512-BQ6GJxKVaOF7Fm+IvH2iFeeGnepzygDr4xmmZ8PH2BEfrtkxzGP9YDpqmr/Drg5beVynJ3+72k1AEQT/JE6NTw== +"@elastic/eui@34.3.0": + version "34.3.0" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-34.3.0.tgz#5cd7d66c86d9644991c4391682f84824126361df" + integrity sha512-cUFh5H7Y95lzbsyRP1yL88mVVe5MAODOhV+O+8jYmuDQH6WrEiDhtSljTKfVYssbP7Bv+qFraVwyUj623dfr2w== dependencies: "@types/chroma-js" "^2.0.0" "@types/lodash" "^4.14.160" @@ -1454,6 +1454,7 @@ chroma-js "^2.1.0" classnames "^2.2.6" lodash "^4.17.21" + mdast-util-to-hast "^10.0.0" numeral "^2.0.6" prop-types "^15.6.0" react-ace "^7.0.5" @@ -1538,22 +1539,22 @@ resolved "https://registry.yarnpkg.com/@elastic/numeral/-/numeral-2.5.1.tgz#96acf39c3d599950646ef8ccfd24a3f057cf4932" integrity sha512-Tby6TKjixRFY+atVNeYUdGr9m0iaOq8230KTwn8BbUhkh7LwozfgKq0U98HRX7n63ZL62szl+cDKTYzh5WPCFQ== -"@elastic/react-search-ui-views@1.5.1": - version "1.5.1" - resolved "https://registry.yarnpkg.com/@elastic/react-search-ui-views/-/react-search-ui-views-1.5.1.tgz#766cd6b6049f7aa8ab711a6a3a4a060ee5fdd0ce" - integrity sha512-x4X2xc/69996IEId3VVBTwPICnx/sschnfQ6YmuU3+myRa+VUPkkAWIK/cBcyBW8TNsLtZHWZrjQYi24+H7YWA== +"@elastic/react-search-ui-views@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@elastic/react-search-ui-views/-/react-search-ui-views-1.6.0.tgz#7211d47c29ef0636c853721491b9905ac7ae58da" + integrity sha512-VADJ18p8HoSPtxKEWFODzId08j0ahyHmHjXv1vP6O/PvtA+ECvi0gDSh/WgdRF792G0e+4d2Dke8LIhxaEvE+w== dependencies: downshift "^3.2.10" rc-pagination "^1.20.1" react-select "^2.4.4" -"@elastic/react-search-ui@^1.5.1": - version "1.5.1" - resolved "https://registry.yarnpkg.com/@elastic/react-search-ui/-/react-search-ui-1.5.1.tgz#2c261226d2eda3834b4779fbeea5693958169ff2" - integrity sha512-SI7uOF+jI+Z2D+2otym+4eLBYnocmxa+NA6VPSBrADZXyn8oUEzA4MBtJtxHLtcj64Tj8Riv0tw3t9q3b8iF+w== +"@elastic/react-search-ui@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@elastic/react-search-ui/-/react-search-ui-1.6.0.tgz#8d547d5e1f0a8eebe94798b29966f51643aa886f" + integrity sha512-bwSKuCQTQiBWr6QufQtZZGu6rcVYfoiUnyZbwZMS6ojedd5XY7FtMcE+QnR6/IIo0M2IUrxD74XtVNqkUhoCRg== dependencies: - "@elastic/react-search-ui-views" "1.5.1" - "@elastic/search-ui" "1.5.1" + "@elastic/react-search-ui-views" "1.6.0" + "@elastic/search-ui" "1.6.0" "@elastic/request-crypto@1.1.4": version "1.1.4" @@ -1568,18 +1569,17 @@ version "0.0.0" uid "" -"@elastic/search-ui-app-search-connector@^1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@elastic/search-ui-app-search-connector/-/search-ui-app-search-connector-1.5.0.tgz#d379132c5015775acfaee5322ec019e9c0559ccc" - integrity sha512-lHuXBjaMaN1fsm1taQMR/7gfpAg4XOsvZOi8u1AoufUw9kGr6Xc00Gznj1qTyH0Qebi2aSmY0NBN6pdIEGvvGQ== +"@elastic/search-ui-app-search-connector@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@elastic/search-ui-app-search-connector/-/search-ui-app-search-connector-1.6.0.tgz#faf1c4a384285648ef7b5ef9cd0e65de0341d2b0" + integrity sha512-6oNvqzo4nuutmCM0zEzYrV6VwG8j0ML43SkaG6UrFzLUd6DeWUVGNN+SLNAlfQDWBUjc2m5EGvgdk/0GOWDZeA== dependencies: - "@babel/runtime" "^7.5.4" - "@elastic/app-search-javascript" "^7.3.0" + "@elastic/app-search-javascript" "^7.13.1" -"@elastic/search-ui@1.5.1": - version "1.5.1" - resolved "https://registry.yarnpkg.com/@elastic/search-ui/-/search-ui-1.5.1.tgz#14c66a66f5e937ef5e24d6266620b49d986fb3ed" - integrity sha512-ssfvX1q76X1UwqYASWtBni4PZ+3SYk1PvHmOjpVf9BYai1OqZLGVaj8Sw+cE1ia56zl5In7viCfciC+CP31ovA== +"@elastic/search-ui@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@elastic/search-ui/-/search-ui-1.6.0.tgz#8b2286cacff44735be96605b2929ca9b469c78de" + integrity sha512-i7htjET9uE4xngyzS8kX3DkSD5XNcr+3FS0Jjx3xRpKVc/dFst4bJyiSeRrQcq+2oBb4mEJJOCFaIrLZg3mdSA== dependencies: date-fns "^1.30.1" deep-equal "^1.0.1" @@ -2616,7 +2616,7 @@ version "0.0.0" uid "" -"@kbn/cli-dev-mode@link:packages/kbn-cli-dev-mode": +"@kbn/cli-dev-mode@link:bazel-bin/packages/kbn-cli-dev-mode": version "0.0.0" uid "" @@ -2692,7 +2692,7 @@ version "0.0.0" uid "" -"@kbn/optimizer@link:packages/kbn-optimizer": +"@kbn/optimizer@link:bazel-bin/packages/kbn-optimizer": version "0.0.0" uid "" @@ -2700,7 +2700,7 @@ version "0.0.0" uid "" -"@kbn/plugin-helpers@link:packages/kbn-plugin-helpers": +"@kbn/plugin-helpers@link:bazel-bin/packages/kbn-plugin-helpers": version "0.0.0" uid "" @@ -2752,6 +2752,10 @@ version "0.0.0" uid "" +"@kbn/securitysolution-t-grid@link:bazel-bin/packages/kbn-securitysolution-t-grid": + version "0.0.0" + uid "" + "@kbn/securitysolution-utils@link:bazel-bin/packages/kbn-securitysolution-utils": version "0.0.0" uid "" @@ -4818,6 +4822,18 @@ resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-1.2.7.tgz#34dc654d34fc058c41c31dbca1ed68071a8fcc17" integrity sha512-51vHWuUyDOi+8XuwPrTw3cFqyh2Slg9y8COYkRfjCPG9TfYqY0hoNPzv/8BrcAy0FeQBzqEo/D/8Nk2caOQJnA== +"@types/d3-color@*": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-2.0.1.tgz#570ea7f8b853461301804efa52bd790a640a26db" + integrity sha512-u7LTCL7RnaavFSmob2rIAJLNwu50i6gFwY9cHFr80BrQURYQBRkJ+Yv47nA3Fm7FeRhdWTiVTeqvSeOuMAOzBQ== + +"@types/d3-interpolate@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-2.0.0.tgz#325029216dc722c1c68c33ccda759f1209d35823" + integrity sha512-Wt1v2zTlEN8dSx8hhx6MoOhWQgTkz0Ukj7owAEIOF2QtI0e219paFX9rf/SLOr/UExWb1TcUzatU8zWwFby6gg== + dependencies: + "@types/d3-color" "*" + "@types/d3-path@*": version "1.0.7" resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-1.0.7.tgz#a0736fceed688a695f48265a82ff7a3369414b81" @@ -10877,7 +10893,7 @@ d3-array@>=2.5, d3-array@^2.3.0, d3-array@^2.7.1: resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.8.0.tgz#f76e10ad47f1f4f75f33db5fc322eb9ffde5ef23" integrity sha512-6V272gsOeg7+9pTW1jSYOR1QE37g95I3my1hBmY+vOUNHRrk9yt4OTz/gK7PMkVAVDrYYq4mq3grTiZ8iJdNIw== -d3-cloud@^1.2.5: +d3-cloud@1.2.5, d3-cloud@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/d3-cloud/-/d3-cloud-1.2.5.tgz#3e91564f2d27fba47fcc7d812eb5081ea24c603d" integrity sha512-4s2hXZgvs0CoUIw31oBAGrHt9Kt/7P9Ik5HIVzISFiWkD0Ga2VLAuO/emO/z1tYIpE7KG2smB4PhMPfFMJpahw== @@ -10894,6 +10910,11 @@ d3-color@1, "d3-color@1 - 2", d3-color@^1.0.3, d3-color@^1.4.0: resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.1.tgz#c52002bf8846ada4424d55d97982fef26eb3bc8a" integrity sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q== +"d3-color@1 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.0.1.tgz#03316e595955d1fcd39d9f3610ad41bb90194d0a" + integrity sha512-6/SlHkDOBLyQSJ1j1Ghs82OIUXpKWlR0hCsw0XrLSQhuUPuCSmLQ1QPH98vpnQxMUQM2/gfAkUEWsupVpd9JGw== + d3-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e" @@ -11008,6 +11029,13 @@ d3-interpolate@^2.0.1: dependencies: d3-color "1 - 2" +d3-interpolate@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== + dependencies: + d3-color "1 - 3" + d3-path@1: version "1.0.9" resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" @@ -18999,7 +19027,7 @@ mdast-util-from-markdown@^0.8.0: parse-entities "^2.0.0" unist-util-stringify-position "^2.0.0" -mdast-util-to-hast@10.0.1: +mdast-util-to-hast@10.0.1, mdast-util-to-hast@^10.0.0: version "10.0.1" resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-10.0.1.tgz#0cfc82089494c52d46eb0e3edb7a4eb2aea021eb" integrity sha512-BW3LM9SEMnjf4HXXVApZMt8gLQWVNXc3jryK0nJu/rOXPOnlkUjmdkDlmxMirpbU9ILncGFIwLH/ubnWBbcdgA== @@ -19013,20 +19041,6 @@ mdast-util-to-hast@10.0.1: unist-util-position "^3.0.0" unist-util-visit "^2.0.0" -mdast-util-to-hast@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-10.0.0.tgz#744dfe7907bac0263398a68af5aba16d104a9a08" - integrity sha512-dRyAC5S4eDcIOdkz4jg0wXbUdlf+5YFu7KppJNHOsMaD7ql5bKIqVcvXYYkcrKjzUkfX8JsKFVMthsU8OWxQ+w== - dependencies: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - mdast-util-definitions "^4.0.0" - mdurl "^1.0.0" - unist-builder "^2.0.0" - unist-util-generated "^1.0.0" - unist-util-position "^3.0.0" - unist-util-visit "^2.0.0" - mdast-util-to-markdown@^0.6.0: version "0.6.1" resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.1.tgz#0e07d3f871e056bffc38a0cf50c7298b56d9e0d6"