diff --git a/.eslintignore b/.eslintignore index 63cd01d6e90db..f757ed9a1bf98 100644 --- a/.eslintignore +++ b/.eslintignore @@ -30,8 +30,6 @@ snapshots.js # package overrides /packages/elastic-eslint-config-kibana -/packages/kbn-interpreter/src/common/lib/grammar.js -/packages/kbn-tinymath/src/grammar.js /packages/kbn-plugin-generator/template /packages/kbn-pm/dist /packages/kbn-test/src/functional_test_runner/__tests__/fixtures/ 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/api/saved-objects/bulk_create.asciidoc b/docs/api/saved-objects/bulk_create.asciidoc index 267ab3891d700..5bd3a7587dde9 100644 --- a/docs/api/saved-objects/bulk_create.asciidoc +++ b/docs/api/saved-objects/bulk_create.asciidoc @@ -45,6 +45,11 @@ experimental[] Create multiple {kib} saved objects. (Optional, string array) Identifiers for the <> in which this object is created. If this is provided, the object is created only in the explicitly defined spaces. If this is not provided, the object is created in the current space (default behavior). +* For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, including +the "All spaces" identifier (`'*'`). +* For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only be +used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed. +* For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used. `version`:: (Optional, number) Specifies the version. diff --git a/docs/api/saved-objects/create.asciidoc b/docs/api/saved-objects/create.asciidoc index d7a368034ef07..e7e25c7d3bba6 100644 --- a/docs/api/saved-objects/create.asciidoc +++ b/docs/api/saved-objects/create.asciidoc @@ -52,6 +52,11 @@ any data that you send to the API is properly formed. (Optional, string array) Identifiers for the <> in which this object is created. If this is provided, the object is created only in the explicitly defined spaces. If this is not provided, the object is created in the current space (default behavior). +* For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, including +the "All spaces" identifier (`'*'`). +* For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only be +used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed. +* For global object types (registered with `namespaceType: 'agnostic'): this option cannot be used. [[saved-objects-api-create-request-codes]] ==== Response code diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index c211751c09b49..5d7ba22841aa1 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -80,11 +80,13 @@ yarn kbn watch-bazel - @kbn/eslint-plugin-eslint - @kbn/expect - @kbn/i18n +- @kbn/interpreter - @kbn/io-ts-utils - @kbn/legacy-logging - @kbn/logging - @kbn/mapbox-gl - @kbn/monaco +- @kbn/optimizer - @kbn/rule-data-utils - @kbn/securitysolution-es-utils - @kbn/securitysolution-hook-utils @@ -100,8 +102,10 @@ yarn kbn watch-bazel - @kbn/server-http-tools - @kbn/server-route-repository - @kbn/std +- @kbn/storybook - @kbn/telemetry-utils - @kbn/tinymath +- @kbn/ui-framework - @kbn/ui-shared-deps - @kbn/utility-types - @kbn/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 9930ab7319f65..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; @@ -116,6 +117,7 @@ readonly links: { readonly addData: string; readonly kibana: string; readonly upgradeAssistant: string; + readonly rollupJobs: string; readonly elasticsearch: Record; readonly siem: { readonly guide: string; @@ -185,5 +187,18 @@ readonly links: { readonly plugins: Record; readonly snapshotRestore: Record; readonly ingest: Record; + readonly fleet: Readonly<{ + guide: string; + fleetServer: string; + fleetServerAddFleetServer: string; + settings: string; + settingsFleetServerHostSettings: string; + troubleshooting: string; + elasticAgent: string; + datastreams: string; + datastreamsNamingScheme: string; + upgradeElasticAgent: string; + upgradeElasticAgent712lower: 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 ab8cdea5e4d86..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) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
} | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md index 3db8bbadfbd6b..4d094ecde7a96 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md @@ -6,7 +6,7 @@ Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md). -Note: this can only be used for multi-namespace object types. +\* For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, including the "All spaces" identifier (`'*'`). \* For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed. \* For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md index 6fc01212a2e41..463c3fe81b702 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md @@ -18,7 +18,7 @@ export interface SavedObjectsBulkCreateObject | [attributes](./kibana-plugin-core-server.savedobjectsbulkcreateobject.attributes.md) | T | | | [coreMigrationVersion](./kibana-plugin-core-server.savedobjectsbulkcreateobject.coremigrationversion.md) | string | A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the current Kibana version, it will result in an error. | | [id](./kibana-plugin-core-server.savedobjectsbulkcreateobject.id.md) | string | | -| [initialNamespaces](./kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md) | string[] | Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).Note: this can only be used for multi-namespace object types. | +| [initialNamespaces](./kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md) | string[] | Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).\* For shareable object types (registered with namespaceType: 'multiple'): this option can be used to specify one or more spaces, including the "All spaces" identifier ('*'). \* For isolated object types (registered with namespaceType: 'single' or namespaceType: 'multiple-isolated'): this option can only be used to specify a single space, and the "All spaces" identifier ('*') is not allowed. \* For global object types (registered with namespaceType: 'agnostic'): this option cannot be used. | | [migrationVersion](./kibana-plugin-core-server.savedobjectsbulkcreateobject.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | | [originId](./kibana-plugin-core-server.savedobjectsbulkcreateobject.originid.md) | string | Optional ID of the original saved object, if this object's id was regenerated | | [references](./kibana-plugin-core-server.savedobjectsbulkcreateobject.references.md) | SavedObjectReference[] | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md index 262b0997cb905..43489b8d2e8a2 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md @@ -6,7 +6,7 @@ Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md). -Note: this can only be used for multi-namespace object types. +\* For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, including the "All spaces" identifier (`'*'`). \* For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed. \* For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md index 1805f389d4e7f..7eaa9c51f5c82 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md @@ -17,7 +17,7 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions | --- | --- | --- | | [coreMigrationVersion](./kibana-plugin-core-server.savedobjectscreateoptions.coremigrationversion.md) | string | A semver value that is used when upgrading objects between Kibana versions. If undefined, this will be automatically set to the current Kibana version when the object is created. If this is set to a non-semver value, or it is set to a semver value greater than the current Kibana version, it will result in an error. | | [id](./kibana-plugin-core-server.savedobjectscreateoptions.id.md) | string | (not recommended) Specify an id for the document | -| [initialNamespaces](./kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md) | string[] | Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).Note: this can only be used for multi-namespace object types. | +| [initialNamespaces](./kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md) | string[] | Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).\* For shareable object types (registered with namespaceType: 'multiple'): this option can be used to specify one or more spaces, including the "All spaces" identifier ('*'). \* For isolated object types (registered with namespaceType: 'single' or namespaceType: 'multiple-isolated'): this option can only be used to specify a single space, and the "All spaces" identifier ('*') is not allowed. \* For global object types (registered with namespaceType: 'agnostic'): this option cannot be used. | | [migrationVersion](./kibana-plugin-core-server.savedobjectscreateoptions.migrationversion.md) | SavedObjectsMigrationVersion | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | | [originId](./kibana-plugin-core-server.savedobjectscreateoptions.originid.md) | string | Optional ID of the original saved object, if this object's id was regenerated | | [overwrite](./kibana-plugin-core-server.savedobjectscreateoptions.overwrite.md) | boolean | Overwrite existing documents (defaults to false) | 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): 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.aggconfigs.getresolvedtimerange.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getresolvedtimerange.md new file mode 100644 index 0000000000000..2af44037292a2 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getresolvedtimerange.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [getResolvedTimeRange](./kibana-plugin-plugins-data-public.aggconfigs.getresolvedtimerange.md) + +## AggConfigs.getResolvedTimeRange() method + +Returns the current time range as moment instance (date math will get resolved using the current "now" value or system time if not set) + +Signature: + +```typescript +getResolvedTimeRange(): import("../..").TimeRangeBounds | undefined; +``` +Returns: + +`import("../..").TimeRangeBounds | undefined` + +Current time range as resolved date. + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md index 45333b6767cac..9e671675b0b29 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md @@ -42,6 +42,7 @@ export declare class AggConfigs | [getAll()](./kibana-plugin-plugins-data-public.aggconfigs.getall.md) | | | | [getRequestAggById(id)](./kibana-plugin-plugins-data-public.aggconfigs.getrequestaggbyid.md) | | | | [getRequestAggs()](./kibana-plugin-plugins-data-public.aggconfigs.getrequestaggs.md) | | | +| [getResolvedTimeRange()](./kibana-plugin-plugins-data-public.aggconfigs.getresolvedtimerange.md) | | Returns the current time range as moment instance (date math will get resolved using the current "now" value or system time if not set) | | [getResponseAggById(id)](./kibana-plugin-plugins-data-public.aggconfigs.getresponseaggbyid.md) | | Find a response agg by it's id. This may be an agg in the aggConfigs, or one created specifically for a response value | | [getResponseAggs()](./kibana-plugin-plugins-data-public.aggconfigs.getresponseaggs.md) | | Gets the AggConfigs (and possibly ResponseAggConfigs) that represent the values that will be produced when all aggs are run.With multi-value metric aggs it is possible for a single agg request to result in multiple agg values, which is why the length of a vis' responseValuesAggs may be different than the vis' aggs {array\[AggConfig\]} | | [getSearchSourceTimeFilter(forceNow)](./kibana-plugin-plugins-data-public.aggconfigs.getsearchsourcetimefilter.md) | | | 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").KueryNode; - toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; + toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").MinimalIndexPattern | undefined, config?: Record | undefined, context?: Record | 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 @@ - - -[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 - -Signature: - -```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 @@ - - -[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 - -Signature: - -```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 @@ Signature: ```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) | Record<string, SerializedFieldFormat<unknown> | undefined> | | -| [fields](./kibana-plugin-plugins-data-public.iindexpattern.fields.md) | IFieldType[] | | | [getFormatterForField](./kibana-plugin-plugins-data-public.iindexpattern.getformatterforfield.md) | (field: IndexPatternField | IndexPatternField['spec'] | IFieldType) => FieldFormat | Look up a formatter for a given field | -| [id](./kibana-plugin-plugins-data-public.iindexpattern.id.md) | string | | | [timeFieldName](./kibana-plugin-plugins-data-public.iindexpattern.timefieldname.md) | string | | | [title](./kibana-plugin-plugins-data-public.iindexpattern.title.md) | string | | | [type](./kibana-plugin-plugins-data-public.iindexpattern.type.md) | string | 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 @@ + + +[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 + +Signature: + +```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 | --- | --- | --- | | [id](./kibana-plugin-plugins-data-public.ikibanasearchresponse.id.md) | string | Some responses may contain a unique id to identify the request this response came from. | | [isPartial](./kibana-plugin-plugins-data-public.ikibanasearchresponse.ispartial.md) | boolean | Indicates whether the results returned are complete or partial | +| [isRestored](./kibana-plugin-plugins-data-public.ikibanasearchresponse.isrestored.md) | boolean | Indicates whether the results returned are from the async-search index | | [isRunning](./kibana-plugin-plugins-data-public.ikibanasearchresponse.isrunning.md) | boolean | Indicates whether search is still in flight | | [loaded](./kibana-plugin-plugins-data-public.ikibanasearchresponse.loaded.md) | number | 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) | RawResponse | 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").KueryNode; - toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; + toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").MinimalIndexPattern | undefined, config?: Record | undefined, context?: Record | 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 @@ + + +[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 + +Signature: + +```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 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [NoSearchIdInSessionError](./kibana-plugin-plugins-data-server.nosearchidinsessionerror.md) + +## NoSearchIdInSessionError class + +Signature: + +```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 NoSearchIdInSessionError 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| <> | Send a message to a Slack channel or user. +a| <> + +| Create an incident in Swimlane. + a| <> | Send a request to a web service. 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 +++++ +Swimlane +++++ + +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 <>. + +[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 <> instead. A boolean value indicating whether to bypass server certificate validation. +`.ssl.rejectUnauthorized` {ess-icon} + | Deprecated. Use <> 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`. <>. 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`. <>. 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 <> instead. Set to `false` to bypass certificate validation for the proxy, if using a proxy for actions. Default: `true`. + | Deprecated. Use <> 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`. <>. | `xpack.actions.rejectUnauthorized` {ess-icon} - | Deprecated. Use <> instead. Set to `false` to bypass certificate validation for actions. Default: `true`. + + | Deprecated. Use <> 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`. <>. + + - 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/setup/settings.asciidoc b/docs/setup/settings.asciidoc index ddb906f390a2d..c3c29adcea18f 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -325,6 +325,9 @@ The time interval policy will rotate the log file every given interval of time. When `includeElasticMapsService` is turned off, only the vector layers configured by <> and the tile layer configured by <> are available in <>. *Default: `true`* +| `map.emsUrl:` + | Specifies the URL of a self hosted <> + | `map.proxyElasticMapsServiceInMaps:` | Set to `true` to proxy all <> Elastic Maps Service requests through the {kib} server. *Default: `false`* diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc index 8c17f8ec93b96..b699c56ebd944 100644 --- a/docs/user/alerting/alerting-getting-started.asciidoc +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -136,9 +136,4 @@ Functionally, {kib} alerting differs in that: At a higher level, {kib} alerting allows rich integrations across use cases like <>, <>, <>, and <>. Pre-packaged *rule types* simplify setup and hide the details of complex, domain-specific detections, while providing a consistent interface across {kib}. -[float] -[[alerting-setup-prerequisites]] -== Prerequisites -<> - -- \ No newline at end of file diff --git a/docs/user/alerting/alerting-setup.asciidoc b/docs/user/alerting/alerting-setup.asciidoc index 39f1af0030e0a..2ae5160069f0a 100644 --- a/docs/user/alerting/alerting-setup.asciidoc +++ b/docs/user/alerting/alerting-setup.asciidoc @@ -1,8 +1,8 @@ [role="xpack"] [[alerting-setup]] -== Alerting Setup +== Alerting Set up ++++ -Setup +Set up ++++ The Alerting feature is automatically enabled in {kib}, but might require some additional configuration. diff --git a/docs/user/alerting/alerting-troubleshooting.asciidoc b/docs/user/alerting/alerting-troubleshooting.asciidoc index b7b0c749dfe14..08655508b3cba 100644 --- a/docs/user/alerting/alerting-troubleshooting.asciidoc +++ b/docs/user/alerting/alerting-troubleshooting.asciidoc @@ -12,6 +12,32 @@ If your problem isn’t described here, please review open issues in the followi Have a question? Contact us in the https://discuss.elastic.co/[discuss forum]. +[float] +[[rule-cannot-decrypt-api-key]] +=== Rule cannot decrypt apiKey + +*Problem*: + +The rule fails to execute and has an `Unable to decrypt attribute "apiKey"` error. + +*Solution*: + +This error happens when the `xpack.encryptedSavedObjects.encryptionKey` value used to create the rule does not match the value used during rule execution. Depending on the scenario, there are different ways to solve this problem: + +[cols="2*<"] +|=== + +| If the value in `xpack.encryptedSavedObjects.encryptionKey` was manually changed, and the previous encryption key is still known. +| Ensure any previous encryption key is included in the keys used for <>. + +| If another {kib} instance with a different encryption key connects to the cluster. +| The other {kib} instance might be trying to run the rule using a different encryption key than what the rule was created with. Ensure the encryption keys among all the {kib} instances are the same, and setting <> for previously used encryption keys. + +| If other scenarios don't apply. +| Generate a new API key for the rule by disabling then enabling the rule. + +|=== + [float] [[rules-small-check-interval-run-late]] === Rules with small check intervals run late @@ -29,7 +55,6 @@ Either tweak the <> or increa For more details, see <>. - [float] [[scheduled-rules-run-late]] === Rules run late diff --git a/docs/user/alerting/defining-rules.asciidoc b/docs/user/alerting/defining-rules.asciidoc deleted file mode 100644 index 686a7bbc8a37b..0000000000000 --- a/docs/user/alerting/defining-rules.asciidoc +++ /dev/null @@ -1,11 +0,0 @@ -[role="xpack"] -[[defining-alerts]] -== Defining rules - -This content has been moved to <>. - -[float] -[[defining-alerts-general-details]] -==== General rule details - -This content has been moved to <>. \ No newline at end of file diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc index 9ab6a2dc46ebf..957d99a54ebaa 100644 --- a/docs/user/alerting/index.asciidoc +++ b/docs/user/alerting/index.asciidoc @@ -1,7 +1,5 @@ include::alerting-getting-started.asciidoc[] include::alerting-setup.asciidoc[] include::create-and-manage-rules.asciidoc[] -include::defining-rules.asciidoc[] -include::rule-management.asciidoc[] include::rule-types.asciidoc[] include::alerting-troubleshooting.asciidoc[] diff --git a/docs/user/alerting/rule-management.asciidoc b/docs/user/alerting/rule-management.asciidoc deleted file mode 100644 index d6349a60e08eb..0000000000000 --- a/docs/user/alerting/rule-management.asciidoc +++ /dev/null @@ -1,5 +0,0 @@ -[role="xpack"] -[[alert-management]] -== Managing rules - -This content has been moved to <>. \ No newline at end of file diff --git a/docs/user/alerting/rule-types.asciidoc b/docs/user/alerting/rule-types.asciidoc index bb840014fe80f..f7f57d2f845a0 100644 --- a/docs/user/alerting/rule-types.asciidoc +++ b/docs/user/alerting/rule-types.asciidoc @@ -15,7 +15,7 @@ see {subscriptions}[the subscription page]. [[stack-rules]] === Stack rules -<> are built into {kib}. To access the *Stack Rules* feature and create and edit rules, users require the `all` privilege. See <> for more information. +<> are built into {kib}. To access the *Stack Rules* feature and create and edit rules, users require the `all` privilege. See <> for more information. [cols="2*<"] |=== 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 +| <> +| ✓ +| ✓ | Derivative -^| X -^| X -^| X -^| X +| ✓ +| ✓ +| ✓ | Max bucket -^| X -^| X -| -^| X +| <> +| ✓ +| ✓ | Min bucket -^| X -^| X -| -^| X +| <> +| ✓ +| ✓ | Sum bucket -^| X -^| X -| -^| X +| <> +| ✓ +| ✓ | 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 +| ✓ +| ✓ +| + +| <> +| Use <> +| ✓ +| + +| <> +| +| ✓ +| + +| <> +| +| ✓ +| + +| 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/drilldowns.asciidoc b/docs/user/dashboard/drilldowns.asciidoc index 0eb4b43466ff9..84c33db31d575 100644 --- a/docs/user/dashboard/drilldowns.asciidoc +++ b/docs/user/dashboard/drilldowns.asciidoc @@ -112,7 +112,7 @@ The following panel types support drilldowns. ^| X ^| X -| TSVB +| TSVB (only for time series visualizations) ^| X ^| X 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/docs/user/dashboard/vega-reference.asciidoc b/docs/user/dashboard/vega-reference.asciidoc index cc384ec041a9d..6829e129cd3b6 100644 --- a/docs/user/dashboard/vega-reference.asciidoc +++ b/docs/user/dashboard/vega-reference.asciidoc @@ -50,6 +50,11 @@ To learn more, read about https://vega.github.io/vega/docs/specification/#autosize[autosize] in the Vega documentation. +WARNING: Autosize in Vega-Lite has https://vega.github.io/vega-lite/docs/size.html#limitations[several limitations] +that can result in a warning like `Autosize "fit" only works for single views and layered views.` +The recommended fix for this warning is to convert your spec to Vega using the <> +`VEGA_DEBUG.vega_spec` output. + [float] [[vega-theme]] ====== Default theme to match {kib} diff --git a/docs/user/security/audit-logging.asciidoc b/docs/user/security/audit-logging.asciidoc index b9fc0c9c4ac46..5808e56d6d289 100644 --- a/docs/user/security/audit-logging.asciidoc +++ b/docs/user/security/audit-logging.asciidoc @@ -93,9 +93,9 @@ Refer to the corresponding {es} logs for potential write errors. | `unknown` | User is creating a connector. | `failure` | User is not authorized to create a connector. -.2+| `alert_create` -| `unknown` | User is creating an alert. -| `failure` | User is not authorized to create an alert. +.2+| `rule_create` +| `unknown` | User is creating a rule. +| `failure` | User is not authorized to create a rule. .2+| `space_create` | `unknown` | User is creating a space. @@ -128,38 +128,38 @@ Refer to the corresponding {es} logs for potential write errors. | `unknown` | User is updating a connector. | `failure` | User is not authorized to update a connector. -.2+| `alert_update` -| `unknown` | User is updating an alert. -| `failure` | User is not authorized to update an alert. +.2+| `rule_update` +| `unknown` | User is updating a rule. +| `failure` | User is not authorized to update a rule. -.2+| `alert_update_api_key` -| `unknown` | User is updating the API key of an alert. -| `failure` | User is not authorized to update the API key of an alert. +.2+| `rule_update_api_key` +| `unknown` | User is updating the API key of a rule. +| `failure` | User is not authorized to update the API key of a rule. -.2+| `alert_enable` -| `unknown` | User is enabling an alert. -| `failure` | User is not authorized to enable an alert. +.2+| `rule_enable` +| `unknown` | User is enabling a rule. +| `failure` | User is not authorized to enable a rule. -.2+| `alert_disable` -| `unknown` | User is disabling an alert. -| `failure` | User is not authorized to disable an alert. +.2+| `rule_disable` +| `unknown` | User is disabling a rule. +| `failure` | User is not authorized to disable a rule. -.2+| `alert_mute` +.2+| `rule_mute` +| `unknown` | User is muting a rule. +| `failure` | User is not authorized to mute a rule. + +.2+| `rule_unmute` +| `unknown` | User is unmuting a rule. +| `failure` | User is not authorized to unmute a rule. + +.2+| `rule_alert_mute` | `unknown` | User is muting an alert. | `failure` | User is not authorized to mute an alert. -.2+| `alert_unmute` +.2+| `rule_alert_unmute` | `unknown` | User is unmuting an alert. | `failure` | User is not authorized to unmute an alert. -.2+| `alert_instance_mute` -| `unknown` | User is muting an alert instance. -| `failure` | User is not authorized to mute an alert instance. - -.2+| `alert_instance_unmute` -| `unknown` | User is unmuting an alert instance. -| `failure` | User is not authorized to unmute an alert instance. - .2+| `space_update` | `unknown` | User is updating a space. | `failure` | User is not authorized to update a space. @@ -183,9 +183,9 @@ Refer to the corresponding {es} logs for potential write errors. | `unknown` | User is deleting a connector. | `failure` | User is not authorized to delete a connector. -.2+| `alert_delete` -| `unknown` | User is deleting an alert. -| `failure` | User is not authorized to delete an alert. +.2+| `rule_delete` +| `unknown` | User is deleting a rule. +| `failure` | User is not authorized to delete a rule. .2+| `space_delete` | `unknown` | User is deleting a space. @@ -218,13 +218,13 @@ Refer to the corresponding {es} logs for potential write errors. | `success` | User has accessed a connector as part of a search operation. | `failure` | User is not authorized to search for connectors. -.2+| `alert_get` -| `success` | User has accessed an alert. -| `failure` | User is not authorized to access an alert. +.2+| `rule_get` +| `success` | User has accessed a rule. +| `failure` | User is not authorized to access a rule. -.2+| `alert_find` -| `success` | User has accessed an alert as part of a search operation. -| `failure` | User is not authorized to search for alerts. +.2+| `rule_find` +| `success` | User has accessed a rule as part of a search operation. +| `failure` | User is not authorized to search for rules. .2+| `space_get` | `success` | User has accessed a space. diff --git a/jest.config.integration.js b/jest.config.integration.js index 50767932a52d7..b6ecb4569b643 100644 --- a/jest.config.integration.js +++ b/jest.config.integration.js @@ -13,7 +13,6 @@ module.exports = { rootDir: '.', roots: ['/src', '/packages'], testMatch: ['**/integration_tests**/*.test.{js,mjs,ts,tsx}'], - testRunner: 'jasmine2', testPathIgnorePatterns: preset.testPathIgnorePatterns.filter( (pattern) => !pattern.includes('integration_tests') ), diff --git a/package.json b/package.json index 310350baf7b2d..f99eb86a43cec 100644 --- a/package.json +++ b/package.json @@ -97,13 +97,13 @@ "yarn": "^1.21.1" }, "dependencies": { - "@elastic/apm-rum": "^5.6.1", - "@elastic/apm-rum-react": "^1.2.5", + "@elastic/apm-rum": "^5.8.0", + "@elastic/apm-rum-react": "^1.2.11", "@elastic/charts": "30.1.0", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.13", - "@elastic/ems-client": "7.13.0", - "@elastic/eui": "33.0.0", + "@elastic/ems-client": "7.14.0", + "@elastic/eui": "34.3.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "^9.0.1-kibana3", "@elastic/maki": "6.3.0", @@ -133,7 +133,7 @@ "@kbn/crypto": "link:bazel-bin/packages/kbn-crypto", "@kbn/mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl", "@kbn/i18n": "link:bazel-bin/packages/kbn-i18n", - "@kbn/interpreter": "link:packages/kbn-interpreter", + "@kbn/interpreter": "link:bazel-bin/packages/kbn-interpreter", "@kbn/io-ts-utils": "link:bazel-bin/packages/kbn-io-ts-utils", "@kbn/legacy-logging": "link:bazel-bin/packages/kbn-legacy-logging", "@kbn/logging": "link:bazel-bin/packages/kbn-logging", @@ -149,12 +149,13 @@ "@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", "@kbn/std": "link:bazel-bin/packages/kbn-std", "@kbn/tinymath": "link:bazel-bin/packages/kbn-tinymath", - "@kbn/ui-framework": "link:packages/kbn-ui-framework", + "@kbn/ui-framework": "link:bazel-bin/packages/kbn-ui-framework", "@kbn/ui-shared-deps": "link:bazel-bin/packages/kbn-ui-shared-deps", "@kbn/utility-types": "link:bazel-bin/packages/kbn-utility-types", "@kbn/common-utils": "link:bazel-bin/packages/kbn-common-utils", @@ -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", @@ -224,7 +227,7 @@ "deep-freeze-strict": "^1.1.1", "deepmerge": "^4.2.2", "del": "^5.1.0", - "elastic-apm-node": "^3.14.0", + "elastic-apm-node": "^3.16.0", "elasticsearch": "^16.7.0", "execa": "^4.0.2", "exit-hook": "^2.2.0", @@ -446,8 +449,6 @@ "@bazel/typescript": "^3.5.1", "@cypress/snapshot": "^2.1.7", "@cypress/webpack-preprocessor": "^5.6.0", - "@elastic/apm-rum": "^5.6.1", - "@elastic/apm-rum-react": "^1.2.5", "@elastic/eslint-config-kibana": "link:bazel-bin/packages/elastic-eslint-config-kibana", "@elastic/eslint-plugin-eui": "0.0.2", "@elastic/github-checks-reporter": "0.0.20b3", @@ -464,11 +465,11 @@ "@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/pm": "link:packages/kbn-pm", - "@kbn/storybook": "link:packages/kbn-storybook", + "@kbn/storybook": "link:bazel-bin/packages/kbn-storybook", "@kbn/telemetry-tools": "link:bazel-bin/packages/kbn-telemetry-tools", "@kbn/test": "link:packages/kbn-test", "@kbn/test-subj-selector": "link:packages/kbn-test-subj-selector", @@ -513,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", @@ -841,4 +843,4 @@ "yargs": "^15.4.1", "zlib": "^1.0.5" } -} \ No newline at end of file +} diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 6208910729625..d9e2f0e1f9985 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", @@ -23,11 +23,13 @@ filegroup( "//packages/kbn-eslint-plugin-eslint:build", "//packages/kbn-expect:build", "//packages/kbn-i18n:build", + "//packages/kbn-interpreter:build", "//packages/kbn-io-ts-utils:build", "//packages/kbn-legacy-logging:build", "//packages/kbn-logging:build", "//packages/kbn-mapbox-gl:build", "//packages/kbn-monaco:build", + "//packages/kbn-optimizer:build", "//packages/kbn-plugin-generator:build", "//packages/kbn-rule-data-utils:build", "//packages/kbn-securitysolution-list-constants:build", @@ -40,12 +42,15 @@ 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", "//packages/kbn-std:build", + "//packages/kbn-storybook:build", "//packages/kbn-telemetry-tools:build", "//packages/kbn-tinymath:build", + "//packages/kbn-ui-framework:build", "//packages/kbn-ui-shared-deps:build", "//packages/kbn-utility-types:build", "//packages/kbn-utils:build", diff --git a/packages/kbn-cli-dev-mode/package.json b/packages/kbn-cli-dev-mode/package.json index dd491de55c075..cf6fcfd88a26d 100644 --- a/packages/kbn-cli-dev-mode/package.json +++ b/packages/kbn-cli-dev-mode/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-interpreter/.babelrc b/packages/kbn-interpreter/.babelrc deleted file mode 100644 index 309b3d5b3233d..0000000000000 --- a/packages/kbn-interpreter/.babelrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/webpack_preset"], - "plugins": [ - "@babel/plugin-transform-modules-commonjs", - ["@babel/plugin-transform-runtime", { - "regenerator": true - }] - ] -} diff --git a/packages/kbn-interpreter/.npmignore b/packages/kbn-interpreter/.npmignore deleted file mode 100644 index b9bc539e63ce4..0000000000000 --- a/packages/kbn-interpreter/.npmignore +++ /dev/null @@ -1,3 +0,0 @@ -src -tasks -.babelrc diff --git a/packages/kbn-interpreter/BUILD.bazel b/packages/kbn-interpreter/BUILD.bazel new file mode 100644 index 0000000000000..4492faabfdf81 --- /dev/null +++ b/packages/kbn-interpreter/BUILD.bazel @@ -0,0 +1,99 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@npm//pegjs:index.bzl", "pegjs") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-interpreter" +PKG_REQUIRE_NAME = "@kbn/interpreter" + +SOURCE_FILES = glob( + [ + "src/**/*", + ] +) + +TYPE_FILES = [] + +SRCS = SOURCE_FILES + TYPE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "common/package.json", + "package.json", +] + +SRC_DEPS = [ + "@npm//lodash", +] + +TYPES_DEPS = [ + "@npm//@types/jest", + "@npm//@types/lodash", + "@npm//@types/node", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +pegjs( + name = "grammar", + data = [ + ":grammar/grammar.pegjs" + ], + output_dir = True, + args = [ + "--allowed-start-rules", + "expression,argument", + "-o", + "$(@D)/index.js", + "./%s/grammar/grammar.pegjs" % package_name() + ], +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + allow_js = True, + 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 + [":grammar"], + 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-interpreter/common/package.json b/packages/kbn-interpreter/common/package.json index 62061138234d9..2f5277a8e8652 100644 --- a/packages/kbn-interpreter/common/package.json +++ b/packages/kbn-interpreter/common/package.json @@ -1,6 +1,5 @@ { "private": true, "main": "../target/common/index.js", - "types": "../target/common/index.d.ts", - "jsnext:main": "../src/common/index.js" + "types": "../target/common/index.d.ts" } \ No newline at end of file diff --git a/packages/kbn-interpreter/src/common/lib/grammar.peg b/packages/kbn-interpreter/grammar/grammar.pegjs similarity index 100% rename from packages/kbn-interpreter/src/common/lib/grammar.peg rename to packages/kbn-interpreter/grammar/grammar.pegjs diff --git a/packages/kbn-interpreter/package.json b/packages/kbn-interpreter/package.json index fc0936f4b5f53..efdb30e105186 100644 --- a/packages/kbn-interpreter/package.json +++ b/packages/kbn-interpreter/package.json @@ -2,11 +2,5 @@ "name": "@kbn/interpreter", "private": "true", "version": "1.0.0", - "license": "SSPL-1.0 OR Elastic License 2.0", - "scripts": { - "interpreter:peg": "../../node_modules/.bin/pegjs src/common/lib/grammar.peg", - "build": "node scripts/build", - "kbn:bootstrap": "node scripts/build --dev", - "kbn:watch": "node scripts/build --dev --watch" - } + "license": "SSPL-1.0 OR Elastic License 2.0" } \ No newline at end of file diff --git a/packages/kbn-interpreter/src/common/index.js b/packages/kbn-interpreter/src/common/index.ts similarity index 76% rename from packages/kbn-interpreter/src/common/index.js rename to packages/kbn-interpreter/src/common/index.ts index b83d8180980cd..524c854b40429 100644 --- a/packages/kbn-interpreter/src/common/index.js +++ b/packages/kbn-interpreter/src/common/index.ts @@ -6,11 +6,19 @@ * Side Public License, v 1. */ -export { fromExpression, toExpression, safeElementFromExpression } from './lib/ast'; +export { + fromExpression, + toExpression, + safeElementFromExpression, + Ast, + ExpressionFunctionAST, +} from './lib/ast'; export { Fn } from './lib/fn'; export { getType } from './lib/get_type'; export { castProvider } from './lib/cast'; -export { parse } from './lib/grammar'; +// @ts-expect-error +// @internal +export { parse } from '../../grammar'; export { getByAlias } from './lib/get_by_alias'; export { Registry } from './lib/registry'; export { addRegistries, register, registryFactory } from './registries'; diff --git a/packages/kbn-interpreter/src/common/lib/ast.d.ts b/packages/kbn-interpreter/src/common/lib/ast.d.ts deleted file mode 100644 index 0e95cb9901df0..0000000000000 --- a/packages/kbn-interpreter/src/common/lib/ast.d.ts +++ /dev/null @@ -1,25 +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 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 type ExpressionArgAST = string | boolean | number | Ast; - -export interface ExpressionFunctionAST { - type: 'function'; - function: string; - arguments: { - [key: string]: ExpressionArgAST[]; - }; -} - -export interface Ast { - type: 'expression'; - chain: ExpressionFunctionAST[]; -} - -export declare function fromExpression(expression: string): Ast; -export declare function toExpression(astObj: Ast, type?: string): string; diff --git a/packages/kbn-interpreter/src/common/lib/ast.from_expression.test.js b/packages/kbn-interpreter/src/common/lib/ast.from_expression.test.js index c67a266e1276a..a098a3fdce0f6 100644 --- a/packages/kbn-interpreter/src/common/lib/ast.from_expression.test.js +++ b/packages/kbn-interpreter/src/common/lib/ast.from_expression.test.js @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { fromExpression } from './ast'; +import { fromExpression } from '@kbn/interpreter/target/common/lib/ast'; import { getType } from './get_type'; describe('ast fromExpression', () => { diff --git a/packages/kbn-interpreter/src/common/lib/ast.to_expression.test.js b/packages/kbn-interpreter/src/common/lib/ast.to_expression.test.js index c60412f05c15a..b500ca06836a4 100644 --- a/packages/kbn-interpreter/src/common/lib/ast.to_expression.test.js +++ b/packages/kbn-interpreter/src/common/lib/ast.to_expression.test.js @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { toExpression } from './ast'; +import { toExpression } from '@kbn/interpreter/common'; describe('ast toExpression', () => { describe('single expression', () => { diff --git a/packages/kbn-interpreter/src/common/lib/ast.js b/packages/kbn-interpreter/src/common/lib/ast.ts similarity index 75% rename from packages/kbn-interpreter/src/common/lib/ast.js rename to packages/kbn-interpreter/src/common/lib/ast.ts index fb471e34ccc69..791c94809f35c 100644 --- a/packages/kbn-interpreter/src/common/lib/ast.js +++ b/packages/kbn-interpreter/src/common/lib/ast.ts @@ -7,12 +7,35 @@ */ import { getType } from './get_type'; -import { parse } from './grammar'; +// @ts-expect-error +import { parse } from '../../../grammar'; -function getArgumentString(arg, argKey, level = 0) { +export type ExpressionArgAST = string | boolean | number | Ast; + +export interface ExpressionFunctionAST { + type: 'function'; + function: string; + arguments: { + [key: string]: ExpressionArgAST[]; + }; +} + +export interface Ast { + /** @internal */ + function: any; + /** @internal */ + arguments: any; + type: 'expression'; + chain: ExpressionFunctionAST[]; + /** @internal */ + replace(regExp: RegExp, s: string): string; +} + +function getArgumentString(arg: Ast, argKey: string | undefined, level = 0) { const type = getType(arg); - function maybeArgKey(argKey, argString) { + // eslint-disable-next-line @typescript-eslint/no-shadow + function maybeArgKey(argKey: string | null | undefined, argString: string) { return argKey == null || argKey === '_' ? argString : `${argKey}=${argString}`; } @@ -36,7 +59,7 @@ function getArgumentString(arg, argKey, level = 0) { throw new Error(`Invalid argument type in AST: ${type}`); } -function getExpressionArgs(block, level = 0) { +function getExpressionArgs(block: Ast, level = 0) { const args = block.arguments; const hasValidArgs = typeof args === 'object' && args != null && !Array.isArray(args); @@ -45,7 +68,7 @@ function getExpressionArgs(block, level = 0) { const argKeys = Object.keys(args); const MAX_LINE_LENGTH = 80; // length before wrapping arguments return argKeys.map((argKey) => - args[argKey].reduce((acc, arg) => { + args[argKey].reduce((acc: any, arg: any) => { const argString = getArgumentString(arg, argKey, level); const lineLength = acc.split('\n').pop().length; @@ -63,12 +86,12 @@ function getExpressionArgs(block, level = 0) { ); } -function fnWithArgs(fnName, args) { +function fnWithArgs(fnName: any, args: any[]) { if (!args || args.length === 0) return fnName; return `${fnName} ${args.join(' ')}`; } -function getExpression(chain, level = 0) { +function getExpression(chain: any[], level = 0) { if (!chain) throw new Error('Expressions must contain a chain'); // break new functions onto new lines if we're not in a nested/sub-expression @@ -90,7 +113,7 @@ function getExpression(chain, level = 0) { .join(separator); } -export function fromExpression(expression, type = 'expression') { +export function fromExpression(expression: string, type = 'expression'): Ast { try { return parse(String(expression), { startRule: type }); } catch (e) { @@ -99,7 +122,7 @@ export function fromExpression(expression, type = 'expression') { } // TODO: OMG This is so bad, we need to talk about the right way to handle bad expressions since some are element based and others not -export function safeElementFromExpression(expression) { +export function safeElementFromExpression(expression: string) { try { return fromExpression(expression); } catch (e) { @@ -116,8 +139,11 @@ Thanks for understanding, } // TODO: Respect the user's existing formatting -export function toExpression(astObj, type = 'expression') { - if (type === 'argument') return getArgumentString(astObj); +export function toExpression(astObj: Ast, type = 'expression'): string { + if (type === 'argument') { + // @ts-ignore + return getArgumentString(astObj); + } const validType = ['expression', 'function'].includes(getType(astObj)); if (!validType) throw new Error('Expression must be an expression or argument function'); diff --git a/packages/kbn-interpreter/src/common/lib/get_type.js b/packages/kbn-interpreter/src/common/lib/get_type.ts similarity index 92% rename from packages/kbn-interpreter/src/common/lib/get_type.js rename to packages/kbn-interpreter/src/common/lib/get_type.ts index 7ae6dab029176..b6dff67bf5dc9 100644 --- a/packages/kbn-interpreter/src/common/lib/get_type.js +++ b/packages/kbn-interpreter/src/common/lib/get_type.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -export function getType(node) { +export function getType(node: any): string { if (node == null) return 'null'; if (typeof node === 'object') { if (!node.type) throw new Error('Objects must have a type property'); diff --git a/packages/kbn-interpreter/src/common/lib/grammar.js b/packages/kbn-interpreter/src/common/lib/grammar.js deleted file mode 100644 index 3f473b1beea63..0000000000000 --- a/packages/kbn-interpreter/src/common/lib/grammar.js +++ /dev/null @@ -1,1053 +0,0 @@ -/* - * Generated by PEG.js 0.10.0. - * - * http://pegjs.org/ - */ - -"use strict"; - -function peg$subclass(child, parent) { - function ctor() { this.constructor = child; } - ctor.prototype = parent.prototype; - child.prototype = new ctor(); -} - -function peg$SyntaxError(message, expected, found, location) { - this.message = message; - this.expected = expected; - this.found = found; - this.location = location; - this.name = "SyntaxError"; - - if (typeof Error.captureStackTrace === "function") { - Error.captureStackTrace(this, peg$SyntaxError); - } -} - -peg$subclass(peg$SyntaxError, Error); - -peg$SyntaxError.buildMessage = function(expected, found) { - var DESCRIBE_EXPECTATION_FNS = { - literal: function(expectation) { - return "\"" + literalEscape(expectation.text) + "\""; - }, - - "class": function(expectation) { - var escapedParts = "", - i; - - for (i = 0; i < expectation.parts.length; i++) { - escapedParts += expectation.parts[i] instanceof Array - ? classEscape(expectation.parts[i][0]) + "-" + classEscape(expectation.parts[i][1]) - : classEscape(expectation.parts[i]); - } - - return "[" + (expectation.inverted ? "^" : "") + escapedParts + "]"; - }, - - any: function(expectation) { - return "any character"; - }, - - end: function(expectation) { - return "end of input"; - }, - - other: function(expectation) { - return expectation.description; - } - }; - - function hex(ch) { - return ch.charCodeAt(0).toString(16).toUpperCase(); - } - - function literalEscape(s) { - return s - .replace(/\\/g, '\\\\') - .replace(/"/g, '\\"') - .replace(/\0/g, '\\0') - .replace(/\t/g, '\\t') - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r') - .replace(/[\x00-\x0F]/g, function(ch) { return '\\x0' + hex(ch); }) - .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return '\\x' + hex(ch); }); - } - - function classEscape(s) { - return s - .replace(/\\/g, '\\\\') - .replace(/\]/g, '\\]') - .replace(/\^/g, '\\^') - .replace(/-/g, '\\-') - .replace(/\0/g, '\\0') - .replace(/\t/g, '\\t') - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r') - .replace(/[\x00-\x0F]/g, function(ch) { return '\\x0' + hex(ch); }) - .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return '\\x' + hex(ch); }); - } - - function describeExpectation(expectation) { - return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation); - } - - function describeExpected(expected) { - var descriptions = new Array(expected.length), - i, j; - - for (i = 0; i < expected.length; i++) { - descriptions[i] = describeExpectation(expected[i]); - } - - descriptions.sort(); - - if (descriptions.length > 0) { - for (i = 1, j = 1; i < descriptions.length; i++) { - if (descriptions[i - 1] !== descriptions[i]) { - descriptions[j] = descriptions[i]; - j++; - } - } - descriptions.length = j; - } - - switch (descriptions.length) { - case 1: - return descriptions[0]; - - case 2: - return descriptions[0] + " or " + descriptions[1]; - - default: - return descriptions.slice(0, -1).join(", ") - + ", or " - + descriptions[descriptions.length - 1]; - } - } - - function describeFound(found) { - return found ? "\"" + literalEscape(found) + "\"" : "end of input"; - } - - return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found."; -}; - -function peg$parse(input, options) { - options = options !== void 0 ? options : {}; - - var peg$FAILED = {}, - - peg$startRuleFunctions = { expression: peg$parseexpression, argument: peg$parseargument }, - peg$startRuleFunction = peg$parseexpression, - - peg$c0 = "|", - peg$c1 = peg$literalExpectation("|", false), - peg$c2 = function(first, fn) { return fn; }, - peg$c3 = function(first, rest) { - return addMeta({ - type: 'expression', - chain: first ? [first].concat(rest) : [] - }, text(), location()); - }, - peg$c4 = peg$otherExpectation("function"), - peg$c5 = function(name, arg_list) { - return addMeta({ - type: 'function', - function: name, - arguments: arg_list - }, text(), location()); - }, - peg$c6 = "=", - peg$c7 = peg$literalExpectation("=", false), - peg$c8 = function(name, value) { - return { name, value }; - }, - peg$c9 = function(value) { - return { name: '_', value }; - }, - peg$c10 = "$", - peg$c11 = peg$literalExpectation("$", false), - peg$c12 = "{", - peg$c13 = peg$literalExpectation("{", false), - peg$c14 = "}", - peg$c15 = peg$literalExpectation("}", false), - peg$c16 = function(expression) { return expression; }, - peg$c17 = function(value) { - return addMeta(value, text(), location()); - }, - peg$c18 = function(arg) { return arg; }, - peg$c19 = function(args) { - return args.reduce((accumulator, { name, value }) => ({ - ...accumulator, - [name]: (accumulator[name] || []).concat(value) - }), {}); - }, - peg$c20 = /^[a-zA-Z0-9_\-]/, - peg$c21 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"], "_", "-"], false, false), - peg$c22 = function(name) { - return name.join(''); - }, - peg$c23 = peg$otherExpectation("literal"), - peg$c24 = "\"", - peg$c25 = peg$literalExpectation("\"", false), - peg$c26 = function(chars) { return chars.join(''); }, - peg$c27 = "'", - peg$c28 = peg$literalExpectation("'", false), - peg$c29 = function(string) { // this also matches nulls, booleans, and numbers - var result = string.join(''); - // Sort of hacky, but PEG doesn't have backtracking so - // a null/boolean/number rule is hard to read, and performs worse - if (result === 'null') return null; - if (result === 'true') return true; - if (result === 'false') return false; - if (isNaN(Number(result))) return result; // 5bears - return Number(result); - }, - peg$c30 = /^[ \t\r\n]/, - peg$c31 = peg$classExpectation([" ", "\t", "\r", "\n"], false, false), - peg$c32 = "\\", - peg$c33 = peg$literalExpectation("\\", false), - peg$c34 = /^["'(){}<>[\]$`|= \t\n\r]/, - peg$c35 = peg$classExpectation(["\"", "'", "(", ")", "{", "}", "<", ">", "[", "]", "$", "`", "|", "=", " ", "\t", "\n", "\r"], false, false), - peg$c36 = function(sequence) { return sequence; }, - peg$c37 = /^[^"'(){}<>[\]$`|= \t\n\r]/, - peg$c38 = peg$classExpectation(["\"", "'", "(", ")", "{", "}", "<", ">", "[", "]", "$", "`", "|", "=", " ", "\t", "\n", "\r"], true, false), - peg$c39 = /^[^"]/, - peg$c40 = peg$classExpectation(["\""], true, false), - peg$c41 = /^[^']/, - peg$c42 = peg$classExpectation(["'"], true, false), - - peg$currPos = 0, - peg$savedPos = 0, - peg$posDetailsCache = [{ line: 1, column: 1 }], - peg$maxFailPos = 0, - peg$maxFailExpected = [], - peg$silentFails = 0, - - peg$result; - - if ("startRule" in options) { - if (!(options.startRule in peg$startRuleFunctions)) { - throw new Error("Can't start parsing from rule \"" + options.startRule + "\"."); - } - - peg$startRuleFunction = peg$startRuleFunctions[options.startRule]; - } - - function text() { - return input.substring(peg$savedPos, peg$currPos); - } - - function location() { - return peg$computeLocation(peg$savedPos, peg$currPos); - } - - function expected(description, location) { - location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos) - - throw peg$buildStructuredError( - [peg$otherExpectation(description)], - input.substring(peg$savedPos, peg$currPos), - location - ); - } - - function error(message, location) { - location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos) - - throw peg$buildSimpleError(message, location); - } - - function peg$literalExpectation(text, ignoreCase) { - return { type: "literal", text: text, ignoreCase: ignoreCase }; - } - - function peg$classExpectation(parts, inverted, ignoreCase) { - return { type: "class", parts: parts, inverted: inverted, ignoreCase: ignoreCase }; - } - - function peg$anyExpectation() { - return { type: "any" }; - } - - function peg$endExpectation() { - return { type: "end" }; - } - - function peg$otherExpectation(description) { - return { type: "other", description: description }; - } - - function peg$computePosDetails(pos) { - var details = peg$posDetailsCache[pos], p; - - if (details) { - return details; - } else { - p = pos - 1; - while (!peg$posDetailsCache[p]) { - p--; - } - - details = peg$posDetailsCache[p]; - details = { - line: details.line, - column: details.column - }; - - while (p < pos) { - if (input.charCodeAt(p) === 10) { - details.line++; - details.column = 1; - } else { - details.column++; - } - - p++; - } - - peg$posDetailsCache[pos] = details; - return details; - } - } - - function peg$computeLocation(startPos, endPos) { - var startPosDetails = peg$computePosDetails(startPos), - endPosDetails = peg$computePosDetails(endPos); - - return { - start: { - offset: startPos, - line: startPosDetails.line, - column: startPosDetails.column - }, - end: { - offset: endPos, - line: endPosDetails.line, - column: endPosDetails.column - } - }; - } - - function peg$fail(expected) { - if (peg$currPos < peg$maxFailPos) { return; } - - if (peg$currPos > peg$maxFailPos) { - peg$maxFailPos = peg$currPos; - peg$maxFailExpected = []; - } - - peg$maxFailExpected.push(expected); - } - - function peg$buildSimpleError(message, location) { - return new peg$SyntaxError(message, null, null, location); - } - - function peg$buildStructuredError(expected, found, location) { - return new peg$SyntaxError( - peg$SyntaxError.buildMessage(expected, found), - expected, - found, - location - ); - } - - function peg$parseexpression() { - var s0, s1, s2, s3, s4, s5, s6, s7; - - s0 = peg$currPos; - s1 = peg$parsespace(); - if (s1 === peg$FAILED) { - s1 = null; - } - if (s1 !== peg$FAILED) { - s2 = peg$parsefunction(); - if (s2 === peg$FAILED) { - s2 = null; - } - if (s2 !== peg$FAILED) { - s3 = []; - s4 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 124) { - s5 = peg$c0; - peg$currPos++; - } else { - s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c1); } - } - if (s5 !== peg$FAILED) { - s6 = peg$parsespace(); - if (s6 === peg$FAILED) { - s6 = null; - } - if (s6 !== peg$FAILED) { - s7 = peg$parsefunction(); - if (s7 !== peg$FAILED) { - peg$savedPos = s4; - s5 = peg$c2(s2, s7); - s4 = s5; - } else { - peg$currPos = s4; - s4 = peg$FAILED; - } - } else { - peg$currPos = s4; - s4 = peg$FAILED; - } - } else { - peg$currPos = s4; - s4 = peg$FAILED; - } - while (s4 !== peg$FAILED) { - s3.push(s4); - s4 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 124) { - s5 = peg$c0; - peg$currPos++; - } else { - s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c1); } - } - if (s5 !== peg$FAILED) { - s6 = peg$parsespace(); - if (s6 === peg$FAILED) { - s6 = null; - } - if (s6 !== peg$FAILED) { - s7 = peg$parsefunction(); - if (s7 !== peg$FAILED) { - peg$savedPos = s4; - s5 = peg$c2(s2, s7); - s4 = s5; - } else { - peg$currPos = s4; - s4 = peg$FAILED; - } - } else { - peg$currPos = s4; - s4 = peg$FAILED; - } - } else { - peg$currPos = s4; - s4 = peg$FAILED; - } - } - if (s3 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c3(s2, s3); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - - return s0; - } - - function peg$parsefunction() { - var s0, s1, s2; - - peg$silentFails++; - s0 = peg$currPos; - s1 = peg$parseidentifier(); - if (s1 !== peg$FAILED) { - s2 = peg$parsearg_list(); - if (s2 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c5(s1, s2); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - peg$silentFails--; - if (s0 === peg$FAILED) { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c4); } - } - - return s0; - } - - function peg$parseargument_assignment() { - var s0, s1, s2, s3, s4, s5; - - s0 = peg$currPos; - s1 = peg$parseidentifier(); - if (s1 !== peg$FAILED) { - s2 = peg$parsespace(); - if (s2 === peg$FAILED) { - s2 = null; - } - if (s2 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 61) { - s3 = peg$c6; - peg$currPos++; - } else { - s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c7); } - } - if (s3 !== peg$FAILED) { - s4 = peg$parsespace(); - if (s4 === peg$FAILED) { - s4 = null; - } - if (s4 !== peg$FAILED) { - s5 = peg$parseargument(); - if (s5 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c8(s1, s5); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - if (s0 === peg$FAILED) { - s0 = peg$currPos; - s1 = peg$parseargument(); - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c9(s1); - } - s0 = s1; - } - - return s0; - } - - function peg$parseargument() { - var s0, s1, s2, s3, s4; - - s0 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 36) { - s1 = peg$c10; - peg$currPos++; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c11); } - } - if (s1 === peg$FAILED) { - s1 = null; - } - if (s1 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 123) { - s2 = peg$c12; - peg$currPos++; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c13); } - } - if (s2 !== peg$FAILED) { - s3 = peg$parseexpression(); - if (s3 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 125) { - s4 = peg$c14; - peg$currPos++; - } else { - s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c15); } - } - if (s4 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c16(s3); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - if (s0 === peg$FAILED) { - s0 = peg$currPos; - s1 = peg$parseliteral(); - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c17(s1); - } - s0 = s1; - } - - return s0; - } - - function peg$parsearg_list() { - var s0, s1, s2, s3, s4; - - s0 = peg$currPos; - s1 = []; - s2 = peg$currPos; - s3 = peg$parsespace(); - if (s3 !== peg$FAILED) { - s4 = peg$parseargument_assignment(); - if (s4 !== peg$FAILED) { - peg$savedPos = s2; - s3 = peg$c18(s4); - s2 = s3; - } else { - peg$currPos = s2; - s2 = peg$FAILED; - } - } else { - peg$currPos = s2; - s2 = peg$FAILED; - } - while (s2 !== peg$FAILED) { - s1.push(s2); - s2 = peg$currPos; - s3 = peg$parsespace(); - if (s3 !== peg$FAILED) { - s4 = peg$parseargument_assignment(); - if (s4 !== peg$FAILED) { - peg$savedPos = s2; - s3 = peg$c18(s4); - s2 = s3; - } else { - peg$currPos = s2; - s2 = peg$FAILED; - } - } else { - peg$currPos = s2; - s2 = peg$FAILED; - } - } - if (s1 !== peg$FAILED) { - s2 = peg$parsespace(); - if (s2 === peg$FAILED) { - s2 = null; - } - if (s2 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c19(s1); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - - return s0; - } - - function peg$parseidentifier() { - var s0, s1, s2; - - s0 = peg$currPos; - s1 = []; - if (peg$c20.test(input.charAt(peg$currPos))) { - s2 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c21); } - } - if (s2 !== peg$FAILED) { - while (s2 !== peg$FAILED) { - s1.push(s2); - if (peg$c20.test(input.charAt(peg$currPos))) { - s2 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c21); } - } - } - } else { - s1 = peg$FAILED; - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c22(s1); - } - s0 = s1; - - return s0; - } - - function peg$parseliteral() { - var s0, s1; - - peg$silentFails++; - s0 = peg$parsephrase(); - if (s0 === peg$FAILED) { - s0 = peg$parseunquoted_string_or_number(); - } - peg$silentFails--; - if (s0 === peg$FAILED) { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c23); } - } - - return s0; - } - - function peg$parsephrase() { - var s0, s1, s2, s3; - - s0 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 34) { - s1 = peg$c24; - peg$currPos++; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c25); } - } - if (s1 !== peg$FAILED) { - s2 = []; - s3 = peg$parsedq_char(); - while (s3 !== peg$FAILED) { - s2.push(s3); - s3 = peg$parsedq_char(); - } - if (s2 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 34) { - s3 = peg$c24; - peg$currPos++; - } else { - s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c25); } - } - if (s3 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c26(s2); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 39) { - s1 = peg$c27; - peg$currPos++; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c28); } - } - if (s1 !== peg$FAILED) { - s2 = []; - s3 = peg$parsesq_char(); - while (s3 !== peg$FAILED) { - s2.push(s3); - s3 = peg$parsesq_char(); - } - if (s2 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 39) { - s3 = peg$c27; - peg$currPos++; - } else { - s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c28); } - } - if (s3 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c26(s2); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } - - return s0; - } - - function peg$parseunquoted_string_or_number() { - var s0, s1, s2; - - s0 = peg$currPos; - s1 = []; - s2 = peg$parseunquoted(); - if (s2 !== peg$FAILED) { - while (s2 !== peg$FAILED) { - s1.push(s2); - s2 = peg$parseunquoted(); - } - } else { - s1 = peg$FAILED; - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c29(s1); - } - s0 = s1; - - return s0; - } - - function peg$parsespace() { - var s0, s1; - - s0 = []; - if (peg$c30.test(input.charAt(peg$currPos))) { - s1 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c31); } - } - if (s1 !== peg$FAILED) { - while (s1 !== peg$FAILED) { - s0.push(s1); - if (peg$c30.test(input.charAt(peg$currPos))) { - s1 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c31); } - } - } - } else { - s0 = peg$FAILED; - } - - return s0; - } - - function peg$parseunquoted() { - var s0, s1, s2; - - s0 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 92) { - s1 = peg$c32; - peg$currPos++; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c33); } - } - if (s1 !== peg$FAILED) { - if (peg$c34.test(input.charAt(peg$currPos))) { - s2 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c35); } - } - if (s2 === peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 92) { - s2 = peg$c32; - peg$currPos++; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c33); } - } - } - if (s2 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c36(s2); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - if (s0 === peg$FAILED) { - if (peg$c37.test(input.charAt(peg$currPos))) { - s0 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c38); } - } - } - - return s0; - } - - function peg$parsedq_char() { - var s0, s1, s2; - - s0 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 92) { - s1 = peg$c32; - peg$currPos++; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c33); } - } - if (s1 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 34) { - s2 = peg$c24; - peg$currPos++; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c25); } - } - if (s2 === peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 92) { - s2 = peg$c32; - peg$currPos++; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c33); } - } - } - if (s2 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c36(s2); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - if (s0 === peg$FAILED) { - if (peg$c39.test(input.charAt(peg$currPos))) { - s0 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c40); } - } - } - - return s0; - } - - function peg$parsesq_char() { - var s0, s1, s2; - - s0 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 92) { - s1 = peg$c32; - peg$currPos++; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c33); } - } - if (s1 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 39) { - s2 = peg$c27; - peg$currPos++; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c28); } - } - if (s2 === peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 92) { - s2 = peg$c32; - peg$currPos++; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c33); } - } - } - if (s2 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c36(s2); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - if (s0 === peg$FAILED) { - if (peg$c41.test(input.charAt(peg$currPos))) { - s0 = input.charAt(peg$currPos); - peg$currPos++; - } else { - s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c42); } - } - } - - return s0; - } - - - function addMeta(node, text, { start: { offset: start }, end: { offset: end } }) { - if (!options.addMeta) return node; - return { node, text, start, end }; - } - - - peg$result = peg$startRuleFunction(); - - if (peg$result !== peg$FAILED && peg$currPos === input.length) { - return peg$result; - } else { - if (peg$result !== peg$FAILED && peg$currPos < input.length) { - peg$fail(peg$endExpectation()); - } - - throw peg$buildStructuredError( - peg$maxFailExpected, - peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null, - peg$maxFailPos < input.length - ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1) - : peg$computeLocation(peg$maxFailPos, peg$maxFailPos) - ); - } -} - -module.exports = { - SyntaxError: peg$SyntaxError, - parse: peg$parse -}; diff --git a/packages/kbn-interpreter/src/common/lib/registry.d.ts b/packages/kbn-interpreter/src/common/lib/registry.d.ts deleted file mode 100644 index 766839ebf0e02..0000000000000 --- a/packages/kbn-interpreter/src/common/lib/registry.d.ts +++ /dev/null @@ -1,25 +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 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 class Registry { - constructor(prop?: string); - - public wrapper(obj: ItemSpec): Item; - - public register(fn: () => ItemSpec): void; - - public toJS(): { [key: string]: any }; - - public toArray(): Item[]; - - public get(name: string): Item; - - public getProp(): string; - - public reset(): void; -} diff --git a/packages/kbn-interpreter/src/common/lib/registry.js b/packages/kbn-interpreter/src/common/lib/registry.ts similarity index 73% rename from packages/kbn-interpreter/src/common/lib/registry.js rename to packages/kbn-interpreter/src/common/lib/registry.ts index 309f92ea24f6d..11f41ff736e96 100644 --- a/packages/kbn-interpreter/src/common/lib/registry.js +++ b/packages/kbn-interpreter/src/common/lib/registry.ts @@ -8,49 +8,59 @@ import { clone } from 'lodash'; -export class Registry { +export class Registry { + private readonly _prop: string; + // eslint-disable-next-line @typescript-eslint/ban-types + private _indexed: Object; + constructor(prop = 'name') { if (typeof prop !== 'string') throw new Error('Registry property name must be a string'); this._prop = prop; this._indexed = new Object(); } - wrapper(obj) { + wrapper(obj: ItemSpec): Item { + // @ts-ignore return obj; } - register(fn) { + register(fn: () => ItemSpec): void { const obj = typeof fn === 'function' ? fn() : fn; + // @ts-ignore if (typeof obj !== 'object' || !obj[this._prop]) { throw new Error(`Registered functions must return an object with a ${this._prop} property`); } + // @ts-ignore this._indexed[obj[this._prop].toLowerCase()] = this.wrapper(obj); } - toJS() { + toJS(): { [key: string]: any } { return Object.keys(this._indexed).reduce((acc, key) => { + // @ts-ignore acc[key] = this.get(key); return acc; }, {}); } - toArray() { + toArray(): Item[] { return Object.keys(this._indexed).map((key) => this.get(key)); } - get(name) { + get(name: string): Item { + // @ts-ignore if (name === undefined) return null; const lowerCaseName = name.toLowerCase(); + // @ts-ignore return this._indexed[lowerCaseName] ? clone(this._indexed[lowerCaseName]) : null; } - getProp() { + getProp(): string { return this._prop; } - reset() { + reset(): void { this._indexed = new Object(); } } diff --git a/packages/kbn-interpreter/tasks/build/__fixtures__/sample.js b/packages/kbn-interpreter/tasks/build/__fixtures__/sample.js deleted file mode 100644 index f831545743f10..0000000000000 --- a/packages/kbn-interpreter/tasks/build/__fixtures__/sample.js +++ /dev/null @@ -1,3 +0,0 @@ -/* eslint-disable */ -import util from 'util'; -console.log(util.format('hello world')); diff --git a/packages/kbn-interpreter/tasks/build/cli.js b/packages/kbn-interpreter/tasks/build/cli.js deleted file mode 100644 index 82e4475b409c3..0000000000000 --- a/packages/kbn-interpreter/tasks/build/cli.js +++ /dev/null @@ -1,82 +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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -const { relative } = require('path'); - -const getopts = require('getopts'); -const del = require('del'); -const supportsColor = require('supports-color'); -const { ToolingLog, withProcRunner, pickLevelFromFlags } = require('@kbn/dev-utils'); - -const { ROOT_DIR, BUILD_DIR } = require('./paths'); - -const unknownFlags = []; -const flags = getopts(process.argv, { - boolean: ['watch', 'dev', 'help', 'debug'], - unknown(name) { - unknownFlags.push(name); - }, -}); - -const log = new ToolingLog({ - level: pickLevelFromFlags(flags), - writeTo: process.stdout, -}); - -if (unknownFlags.length) { - log.error(`Unknown flag(s): ${unknownFlags.join(', ')}`); - flags.help = true; - process.exitCode = 1; -} - -if (flags.help) { - log.info(` - Simple build tool for @kbn/interpreter package - - --dev Build for development, include source maps - --watch Run in watch mode - --debug Turn on debug logging - `); - process.exit(); -} - -withProcRunner(log, async (proc) => { - log.info('Deleting old output'); - await del(BUILD_DIR); - - const cwd = ROOT_DIR; - const env = { ...process.env }; - if (supportsColor.stdout) { - env.FORCE_COLOR = 'true'; - } - - log.info(`Starting babel ${flags.watch ? ' in watch mode' : ''}`); - await Promise.all([ - proc.run('babel ', { - cmd: 'babel', - args: [ - 'src', - '--ignore', - `*.test.js`, - '--out-dir', - relative(cwd, BUILD_DIR), - '--copy-files', - ...(flags.dev ? ['--source-maps', 'inline'] : []), - ...(flags.watch ? ['--watch'] : ['--quiet']), - ], - wait: true, - env, - cwd, - }), - ]); - - log.success('Complete'); -}).catch((error) => { - log.error(error); - process.exit(1); -}); diff --git a/packages/kbn-interpreter/tasks/build/paths.js b/packages/kbn-interpreter/tasks/build/paths.js deleted file mode 100644 index a4cdba90a110a..0000000000000 --- a/packages/kbn-interpreter/tasks/build/paths.js +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -const { resolve } = require('path'); - -exports.ROOT_DIR = resolve(__dirname, '../../'); -exports.SOURCE_DIR = resolve(exports.ROOT_DIR, 'src'); -exports.BUILD_DIR = resolve(exports.ROOT_DIR, 'target'); - -exports.BABEL_PRESET_PATH = require.resolve('@kbn/babel-preset/webpack_preset'); diff --git a/packages/kbn-interpreter/tsconfig.json b/packages/kbn-interpreter/tsconfig.json index 3b81bbb118a55..011ed877146e8 100644 --- a/packages/kbn-interpreter/tsconfig.json +++ b/packages/kbn-interpreter/tsconfig.json @@ -1,7 +1,21 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-interpreter" + "allowJs": true, + "incremental": true, + "outDir": "./target", + "declaration": true, + "declarationMap": true, + "rootDir": "src", + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-interpreter/src", + "stripInternal": true, + "types": [ + "jest", + "node" + ] }, - "include": ["index.d.ts", "src/**/*.d.ts"] + "include": [ + "src/**/*", + ] } diff --git a/packages/kbn-monaco/src/monaco_imports.ts b/packages/kbn-monaco/src/monaco_imports.ts index 92ea23347c374..3f689e6ec0c01 100644 --- a/packages/kbn-monaco/src/monaco_imports.ts +++ b/packages/kbn-monaco/src/monaco_imports.ts @@ -7,7 +7,6 @@ */ /* eslint-disable @kbn/eslint/module_migration */ - import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import 'monaco-editor/esm/vs/base/common/worker/simpleWorker'; @@ -23,4 +22,7 @@ import 'monaco-editor/esm/vs/editor/contrib/hover/hover.js'; // Needed for hover import 'monaco-editor/esm/vs/editor/contrib/parameterHints/parameterHints.js'; // Needed for signature import 'monaco-editor/esm/vs/editor/contrib/bracketMatching/bracketMatching.js'; // Needed for brackets matching highlight +import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js'; // Needed for basic javascript support +import 'monaco-editor/esm/vs/basic-languages/xml/xml.contribution.js'; // Needed for basic xml support + export { monaco }; 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 9adc075a7983f..c6960621359c7 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -34,7 +34,7 @@ pageLoadAssetSize: indexManagement: 140608 indexPatternManagement: 28222 infra: 184320 - fleet: 450005 + fleet: 465774 ingestPipelines: 58003 inputControlVis: 172675 inspector: 148711 @@ -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/package.json b/packages/kbn-plugin-helpers/package.json index 2d642d9ede13b..36a37075191a3 100644 --- a/packages/kbn-plugin-helpers/package.json +++ b/packages/kbn-plugin-helpers/package.json @@ -15,8 +15,5 @@ "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-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 1311eb4d7c638..5be9dff630ed5 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -48479,7 +48479,7 @@ async function runBazelCommandWithRunner(bazelCommandRunner, bazelArgs, offline stdio: 'pipe' }); - if (offline || !offline) { + if (offline) { bazelArgs = [...bazelArgs, '--config=offline']; } @@ -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-pm/src/utils/bazel/run.ts b/packages/kbn-pm/src/utils/bazel/run.ts index 5f3743876e0e4..c030081e53daa 100644 --- a/packages/kbn-pm/src/utils/bazel/run.ts +++ b/packages/kbn-pm/src/utils/bazel/run.ts @@ -29,7 +29,7 @@ async function runBazelCommandWithRunner( stdio: 'pipe', }; - if (offline || !offline) { + if (offline) { bazelArgs = [...bazelArgs, '--config=offline']; } 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([]); const [paginationInfo, setPagination] = useState(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-list-utils/src/helpers/index.ts b/packages/kbn-securitysolution-list-utils/src/helpers/index.ts index a483da152ac89..d208624b69fc5 100644 --- a/packages/kbn-securitysolution-list-utils/src/helpers/index.ts +++ b/packages/kbn-securitysolution-list-utils/src/helpers/index.ts @@ -95,6 +95,10 @@ export const filterExceptionItems = ( } }, []); + if (entries.length === 0) { + return acc; + } + const item = { ...exception, entries }; if (exceptionListItemSchema.is(item)) { 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: ['/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-storybook/typings.d.ts b/packages/kbn-securitysolution-t-grid/src/index.ts similarity index 75% rename from packages/kbn-storybook/typings.d.ts rename to packages/kbn-securitysolution-t-grid/src/index.ts index b940de2829909..0c2e9a7dbea8b 100644 --- a/packages/kbn-storybook/typings.d.ts +++ b/packages/kbn-securitysolution-t-grid/src/index.ts @@ -6,5 +6,6 @@ * Side Public License, v 1. */ -// Storybook react doesn't declare this in its typings, but it's there. -declare module '@storybook/react/standalone'; +export * from './constants'; +export * from './utils'; +export * from './mock'; diff --git a/packages/kbn-interpreter/scripts/build.js b/packages/kbn-securitysolution-t-grid/src/mock/index.ts similarity index 90% rename from packages/kbn-interpreter/scripts/build.js rename to packages/kbn-securitysolution-t-grid/src/mock/index.ts index 21b7f86c6bc34..dc1b63dfc33b0 100644 --- a/packages/kbn-interpreter/scripts/build.js +++ b/packages/kbn-securitysolution-t-grid/src/mock/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -require('../tasks/build/cli'); +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-storybook/BUILD.bazel b/packages/kbn-storybook/BUILD.bazel new file mode 100644 index 0000000000000..e18256aeb8da4 --- /dev/null +++ b/packages/kbn-storybook/BUILD.bazel @@ -0,0 +1,98 @@ +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-storybook" +PKG_REQUIRE_NAME = "@kbn/storybook" + +SOURCE_FILES = glob( + [ + "lib/**/*.ts", + "lib/**/*.tsx", + "*.ts", + ], + exclude = ["**/*.test.*"], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "preset/package.json", + "templates/index.ejs", + "package.json", + "README.md", + "preset.js", +] + +SRC_DEPS = [ + "//packages/kbn-dev-utils", + "//packages/kbn-ui-shared-deps", + "@npm//@storybook/addons", + "@npm//@storybook/api", + "@npm//@storybook/components", + "@npm//@storybook/core", + "@npm//@storybook/node-logger", + "@npm//@storybook/react", + "@npm//@storybook/theming", + "@npm//loader-utils", + "@npm//react", + "@npm//webpack", + "@npm//webpack-merge", +] + +TYPES_DEPS = [ + "@npm//@types/loader-utils", + "@npm//@types/node", + "@npm//@types/webpack", + "@npm//@types/webpack-merge", +] + +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, + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-storybook/package.json b/packages/kbn-storybook/package.json index f2e4c9b3418b1..f3c12f19a0793 100644 --- a/packages/kbn-storybook/package.json +++ b/packages/kbn-storybook/package.json @@ -7,10 +7,5 @@ "types": "./target/index.d.ts", "kibana": { "devOnly": true - }, - "scripts": { - "build": "../../node_modules/.bin/tsc", - "kbn:bootstrap": "yarn build", - "kbn:watch": "yarn build --watch" } } \ No newline at end of file diff --git a/packages/kbn-storybook/preset.js b/packages/kbn-storybook/preset.js index c1b7195c141b4..be0012a3818b1 100644 --- a/packages/kbn-storybook/preset.js +++ b/packages/kbn-storybook/preset.js @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +// eslint-disable-next-line const webpackConfig = require('./target/webpack.config').default; module.exports = { diff --git a/packages/kbn-storybook/preset/package.json b/packages/kbn-storybook/preset/package.json new file mode 100644 index 0000000000000..7cd7517d64dde --- /dev/null +++ b/packages/kbn-storybook/preset/package.json @@ -0,0 +1,4 @@ +{ + "private": true, + "main": "../preset.js" +} \ No newline at end of file diff --git a/packages/kbn-storybook/lib/templates/index.ejs b/packages/kbn-storybook/templates/index.ejs similarity index 100% rename from packages/kbn-storybook/lib/templates/index.ejs rename to packages/kbn-storybook/templates/index.ejs diff --git a/packages/kbn-storybook/tsconfig.json b/packages/kbn-storybook/tsconfig.json index 586f5ea32c056..1f6886c45c505 100644 --- a/packages/kbn-storybook/tsconfig.json +++ b/packages/kbn-storybook/tsconfig.json @@ -1,14 +1,15 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, + "incremental": true, "outDir": "target", "skipLibCheck": true, "declaration": true, "declarationMap": true, "sourceMap": true, "sourceRoot": "../../../../packages/kbn-storybook", + "target": "es2015", "types": ["node"] }, - "include": ["*.ts", "lib/**/*.ts", "lib/**/*.tsx", "../../typings/index.d.ts"] + "include": ["*.ts", "lib/**/*.ts", "lib/**/*.tsx"] } diff --git a/packages/kbn-storybook/typings.ts b/packages/kbn-storybook/typings.ts new file mode 100644 index 0000000000000..6c5d8f4da5709 --- /dev/null +++ b/packages/kbn-storybook/typings.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// Storybook react doesn't declare this in its typings, but it's there. +declare module '@storybook/react/standalone'; + +// Storybook references this module. It's @ts-ignored in the codebase but when +// built into its dist it strips that out. Add it here to avoid a type checking +// error. +// +// See https://github.com/storybookjs/storybook/issues/11684 +declare module 'react-syntax-highlighter/dist/cjs/create-element'; +declare module 'react-syntax-highlighter/dist/cjs/prism-light'; diff --git a/packages/kbn-storybook/webpack.config.ts b/packages/kbn-storybook/webpack.config.ts index 972caf8d481fe..41d3ee1f7ee5c 100644 --- a/packages/kbn-storybook/webpack.config.ts +++ b/packages/kbn-storybook/webpack.config.ts @@ -94,7 +94,7 @@ export default function ({ config: storybookConfig }: { config: Configuration }) return plugin.options && typeof plugin.options.template === 'string'; }); if (htmlWebpackPlugin) { - htmlWebpackPlugin.options.template = require.resolve('../lib/templates/index.ejs'); + htmlWebpackPlugin.options.template = require.resolve('../templates/index.ejs'); } return webpackMerge(storybookConfig, config); 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(?![\\/\\\\](monaco-editor|react-monaco-editor))[/\\\\].+\\.js$', + '[/\\\\]node_modules(?![\\/\\\\](monaco-editor|react-monaco-editor|d3-interpolate|d3-color))[/\\\\].+\\.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/functional_tests/lib/auth.ts b/packages/kbn-test/src/functional_tests/lib/auth.ts deleted file mode 100644 index abd1e0f9e7d5e..0000000000000 --- a/packages/kbn-test/src/functional_tests/lib/auth.ts +++ /dev/null @@ -1,188 +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 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 util from 'util'; -import { format as formatUrl } from 'url'; -import request from 'request'; -import type { ToolingLog } from '@kbn/dev-utils'; - -export const DEFAULT_SUPERUSER_PASS = 'changeme'; -const readFile = util.promisify(fs.readFile); - -function delay(delayMs: number) { - return new Promise((res) => setTimeout(res, delayMs)); -} - -interface UpdateCredentialsOptions { - port: number; - auth: string; - username: string; - password: string; - retries?: number; - protocol: string; - caCert?: Buffer | string; -} -async function updateCredentials({ - port, - auth, - username, - password, - retries = 10, - protocol, - caCert, -}: UpdateCredentialsOptions): Promise { - const result = await new Promise<{ body: any; httpResponse: request.Response }>( - (resolve, reject) => - request( - { - method: 'PUT', - uri: formatUrl({ - protocol: `${protocol}:`, - auth, - hostname: 'localhost', - port, - pathname: `/_security/user/${username}/_password`, - }), - json: true, - body: { password }, - ca: caCert, - }, - (err, httpResponse, body) => { - if (err) return reject(err); - resolve({ httpResponse, body }); - } - ) - ); - - const { body, httpResponse } = result; - const { statusCode } = httpResponse; - - if (statusCode === 200) { - return; - } - - if (retries > 0) { - await delay(2500); - return await updateCredentials({ - port, - auth, - username, - password, - retries: retries - 1, - protocol, - caCert, - }); - } - - throw new Error(`${statusCode} response, expected 200 -- ${JSON.stringify(body)}`); -} - -interface SetupUsersOptions { - log: ToolingLog; - esPort: number; - updates: Array<{ username: string; password: string; roles?: string[] }>; - protocol?: string; - caPath?: string; -} - -export async function setupUsers({ - log, - esPort, - updates, - protocol = 'http', - caPath, -}: SetupUsersOptions): Promise { - // track the current credentials for the `elastic` user as - // they will likely change as we apply updates - let auth = `elastic:${DEFAULT_SUPERUSER_PASS}`; - const caCert = caPath ? await readFile(caPath) : undefined; - - for (const { username, password, roles } of updates) { - // If working with a built-in user, just change the password - if (['logstash_system', 'elastic', 'kibana'].includes(username)) { - await updateCredentials({ port: esPort, auth, username, password, protocol, caCert }); - log.info('setting %j user password to %j', username, password); - - // If not a builtin user, add them - } else { - await insertUser({ port: esPort, auth, username, password, roles, protocol, caCert }); - log.info('Added %j user with password to %j', username, password); - } - - if (username === 'elastic') { - auth = `elastic:${password}`; - } - } -} - -interface InserUserOptions { - port: number; - auth: string; - username: string; - password: string; - roles?: string[]; - retries?: number; - protocol: string; - caCert?: Buffer | string; -} -async function insertUser({ - port, - auth, - username, - password, - roles = [], - retries = 10, - protocol, - caCert, -}: InserUserOptions): Promise { - const result = await new Promise<{ body: any; httpResponse: request.Response }>( - (resolve, reject) => - request( - { - method: 'POST', - uri: formatUrl({ - protocol: `${protocol}:`, - auth, - hostname: 'localhost', - port, - pathname: `/_security/user/${username}`, - }), - json: true, - body: { password, roles }, - ca: caCert, - }, - (err, httpResponse, body) => { - if (err) return reject(err); - resolve({ httpResponse, body }); - } - ) - ); - - const { body, httpResponse } = result; - const { statusCode } = httpResponse; - if (statusCode === 200) { - return; - } - - if (retries > 0) { - await delay(2500); - return await insertUser({ - port, - auth, - username, - password, - roles, - retries: retries - 1, - protocol, - caCert, - }); - } - - throw new Error(`${statusCode} response, expected 200 -- ${JSON.stringify(body)}`); -} diff --git a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts index 7ba9a3c1c4733..da83d8285a6b5 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts +++ b/packages/kbn-test/src/functional_tests/lib/run_elasticsearch.ts @@ -12,8 +12,6 @@ import { KIBANA_ROOT } from './paths'; import type { Config } from '../../functional_test_runner/'; import { createTestEsCluster } from '../../es'; -import { setupUsers, DEFAULT_SUPERUSER_PASS } from './auth'; - interface RunElasticsearchOptions { log: ToolingLog; esFrom: string; @@ -34,9 +32,7 @@ export async function runElasticsearch({ const cluster = createTestEsCluster({ port: config.get('servers.elasticsearch.port'), - password: isSecurityEnabled - ? DEFAULT_SUPERUSER_PASS - : config.get('servers.elasticsearch.password'), + password: isSecurityEnabled ? 'changeme' : config.get('servers.elasticsearch.password'), license, log, basePath: resolve(KIBANA_ROOT, '.es'), @@ -49,22 +45,5 @@ export async function runElasticsearch({ await cluster.start(); - if (isSecurityEnabled) { - await setupUsers({ - log, - esPort: config.get('servers.elasticsearch.port'), - updates: [config.get('servers.elasticsearch'), config.get('servers.kibana')], - protocol: config.get('servers.elasticsearch').protocol, - caPath: getRelativeCertificateAuthorityPath(config.get('kbnTestServer.serverArgs')), - }); - } - return cluster; } - -function getRelativeCertificateAuthorityPath(esConfig: string[] = []) { - const caConfig = esConfig.find( - (config) => config.indexOf('--elasticsearch.ssl.certificateAuthorities') === 0 - ); - return caConfig ? caConfig.split('=')[1] : undefined; -} diff --git a/packages/kbn-test/src/index.ts b/packages/kbn-test/src/index.ts index dd5343b0118b3..af100a33ea3a7 100644 --- a/packages/kbn-test/src/index.ts +++ b/packages/kbn-test/src/index.ts @@ -29,8 +29,6 @@ export { esTestConfig, createTestEsCluster } from './es'; export { kbnTestConfig, kibanaServerTestUser, kibanaTestUser, adminTestUser } from './kbn'; -export { setupUsers, DEFAULT_SUPERUSER_PASS } from './functional_tests/lib/auth'; - export { readConfigFile } from './functional_test_runner/lib/config/read_config_file'; export { runFtrCli } from './functional_test_runner/cli'; 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/packages/kbn-ui-framework/BUILD.bazel b/packages/kbn-ui-framework/BUILD.bazel new file mode 100644 index 0000000000000..f8cf5035bdc5f --- /dev/null +++ b/packages/kbn-ui-framework/BUILD.bazel @@ -0,0 +1,47 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-ui-framework" +PKG_REQUIRE_NAME = "@kbn/ui-framework" + +SOURCE_FILES = glob([ + "dist/**/*", +]) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +DEPS = [] + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES + [ + ":srcs", + ], + deps = DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js index 438b1e0b2e77b..9d18c8033ff67 100644 --- a/packages/kbn-ui-shared-deps/webpack.config.js +++ b/packages/kbn-ui-shared-deps/webpack.config.js @@ -7,7 +7,6 @@ */ const Path = require('path'); -const Os = require('os'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); @@ -31,7 +30,8 @@ module.exports = { 'kbn-ui-shared-deps.v8.light': ['@elastic/eui/dist/eui_theme_amsterdam_light.css'], }, context: __dirname, - devtool: 'cheap-source-map', + // cheap-source-map should be used if needed + devtool: false, output: { path: UiSharedDeps.distDir, filename: '[name].js', @@ -39,7 +39,6 @@ module.exports = { devtoolModuleFilenameTemplate: (info) => `kbn-ui-shared-deps/${Path.relative(REPO_ROOT, info.absoluteResourcePath)}`, library: '__kbnSharedDeps__', - futureEmitAssets: true, }, module: { @@ -111,7 +110,7 @@ module.exports = { optimization: { minimizer: [ new CssMinimizerPlugin({ - parallel: Math.min(Os.cpus().length, 2), + parallel: false, minimizerOptions: { preset: [ 'default', @@ -125,7 +124,7 @@ module.exports = { cache: false, sourceMap: false, extractComments: false, - parallel: Math.min(Os.cpus().length, 2), + parallel: false, terserOptions: { compress: true, mangle: true, diff --git a/src/core/public/apm_system.ts b/src/core/public/apm_system.ts index 32fc330375991..f5af7011e632e 100644 --- a/src/core/public/apm_system.ts +++ b/src/core/public/apm_system.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { ApmBase } from '@elastic/apm-rum'; +import type { ApmBase, AgentConfigOptions } from '@elastic/apm-rum'; import { modifyUrl } from '@kbn/std'; import type { InternalApplicationStart } from './application'; @@ -18,9 +18,8 @@ const HTTP_REQUEST_TRANSACTION_NAME_REGEX = /^(GET|POST|PUT|HEAD|PATCH|DELETE|OP * that lives in the Kibana Platform. */ -interface ApmConfig { - // AgentConfigOptions is not exported from @elastic/apm-rum - active?: boolean; +interface ApmConfig extends AgentConfigOptions { + // Kibana-specific config settings: globalLabels?: Record; } diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index 3ed164088bf5c..de9e4d4496f3b 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -15,13 +15,13 @@ import { import { createElement } from 'react'; import { BehaviorSubject, Subject } from 'rxjs'; import { bufferCount, take, takeUntil } from 'rxjs/operators'; -import { shallow, mount } from 'enzyme'; +import { mount, shallow } from 'enzyme'; import { httpServiceMock } from '../http/http_service.mock'; import { overlayServiceMock } from '../overlays/overlay_service.mock'; import { MockLifecycle } from './test_types'; import { ApplicationService } from './application_service'; -import { App, PublicAppInfo, AppNavLinkStatus, AppStatus, AppUpdater } from './types'; +import { App, AppDeepLink, AppNavLinkStatus, AppStatus, AppUpdater, PublicAppInfo } from './types'; import { act } from 'react-dom/test-utils'; const createApp = (props: Partial): App => { @@ -365,6 +365,85 @@ describe('#setup()', () => { expect(MockHistory.push).toHaveBeenCalledWith('/app/app1', undefined); MockHistory.push.mockClear(); }); + + it('preserves the deep links if the update does not modify them', async () => { + const setup = service.setup(setupDeps); + + const pluginId = Symbol('plugin'); + const updater$ = new BehaviorSubject((app) => ({})); + + const deepLinks: AppDeepLink[] = [ + { + id: 'foo', + title: 'Foo', + searchable: true, + navLinkStatus: AppNavLinkStatus.visible, + path: '/foo', + }, + { + id: 'bar', + title: 'Bar', + searchable: false, + navLinkStatus: AppNavLinkStatus.hidden, + path: '/bar', + }, + ]; + + setup.register(pluginId, createApp({ id: 'app1', deepLinks, updater$ })); + + const { applications$ } = await service.start(startDeps); + + updater$.next((app) => ({ defaultPath: '/foo' })); + + let appInfos = await applications$.pipe(take(1)).toPromise(); + + expect(appInfos.get('app1')!.deepLinks).toEqual([ + { + deepLinks: [], + id: 'foo', + keywords: [], + navLinkStatus: 1, + path: '/foo', + searchable: true, + title: 'Foo', + }, + { + deepLinks: [], + id: 'bar', + keywords: [], + navLinkStatus: 3, + path: '/bar', + searchable: false, + title: 'Bar', + }, + ]); + + updater$.next((app) => ({ + deepLinks: [ + { + id: 'bar', + title: 'Bar', + searchable: false, + navLinkStatus: AppNavLinkStatus.hidden, + path: '/bar', + }, + ], + })); + + appInfos = await applications$.pipe(take(1)).toPromise(); + + expect(appInfos.get('app1')!.deepLinks).toEqual([ + { + deepLinks: [], + id: 'bar', + keywords: [], + navLinkStatus: 3, + path: '/bar', + searchable: false, + title: 'Bar', + }, + ]); + }); }); }); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 8c6090caabce1..2e804bf2f5413 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -54,6 +54,7 @@ function filterAvailable(m: Map, capabilities: Capabilities) { ) ); } + const findMounter = (mounters: Map, appRoute?: string) => [...mounters].find(([, mounter]) => mounter.appRoute === appRoute); @@ -414,13 +415,11 @@ const updateStatus = (app: App, statusUpdaters: AppUpdaterWrapper[]): App => { changes.navLinkStatus ?? AppNavLinkStatus.default, fields.navLinkStatus ?? AppNavLinkStatus.default ), - // deepLinks take the last defined update - deepLinks: fields.deepLinks - ? populateDeepLinkDefaults(fields.deepLinks) - : changes.deepLinks, + ...(fields.deepLinks ? { deepLinks: populateDeepLinkDefaults(fields.deepLinks) } : {}), }; } }); + return { ...app, ...changes, diff --git a/src/core/public/application/utils/get_app_info.test.ts b/src/core/public/application/utils/get_app_info.test.ts index fa1e2dd9a4537..25614d1d1dca9 100644 --- a/src/core/public/application/utils/get_app_info.test.ts +++ b/src/core/public/application/utils/get_app_info.test.ts @@ -185,15 +185,18 @@ describe('getAppInfo', () => { it('adds default deepLinks when needed', () => { const app = createApp({ + order: 3, deepLinks: [ { id: 'sub-id', title: 'sub-title', + order: 2, deepLinks: [ { id: 'sub-sub-id', title: 'sub-sub-title', path: '/sub-sub', + order: 1, keywords: ['sub sub'], }, ], @@ -210,12 +213,14 @@ describe('getAppInfo', () => { searchable: true, appRoute: `/app/some-id`, keywords: [], + order: 3, deepLinks: [ { id: 'sub-id', title: 'sub-title', navLinkStatus: AppNavLinkStatus.hidden, searchable: true, + order: 2, keywords: [], deepLinks: [ { @@ -223,6 +228,7 @@ describe('getAppInfo', () => { title: 'sub-sub-title', navLinkStatus: AppNavLinkStatus.hidden, searchable: true, + order: 1, path: '/sub-sub', keywords: ['sub sub'], deepLinks: [], diff --git a/src/core/public/application/utils/get_app_info.ts b/src/core/public/application/utils/get_app_info.ts index 6c753b7a71a0f..b5a3f0b0a0f13 100644 --- a/src/core/public/application/utils/get_app_info.ts +++ b/src/core/public/application/utils/get_app_info.ts @@ -41,9 +41,7 @@ function getDeepLinkInfos(deepLinks?: AppDeepLink[]): PublicAppDeepLinkInfo[] { return deepLinks.map( ({ navLinkStatus = AppNavLinkStatus.default, ...rawDeepLink }): PublicAppDeepLinkInfo => { return { - id: rawDeepLink.id, - title: rawDeepLink.title, - path: rawDeepLink.path, + ...rawDeepLink, keywords: rawDeepLink.keywords ?? [], navLinkStatus: navLinkStatus === AppNavLinkStatus.default ? AppNavLinkStatus.hidden : navLinkStatus, 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 } = {}) { @@ -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(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(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]} > - - - - } - /> - - -
-
+ + +
+
+ +
-
-
-
- - - -
+ data-euiicon-type="home" + /> + + + Home + + + + + +
-
-
- - + +
+
+ + + + +

+ Recently viewed +

+
+
+ + } + 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" > - - - -

- Recently viewed -

-
-
- - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" +
-
- -
-
+ +
+ +
+ + + +
+
+ - -
+
+
-
- - - -
+ recent 2 + + + + + +
- -
+
+
-
-
- -
-
- + + + +
+
+ +
-
- + + + + + +

+ Analytics +

+
+
+ + } + 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" > - - - - - - -

- Analytics -

-
-
- - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" +
-
- -
-
+ +
+ +
+ + + +
+
+ - -
+
+
-
- - - -
+ dashboard + + + + + +
- -
+
+
-
-
- + + + + + + + + + +

+ Observability +

+
+
+ + } + 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" > - - - - - - -

- Observability -

-
-
- - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" +
-
- -
-
+ +
+ +
+ + + +
+
+ - -
+
+
-
- - - -
+ logs + + + + + +
- -
+
+
-
-
- + + + + + + + + + +

+ Security +

+
+
+ + } + 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" > - - - - - - -

- Security -

-
-
- - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" +
-
- -
-
+ +
+ +
+ + + +
+
+ - -
+
+
-
- - - -
+ siem + + + + + +
- -
+
+
-
-
- + + + + + + + + + +

+ Management +

+
+
+ + } + 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" > - - - - - - -

- Management -

-
-
- - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" +
-
- -
-
+ +
+ +
+ + + +
+
+ - -
+
+
-
- - - -
+ monitoring + + + + + +
- -
+
+
-
-
- + + + +
-
- - - -
+ canvas + + + + + +
- - - +
+
+ + +
-
- -
    - - - - Dock navigation - - , - } - } - color="subdued" - data-test-subj="collapsible-nav-lock" - iconType="lockOpen" - label="Dock navigation" - onClick={[Function]} - size="xs" - > -
  • - -
  • -
    -
-
-
+ , + } + } + color="subdued" + data-test-subj="collapsible-nav-lock" + iconType="lockOpen" + label="Dock navigation" + onClick={[Function]} + size="xs" + > +
  • + +
  • + + +
    - - -
    - - - - - - - -
    - +
    + + + +
    + + `; @@ -2770,42 +2706,57 @@ exports[`CollapsibleNav renders the default nav 3`] = ` isOpen={false} onClose={[Function]} > - - -
    -
    +
    + + + + +

    + Recently viewed +

    +
    +
    + + } + 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" > - - - -

    - Recently viewed -

    -
    -
    - - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" +
    -
    - -
    -
    + +
    + +
    + + + +
    +
    + - -
    +
    +
    -
    - -
    - -
    -

    - No recently viewed items -

    -
    -
    -
    -
    -
    +

    + No recently viewed items +

    +
    + +
    +
    -
    -
    + + -
    -
    - -
    -
    - + + + +
    +
    + +
    -
    - - + +
    -
    - -
      - - - - Undock navigation - - , - } - } - color="subdued" - data-test-subj="collapsible-nav-lock" - iconType="lock" - label="Undock navigation" - onClick={[Function]} - size="xs" - > -
    • - -
    • -
      -
    -
    -
    + , + } + } + color="subdued" + data-test-subj="collapsible-nav-lock" + iconType="lock" + label="Undock navigation" + onClick={[Function]} + size="xs" + > +
  • + +
  • + + +
    - - -
    - - - - - - - -
    - +
    + + + +
    + + `; 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]} > - - -
    -
    +
    + +
    +
    + +
    -
    -
    -
    - - - -
    + data-euiicon-type="home" + /> + + + Home + + + + + +
    -
    -
    - - + +
    +
    + + + + +

    + Recently viewed +

    +
    +
    + + } + 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" > - - - -

    - Recently viewed -

    -
    -
    - - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" +
    -
    - -
    -
    + +
    + +
    + + + +
    +
    + - -
    +
    +
    -
    - - - -
    + dashboard + + + + + +
    - -
    +
    +
    -
    -
    - -
    -
    - + + + +
    +
    + +
    -
    - +
    + +
      + +
    • + +
    • +
      +
    +
    +
    +
    + + +
    + + + Undock navigation + + , + } + } + color="subdued" + data-test-subj="collapsible-nav-lock" + iconType="lock" + label="Undock navigation" onClick={[Function]} - size="s" + size="xs" >
  • @@ -5445,163 +5540,11 @@ exports[`Header renders 1`] = `
    - - -
    -
    - -
      - - - - Undock navigation - - , - } - } - color="subdued" - data-test-subj="collapsible-nav-lock" - iconType="lock" - label="Undock navigation" - onClick={[Function]} - size="xs" - > -
    • - -
    • -
      -
    -
    -
    -
    -
    -
    -
    -
    - - - - - - - -
    + + + + + 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) { 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 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 06277d9351922..502b22a6f8e89 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -22,6 +22,7 @@ export class DocLinksService { const ELASTIC_WEBSITE_URL = 'https://www.elastic.co/'; const ELASTICSEARCH_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`; const KIBANA_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/`; + const FLEET_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/fleet/${DOC_LINK_VERSION}/`; const PLUGIN_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/plugins/${DOC_LINK_VERSION}/`; return deepFreeze({ @@ -136,6 +137,7 @@ export class DocLinksService { addData: `${KIBANA_DOCS}connect-to-elasticsearch.html`, kibana: `${KIBANA_DOCS}index.html`, upgradeAssistant: `${KIBANA_DOCS}upgrade-assistant.html`, + rollupJobs: `${KIBANA_DOCS}data-rollups.html`, elasticsearch: { docsBase: `${ELASTICSEARCH_DOCS}`, asyncSearch: `${ELASTICSEARCH_DOCS}async-search-intro.html`, @@ -202,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`, @@ -400,6 +403,19 @@ export class DocLinksService { urlDecode: `${ELASTICSEARCH_DOCS}urldecode-processor.html`, userAgent: `${ELASTICSEARCH_DOCS}user-agent-processor.html`, }, + fleet: { + guide: `${FLEET_DOCS}index.html`, + fleetServer: `${FLEET_DOCS}fleet-server.html`, + fleetServerAddFleetServer: `${FLEET_DOCS}fleet-server.html#add-fleet-server`, + settings: `${FLEET_DOCS}fleet-settings.html#fleet-server-hosts-setting`, + settingsFleetServerHostSettings: `${FLEET_DOCS}fleet-settings.html#fleet-server-hosts-setting`, + troubleshooting: `${FLEET_DOCS}fleet-troubleshooting.html`, + elasticAgent: `${FLEET_DOCS}elastic-agent-installation-configuration.html`, + datastreams: `${FLEET_DOCS}data-streams.html`, + datastreamsNamingScheme: `${FLEET_DOCS}data-streams.html#data-streams-naming-scheme`, + upgradeElasticAgent: `${FLEET_DOCS}upgrade-elastic-agent.html`, + upgradeElasticAgent712lower: `${FLEET_DOCS}upgrade-elastic-agent.html#upgrade-7.12-lower`, + }, }, }); } @@ -508,6 +524,7 @@ export interface DocLinksStart { }; readonly search: { readonly sessions: string; + readonly sessionLimits: string; }; readonly indexPatterns: { readonly introduction: string; @@ -518,6 +535,7 @@ export interface DocLinksStart { readonly addData: string; readonly kibana: string; readonly upgradeAssistant: string; + readonly rollupJobs: string; readonly elasticsearch: Record; readonly siem: { readonly guide: string; @@ -587,5 +605,18 @@ export interface DocLinksStart { readonly plugins: Record; readonly snapshotRestore: Record; readonly ingest: Record; + readonly fleet: Readonly<{ + guide: string; + fleetServer: string; + fleetServerAddFleetServer: string; + settings: string; + settingsFleetServerHostSettings: string; + troubleshooting: string; + elasticAgent: string; + datastreams: string; + datastreamsNamingScheme: string; + upgradeElasticAgent: string; + upgradeElasticAgent712lower: 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`] = `"
    Flyout content
    "`; +exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"
    Flyout content
    "`; 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`] = `"
    Flyout content 2
    "`; +exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
    Flyout content 2
    "`; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index d3426b50f7614..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; @@ -595,6 +596,7 @@ export interface DocLinksStart { readonly addData: string; readonly kibana: string; readonly upgradeAssistant: string; + readonly rollupJobs: string; readonly elasticsearch: Record; readonly siem: { readonly guide: string; @@ -664,6 +666,19 @@ export interface DocLinksStart { readonly plugins: Record; readonly snapshotRestore: Record; readonly ingest: Record; + readonly fleet: Readonly<{ + guide: string; + fleetServer: string; + fleetServerAddFleetServer: string; + settings: string; + settingsFleetServerHostSettings: string; + troubleshooting: string; + elasticAgent: string; + datastreams: string; + datastreamsNamingScheme: string; + upgradeElasticAgent: string; + upgradeElasticAgent712lower: string; + }>; }; } @@ -1617,6 +1632,6 @@ export interface UserProvidedValues { // 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/core_usage_data/core_usage_data_service.ts b/src/core/server/core_usage_data/core_usage_data_service.ts index dc24f889cd8dd..afe1b45175f86 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.ts @@ -126,14 +126,12 @@ export class CoreUsageDataService implements CoreService any) => fn.bind(null, {}); let certificate: string; let key: string; -beforeAll(() => { - certificate = readFileSync(KBN_CERT_PATH, 'utf8'); - key = readFileSync(KBN_KEY_PATH, 'utf8'); +beforeAll(async () => { + certificate = await readFile(KBN_CERT_PATH, 'utf8'); + key = await readFile(KBN_KEY_PATH, 'utf8'); }); beforeEach(() => { @@ -1409,6 +1410,19 @@ describe('setup contract', () => { }); describe('#registerStaticDir', () => { + const assetFolder = join(__dirname, 'integration_tests', 'fixtures', 'static'); + let tempDir: string; + + beforeAll(async () => { + tempDir = await mkdtemp('cache-test'); + }); + + afterAll(async () => { + if (tempDir) { + await rmdir(tempDir, { recursive: true }); + } + }); + test('does not throw if called after stop', async () => { const { registerStaticDir } = await server.setup(config); await server.stop(); @@ -1416,6 +1430,111 @@ describe('setup contract', () => { registerStaticDir('/path1/{path*}', '/path/to/resource'); }).not.toThrow(); }); + + test('returns correct headers for static assets', async () => { + const { registerStaticDir, server: innerServer } = await server.setup(config); + + registerStaticDir('/static/{path*}', assetFolder); + + await server.start(); + const response = await supertest(innerServer.listener) + .get('/static/some_json.json') + .expect(200); + + expect(response.get('cache-control')).toEqual('must-revalidate'); + expect(response.get('etag')).not.toBeUndefined(); + }); + + test('returns compressed version if present', async () => { + const { registerStaticDir, server: innerServer } = await server.setup(config); + + registerStaticDir('/static/{path*}', assetFolder); + + await server.start(); + const response = await supertest(innerServer.listener) + .get('/static/compression_available.json') + .set('accept-encoding', 'gzip') + .expect(200); + + expect(response.get('cache-control')).toEqual('must-revalidate'); + expect(response.get('etag')).not.toBeUndefined(); + expect(response.get('content-encoding')).toEqual('gzip'); + }); + + test('returns uncompressed version if compressed asset is not available', async () => { + const { registerStaticDir, server: innerServer } = await server.setup(config); + + registerStaticDir('/static/{path*}', assetFolder); + + await server.start(); + const response = await supertest(innerServer.listener) + .get('/static/some_json.json') + .set('accept-encoding', 'gzip') + .expect(200); + + expect(response.get('cache-control')).toEqual('must-revalidate'); + expect(response.get('etag')).not.toBeUndefined(); + expect(response.get('content-encoding')).toBeUndefined(); + }); + + test('returns a 304 if etag value matches', async () => { + const { registerStaticDir, server: innerServer } = await server.setup(config); + + registerStaticDir('/static/{path*}', assetFolder); + + await server.start(); + const response = await supertest(innerServer.listener) + .get('/static/some_json.json') + .expect(200); + + const etag = response.get('etag'); + expect(etag).not.toBeUndefined(); + + await supertest(innerServer.listener) + .get('/static/some_json.json') + .set('If-None-Match', etag) + .expect(304); + }); + + test('serves content if etag values does not match', async () => { + const { registerStaticDir, server: innerServer } = await server.setup(config); + + registerStaticDir('/static/{path*}', assetFolder); + + await server.start(); + + await supertest(innerServer.listener) + .get('/static/some_json.json') + .set('If-None-Match', `"definitely not a valid etag"`) + .expect(200); + }); + + test('dynamically updates depending on the content of the file', async () => { + const tempFile = join(tempDir, 'some_file.json'); + + const { registerStaticDir, server: innerServer } = await server.setup(config); + registerStaticDir('/static/{path*}', tempDir); + + await server.start(); + + await supertest(innerServer.listener).get('/static/some_file.json').expect(404); + + await writeFile(tempFile, `{ "over": 9000 }`); + + let response = await supertest(innerServer.listener) + .get('/static/some_file.json') + .expect(200); + + const etag1 = response.get('etag'); + + await writeFile(tempFile, `{ "over": 42 }`); + + response = await supertest(innerServer.listener).get('/static/some_file.json').expect(200); + + const etag2 = response.get('etag'); + + expect(etag1).not.toEqual(etag2); + }); }); describe('#registerOnPreRouting', () => { diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 8b4c3b9416152..d43d86d587d06 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -465,7 +465,13 @@ export class HttpServer { lookupCompressed: true, }, }, - options: { auth: false }, + options: { + auth: false, + cache: { + privacy: 'public', + otherwise: 'must-revalidate', + }, + }, }); } diff --git a/src/core/server/http/integration_tests/fixtures/static/compression_available.json b/src/core/server/http/integration_tests/fixtures/static/compression_available.json new file mode 100644 index 0000000000000..1f878fb465cff --- /dev/null +++ b/src/core/server/http/integration_tests/fixtures/static/compression_available.json @@ -0,0 +1,3 @@ +{ + "hello": "dolly" +} diff --git a/src/core/server/http/integration_tests/fixtures/static/compression_available.json.gz b/src/core/server/http/integration_tests/fixtures/static/compression_available.json.gz new file mode 100644 index 0000000000000..e77819d2e8e59 Binary files /dev/null and b/src/core/server/http/integration_tests/fixtures/static/compression_available.json.gz differ diff --git a/src/core/server/http/integration_tests/fixtures/static/some_json.json b/src/core/server/http/integration_tests/fixtures/static/some_json.json new file mode 100644 index 0000000000000..c8c4105eb57cd --- /dev/null +++ b/src/core/server/http/integration_tests/fixtures/static/some_json.json @@ -0,0 +1,3 @@ +{ + "foo": "bar" +} diff --git a/src/core/server/http/integration_tests/request.test.ts b/src/core/server/http/integration_tests/request.test.ts index 7571184363d2e..dfc47098724cc 100644 --- a/src/core/server/http/integration_tests/request.test.ts +++ b/src/core/server/http/integration_tests/request.test.ts @@ -163,24 +163,24 @@ describe('KibanaRequest', () => { describe('events', () => { describe('aborted$', () => { - it('emits once and completes when request aborted', async (done) => { + it('emits once and completes when request aborted', async () => { expect.assertions(1); const { server: innerServer, createRouter } = await server.setup(setupDeps); const router = createRouter('/'); const nextSpy = jest.fn(); - router.get({ path: '/', validate: false }, async (context, request, res) => { - request.events.aborted$.subscribe({ - next: nextSpy, - complete: () => { - expect(nextSpy).toHaveBeenCalledTimes(1); - done(); - }, - }); - // prevents the server to respond - await delay(30000); - return res.ok({ body: 'ok' }); + const done = new Promise((resolve) => { + router.get({ path: '/', validate: false }, async (context, request, res) => { + request.events.aborted$.subscribe({ + next: nextSpy, + complete: resolve, + }); + + // prevents the server to respond + await delay(30000); + return res.ok({ body: 'ok' }); + }); }); await server.start(); @@ -191,6 +191,8 @@ describe('KibanaRequest', () => { .end(); setTimeout(() => incomingRequest.abort(), 50); + await done; + expect(nextSpy).toHaveBeenCalledTimes(1); }); it('completes & does not emit when request handled', async () => { @@ -299,25 +301,24 @@ describe('KibanaRequest', () => { expect(completeSpy).toHaveBeenCalledTimes(1); }); - it('emits once and completes when response is aborted', async (done) => { + it('emits once and completes when response is aborted', async () => { expect.assertions(2); const { server: innerServer, createRouter } = await server.setup(setupDeps); const router = createRouter('/'); const nextSpy = jest.fn(); - router.get({ path: '/', validate: false }, async (context, req, res) => { - req.events.completed$.subscribe({ - next: nextSpy, - complete: () => { - expect(nextSpy).toHaveBeenCalledTimes(1); - done(); - }, - }); + const done = new Promise((resolve) => { + router.get({ path: '/', validate: false }, async (context, req, res) => { + req.events.completed$.subscribe({ + next: nextSpy, + complete: resolve, + }); - expect(nextSpy).not.toHaveBeenCalled(); - await delay(30000); - return res.ok({ body: 'ok' }); + expect(nextSpy).not.toHaveBeenCalled(); + await delay(30000); + return res.ok({ body: 'ok' }); + }); }); await server.start(); @@ -327,6 +328,8 @@ describe('KibanaRequest', () => { // end required to send request .end(); setTimeout(() => incomingRequest.abort(), 50); + await done; + expect(nextSpy).toHaveBeenCalledTimes(1); }); }); }); diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.ts b/src/core/server/saved_objects/migrations/core/index_migrator.ts index 14dba1db9b624..0ec6fe89de1f1 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.ts @@ -187,12 +187,8 @@ async function migrateSourceToDest(context: Context) { await Index.write( client, dest.indexName, - await migrateRawDocs( - serializer, - documentMigrator.migrateAndConvert, - // @ts-expect-error @elastic/elasticsearch `Hit._id` may be a string | number in ES, but we always expect strings in the SO index. - docs - ) + // @ts-expect-error @elastic/elasticsearch _source is optional + await migrateRawDocs(serializer, documentMigrator.migrateAndConvert, docs) ); } } diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts index f4e0dd8fffcab..4c9e37d17f2e7 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts @@ -95,6 +95,12 @@ describe('migration v2', () => { }, ], }, + // reporting loads headless browser, that prevents nodejs process from exiting. + xpack: { + reporting: { + enabled: false, + }, + }, }, { oss, diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 22c40a547f419..4456784fdbc0b 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -525,15 +525,22 @@ describe('SavedObjectsRepository', () => { const ns2 = 'bar-namespace'; const ns3 = 'baz-namespace'; const objects = [ - { ...obj1, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns2] }, - { ...obj2, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns3] }, + { ...obj1, type: 'dashboard', initialNamespaces: [ns2] }, + { ...obj1, type: MULTI_NAMESPACE_ISOLATED_TYPE, initialNamespaces: [ns2] }, + { ...obj1, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns2, ns3] }, ]; await bulkCreateSuccess(objects, { namespace, overwrite: true }); const body = [ - expect.any(Object), + { index: expect.objectContaining({ _id: `${ns2}:dashboard:${obj1.id}` }) }, + expect.objectContaining({ namespace: ns2 }), + { + index: expect.objectContaining({ + _id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${obj1.id}`, + }), + }, expect.objectContaining({ namespaces: [ns2] }), - expect.any(Object), - expect.objectContaining({ namespaces: [ns3] }), + { index: expect.objectContaining({ _id: `${MULTI_NAMESPACE_TYPE}:${obj1.id}` }) }, + expect.objectContaining({ namespaces: [ns2, ns3] }), ]; expect(client.bulk).toHaveBeenCalledWith( expect.objectContaining({ body }), @@ -649,24 +656,19 @@ describe('SavedObjectsRepository', () => { ).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"')); }); - it(`returns error when initialNamespaces is used with a non-shareable object`, async () => { - const test = async (objType) => { - const obj = { ...obj3, type: objType, initialNamespaces: [] }; - await bulkCreateError( + it(`returns error when initialNamespaces is used with a space-agnostic object`, async () => { + const obj = { ...obj3, type: NAMESPACE_AGNOSTIC_TYPE, initialNamespaces: [] }; + await bulkCreateError( + obj, + undefined, + expectErrorResult( obj, - undefined, - expectErrorResult( - obj, - createBadRequestError('"initialNamespaces" can only be used on multi-namespace types') - ) - ); - }; - await test('dashboard'); - await test(NAMESPACE_AGNOSTIC_TYPE); - await test(MULTI_NAMESPACE_ISOLATED_TYPE); + createBadRequestError('"initialNamespaces" cannot be used on space-agnostic types') + ) + ); }); - it(`throws when options.initialNamespaces is used with a shareable type and is empty`, async () => { + it(`returns error when initialNamespaces is empty`, async () => { const obj = { ...obj3, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [] }; await bulkCreateError( obj, @@ -678,6 +680,26 @@ describe('SavedObjectsRepository', () => { ); }); + it(`returns error when initialNamespaces is used with a space-isolated object and does not specify a single space`, async () => { + const doTest = async (objType, initialNamespaces) => { + const obj = { ...obj3, type: objType, initialNamespaces }; + await bulkCreateError( + obj, + undefined, + expectErrorResult( + obj, + createBadRequestError( + '"initialNamespaces" can only specify a single space when used with space-isolated types' + ) + ) + ); + }; + await doTest('dashboard', ['spacex', 'spacey']); + await doTest('dashboard', ['*']); + await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['spacex', 'spacey']); + await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['*']); + }); + it(`returns error when type is invalid`, async () => { const obj = { ...obj3, type: 'unknownType' }; await bulkCreateError(obj, undefined, expectErrorInvalidType(obj)); @@ -1865,12 +1887,46 @@ describe('SavedObjectsRepository', () => { }); it(`adds initialNamespaces instead of namespace`, async () => { - const options = { id, namespace, initialNamespaces: ['bar-namespace', 'baz-namespace'] }; - await createSuccess(MULTI_NAMESPACE_TYPE, attributes, options); - expect(client.create).toHaveBeenCalledWith( + const ns2 = 'bar-namespace'; + const ns3 = 'baz-namespace'; + await savedObjectsRepository.create('dashboard', attributes, { + id, + namespace, + initialNamespaces: [ns2], + }); + await savedObjectsRepository.create(MULTI_NAMESPACE_ISOLATED_TYPE, attributes, { + id, + namespace, + initialNamespaces: [ns2], + }); + await savedObjectsRepository.create(MULTI_NAMESPACE_TYPE, attributes, { + id, + namespace, + initialNamespaces: [ns2, ns3], + }); + + expect(client.create).toHaveBeenCalledTimes(3); + expect(client.create).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + id: `${ns2}:dashboard:${id}`, + body: expect.objectContaining({ namespace: ns2 }), + }), + expect.anything() + ); + expect(client.create).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:${id}`, + body: expect.objectContaining({ namespaces: [ns2] }), + }), + expect.anything() + ); + expect(client.create).toHaveBeenNthCalledWith( + 3, expect.objectContaining({ id: `${MULTI_NAMESPACE_TYPE}:${id}`, - body: expect.objectContaining({ namespaces: options.initialNamespaces }), + body: expect.objectContaining({ namespaces: [ns2, ns3] }), }), expect.anything() ); @@ -1892,29 +1948,40 @@ describe('SavedObjectsRepository', () => { }); describe('errors', () => { - it(`throws when options.initialNamespaces is used with a non-shareable object`, async () => { - const test = async (objType) => { - await expect( - savedObjectsRepository.create(objType, attributes, { initialNamespaces: [namespace] }) - ).rejects.toThrowError( - createBadRequestError( - '"options.initialNamespaces" can only be used on multi-namespace types' - ) - ); - }; - await test('dashboard'); - await test(MULTI_NAMESPACE_ISOLATED_TYPE); - await test(NAMESPACE_AGNOSTIC_TYPE); + it(`throws when options.initialNamespaces is used with a space-agnostic object`, async () => { + await expect( + savedObjectsRepository.create(NAMESPACE_AGNOSTIC_TYPE, attributes, { + initialNamespaces: [namespace], + }) + ).rejects.toThrowError( + createBadRequestError('"initialNamespaces" cannot be used on space-agnostic types') + ); }); - it(`throws when options.initialNamespaces is used with a shareable type and is empty`, async () => { + it(`throws when options.initialNamespaces is empty`, async () => { await expect( savedObjectsRepository.create(MULTI_NAMESPACE_TYPE, attributes, { initialNamespaces: [] }) ).rejects.toThrowError( - createBadRequestError('"options.initialNamespaces" must be a non-empty array of strings') + createBadRequestError('"initialNamespaces" must be a non-empty array of strings') ); }); + it(`throws when options.initialNamespaces is used with a space-isolated object and does not specify a single space`, async () => { + const doTest = async (objType, initialNamespaces) => { + await expect( + savedObjectsRepository.create(objType, attributes, { initialNamespaces }) + ).rejects.toThrowError( + createBadRequestError( + '"initialNamespaces" can only specify a single space when used with space-isolated types' + ) + ); + }; + await doTest('dashboard', ['spacex', 'spacey']); + await doTest('dashboard', ['*']); + await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['spacex', 'spacey']); + await doTest(MULTI_NAMESPACE_ISOLATED_TYPE, ['*']); + }); + it(`throws when options.namespace is '*'`, async () => { await expect( savedObjectsRepository.create(type, attributes, { namespace: ALL_NAMESPACES_STRING }) diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 6b51bd57248a1..c9fa50da55df1 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -283,28 +283,18 @@ export class SavedObjectsRepository { } = options; const namespace = normalizeNamespace(options.namespace); - if (initialNamespaces) { - if (!this._registry.isShareable(type)) { - throw SavedObjectsErrorHelpers.createBadRequestError( - '"options.initialNamespaces" can only be used on multi-namespace types' - ); - } else if (!initialNamespaces.length) { - throw SavedObjectsErrorHelpers.createBadRequestError( - '"options.initialNamespaces" must be a non-empty array of strings' - ); - } - } + this.validateInitialNamespaces(type, initialNamespaces); if (!this._allowedTypes.includes(type)) { throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type); } const time = this._getCurrentTime(); - let savedObjectNamespace; + let savedObjectNamespace: string | undefined; let savedObjectNamespaces: string[] | undefined; - if (this._registry.isSingleNamespace(type) && namespace) { - savedObjectNamespace = namespace; + if (this._registry.isSingleNamespace(type)) { + savedObjectNamespace = initialNamespaces ? initialNamespaces[0] : namespace; } else if (this._registry.isMultiNamespace(type)) { if (id && overwrite) { // we will overwrite a multi-namespace saved object if it exists; if that happens, ensure we preserve its included namespaces @@ -369,32 +359,29 @@ export class SavedObjectsRepository { let bulkGetRequestIndexCounter = 0; const expectedResults: Either[] = objects.map((object) => { + const { type, id, initialNamespaces } = object; let error: DecoratedError | undefined; - if (!this._allowedTypes.includes(object.type)) { - error = SavedObjectsErrorHelpers.createUnsupportedTypeError(object.type); - } else if (object.initialNamespaces) { - if (!this._registry.isShareable(object.type)) { - error = SavedObjectsErrorHelpers.createBadRequestError( - '"initialNamespaces" can only be used on multi-namespace types' - ); - } else if (!object.initialNamespaces.length) { - error = SavedObjectsErrorHelpers.createBadRequestError( - '"initialNamespaces" must be a non-empty array of strings' - ); + if (!this._allowedTypes.includes(type)) { + error = SavedObjectsErrorHelpers.createUnsupportedTypeError(type); + } else { + try { + this.validateInitialNamespaces(type, initialNamespaces); + } catch (e) { + error = e; } } if (error) { return { tag: 'Left' as 'Left', - error: { id: object.id, type: object.type, error: errorContent(error) }, + error: { id, type, error: errorContent(error) }, }; } - const method = object.id && overwrite ? 'index' : 'create'; - const requiresNamespacesCheck = object.id && this._registry.isMultiNamespace(object.type); + const method = id && overwrite ? 'index' : 'create'; + const requiresNamespacesCheck = id && this._registry.isMultiNamespace(type); - if (object.id == null) { + if (id == null) { object.id = SavedObjectsUtils.generateId(); } @@ -434,8 +421,8 @@ export class SavedObjectsRepository { return expectedBulkGetResult; } - let savedObjectNamespace; - let savedObjectNamespaces; + let savedObjectNamespace: string | undefined; + let savedObjectNamespaces: string[] | undefined; let versionProperties; const { esRequestIndex, @@ -469,7 +456,7 @@ export class SavedObjectsRepository { versionProperties = getExpectedVersionProperties(version, actualResult); } else { if (this._registry.isSingleNamespace(object.type)) { - savedObjectNamespace = namespace; + savedObjectNamespace = initialNamespaces ? initialNamespaces[0] : namespace; } else if (this._registry.isMultiNamespace(object.type)) { savedObjectNamespaces = initialNamespaces || getSavedObjectNamespaces(namespace); } @@ -897,10 +884,10 @@ export class SavedObjectsRepository { total: body.hits.total, saved_objects: body.hits.hits.map( (hit: estypes.SearchHit): SavedObjectsFindResult => ({ - // @ts-expect-error @elastic/elasticsearch declared Id as string | number + // @ts-expect-error @elastic/elasticsearch _source is optional ...this._rawToSavedObject(hit), score: hit._score!, - // @ts-expect-error @elastic/elasticsearch declared sort as string | number + // @ts-expect-error @elastic/elasticsearch _source is optional sort: hit.sort, }) ), @@ -2080,6 +2067,29 @@ export class SavedObjectsRepository { const object = await this.get(type, id, options); return { saved_object: object, outcome: 'exactMatch' }; } + + private validateInitialNamespaces(type: string, initialNamespaces: string[] | undefined) { + if (!initialNamespaces) { + return; + } + + if (this._registry.isNamespaceAgnostic(type)) { + throw SavedObjectsErrorHelpers.createBadRequestError( + '"initialNamespaces" cannot be used on space-agnostic types' + ); + } else if (!initialNamespaces.length) { + throw SavedObjectsErrorHelpers.createBadRequestError( + '"initialNamespaces" must be a non-empty array of strings' + ); + } else if ( + !this._registry.isShareable(type) && + (initialNamespaces.length > 1 || initialNamespaces.includes(ALL_NAMESPACES_STRING)) + ) { + throw SavedObjectsErrorHelpers.createBadRequestError( + '"initialNamespaces" can only specify a single space when used with space-isolated types' + ); + } + } } /** diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index af682cfb81296..1423050145695 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -63,7 +63,11 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions { * Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in * {@link SavedObjectsCreateOptions}. * - * Note: this can only be used for multi-namespace object types. + * * For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, + * including the "All spaces" identifier (`'*'`). + * * For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only + * be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed. + * * For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used. */ initialNamespaces?: string[]; } @@ -96,7 +100,11 @@ export interface SavedObjectsBulkCreateObject { * Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in * {@link SavedObjectsCreateOptions}. * - * Note: this can only be used for multi-namespace object types. + * * For shareable object types (registered with `namespaceType: 'multiple'`): this option can be used to specify one or more spaces, + * including the "All spaces" identifier (`'*'`). + * * For isolated object types (registered with `namespaceType: 'single'` or `namespaceType: 'multiple-isolated'`): this option can only + * be used to specify a single space, and the "All spaces" identifier (`'*'`) is not allowed. + * * For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used. */ initialNamespaces?: string[]; } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 9e7721fde90e7..fcecf39f7e53a 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2901,7 +2901,7 @@ export class SavedObjectsRepository { resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; updateObjectsSpaces(objects: SavedObjectsUpdateObjectsSpacesObject[], spacesToAdd: string[], spacesToRemove: string[], options?: SavedObjectsUpdateObjectsSpacesOptions): Promise; -} + } // @public export interface SavedObjectsRepositoryFactory { 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> = []; + const subscription = service + .getAll$() + .subscribe((pluginStatuses) => statusUpdates.push(pluginStatuses)); + + const aStatus$ = new BehaviorSubject({ + 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(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; pluginDependencies: ReadonlyMap; @@ -23,6 +32,7 @@ export class PluginsStatusService { private readonly pluginStatuses = new Map>(); private readonly update$ = new BehaviorSubject(true); private readonly defaultInheritedStatus$: Observable; + 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) { + 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> { 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 - ] - ) + .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]; + }) .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 { } 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/core/server/ui_settings/integration_tests/index.test.ts b/src/core/server/ui_settings/integration_tests/index.test.ts index 6c7cdfa43cf57..61e55284a20b8 100644 --- a/src/core/server/ui_settings/integration_tests/index.test.ts +++ b/src/core/server/ui_settings/integration_tests/index.test.ts @@ -17,7 +17,7 @@ const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo. const savedObjectIndex = `.kibana_${kibanaVersion}_001`; describe('uiSettings/routes', function () { - jest.setTimeout(10000); + jest.setTimeout(120_000); beforeAll(startServers); /* eslint-disable jest/valid-describe */ diff --git a/src/core/server/ui_settings/integration_tests/lib/servers.ts b/src/core/server/ui_settings/integration_tests/lib/servers.ts index b18d9926649aa..96ba08a0728ab 100644 --- a/src/core/server/ui_settings/integration_tests/lib/servers.ts +++ b/src/core/server/ui_settings/integration_tests/lib/servers.ts @@ -75,8 +75,10 @@ export function getServices() { export async function stopServers() { services = null!; - if (servers) { + if (esServer) { await esServer.stop(); + } + if (kbn) { await kbn.stop(); } } diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts index ba22ecb3b6376..2995ffd08e5c0 100644 --- a/src/core/test_helpers/kbn_server.ts +++ b/src/core/test_helpers/kbn_server.ts @@ -7,15 +7,7 @@ */ import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; -import { - createTestEsCluster, - DEFAULT_SUPERUSER_PASS, - esTestConfig, - kbnTestConfig, - kibanaServerTestUser, - kibanaTestUser, - setupUsers, -} from '@kbn/test'; +import { createTestEsCluster, esTestConfig, kibanaServerTestUser, kibanaTestUser } from '@kbn/test'; import { defaultsDeep } from 'lodash'; import { resolve } from 'path'; import { BehaviorSubject } from 'rxjs'; @@ -208,7 +200,6 @@ export function createTestServers({ defaultsDeep({}, settings.es ?? {}, { log, license, - password: license === 'trial' ? DEFAULT_SUPERUSER_PASS : undefined, }) ); @@ -224,19 +215,7 @@ export function createTestServers({ await es.start(); if (['gold', 'trial'].includes(license)) { - await setupUsers({ - log, - esPort: esTestConfig.getUrlParts().port, - updates: [ - ...usersToBeAdded, - // user elastic - esTestConfig.getUrlParts() as { username: string; password: string }, - // user kibana - kbnTestConfig.getUrlParts() as { username: string; password: string }, - ], - }); - - // Override provided configs, we know what the elastic user is now + // Override provided configs kbnSettings.elasticsearch = { hosts: [esTestConfig.getUrl()], username: kibanaServerTestUser.username, diff --git a/src/core/types/elasticsearch/search.ts b/src/core/types/elasticsearch/search.ts index 36a684fb097a5..0960fb189a341 100644 --- a/src/core/types/elasticsearch/search.ts +++ b/src/core/types/elasticsearch/search.ts @@ -417,7 +417,9 @@ export type AggregateOf< { key: string; from?: number; + from_as_string?: string; to?: number; + to_as_string?: string; doc_count: number; }, TAggregationContainer extends { range: { ranges: Array } } 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/license_checker/config.ts b/src/dev/license_checker/config.ts index ebf56166a8922..b3b7bf5e8eed7 100644 --- a/src/dev/license_checker/config.ts +++ b/src/dev/license_checker/config.ts @@ -10,6 +10,7 @@ // used as dependencies or dev dependencies export const LICENSE_ALLOWED = [ 'Elastic-License', + 'Elastic License 2.0', 'SSPL-1.0 OR Elastic License 2.0', '0BSD', '(BSD-2-Clause OR MIT OR Apache-2.0)', @@ -72,6 +73,7 @@ export const DEV_ONLY_LICENSE_ALLOWED = ['MPL-2.0']; export const LICENSE_OVERRIDES = { 'jsts@1.6.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts '@mapbox/jsonlint-lines-primitives@2.0.2': ['MIT'], // license in readme https://github.com/tmcw/jsonlint + '@elastic/ems-client@7.14.0': ['Elastic License 2.0'], // TODO can be removed if the https://github.com/jindw/xmldom/issues/239 is released 'xmldom@0.1.27': ['MIT'], diff --git a/src/dev/run_licenses_csv_report.js b/src/dev/run_licenses_csv_report.js index 8a612c9e3d878..1923eddff33e9 100644 --- a/src/dev/run_licenses_csv_report.js +++ b/src/dev/run_licenses_csv_report.js @@ -71,7 +71,8 @@ run( licenses: [ 'Custom;https://www.redhat.com/licenses/EULA_Red_Hat_Universal_Base_Image_English_20190422.pdf', ], - sourceURL: 'https://oss-dependencies.elastic.co/redhat/ubi/ubi-minimal-8-source.tar.gz', + sourceURL: + 'https://oss-dependencies.elastic.co/red-hat-universal-base-image-minimal/8/ubi-minimal-8-source.tar.gz', } ); 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/charts/public/services/palettes/types.ts b/src/plugins/charts/public/services/palettes/types.ts index 6f13f62178364..7a870504270d7 100644 --- a/src/plugins/charts/public/services/palettes/types.ts +++ b/src/plugins/charts/public/services/palettes/types.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Ast } from '@kbn/interpreter/common'; +import { ExpressionAstExpression } from '../../../../expressions/common/ast'; /** * Information about a series in a chart used to determine its color. @@ -78,7 +78,7 @@ export interface PaletteDefinition { * This function should be used to pass the palette to the expression function applying color and other styles * @param state The internal state of the palette */ - toExpression: (state?: T) => Ast; + toExpression: (state?: T) => ExpressionAstExpression; /** * Color a series according to the internal rules of the palette. * @param series The current series along with its ancestors. 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 ( - +

    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`] = ` } > -
    -
    +
    @@ -950,7 +950,7 @@ exports[`DashboardEmptyScreen renders correctly with view mode 1`] = ` } > -
    -
    +
    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(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 = {} ) { 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 = get(filter, ['query', 'match', fieldName]); diff --git a/packages/kbn-interpreter/src/common/index.d.ts b/src/plugins/data/common/es_query/es_query/types.ts similarity index 67% rename from packages/kbn-interpreter/src/common/index.d.ts rename to src/plugins/data/common/es_query/es_query/types.ts index 6f54d07590973..2133736516049 100644 --- a/packages/kbn-interpreter/src/common/index.d.ts +++ b/src/plugins/data/common/es_query/es_query/types.ts @@ -6,7 +6,9 @@ * Side Public License, v 1. */ -export { Registry } from './lib/registry'; +import { IFieldType } from '../../index_patterns'; -export { fromExpression, toExpression, Ast, ExpressionFunctionAST } from './lib/ast'; -export { getType } from './lib/get_type'; +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, context?: Record ): 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 = {}, context: Record = {} ) { 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 = {}, context: Record = {} ) { 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 = {}, context: Record = {} ) { 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 = {}, context: Record = {} ) { 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 = {}, context: Record = {} ) { 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 = {}, context: Record = {} ) { 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 = {}, context: Record = {} ) { 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 = {}, context: Record = {} ) { 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 = {}, context: Record = {} ) { 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, context?: Record ) { 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, context?: Record ) => JsonValue; 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/agg_config.ts b/src/plugins/data/common/search/aggs/agg_config.ts index 3c83b5bdf6084..9a35cf983c805 100644 --- a/src/plugins/data/common/search/aggs/agg_config.ts +++ b/src/plugins/data/common/search/aggs/agg_config.ts @@ -192,9 +192,8 @@ export class AggConfig { } else if (!this.aggConfigs.timeRange) { return; } - return moment.duration( - moment(this.aggConfigs.timeRange.to).diff(this.aggConfigs.timeRange.from) - ); + const resolvedBounds = this.aggConfigs.getResolvedTimeRange()!; + return moment.duration(moment(resolvedBounds.max).diff(resolvedBounds.min)); } return parsedTimeShift; } diff --git a/src/plugins/data/common/search/aggs/agg_configs.ts b/src/plugins/data/common/search/aggs/agg_configs.ts index 8593a0b0ed0fa..c205b46e077f0 100644 --- a/src/plugins/data/common/search/aggs/agg_configs.ts +++ b/src/plugins/data/common/search/aggs/agg_configs.ts @@ -23,7 +23,7 @@ import { IAggType } from './agg_type'; import { AggTypesRegistryStart } from './agg_types_registry'; import { AggGroupNames } from './agg_groups'; import { IndexPattern } from '../../index_patterns/index_patterns/index_pattern'; -import { TimeRange, getTime, isRangeFilter } from '../../../common'; +import { TimeRange, getTime, isRangeFilter, calculateBounds } from '../../../common'; import { IBucketAggConfig } from './buckets'; import { insertTimeShiftSplit, mergeTimeShifts } from './utils/time_splits'; @@ -127,6 +127,19 @@ export class AggConfigs { this.aggs.forEach(updateAggTimeRange); } + /** + * Returns the current time range as moment instance (date math will get resolved using the current "now" value or system time if not set) + * @returns Current time range as resolved date. + */ + getResolvedTimeRange() { + return ( + this.timeRange && + calculateBounds(this.timeRange, { + forceNow: this.forceNow, + }) + ); + } + // clone method will reuse existing AggConfig in the list (will not create new instances) clone({ enabledOnly = true } = {}) { const filterAggs = (agg: AggConfig) => { 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 { */ 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/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 d56727b468da6..2849b93b14483 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -259,6 +259,7 @@ export class AggConfigs { getRequestAggById(id: string): AggConfig | undefined; // (undocumented) getRequestAggs(): AggConfig[]; + getResolvedTimeRange(): import("../..").TimeRangeBounds | undefined; getResponseAggById(id: string): AggConfig | undefined; getResponseAggs(): AggConfig[]; // (undocumented) @@ -807,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; @@ -857,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").KueryNode; - toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; + toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | 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) @@ -866,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[]; @@ -1285,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 | undefined>; - // (undocumented) - fields: IFieldType[]; getFormatterForField?: (field: IndexPatternField | IndexPatternField['spec'] | IFieldType) => FieldFormat; // (undocumented) getTimeField?(): IFieldType | undefined; // (undocumented) - id?: string; - // (undocumented) timeFieldName?: string; // (undocumented) title: string; @@ -1350,6 +1348,7 @@ export interface IKibanaSearchRequest { export interface IKibanaSearchResponse { id?: string; isPartial?: boolean; + isRestored?: boolean; isRunning?: boolean; loaded?: number; rawResponse: RawResponse; @@ -2729,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']) => ( + <> + + It needs more time to fully render. You can wait here or come back to it later. + + + + + + + +); 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; let bfetchSetup: jest.Mocked; @@ -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).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/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`] = `
    + +

    + + No data available + +

    +
    - -

    - - No data available - -

    -
    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>; @@ -287,24 +288,48 @@ export class SearchService implements Plugin { 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").KueryNode; - toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IIndexPattern | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; + toElasticsearchQuery: (node: import("../common").KueryNode, indexPattern?: import("../common").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | 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/layout/discover_layout.test.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx index 2fd394d98281b..57a9d518f838e 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx @@ -18,7 +18,6 @@ import { createSearchSourceMock } from '../../../../../../../data/common/search/ import { IndexPattern, IndexPatternAttributes } from '../../../../../../../data/common'; import { SavedObject } from '../../../../../../../../core/types'; import { indexPatternWithTimefieldMock } from '../../../../../__mocks__/index_pattern_with_timefield'; -import { DiscoverSearchSessionManager } from '../../services/discover_search_session'; import { GetStateReturn } from '../../services/discover_state'; import { DiscoverLayoutProps } from './types'; import { SavedSearchDataSubject } from '../../services/use_saved_search'; @@ -50,11 +49,12 @@ function getProps(indexPattern: IndexPattern): DiscoverLayoutProps { indexPattern, indexPatternList, navigateTo: jest.fn(), + onChangeIndexPattern: jest.fn(), + onUpdateQuery: jest.fn(), resetQuery: jest.fn(), savedSearch: savedSearchMock, savedSearchData$: savedSearch$, savedSearchRefetch$: new Subject(), - searchSessionManager: {} as DiscoverSearchSessionManager, searchSource: searchSourceMock, services, state: { columns: [] }, diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx index 0430614d413b6..a10674323e5cb 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx @@ -36,10 +36,8 @@ import { SortPairArr } from '../../../../angular/doc_table/lib/get_sort'; import { DOC_HIDE_TIME_COLUMN_SETTING, DOC_TABLE_LEGACY, - MODIFY_COLUMNS_ON_SWITCH, SAMPLE_SIZE_SETTING, SEARCH_FIELDS_FROM_SOURCE, - SORT_DEFAULT_ORDER_SETTING, } from '../../../../../../common'; import { popularizeField } from '../../../../helpers/popularize_field'; import { DocViewFilterFn } from '../../../../doc_views/doc_views_types'; @@ -52,7 +50,6 @@ import { InspectorSession } from '../../../../../../../inspector/public'; import { DiscoverUninitialized } from '../uninitialized/uninitialized'; import { SavedSearchDataMessage } from '../../services/use_saved_search'; import { useDataGridColumns } from '../../../../helpers/use_data_grid_columns'; -import { getSwitchIndexPatternAppState } from '../../utils/get_switch_index_pattern_app_state'; import { FetchStatus } from '../../../../types'; const DocTableLegacyMemoized = React.memo(DocTableLegacy); @@ -72,26 +69,20 @@ export function DiscoverLayout({ indexPattern, indexPatternList, navigateTo, + onChangeIndexPattern, + onUpdateQuery, savedSearchRefetch$, resetQuery, savedSearchData$, savedSearch, - searchSessionManager, searchSource, services, state, stateContainer, }: DiscoverLayoutProps) { - const { - trackUiMetric, - capabilities, - indexPatterns, - data, - uiSettings: config, - filterManager, - } = services; + const { trackUiMetric, capabilities, indexPatterns, data, uiSettings, filterManager } = services; - const sampleSize = useMemo(() => config.get(SAMPLE_SIZE_SETTING), [config]); + const sampleSize = useMemo(() => uiSettings.get(SAMPLE_SIZE_SETTING), [uiSettings]); const [expandedDoc, setExpandedDoc] = useState(undefined); const [inspectorSession, setInspectorSession] = useState(undefined); const scrollableDesktop = useRef(null); @@ -121,42 +112,21 @@ export function DiscoverLayout({ }; }, [savedSearchData$, fetchState]); - const isMobile = () => { - // collapse icon isn't displayed in mobile view, use it to detect which view is displayed - return collapseIcon && !collapseIcon.current; - }; + // collapse icon isn't displayed in mobile view, use it to detect which view is displayed + const isMobile = () => collapseIcon && !collapseIcon.current; const timeField = useMemo(() => { return indexPatternsUtils.isDefault(indexPattern) ? indexPattern.timeFieldName : undefined; }, [indexPattern]); const [isSidebarClosed, setIsSidebarClosed] = useState(false); - const isLegacy = useMemo(() => services.uiSettings.get(DOC_TABLE_LEGACY), [services]); - const useNewFieldsApi = useMemo(() => !services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [ - services, - ]); - - const unmappedFieldsConfig = useMemo( - () => ({ - showUnmappedFields: useNewFieldsApi, - }), - [useNewFieldsApi] - ); + const isLegacy = useMemo(() => uiSettings.get(DOC_TABLE_LEGACY), [uiSettings]); + const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]); const resultState = useMemo(() => getResultState(fetchStatus, rows!), [fetchStatus, rows]); - const updateQuery = useCallback( - (_payload, isUpdate?: boolean) => { - if (isUpdate === false) { - searchSessionManager.removeSearchSessionIdFromURL({ replace: false }); - savedSearchRefetch$.next(); - } - }, - [savedSearchRefetch$, searchSessionManager] - ); - const { columns, onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useDataGridColumns({ capabilities, - config, + config: uiSettings, indexPattern, indexPatterns, setAppState: stateContainer.setAppState, @@ -243,42 +213,8 @@ export function DiscoverLayout({ const contentCentered = resultState === 'uninitialized'; const showTimeCol = useMemo( - () => !config.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName, - [config, indexPattern.timeFieldName] - ); - - const onChangeIndexPattern = useCallback( - async (id: string) => { - const nextIndexPattern = await indexPatterns.get(id); - if (nextIndexPattern && indexPattern) { - /** - * Without resetting the fetch state, e.g. a time column would be displayed when switching - * from a index pattern without to a index pattern with time filter for a brief moment - * That's because appState is updated before savedSearchData$ - * The following line of code catches this, but should be improved - */ - savedSearchData$.next({ rows: [], state: FetchStatus.LOADING, fieldCounts: {} }); - - const nextAppState = getSwitchIndexPatternAppState( - indexPattern, - nextIndexPattern, - state.columns || [], - (state.sort || []) as SortPairArr[], - config.get(MODIFY_COLUMNS_ON_SWITCH), - config.get(SORT_DEFAULT_ORDER_SETTING) - ); - stateContainer.setAppState(nextAppState); - } - }, - [ - config, - indexPattern, - indexPatterns, - savedSearchData$, - state.columns, - state.sort, - stateContainer, - ] + () => !uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName, + [uiSettings, indexPattern.timeFieldName] ); return ( @@ -294,7 +230,7 @@ export function DiscoverLayout({ searchSource={searchSource} services={services} stateContainer={stateContainer} - updateQuery={updateQuery} + updateQuery={onUpdateQuery} />

    @@ -316,7 +252,6 @@ export function DiscoverLayout({ state={state} isClosed={isSidebarClosed} trackUiMetric={trackUiMetric} - unmappedFieldsConfig={unmappedFieldsConfig} useNewFieldsApi={useNewFieldsApi} onEditRuntimeField={onEditRuntimeField} /> @@ -373,7 +308,7 @@ export function DiscoverLayout({ > >; - resetQuery: () => void; navigateTo: (url: string) => void; + onChangeIndexPattern: (id: string) => void; + onUpdateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; + resetQuery: () => void; savedSearch: SavedSearch; savedSearchData$: SavedSearchDataSubject; savedSearchRefetch$: SavedSearchRefetchSubject; - searchSessionManager: DiscoverSearchSessionManager; searchSource: ISearchSource; services: DiscoverServices; state: AppState; 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(); + 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(); + 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.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx index e60dabd1d8d8c..26a3c482e9d3c 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx @@ -8,7 +8,7 @@ import './discover_field.scss'; -import React, { useState } from 'react'; +import React, { useState, useCallback, memo } from 'react'; import { EuiPopover, EuiPopoverTitle, @@ -29,6 +29,172 @@ import { IndexPatternField, IndexPattern } from '../../../../../../../data/publi import { getFieldTypeName } from './lib/get_field_type_name'; import { DiscoverFieldDetailsFooter } from './discover_field_details_footer'; +function wrapOnDot(str?: string) { + // u200B is a non-width white-space character, which allows + // the browser to efficiently word-wrap right after the dot + // without us having to draw a lot of extra DOM elements, etc + return str ? str.replace(/\./g, '.\u200B') : ''; +} + +const FieldInfoIcon: React.FC = memo(() => ( + + + +)); + +const DiscoverFieldTypeIcon: React.FC<{ field: IndexPatternField }> = memo(({ field }) => ( + +)); + +const FieldName: React.FC<{ field: IndexPatternField }> = memo(({ field }) => { + const title = + field.displayName !== field.name + ? i18n.translate('discover.field.title', { + defaultMessage: '{fieldName} ({fieldDisplayName})', + values: { + fieldName: field.name, + fieldDisplayName: field.displayName, + }, + }) + : field.displayName; + + return ( + + {wrapOnDot(field.displayName)} + + ); +}); + +interface ActionButtonProps { + field: IndexPatternField; + isSelected?: boolean; + alwaysShow: boolean; + toggleDisplay: (field: IndexPatternField) => void; +} + +const ActionButton: React.FC = memo( + ({ field, isSelected, alwaysShow, toggleDisplay }) => { + const actionBtnClassName = classNames('dscSidebarItem__action', { + ['dscSidebarItem__mobile']: alwaysShow, + }); + if (field.name === '_source') { + return null; + } + if (!isSelected) { + return ( + + ) => { + if (ev.type === 'click') { + ev.currentTarget.focus(); + } + ev.preventDefault(); + ev.stopPropagation(); + toggleDisplay(field); + }} + data-test-subj={`fieldToggle-${field.name}`} + aria-label={i18n.translate('discover.fieldChooser.discoverField.addButtonAriaLabel', { + defaultMessage: 'Add {field} to table', + values: { field: field.name }, + })} + /> + + ); + } else { + return ( + + ) => { + if (ev.type === 'click') { + ev.currentTarget.focus(); + } + ev.preventDefault(); + ev.stopPropagation(); + toggleDisplay(field); + }} + data-test-subj={`fieldToggle-${field.name}`} + aria-label={i18n.translate( + 'discover.fieldChooser.discoverField.removeButtonAriaLabel', + { + defaultMessage: 'Remove {field} from table', + values: { field: field.name }, + } + )} + /> + + ); + } + } +); + +interface MultiFieldsProps { + multiFields: NonNullable; + toggleDisplay: (field: IndexPatternField) => void; + alwaysShowActionButton: boolean; +} + +const MultiFields: React.FC = memo( + ({ multiFields, toggleDisplay, alwaysShowActionButton }) => ( + + +
    + {i18n.translate('discover.fieldChooser.discoverField.multiFields', { + defaultMessage: 'Multi fields', + })} +
    +
    + {multiFields.map((entry) => ( + } + fieldAction={ + + } + fieldName={} + key={entry.field.name} + /> + ))} +
    + ) +); + export interface DiscoverFieldProps { /** * Determines whether add/remove button is displayed not only when focused @@ -85,7 +251,7 @@ export interface DiscoverFieldProps { onDeleteField?: (fieldName: string) => void; } -export function DiscoverField({ +function DiscoverFieldComponent({ alwaysShowActionButton = false, field, indexPattern, @@ -99,133 +265,22 @@ export function DiscoverField({ onEditField, onDeleteField, }: DiscoverFieldProps) { - const addLabelAria = i18n.translate('discover.fieldChooser.discoverField.addButtonAriaLabel', { - defaultMessage: 'Add {field} to table', - values: { field: field.name }, - }); - const removeLabelAria = i18n.translate( - 'discover.fieldChooser.discoverField.removeButtonAriaLabel', - { - defaultMessage: 'Remove {field} from table', - values: { field: field.name }, - } - ); - const [infoIsOpen, setOpen] = useState(false); - const toggleDisplay = (f: IndexPatternField, isSelected: boolean) => { - if (isSelected) { - onRemoveField(f.name); - } else { - onAddField(f.name); - } - }; + const toggleDisplay = useCallback( + (f: IndexPatternField) => { + if (selected) { + onRemoveField(f.name); + } else { + onAddField(f.name); + } + }, + [onAddField, onRemoveField, selected] + ); - function togglePopover() { + const togglePopover = useCallback(() => { setOpen(!infoIsOpen); - } - - function wrapOnDot(str?: string) { - // u200B is a non-width white-space character, which allows - // the browser to efficiently word-wrap right after the dot - // without us having to draw a lot of extra DOM elements, etc - return str ? str.replace(/\./g, '.\u200B') : ''; - } - - const getDscFieldIcon = (indexPatternField: IndexPatternField) => { - return ( - - ); - }; - - const dscFieldIcon = getDscFieldIcon(field); - - const getTitle = (indexPatternField: IndexPatternField) => { - return indexPatternField.displayName !== indexPatternField.name - ? i18n.translate('discover.field.title', { - defaultMessage: '{fieldName} ({fieldDisplayName})', - values: { - fieldName: indexPatternField.name, - fieldDisplayName: indexPatternField.displayName, - }, - }) - : indexPatternField.displayName; - }; - - const getFieldName = (indexPatternField: IndexPatternField) => { - return ( - - {wrapOnDot(indexPatternField.displayName)} - - ); - }; - const fieldName = getFieldName(field); - - const actionBtnClassName = classNames('dscSidebarItem__action', { - ['dscSidebarItem__mobile']: alwaysShowActionButton, - }); - const getActionButton = (f: IndexPatternField, isSelected?: boolean) => { - if (f.name !== '_source' && !isSelected) { - return ( - - ) => { - if (ev.type === 'click') { - ev.currentTarget.focus(); - } - ev.preventDefault(); - ev.stopPropagation(); - toggleDisplay(f, false); - }} - data-test-subj={`fieldToggle-${f.name}`} - aria-label={addLabelAria} - /> - - ); - } else if (f.name !== '_source' && isSelected) { - return ( - - ) => { - if (ev.type === 'click') { - ev.currentTarget.focus(); - } - ev.preventDefault(); - ev.stopPropagation(); - toggleDisplay(f, isSelected); - }} - data-test-subj={`fieldToggle-${f.name}`} - aria-label={removeLabelAria} - /> - - ); - } - }; - - const actionButton = getActionButton(field, selected); + }, [infoIsOpen]); if (field.type === '_source') { return ( @@ -233,71 +288,20 @@ export function DiscoverField({ size="s" className="dscSidebarItem" dataTestSubj={`field-${field.name}-showDetails`} - fieldIcon={dscFieldIcon} - fieldAction={actionButton} - fieldName={fieldName} + fieldIcon={} + fieldAction={ + + } + fieldName={} /> ); } - const getFieldInfoIcon = () => { - if (field.type !== 'conflict') { - return null; - } - return ( - - - - ); - }; - - const fieldInfoIcon = getFieldInfoIcon(); - - const shouldRenderMultiFields = !!multiFields; - const renderMultiFields = () => { - if (!multiFields) { - return null; - } - return ( - - -
    - {i18n.translate('discover.fieldChooser.discoverField.multiFields', { - defaultMessage: 'Multi fields', - })} -
    -
    - {multiFields.map((entry) => ( - {}} - dataTestSubj={`field-${entry.field.name}-showDetails`} - fieldIcon={getDscFieldIcon(entry.field)} - fieldAction={getActionButton(entry.field, entry.isSelected)} - fieldName={getFieldName(entry.field)} - key={entry.field.name} - /> - ))} -
    - ); - }; - const isRuntimeField = Boolean(indexPattern.getFieldByName(field.name)?.runtimeField); const isUnknownField = field.type === 'unknown' || field.type === 'unknown_selected'; const canEditField = onEditField && (!isUnknownField || isRuntimeField); @@ -334,9 +338,7 @@ export function DiscoverField({ > { - if (onDeleteField) { - onDeleteField(field.name); - } + onDeleteField?.(field.name); }} iconType="trash" data-test-subj={`discoverFieldListPanelDelete-${field.name}`} @@ -352,6 +354,8 @@ export function DiscoverField({ ); + const details = getDetails(field); + return ( { - togglePopover(); - }} + onClick={togglePopover} dataTestSubj={`field-${field.name}-showDetails`} - fieldIcon={dscFieldIcon} - fieldAction={actionButton} - fieldName={fieldName} - fieldInfoIcon={fieldInfoIcon} + fieldIcon={} + fieldAction={ + + } + fieldName={} + fieldInfoIcon={field.type === 'conflict' && } /> } isOpen={infoIsOpen} @@ -384,26 +393,33 @@ export function DiscoverField({

    {infoIsOpen && ( - - )} - {shouldRenderMultiFields ? ( <> - {renderMultiFields()} - + {multiFields && ( + + )} + {!details.error && ( + + )} - ) : null} + )} ); } + +export const DiscoverField = memo(DiscoverFieldComponent); diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.tsx index d7008ba3e310f..ffa7b30de5280 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field_details.tsx @@ -20,7 +20,6 @@ import { import { Bucket, FieldDetails } from './types'; import { IndexPatternField, IndexPattern } from '../../../../../../../data/public'; import './discover_field_details.scss'; -import { DiscoverFieldDetailsFooter } from './discover_field_details_footer'; interface DiscoverFieldDetailsProps { field: IndexPatternField; @@ -28,7 +27,6 @@ interface DiscoverFieldDetailsProps { details: FieldDetails; onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; - showFooter?: boolean; } export function DiscoverFieldDetails({ @@ -37,7 +35,6 @@ export function DiscoverFieldDetails({ details, onAddFilter, trackUiMetric, - showFooter = true, }: DiscoverFieldDetailsProps) { const warnings = getWarnings(field); const [showVisualizeLink, setShowVisualizeLink] = useState(false); @@ -111,14 +108,6 @@ export function DiscoverFieldDetails({ )}
    - {!details.error && showFooter && ( - - )} ); } diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx index 0bebec61657b4..7f8866a2ee369 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar.tsx @@ -21,6 +21,7 @@ import { EuiPageSideBar, useResizeObserver, } from '@elastic/eui'; +import useShallowCompareEffect from 'react-use/lib/useShallowCompareEffect'; import { isEqual, sortBy } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -81,7 +82,6 @@ export function DiscoverSidebar({ trackUiMetric, useNewFieldsApi = false, useFlyout = false, - unmappedFieldsConfig, onEditRuntimeField, onChangeIndexPattern, setFieldEditorRef, @@ -128,25 +128,8 @@ export function DiscoverSidebar({ popular: popularFields, unpopular: unpopularFields, } = useMemo( - () => - groupFields( - fields, - columns, - popularLimit, - fieldCounts, - fieldFilter, - useNewFieldsApi, - !!unmappedFieldsConfig?.showUnmappedFields - ), - [ - fields, - columns, - popularLimit, - fieldCounts, - fieldFilter, - useNewFieldsApi, - unmappedFieldsConfig?.showUnmappedFields, - ] + () => groupFields(fields, columns, popularLimit, fieldCounts, fieldFilter, useNewFieldsApi), + [fields, columns, popularLimit, fieldCounts, fieldFilter, useNewFieldsApi] ); const paginate = useCallback(() => { @@ -205,7 +188,7 @@ export function DiscoverSidebar({ return result; }, [fields]); - const multiFields = useMemo(() => { + const calculateMultiFields = () => { if (!useNewFieldsApi || !fields) { return undefined; } @@ -224,7 +207,13 @@ export function DiscoverSidebar({ map.set(parent, value); }); return map; - }, [fields, useNewFieldsApi, selectedFields]); + }; + + const [multiFields, setMultiFields] = useState(() => calculateMultiFields()); + + useShallowCompareEffect(() => { + setMultiFields(calculateMultiFields()); + }, [fields, selectedFields, useNewFieldsApi]); const deleteField = useMemo( () => diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx index 2ad75806173eb..6973221fd3624 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx @@ -25,7 +25,6 @@ import { } from './discover_sidebar_responsive'; import { DiscoverServices } from '../../../../../build_services'; import { ElasticSearchHit } from '../../../../doc_views/doc_views_types'; -import { DiscoverSidebar } from './discover_sidebar'; const mockServices = ({ history: () => ({ @@ -132,14 +131,4 @@ describe('discover responsive sidebar', function () { findTestSubject(comp, 'plus-extension-gif').simulate('click'); expect(props.onAddFilter).toHaveBeenCalled(); }); - it('renders sidebar with unmapped fields config', function () { - const unmappedFieldsConfig = { - showUnmappedFields: false, - }; - const componentProps = { ...props, unmappedFieldsConfig }; - const component = mountWithIntl(); - const discoverSidebar = component.find(DiscoverSidebar); - expect(discoverSidebar).toHaveLength(1); - expect(discoverSidebar.props().unmappedFieldsConfig).toEqual(unmappedFieldsConfig); - }); }); diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx index cc33601f77728..003bb22599e48 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx @@ -105,15 +105,6 @@ export interface DiscoverSidebarResponsiveProps { * Read from the Fields API */ useNewFieldsApi?: boolean; - /** - * an object containing properties for proper handling of unmapped fields - */ - unmappedFieldsConfig?: { - /** - * determines whether to display unmapped fields - */ - showUnmappedFields: boolean; - }; /** * callback to execute on edit runtime field */ diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.test.ts b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.test.ts index 5869720635621..cd9f6b3cac4a5 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.test.ts +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.test.ts @@ -244,8 +244,7 @@ describe('group_fields', function () { 5, fieldCounts, fieldFilterState, - true, - false + true ); expect(actual.unpopular).toEqual([]); }); @@ -270,8 +269,7 @@ describe('group_fields', function () { 5, fieldCounts, fieldFilterState, - false, - undefined + false ); expect(actual.unpopular.map((field) => field.name)).toEqual(['unknown_field']); }); diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.tsx index dc6cbcedc8086..2007d32fe84be 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/group_fields.tsx @@ -24,9 +24,9 @@ export function groupFields( popularLimit: number, fieldCounts: Record, fieldFilterState: FieldFilterState, - useNewFieldsApi: boolean, - showUnmappedFields = true + useNewFieldsApi: boolean ): GroupedFields { + const showUnmappedFields = useNewFieldsApi; const result: GroupedFields = { selected: [], popular: [], diff --git a/src/plugins/discover/public/application/apps/main/discover_main_app.tsx b/src/plugins/discover/public/application/apps/main/discover_main_app.tsx index 5cc7147b49ff9..07939fff6e7f4 100644 --- a/src/plugins/discover/public/application/apps/main/discover_main_app.tsx +++ b/src/plugins/discover/public/application/apps/main/discover_main_app.tsx @@ -5,15 +5,12 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React, { useMemo, useCallback, useEffect } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { History } from 'history'; import { DiscoverLayout } from './components/layout'; -import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../common'; -import { useSavedSearch as useSavedSearchData } from './services/use_saved_search'; import { setBreadcrumbsTitle } from '../../helpers/breadcrumbs'; import { addHelpMenuToAppChrome } from '../../components/help_menu/help_menu_util'; import { useDiscoverState } from './services/use_discover_state'; -import { useSearchSession } from './services/use_search_session'; import { useUrl } from './services/use_url'; import { IndexPattern, IndexPatternAttributes, SavedObject } from '../../../../../data/common'; import { DiscoverServices } from '../../../build_services'; @@ -55,18 +52,20 @@ export function DiscoverMainApp(props: DiscoverMainProps) { const { services, history, navigateTo, indexPatternList } = props.opts; const { chrome, docLinks, uiSettings: config, data } = services; - const useNewFieldsApi = useMemo(() => !config.get(SEARCH_FIELDS_FROM_SOURCE), [config]); - /** * State related logic */ const { - stateContainer, - state, + data$, indexPattern, - searchSource, - savedSearch, + onChangeIndexPattern, + onUpdateQuery, + refetch$, resetSavedSearch, + savedSearch, + searchSource, + state, + stateContainer, } = useDiscoverState({ services, history, @@ -79,25 +78,6 @@ export function DiscoverMainApp(props: DiscoverMainProps) { */ useUrl({ history, resetSavedSearch }); - /** - * Search session logic - */ - const searchSessionManager = useSearchSession({ services, history, stateContainer, savedSearch }); - - /** - * Data fetching logic - */ - const { data$, refetch$ } = useSavedSearchData({ - indexPattern, - savedSearch, - searchSessionManager, - searchSource, - services, - state, - stateContainer, - useNewFieldsApi, - }); - /** * SavedSearch depended initializing */ @@ -115,11 +95,6 @@ export function DiscoverMainApp(props: DiscoverMainProps) { */ useEffect(() => { addHelpMenuToAppChrome(chrome, docLinks); - stateContainer.replaceUrlAppState({}).then(() => { - stateContainer.startSync(); - }); - - return () => stateContainer.stopSync(); }, [stateContainer, chrome, docLinks]); const resetQuery = useCallback(() => { @@ -130,12 +105,13 @@ export function DiscoverMainApp(props: DiscoverMainProps) { ; + /** + * Function starting state sync when Discover main is loaded + */ + initializeAndSync: ( + indexPattern: IndexPattern, + filterManager: FilterManager, + data: DataPublicPluginStart + ) => () => void; /** * Start sync between state and URL */ @@ -204,16 +216,18 @@ export function getState({ stateStorage, }); + const replaceUrlAppState = async (newPartial: AppState = {}) => { + const state = { ...appStateContainer.getState(), ...newPartial }; + await stateStorage.set(APP_STATE_URL_KEY, state, { replace: true }); + }; + return { kbnUrlStateStorage: stateStorage, appStateContainer: appStateContainerModified, startSync: start, stopSync: stop, setAppState: (newPartial: AppState) => setState(appStateContainerModified, newPartial), - replaceUrlAppState: async (newPartial: AppState = {}) => { - const state = { ...appStateContainer.getState(), ...newPartial }; - await stateStorage.set(APP_STATE_URL_KEY, state, { replace: true }); - }, + replaceUrlAppState, resetInitialAppState: () => { initialAppState = appStateContainer.getState(); }, @@ -224,6 +238,50 @@ export function getState({ getPreviousAppState: () => previousAppState, flushToUrl: () => stateStorage.kbnUrlControls.flush(), isAppStateDirty: () => !isEqualState(initialAppState, appStateContainer.getState()), + initializeAndSync: ( + indexPattern: IndexPattern, + filterManager: FilterManager, + data: DataPublicPluginStart + ) => { + if (appStateContainer.getState().index !== indexPattern.id) { + // used index pattern is different than the given by url/state which is invalid + setState(appStateContainerModified, { index: indexPattern.id }); + } + // sync initial app filters from state to filterManager + const filters = appStateContainer.getState().filters; + if (filters) { + filterManager.setAppFilters(cloneDeep(filters)); + } + const query = appStateContainer.getState().query; + if (query) { + data.query.queryString.setQuery(query); + } + + const stopSyncingQueryAppStateWithStateContainer = connectToQueryState( + data.query, + appStateContainer, + { + filters: esFilters.FilterStateStore.APP_STATE, + query: true, + } + ); + + // syncs `_g` portion of url with query services + const { stop: stopSyncingGlobalStateWithUrl } = syncQueryStateWithUrl( + data.query, + stateStorage + ); + + replaceUrlAppState({}).then(() => { + start(); + }); + + return () => { + stopSyncingQueryAppStateWithStateContainer(); + stopSyncingGlobalStateWithUrl(); + stop(); + }; + }, }; } diff --git a/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts b/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts index 051a2d2dcd9cc..4c3d819f063a0 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts +++ b/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts @@ -62,10 +62,6 @@ describe('test useDiscoverState', () => { }); }); - await act(async () => { - result.current.stateContainer.startSync(); - }); - const initialColumns = result.current.state.columns; await act(async () => { result.current.setState({ columns: ['123'] }); diff --git a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts index a3546d54cd493..3c736f09a8296 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts +++ b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts @@ -6,19 +6,25 @@ * Side Public License, v 1. */ import { useMemo, useEffect, useState, useCallback } from 'react'; -import { cloneDeep } from 'lodash'; +import { isEqual } from 'lodash'; import { History } from 'history'; import { getState } from './discover_state'; import { getStateDefaults } from '../utils/get_state_defaults'; -import { - esFilters, - connectToQueryState, - syncQueryStateWithUrl, - IndexPattern, -} from '../../../../../../data/public'; +import { IndexPattern } from '../../../../../../data/public'; import { DiscoverServices } from '../../../../build_services'; import { SavedSearch } from '../../../../saved_searches'; import { loadIndexPattern } from '../utils/resolve_index_pattern'; +import { useSavedSearch as useSavedSearchData } from './use_saved_search'; +import { + MODIFY_COLUMNS_ON_SWITCH, + SEARCH_FIELDS_FROM_SOURCE, + SEARCH_ON_PAGE_LOAD_SETTING, + SORT_DEFAULT_ORDER_SETTING, +} from '../../../../../common'; +import { useSearchSession } from './use_search_session'; +import { FetchStatus } from '../../../types'; +import { getSwitchIndexPatternAppState } from '../utils/get_switch_index_pattern_app_state'; +import { SortPairArr } from '../../../angular/doc_table/lib/get_sort'; export function useDiscoverState({ services, @@ -31,9 +37,11 @@ export function useDiscoverState({ history: History; initialIndexPattern: IndexPattern; }) { - const { uiSettings: config, data, filterManager } = services; + const { uiSettings: config, data, filterManager, indexPatterns } = services; const [indexPattern, setIndexPattern] = useState(initialIndexPattern); const [savedSearch, setSavedSearch] = useState(initialSavedSearch); + const useNewFieldsApi = useMemo(() => !config.get(SEARCH_FIELDS_FROM_SOURCE), [config]); + const timefilter = data.query.timefilter.timefilter; const searchSource = useMemo(() => { savedSearch.searchSource.setField('index', indexPattern); @@ -57,73 +65,80 @@ export function useDiscoverState({ [config, data, history, savedSearch, services.core.notifications.toasts] ); - const { appStateContainer, getPreviousAppState } = stateContainer; + const { appStateContainer } = stateContainer; const [state, setState] = useState(appStateContainer.getState()); - useEffect(() => { - if (stateContainer.appStateContainer.getState().index !== indexPattern.id) { - // used index pattern is different than the given by url/state which is invalid - stateContainer.setAppState({ index: indexPattern.id }); - } - // sync initial app filters from state to filterManager - const filters = appStateContainer.getState().filters; - if (filters) { - filterManager.setAppFilters(cloneDeep(filters)); - } - const query = appStateContainer.getState().query; - if (query) { - data.query.queryString.setQuery(query); - } + /** + * Search session logic + */ + const searchSessionManager = useSearchSession({ services, history, stateContainer, savedSearch }); - const stopSyncingQueryAppStateWithStateContainer = connectToQueryState( - data.query, - appStateContainer, - { - filters: esFilters.FilterStateStore.APP_STATE, - query: true, - } - ); + const initialFetchStatus: FetchStatus = useMemo(() => { + // A saved search is created on every page load, so we check the ID to see if we're loading a + // previously saved search or if it is just transient + const shouldSearchOnPageLoad = + config.get(SEARCH_ON_PAGE_LOAD_SETTING) || + savedSearch.id !== undefined || + timefilter.getRefreshInterval().pause === false || + searchSessionManager.hasSearchSessionIdInURL(); + return shouldSearchOnPageLoad ? FetchStatus.LOADING : FetchStatus.UNINITIALIZED; + }, [config, savedSearch.id, searchSessionManager, timefilter]); - // syncs `_g` portion of url with query services - const { stop: stopSyncingGlobalStateWithUrl } = syncQueryStateWithUrl( - data.query, - stateContainer.kbnUrlStateStorage - ); + /** + * Data fetching logic + */ + const { data$, refetch$, reset } = useSavedSearchData({ + indexPattern, + initialFetchStatus, + searchSessionManager, + searchSource, + services, + stateContainer, + useNewFieldsApi, + }); + + useEffect(() => { + const stopSync = stateContainer.initializeAndSync(indexPattern, filterManager, data); return () => { - stopSyncingQueryAppStateWithStateContainer(); - stopSyncingGlobalStateWithUrl(); + stopSync(); }; - }, [ - appStateContainer, - config, - data.query, - data.search.session, - getPreviousAppState, - indexPattern.id, - filterManager, - services.indexPatterns, - stateContainer, - ]); + }, [stateContainer, filterManager, data, indexPattern]); + /** + * Track state changes that should trigger a fetch + */ useEffect(() => { - const unsubscribe = stateContainer.appStateContainer.subscribe(async (nextState) => { + const unsubscribe = appStateContainer.subscribe(async (nextState) => { + const { hideChart, interval, sort, index } = state; + // chart was hidden, now it should be displayed, so data is needed + const chartDisplayChanged = nextState.hideChart !== hideChart && hideChart; + const chartIntervalChanged = nextState.interval !== interval; + const docTableSortChanged = !isEqual(nextState.sort, sort); + const indexPatternChanged = !isEqual(nextState.index, index); // NOTE: this is also called when navigating from discover app to context app - if (nextState.index && state.index !== nextState.index) { - const nextIndexPattern = await loadIndexPattern( - nextState.index, - services.indexPatterns, - config - ); + if (nextState.index && indexPatternChanged) { + /** + * Without resetting the fetch state, e.g. a time column would be displayed when switching + * from a index pattern without to a index pattern with time filter for a brief moment + * That's because appState is updated before savedSearchData$ + * The following line of code catches this, but should be improved + */ + reset(); + const nextIndexPattern = await loadIndexPattern(nextState.index, indexPatterns, config); if (nextIndexPattern) { setIndexPattern(nextIndexPattern.loaded); } } + + if (chartDisplayChanged || chartIntervalChanged || docTableSortChanged) { + refetch$.next(); + } setState(nextState); }); return () => unsubscribe(); - }, [config, services.indexPatterns, state.index, stateContainer.appStateContainer, setState]); + }, [config, indexPatterns, appStateContainer, setState, state, refetch$, data$, reset]); const resetSavedSearch = useCallback( async (id?: string) => { @@ -143,13 +158,62 @@ export function useDiscoverState({ [services, indexPattern, config, data, stateContainer, savedSearch.id] ); + /** + * Function triggered when user changes index pattern in the sidebar + */ + const onChangeIndexPattern = useCallback( + async (id: string) => { + const nextIndexPattern = await indexPatterns.get(id); + if (nextIndexPattern && indexPattern) { + const nextAppState = getSwitchIndexPatternAppState( + indexPattern, + nextIndexPattern, + state.columns || [], + (state.sort || []) as SortPairArr[], + config.get(MODIFY_COLUMNS_ON_SWITCH), + config.get(SORT_DEFAULT_ORDER_SETTING) + ); + stateContainer.setAppState(nextAppState); + } + }, + [config, indexPattern, indexPatterns, state.columns, state.sort, stateContainer] + ); + /** + * Function triggered when the user changes the query in the search bar + */ + const onUpdateQuery = useCallback( + (_payload, isUpdate?: boolean) => { + if (isUpdate === false) { + searchSessionManager.removeSearchSessionIdFromURL({ replace: false }); + refetch$.next(); + } + }, + [refetch$, searchSessionManager] + ); + + /** + * Initial data fetching, also triggered when index pattern changes + */ + useEffect(() => { + if (!indexPattern) { + return; + } + if (initialFetchStatus === FetchStatus.LOADING) { + refetch$.next(); + } + }, [initialFetchStatus, refetch$, indexPattern, data$]); + return { - state, - setState, - stateContainer, + data$, indexPattern, - searchSource, - savedSearch, + refetch$, resetSavedSearch, + onChangeIndexPattern, + onUpdateQuery, + savedSearch, + searchSource, + setState, + state, + stateContainer, }; } diff --git a/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts b/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts index 5976c8fea5ea4..128c94f284f56 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts +++ b/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts @@ -12,9 +12,10 @@ import { discoverServiceMock } from '../../../../__mocks__/services'; import { savedSearchMock } from '../../../../__mocks__/saved_search'; import { indexPatternMock } from '../../../../__mocks__/index_pattern'; import { useSavedSearch } from './use_saved_search'; -import { AppState, getState } from './discover_state'; +import { getState } from './discover_state'; import { uiSettingsMock } from '../../../../__mocks__/ui_settings'; import { useDiscoverState } from './use_discover_state'; +import { FetchStatus } from '../../../types'; describe('test useSavedSearch', () => { test('useSavedSearch return is valid', async () => { @@ -28,11 +29,10 @@ describe('test useSavedSearch', () => { const { result } = renderHook(() => { return useSavedSearch({ indexPattern: indexPatternMock, - savedSearch: savedSearchMock, + initialFetchStatus: FetchStatus.LOADING, searchSessionManager, searchSource: savedSearchMock.searchSource.createCopy(), services: discoverServiceMock, - state: {} as AppState, stateContainer, useNewFieldsApi: true, }); @@ -69,11 +69,10 @@ describe('test useSavedSearch', () => { const { result, waitForValueToChange } = renderHook(() => { return useSavedSearch({ indexPattern: indexPatternMock, - savedSearch: savedSearchMock, + initialFetchStatus: FetchStatus.LOADING, searchSessionManager, searchSource: resultState.current.searchSource, services: discoverServiceMock, - state: {} as AppState, stateContainer, useNewFieldsApi: true, }); @@ -88,4 +87,47 @@ describe('test useSavedSearch', () => { expect(result.current.data$.value.hits).toBe(0); expect(result.current.data$.value.rows).toEqual([]); }); + + test('reset sets back to initial state', async () => { + const { history, searchSessionManager } = createSearchSessionMock(); + const stateContainer = getState({ + getStateDefaults: () => ({ index: 'the-index-pattern-id' }), + history, + uiSettings: uiSettingsMock, + }); + + discoverServiceMock.data.query.timefilter.timefilter.getTime = jest.fn(() => { + return { from: '2021-05-01T20:00:00Z', to: '2021-05-02T20:00:00Z' }; + }); + + const { result: resultState } = renderHook(() => { + return useDiscoverState({ + services: discoverServiceMock, + history, + initialIndexPattern: indexPatternMock, + initialSavedSearch: savedSearchMock, + }); + }); + + const { result, waitForValueToChange } = renderHook(() => { + return useSavedSearch({ + indexPattern: indexPatternMock, + initialFetchStatus: FetchStatus.LOADING, + searchSessionManager, + searchSource: resultState.current.searchSource, + services: discoverServiceMock, + stateContainer, + useNewFieldsApi: true, + }); + }); + + result.current.refetch$.next(); + + await waitForValueToChange(() => { + return result.current.data$.value.state === FetchStatus.COMPLETE; + }); + + result.current.reset(); + expect(result.current.data$.value.state).toBe(FetchStatus.LOADING); + }); }); diff --git a/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts b/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts index 2b0d951724869..8c847b54078eb 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts +++ b/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts @@ -5,11 +5,10 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { useEffect, useRef, useCallback, useMemo } from 'react'; +import { useEffect, useRef, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { merge, Subject, BehaviorSubject } from 'rxjs'; import { debounceTime, tap, filter } from 'rxjs/operators'; -import { isEqual } from 'lodash'; import { DiscoverServices } from '../../../../build_services'; import { DiscoverSearchSessionManager } from './discover_search_session'; import { @@ -18,13 +17,11 @@ import { SearchSource, tabifyAggResponse, } from '../../../../../../data/common'; -import { SavedSearch } from '../../../../saved_searches'; -import { AppState, GetStateReturn } from './discover_state'; +import { GetStateReturn } from './discover_state'; import { ElasticSearchHit } from '../../../doc_views/doc_views_types'; import { RequestAdapter } from '../../../../../../inspector/public'; import { AutoRefreshDoneFn, search } from '../../../../../../data/public'; import { calcFieldCounts } from '../utils/calc_field_counts'; -import { SEARCH_ON_PAGE_LOAD_SETTING } from '../../../../../common'; import { validateTimeRange } from '../utils/validate_time_range'; import { updateSearchSource } from '../utils/update_search_source'; import { SortOrder } from '../../../../saved_searches/types'; @@ -40,6 +37,7 @@ export type SavedSearchRefetchSubject = Subject; export interface UseSavedSearch { refetch$: SavedSearchRefetchSubject; data$: SavedSearchDataSubject; + reset: () => void; } export type SavedSearchRefetchMsg = 'reset' | undefined; @@ -59,48 +57,27 @@ export interface SavedSearchDataMessage { /** * This hook return 2 observables, refetch$ allows to trigger data fetching, data$ to subscribe * to the data fetching - * @param indexPattern - * @param savedSearch - * @param searchSessionManager - * @param searchSource - * @param services - * @param state - * @param stateContainer - * @param useNewFieldsApi */ export const useSavedSearch = ({ indexPattern, - savedSearch, + initialFetchStatus, searchSessionManager, searchSource, services, - state, stateContainer, useNewFieldsApi, }: { indexPattern: IndexPattern; - savedSearch: SavedSearch; + initialFetchStatus: FetchStatus; searchSessionManager: DiscoverSearchSessionManager; searchSource: SearchSource; services: DiscoverServices; - state: AppState; stateContainer: GetStateReturn; useNewFieldsApi: boolean; }): UseSavedSearch => { - const { data, filterManager, uiSettings } = services; + const { data, filterManager } = services; const timefilter = data.query.timefilter.timefilter; - const initFetchState: FetchStatus = useMemo(() => { - // A saved search is created on every page load, so we check the ID to see if we're loading a - // previously saved search or if it is just transient - const shouldSearchOnPageLoad = - uiSettings.get(SEARCH_ON_PAGE_LOAD_SETTING) || - savedSearch.id !== undefined || - timefilter.getRefreshInterval().pause === false || - searchSessionManager.hasSearchSessionIdInURL(); - return shouldSearchOnPageLoad ? FetchStatus.LOADING : FetchStatus.UNINITIALIZED; - }, [uiSettings, savedSearch.id, searchSessionManager, timefilter]); - /** * The observable the UI (aka React component) subscribes to get notified about * the changes in the data fetching process (high level: fetching started, data was received) @@ -108,7 +85,7 @@ export const useSavedSearch = ({ const data$: SavedSearchDataSubject = useSingleton( () => new BehaviorSubject({ - state: initFetchState, + state: initialFetchStatus, }) ); /** @@ -123,15 +100,14 @@ export const useSavedSearch = ({ */ const refs = useRef<{ abortController?: AbortController; - /** - * used to compare a new state against an old one, to evaluate if data needs to be fetched - */ - appState: AppState; /** * handler emitted by `timefilter.getAutoRefreshFetch$()` * to notify when data completed loading and to start a new autorefresh loop */ autoRefreshDoneCb?: AutoRefreshDoneFn; + /** + * Number of fetches used for functional testing + */ fetchCounter: number; /** * needed to right auto refresh behavior, a new auto refresh shouldnt be triggered when @@ -144,12 +120,34 @@ export const useSavedSearch = ({ */ fieldCounts: Record; }>({ - appState: state, fetchCounter: 0, fieldCounts: {}, - fetchStatus: initFetchState, + fetchStatus: initialFetchStatus, }); + /** + * Resets the fieldCounts cache and sends a reset message + * It is set to initial state (no documents, fetchCounter to 0) + * Needed when index pattern is switched or a new runtime field is added + */ + const sendResetMsg = useCallback( + (fetchStatus?: FetchStatus) => { + refs.current.fieldCounts = {}; + refs.current.fetchStatus = fetchStatus ?? initialFetchStatus; + data$.next({ + state: initialFetchStatus, + fetchCounter: 0, + rows: [], + fieldCounts: {}, + chartData: undefined, + bucketInterval: undefined, + }); + }, + [data$, initialFetchStatus] + ); + /** + * Function to fetch data from ElasticSearch + */ const fetchAll = useCallback( (reset = false) => { if (!validateTimeRange(timefilter.getTime(), services.toastNotifications)) { @@ -161,23 +159,18 @@ export const useSavedSearch = ({ refs.current.abortController = new AbortController(); const sessionId = searchSessionManager.getNextSearchSessionId(); - // Let the UI know, data fetching started - const loadingMessage: SavedSearchDataMessage = { - state: FetchStatus.LOADING, - fetchCounter: ++refs.current.fetchCounter, - }; - if (reset) { - // when runtime field was added, changed, deleted, index pattern was switched - loadingMessage.rows = []; - loadingMessage.fieldCounts = {}; - loadingMessage.chartData = undefined; - loadingMessage.bucketInterval = undefined; + sendResetMsg(FetchStatus.LOADING); + } else { + // Let the UI know, data fetching started + data$.next({ + state: FetchStatus.LOADING, + fetchCounter: ++refs.current.fetchCounter, + }); + refs.current.fetchStatus = FetchStatus.LOADING; } - data$.next(loadingMessage); - refs.current.fetchStatus = loadingMessage.state; - const { sort } = stateContainer.appStateContainer.getState(); + const { sort, hideChart, interval } = stateContainer.appStateContainer.getState(); updateSearchSource(searchSource, false, { indexPattern, services, @@ -185,8 +178,8 @@ export const useSavedSearch = ({ useNewFieldsApi, }); const chartAggConfigs = - indexPattern.timeFieldName && !state.hideChart && state.interval - ? getChartAggConfigs(searchSource, state.interval, data) + indexPattern.timeFieldName && !hideChart && interval + ? getChartAggConfigs(searchSource, interval, data) : undefined; if (!chartAggConfigs) { @@ -217,16 +210,12 @@ export const useSavedSearch = ({ state: FetchStatus.COMPLETE, rows: documents, inspectorAdapters, - fieldCounts: calcFieldCounts( - reset ? {} : refs.current.fieldCounts, - documents, - indexPattern - ), + fieldCounts: calcFieldCounts(refs.current.fieldCounts, documents, indexPattern), hits: res.rawResponse.hits.total as number, }; if (chartAggConfigs) { - const bucketAggConfig = chartAggConfigs!.aggs[1]; + const bucketAggConfig = chartAggConfigs.aggs[1]; const tabifiedData = tabifyAggResponse(chartAggConfigs, res.rawResponse); const dimensions = getDimensions(chartAggConfigs, data); if (dimensions) { @@ -259,14 +248,13 @@ export const useSavedSearch = ({ [ timefilter, services, + searchSessionManager, stateContainer.appStateContainer, searchSource, indexPattern, useNewFieldsApi, - state.hideChart, - state.interval, data, - searchSessionManager, + sendResetMsg, data$, ] ); @@ -306,32 +294,9 @@ export const useSavedSearch = ({ fetchAll, ]); - /** - * Track state changes that should trigger a fetch - */ - useEffect(() => { - const prevAppState = refs.current.appState; - - // chart was hidden, now it should be displayed, so data is needed - const chartDisplayChanged = state.hideChart !== prevAppState.hideChart && !state.hideChart; - const chartIntervalChanged = state.interval !== prevAppState.interval; - const docTableSortChanged = !isEqual(state.sort, prevAppState.sort); - const indexPatternChanged = !isEqual(state.index, prevAppState.index); - - refs.current.appState = state; - if (chartDisplayChanged || chartIntervalChanged || docTableSortChanged || indexPatternChanged) { - refetch$.next(indexPatternChanged ? 'reset' : undefined); - } - }, [refetch$, state.interval, state.sort, state]); - - useEffect(() => { - if (initFetchState === FetchStatus.LOADING) { - refetch$.next(); - } - }, [initFetchState, refetch$]); - return { refetch$, data$, + reset: sendResetMsg, }; }; 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(); findTestSubject(component, 'docTableDetailsFlyout').simulate('keydown', { key: 'ArrowRight' }); diff --git a/src/plugins/discover/public/application/components/doc/doc.tsx b/src/plugins/discover/public/application/components/doc/doc.tsx index e38709b465174..ed8bcf30d2bd1 100644 --- a/src/plugins/discover/public/application/components/doc/doc.tsx +++ b/src/plugins/discover/public/application/components/doc/doc.tsx @@ -10,9 +10,10 @@ import React from 'react'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { EuiCallOut, EuiLink, EuiLoadingSpinner, EuiPageContent, EuiPage } from '@elastic/eui'; import { IndexPatternsContract } from 'src/plugins/data/public'; -import { ElasticRequestState, useEsDocSearch } from './use_es_doc_search'; +import { useEsDocSearch } from './use_es_doc_search'; import { getServices } from '../../../kibana_services'; import { DocViewer } from '../doc_viewer/doc_viewer'; +import { ElasticRequestState } from './elastic_request_state'; export interface DocProps { /** @@ -32,6 +33,10 @@ export interface DocProps { * IndexPatternService to get a given index pattern by ID */ indexPatternService: IndexPatternsContract; + /** + * If set, will always request source, regardless of the global `fieldsFromSource` setting + */ + requestSource?: boolean; } export function Doc(props: DocProps) { diff --git a/src/plugins/discover/public/application/components/doc/elastic_request_state.ts b/src/plugins/discover/public/application/components/doc/elastic_request_state.ts new file mode 100644 index 0000000000000..241e37c47a7e7 --- /dev/null +++ b/src/plugins/discover/public/application/components/doc/elastic_request_state.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. + */ + +export enum ElasticRequestState { + Loading, + NotFound, + Found, + Error, + NotFoundIndexPattern, +} diff --git a/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx b/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx index f3a6b274649f5..9fdb564cb518d 100644 --- a/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx +++ b/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx @@ -7,11 +7,12 @@ */ import { renderHook, act } from '@testing-library/react-hooks'; -import { buildSearchBody, useEsDocSearch, ElasticRequestState } from './use_es_doc_search'; +import { buildSearchBody, useEsDocSearch } from './use_es_doc_search'; import { DocProps } from './doc'; import { Observable } from 'rxjs'; import { SEARCH_FIELDS_FROM_SOURCE as mockSearchFieldsFromSource } from '../../../../common'; import { IndexPattern } from 'src/plugins/data/common'; +import { ElasticRequestState } from './elastic_request_state'; const mockSearchResult = new Observable(); @@ -88,6 +89,36 @@ describe('Test of helper / hook', () => { `); }); + test('buildSearchBody with requestSource', () => { + const indexPattern = ({ + getComputedFields: () => ({ storedFields: [], scriptFields: [], docvalueFields: [] }), + } as unknown) as IndexPattern; + const actual = buildSearchBody('1', indexPattern, true, true); + expect(actual).toMatchInlineSnapshot(` + Object { + "body": Object { + "_source": true, + "fields": Array [ + Object { + "field": "*", + "include_unmapped": "true", + }, + ], + "query": Object { + "ids": Object { + "values": Array [ + "1", + ], + }, + }, + "runtime_mappings": Object {}, + "script_fields": Array [], + "stored_fields": Array [], + }, + } + `); + }); + test('buildSearchBody with runtime fields', () => { const indexPattern = ({ getComputedFields: () => ({ @@ -155,7 +186,11 @@ describe('Test of helper / hook', () => { await act(async () => { hook = renderHook((p: DocProps) => useEsDocSearch(p), { initialProps: props }); }); - expect(hook.result.current).toEqual([ElasticRequestState.Loading, null, indexPattern]); + expect(hook.result.current.slice(0, 3)).toEqual([ + ElasticRequestState.Loading, + null, + indexPattern, + ]); expect(getMock).toHaveBeenCalled(); }); }); diff --git a/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts b/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts index 7a3320d43c8b5..71a32b758aca7 100644 --- a/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts +++ b/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts @@ -6,23 +6,16 @@ * Side Public License, v 1. */ -import { useEffect, useState, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import type { estypes } from '@elastic/elasticsearch'; -import { IndexPattern, getServices } from '../../../kibana_services'; +import { getServices, IndexPattern } from '../../../kibana_services'; import { DocProps } from './doc'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../common'; +import { ElasticRequestState } from './elastic_request_state'; type RequestBody = Pick; -export enum ElasticRequestState { - Loading, - NotFound, - Found, - Error, - NotFoundIndexPattern, -} - /** * helper function to build a query body for Elasticsearch * https://www.elastic.co/guide/en/elasticsearch/reference/current//query-dsl-ids-query.html @@ -30,7 +23,8 @@ export enum ElasticRequestState { export function buildSearchBody( id: string, indexPattern: IndexPattern, - useNewFieldsApi: boolean + useNewFieldsApi: boolean, + requestAllFields?: boolean ): RequestBody | undefined { const computedFields = indexPattern.getComputedFields(); const runtimeFields = computedFields.runtimeFields as estypes.MappingRuntimeFields; @@ -52,6 +46,9 @@ export function buildSearchBody( // @ts-expect-error request.body.fields = [{ field: '*', include_unmapped: 'true' }]; request.body.runtime_mappings = runtimeFields ? runtimeFields : {}; + if (requestAllFields) { + request.body._source = true; + } } else { request.body._source = true; } @@ -67,47 +64,50 @@ export function useEsDocSearch({ index, indexPatternId, indexPatternService, -}: DocProps): [ElasticRequestState, ElasticSearchHit | null, IndexPattern | null] { + requestSource, +}: DocProps): [ElasticRequestState, ElasticSearchHit | null, IndexPattern | null, () => void] { const [indexPattern, setIndexPattern] = useState(null); const [status, setStatus] = useState(ElasticRequestState.Loading); const [hit, setHit] = useState(null); const { data, uiSettings } = useMemo(() => getServices(), []); const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]); - useEffect(() => { - async function requestData() { - try { - const indexPatternEntity = await indexPatternService.get(indexPatternId); - setIndexPattern(indexPatternEntity); + const requestData = useCallback(async () => { + try { + const indexPatternEntity = await indexPatternService.get(indexPatternId); + setIndexPattern(indexPatternEntity); - const { rawResponse } = await data.search - .search({ - params: { - index, - body: buildSearchBody(id, indexPatternEntity, useNewFieldsApi)?.body, - }, - }) - .toPromise(); + const { rawResponse } = await data.search + .search({ + params: { + index, + body: buildSearchBody(id, indexPatternEntity, useNewFieldsApi, requestSource)?.body, + }, + }) + .toPromise(); - const hits = rawResponse.hits; + const hits = rawResponse.hits; - if (hits?.hits?.[0]) { - setStatus(ElasticRequestState.Found); - setHit(hits.hits[0]); - } else { - setStatus(ElasticRequestState.NotFound); - } - } catch (err) { - if (err.savedObjectId) { - setStatus(ElasticRequestState.NotFoundIndexPattern); - } else if (err.status === 404) { - setStatus(ElasticRequestState.NotFound); - } else { - setStatus(ElasticRequestState.Error); - } + if (hits?.hits?.[0]) { + setStatus(ElasticRequestState.Found); + setHit(hits.hits[0]); + } else { + setStatus(ElasticRequestState.NotFound); + } + } catch (err) { + if (err.savedObjectId) { + setStatus(ElasticRequestState.NotFoundIndexPattern); + } else if (err.status === 404) { + setStatus(ElasticRequestState.NotFound); + } else { + setStatus(ElasticRequestState.Error); } } + }, [id, index, indexPatternId, indexPatternService, data.search, useNewFieldsApi, requestSource]); + + useEffect(() => { requestData(); - }, [id, index, indexPatternId, indexPatternService, data.search, useNewFieldsApi]); - return [status, hit, indexPattern]; + }, [requestData]); + + return [status, hit, indexPattern, requestData]; } diff --git a/src/plugins/discover/public/application/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap b/src/plugins/discover/public/application/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap index 8f07614813495..31dd6347218b5 100644 --- a/src/plugins/discover/public/application/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap +++ b/src/plugins/discover/public/application/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap @@ -1,21 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`returns the \`JsonCodeEditor\` component 1`] = ` - - - -
    - - - -
    -
    - - - -
    + onEditorDidMount={[Function]} +/> `; diff --git a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.scss b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.scss index 5521df5b363ac..335805ed28493 100644 --- a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.scss +++ b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.scss @@ -1,3 +1,3 @@ .dscJsonCodeEditor { - width: 100% + width: 100%; } diff --git a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx index b8427bb6bbdd2..f1ecd3ae3b70b 100644 --- a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx +++ b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx @@ -9,17 +9,8 @@ import './json_code_editor.scss'; import React, { useCallback } from 'react'; -import { i18n } from '@kbn/i18n'; -import { monaco, XJsonLang } from '@kbn/monaco'; -import { EuiButtonEmpty, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { CodeEditor } from '../../../../../kibana_react/public'; - -const codeEditorAriaLabel = i18n.translate('discover.json.codeEditorAriaLabel', { - defaultMessage: 'Read only JSON view of an elasticsearch document', -}); -const copyToClipboardLabel = i18n.translate('discover.json.copyToClipboardLabel', { - defaultMessage: 'Copy to clipboard', -}); +import { monaco } from '@kbn/monaco'; +import { JsonCodeEditorCommon } from './json_code_editor_common'; interface JsonCodeEditorProps { json: Record; @@ -47,45 +38,11 @@ export const JsonCodeEditor = ({ json, width, hasLineNumbers }: JsonCodeEditorPr }, []); return ( - - - -
    - - {(copy) => ( - - {copyToClipboardLabel} - - )} - -
    -
    - - {}} - editorDidMount={setEditorCalculatedHeight} - aria-label={codeEditorAriaLabel} - options={{ - automaticLayout: true, - fontSize: 12, - lineNumbers: hasLineNumbers ? 'on' : 'off', - minimap: { - enabled: false, - }, - overviewRulerBorder: false, - readOnly: true, - scrollbar: { - alwaysConsumeMouseWheel: false, - }, - scrollBeyondLastLine: false, - wordWrap: 'on', - wrappingIndent: 'indent', - }} - /> - -
    + ); }; diff --git a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor_common.tsx b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor_common.tsx new file mode 100644 index 0000000000000..e5ab8bf4d1a0d --- /dev/null +++ b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor_common.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 './json_code_editor.scss'; + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { monaco, XJsonLang } from '@kbn/monaco'; +import { EuiButtonEmpty, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { CodeEditor } from '../../../../../kibana_react/public'; + +const codeEditorAriaLabel = i18n.translate('discover.json.codeEditorAriaLabel', { + defaultMessage: 'Read only JSON view of an elasticsearch document', +}); +const copyToClipboardLabel = i18n.translate('discover.json.copyToClipboardLabel', { + defaultMessage: 'Copy to clipboard', +}); + +interface JsonCodeEditorCommonProps { + jsonValue: string; + onEditorDidMount: (editor: monaco.editor.IStandaloneCodeEditor) => void; + width?: string | number; + hasLineNumbers?: boolean; +} + +export const JsonCodeEditorCommon = ({ + jsonValue, + width, + hasLineNumbers, + onEditorDidMount, +}: JsonCodeEditorCommonProps) => { + if (jsonValue === '') { + return null; + } + return ( + + + +
    + + {(copy) => ( + + {copyToClipboardLabel} + + )} + +
    +
    + + {}} + editorDidMount={onEditorDidMount} + aria-label={codeEditorAriaLabel} + options={{ + automaticLayout: true, + fontSize: 12, + lineNumbers: hasLineNumbers ? 'on' : 'off', + minimap: { + enabled: false, + }, + overviewRulerBorder: false, + readOnly: true, + scrollbar: { + alwaysConsumeMouseWheel: false, + }, + scrollBeyondLastLine: false, + wordWrap: 'on', + wrappingIndent: 'indent', + }} + /> + +
    + ); +}; + +export const JSONCodeEditorCommonMemoized = React.memo((props: JsonCodeEditorCommonProps) => { + return ; +}); 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 new file mode 100644 index 0000000000000..68786871825ac --- /dev/null +++ b/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap @@ -0,0 +1,760 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Source Viewer component renders error state 1`] = ` + + + Could not fetch data at this time. Refresh the tab to try again. + + + Refresh + + + } + iconType="alert" + title={ +

    + An Error Occurred +

    + } + > +
    + + + + +
    + + +

    + An Error Occurred +

    +
    + + + +
    + + +
    +
    + Could not fetch data at this time. Refresh the tab to try again. + +
    + + + + + + +
    +
    + + + +
    + + +`; + +exports[`Source Viewer component renders json code editor 1`] = ` + + + + +
    + +
    + +
    + +
    + + + + + + + + + +
    +
    + + +
    + + + } + > + + + + + + +
    +
    +
    + + + + +`; + +exports[`Source Viewer component renders loading state 1`] = ` + +
    + + + + +
    + +
    + + Loading JSON + +
    +
    +
    +
    +
    +
    +`; diff --git a/src/plugins/discover/public/application/components/source_viewer/source_viewer.scss b/src/plugins/discover/public/application/components/source_viewer/source_viewer.scss new file mode 100644 index 0000000000000..224e84ca50b52 --- /dev/null +++ b/src/plugins/discover/public/application/components/source_viewer/source_viewer.scss @@ -0,0 +1,14 @@ +.sourceViewer__loading { + display: flex; + flex-direction: row; + justify-content: left; + flex: 1 0 100%; + text-align: center; + height: 100%; + width: 100%; + margin-top: $euiSizeS; +} + +.sourceViewer__loadingSpinner { + margin-right: $euiSizeS; +} diff --git a/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx b/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx new file mode 100644 index 0000000000000..86433e5df6401 --- /dev/null +++ b/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.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 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 { mountWithIntl } from '@kbn/test/jest'; +import { SourceViewer } from './source_viewer'; +import * as hooks from '../doc/use_es_doc_search'; +import * as useUiSettingHook from 'src/plugins/kibana_react/public/ui_settings/use_ui_setting'; +import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; +import { JsonCodeEditorCommon } from '../json_code_editor/json_code_editor_common'; + +jest.mock('../../../kibana_services', () => ({ + getServices: jest.fn(), +})); + +import { getServices, IndexPattern } from '../../../kibana_services'; + +const mockIndexPattern = { + getComputedFields: () => [], +} as never; +const getMock = jest.fn(() => Promise.resolve(mockIndexPattern)); +const mockIndexPatternService = ({ + get: getMock, +} as unknown) as IndexPattern; + +(getServices as jest.Mock).mockImplementation(() => ({ + uiSettings: { + get: (key: string) => { + if (key === 'discover:useNewFieldsApi') { + return true; + } + }, + }, + data: { + indexPatternService: mockIndexPatternService, + }, +})); +describe('Source Viewer component', () => { + test('renders loading state', () => { + jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [0, null, null, () => {}]); + + const comp = mountWithIntl( + + ); + expect(comp).toMatchSnapshot(); + const loadingIndicator = comp.find(EuiLoadingSpinner); + expect(loadingIndicator).not.toBe(null); + }); + + test('renders error state', () => { + jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [3, null, null, () => {}]); + + const comp = mountWithIntl( + + ); + expect(comp).toMatchSnapshot(); + const errorPrompt = comp.find(EuiEmptyPrompt); + expect(errorPrompt.length).toBe(1); + const refreshButton = comp.find(EuiButton); + expect(refreshButton.length).toBe(1); + }); + + test('renders json code editor', () => { + const mockHit = { + _index: 'logstash-2014.09.09', + _type: 'doc', + _id: 'id123', + _score: 1, + _source: { + message: 'Lorem ipsum dolor sit amet', + extension: 'html', + not_mapped: 'yes', + bytes: 100, + objectArray: [{ foo: true }], + relatedContent: { + test: 1, + }, + scripted: 123, + _underscore: 123, + }, + } as never; + jest + .spyOn(hooks, 'useEsDocSearch') + .mockImplementation(() => [2, mockHit, mockIndexPattern, () => {}]); + jest.spyOn(useUiSettingHook, 'useUiSetting').mockImplementation(() => { + return false; + }); + const comp = mountWithIntl( + + ); + expect(comp).toMatchSnapshot(); + const jsonCodeEditor = comp.find(JsonCodeEditorCommon); + expect(jsonCodeEditor).not.toBe(null); + }); +}); diff --git a/src/plugins/discover/public/application/components/source_viewer/source_viewer.tsx b/src/plugins/discover/public/application/components/source_viewer/source_viewer.tsx new file mode 100644 index 0000000000000..94a12c04613a9 --- /dev/null +++ b/src/plugins/discover/public/application/components/source_viewer/source_viewer.tsx @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 './source_viewer.scss'; + +import React, { useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { monaco } from '@kbn/monaco'; +import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useEsDocSearch } from '../doc/use_es_doc_search'; +import { JSONCodeEditorCommonMemoized } from '../json_code_editor/json_code_editor_common'; +import { ElasticRequestState } from '../doc/elastic_request_state'; +import { getServices } from '../../../../public/kibana_services'; +import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../common'; + +interface SourceViewerProps { + id: string; + index: string; + indexPatternId: string; + hasLineNumbers: boolean; + width?: number; +} + +export const SourceViewer = ({ + id, + index, + indexPatternId, + width, + hasLineNumbers, +}: SourceViewerProps) => { + const [editor, setEditor] = useState(); + const [jsonValue, setJsonValue] = useState(''); + const indexPatternService = getServices().data.indexPatterns; + const useNewFieldsApi = !getServices().uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); + const [reqState, hit, , requestData] = useEsDocSearch({ + id, + index, + indexPatternId, + indexPatternService, + requestSource: useNewFieldsApi, + }); + + useEffect(() => { + if (reqState === ElasticRequestState.Found && hit) { + setJsonValue(JSON.stringify(hit, undefined, 2)); + } + }, [reqState, hit]); + + // setting editor height based on lines height and count to stretch and fit its content + useEffect(() => { + if (!editor) { + return; + } + const editorElement = editor.getDomNode(); + + if (!editorElement) { + return; + } + + const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight); + const lineCount = editor.getModel()?.getLineCount() || 1; + const height = editor.getTopForLineNumber(lineCount + 1) + lineHeight; + if (!jsonValue || jsonValue === '') { + editorElement.style.height = '0px'; + } else { + editorElement.style.height = `${height}px`; + } + editor.layout(); + }, [editor, jsonValue]); + + const loadingState = ( +
    + + + + +
    + ); + + const errorMessageTitle = ( +

    + {i18n.translate('discover.sourceViewer.errorMessageTitle', { + defaultMessage: 'An Error Occurred', + })} +

    + ); + const errorMessage = ( +
    + {i18n.translate('discover.sourceViewer.errorMessage', { + defaultMessage: 'Could not fetch data at this time. Refresh the tab to try again.', + })} + + + {i18n.translate('discover.sourceViewer.refresh', { + defaultMessage: 'Refresh', + })} + +
    + ); + const errorState = ( + + ); + + if ( + reqState === ElasticRequestState.Error || + reqState === ElasticRequestState.NotFound || + reqState === ElasticRequestState.NotFoundIndexPattern + ) { + return errorState; + } + + if (reqState === ElasticRequestState.Loading || jsonValue === '') { + return loadingState; + } + + return ( + setEditor(editorNode)} + /> + ); +}; 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; + +export interface DiscoverAppLocatorDependencies { + useHash: boolean; +} + +export class DiscoverAppLocatorDefinition implements LocatorDefinition { + 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('_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 139b23d28a1d4..ec89f7516e92d 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -37,7 +37,7 @@ import { UrlGeneratorState } from '../../share/public'; import { DocViewInput, DocViewInputFn } from './application/doc_views/doc_views_types'; import { DocViewsRegistry } from './application/doc_views/doc_views_registry'; import { DocViewTable } from './application/components/table/table'; -import { JsonCodeEditor } from './application/components/json_code_editor/json_code_editor'; + import { setDocViewsRegistry, setUrlTracker, @@ -59,10 +59,12 @@ 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/'; import { IndexPatternFieldEditorStart } from '../../../plugins/index_pattern_field_editor/public'; +import { SourceViewer } from './application/components/source_viewer/source_viewer'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -82,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 - * const url = await plugins.discover.urlGenerator.createUrl({ + * 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: { @@ -103,7 +156,7 @@ export interface DiscoverStart { * }); * ``` */ - readonly urlGenerator: undefined | UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>; + readonly locator: undefined | DiscoverAppLocator; } /** @@ -155,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 +237,14 @@ 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({ @@ -193,8 +259,14 @@ export class DiscoverPlugin defaultMessage: 'JSON', }), order: 20, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - component: ({ hit }) => , + component: ({ hit, indexPattern }) => ( + + ), }); const { @@ -273,6 +345,7 @@ export class DiscoverPlugin // make sure the index pattern list is up to date await dataStart.indexPatterns.clearCache(); + const { renderApp } = await import('./application/application'); params.element.classList.add('dscAppWrapper'); const unmount = await renderApp(innerAngularName, params.element); @@ -316,6 +389,7 @@ export class DiscoverPlugin docViews: { addDocView: this.docViewsRegistry.addDocView.bind(this.docViewsRegistry), }, + locator: this.locator, }; } @@ -360,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 = ({ 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 = ( {title}

    } body={ - <> - {cause ? message || errorString :

    {message || errorString}

    } - {cause && ( - <> - -
      - {cause.map((causeMsg, i) => ( -
    • {causeMsg}
    • - ))} -
    - - )} - + error && ( + <> + {cause ? message || errorString :

    {message || errorString}

    } + {cause && ( + <> + +
      + {cause.map((causeMsg, i) => ( +
    • {causeMsg}
    • + ))} +
    + + )} + + ) } iconType="alert" actions={actions} diff --git a/packages/kbn-interpreter/src/common/lib/get_type.d.ts b/src/plugins/es_ui_shared/public/components/page_loading/index.ts similarity index 87% rename from packages/kbn-interpreter/src/common/lib/get_type.d.ts rename to src/plugins/es_ui_shared/public/components/page_loading/index.ts index 568658c780333..3e7b93bb4e7c3 100644 --- a/packages/kbn-interpreter/src/common/lib/get_type.d.ts +++ b/src/plugins/es_ui_shared/public/components/page_loading/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export declare function getType(node: any): string; +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 ( + + } + body={{children}} + data-test-subj="sectionLoading" + /> + + ); +}; 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/expressions/common/expression_functions/specs/math_column.ts b/src/plugins/expressions/common/expression_functions/specs/math_column.ts index 0ff8faf3ce55a..633d912c29502 100644 --- a/src/plugins/expressions/common/expression_functions/specs/math_column.ts +++ b/src/plugins/expressions/common/expression_functions/specs/math_column.ts @@ -69,25 +69,40 @@ export const mathColumn: ExpressionFunctionDefinition< return id === args.id; }); if (existingColumnIndex > -1) { - throw new Error('ID must be unique'); + throw new Error( + i18n.translate('expressions.functions.mathColumn.uniqueIdError', { + defaultMessage: 'ID must be unique', + }) + ); } const newRows = input.rows.map((row) => { - return { - ...row, - [args.id]: math.fn( - { - type: 'datatable', - columns: input.columns, - rows: [row], - }, - { - expression: args.expression, - onError: args.onError, - }, - context - ), - }; + const result = math.fn( + { + type: 'datatable', + columns: input.columns, + rows: [row], + }, + { + expression: args.expression, + onError: args.onError, + }, + context + ); + + if (Array.isArray(result)) { + if (result.length === 1) { + return { ...row, [args.id]: result[0] }; + } + throw new Error( + i18n.translate('expressions.functions.mathColumn.arrayValueError', { + defaultMessage: 'Cannot perform math on array values at {name}', + values: { name: args.name }, + }) + ); + } + + return { ...row, [args.id]: result }; }); const type = newRows.length ? getType(newRows[0][args.id]) : 'null'; const newColumn: DatatableColumn = { diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts index bc6699a2b689b..e0fb0a3a9f23d 100644 --- a/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts +++ b/src/plugins/expressions/common/expression_functions/specs/tests/math_column.test.ts @@ -34,6 +34,30 @@ describe('mathColumn', () => { }); }); + it('extracts a single array value, but not a multi-value array', () => { + const arrayTable = { + ...testTable, + rows: [ + { + name: 'product1', + time: 1517842800950, // 05 Feb 2018 15:00:00 GMT + price: [605, 500], + quantity: [100], + in_stock: true, + }, + ], + }; + const args = { + id: 'output', + name: 'output', + expression: 'quantity', + }; + expect(fn(arrayTable, args).rows[0].output).toEqual(100); + expect(() => fn(arrayTable, { ...args, expression: 'price' })).toThrowError( + `Cannot perform math on array values` + ); + }); + it('handles onError', () => { const args = { id: 'output', diff --git a/src/plugins/expressions/common/expression_types/get_type.test.ts b/src/plugins/expressions/common/expression_types/get_type.test.ts index 6eca54d2aea44..b1a9cb703182f 100644 --- a/src/plugins/expressions/common/expression_types/get_type.test.ts +++ b/src/plugins/expressions/common/expression_types/get_type.test.ts @@ -30,6 +30,7 @@ describe('getType()', () => { }); test('throws if object has no .type property', () => { + expect(() => getType([])).toThrow(); expect(() => getType({})).toThrow(); expect(() => getType({ _type: 'foo' })).toThrow(); expect(() => getType({ tipe: 'foo' })).toThrow(); diff --git a/src/plugins/expressions/common/expression_types/get_type.ts b/src/plugins/expressions/common/expression_types/get_type.ts index e29a610b3ed90..052508df41329 100644 --- a/src/plugins/expressions/common/expression_types/get_type.ts +++ b/src/plugins/expressions/common/expression_types/get_type.ts @@ -8,6 +8,9 @@ export function getType(node: any) { if (node == null) return 'null'; + if (Array.isArray(node)) { + throw new Error('Unexpected array value encountered.'); + } if (typeof node === 'object') { if (!node.type) throw new Error('Objects must have a type property'); return node.type; diff --git a/src/plugins/home/public/application/components/tutorial_directory.js b/src/plugins/home/public/application/components/tutorial_directory.js index 1fda865ebd847..d7e6c07d6dd18 100644 --- a/src/plugins/home/public/application/components/tutorial_directory.js +++ b/src/plugins/home/public/application/components/tutorial_directory.js @@ -9,27 +9,15 @@ import _ from 'lodash'; import React from 'react'; import PropTypes from 'prop-types'; +import { EuiFlexItem, EuiFlexGrid, EuiFlexGroup, EuiSpacer } from '@elastic/eui'; +import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { Synopsis } from './synopsis'; import { SampleDataSetCards } from './sample_data_set_cards'; import { getServices } from '../kibana_services'; - -import { - EuiPage, - EuiTabs, - EuiTab, - EuiFlexItem, - EuiFlexGrid, - EuiFlexGroup, - EuiSpacer, - EuiTitle, - EuiPageBody, -} from '@elastic/eui'; - +import { KibanaPageTemplate } from '../../../../kibana_react/public'; import { getTutorials } from '../load_tutorials'; -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - const ALL_TAB_ID = 'all'; const SAMPLE_DATA_TAB_ID = 'sampleData'; @@ -184,17 +172,13 @@ class TutorialDirectoryUi extends React.Component { }); }; - renderTabs = () => { - return this.tabs.map((tab, index) => ( - this.onSelectedTabChanged(tab.id)} - isSelected={tab.id === this.state.selectedTabId} - key={index} - > - {tab.name} - - )); + getTabs = () => { + return this.tabs.map((tab) => ({ + label: tab.name, + onClick: () => this.onSelectedTabChanged(tab.id), + isSelected: tab.id === this.state.selectedTabId, + 'data-test-subj': `homeTab-${tab.id}`, + })); }; renderTabContent = () => { @@ -258,41 +242,31 @@ class TutorialDirectoryUi extends React.Component { ) : null; }; - renderHeader = () => { - const notices = this.renderNotices(); + render() { const headerLinks = this.renderHeaderLinks(); + const tabs = this.getTabs(); + const notices = this.renderNotices(); return ( - <> - - - -

    - -

    -
    -
    - {headerLinks ? {headerLinks} : null} -
    - {notices} - - ); - }; - - render() { - return ( - - - {this.renderHeader()} - - {this.renderTabs()} - - {this.renderTabContent()} - - + + ), + tabs, + rightSideItems: headerLinks ? [headerLinks] : [], + }} + > + {notices && ( + <> + {notices} + + + )} + {this.renderTabContent()} + ); } } 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 ( -
    + {/* Name */} 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" > 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`] = ` >
    & { +export type KibanaPageTemplateSolutionNavProps = Partial> & { /** * Name of the solution, i.e. "Observability" */ diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/constants.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/constants.ts new file mode 100644 index 0000000000000..1753c87c9d005 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/constants.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 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. + */ + +/** + * Roll daily indices every 24h + */ +export const ROLL_DAILY_INDICES_INTERVAL = 24 * 60 * 60 * 1000; + +/** + * Start rolling indices after 5 minutes up + */ +export const ROLL_INDICES_START = 5 * 60 * 1000; + +/** + * Reset the event loop delay historgram every 1 hour + */ +export const MONITOR_EVENT_LOOP_DELAYS_INTERVAL = 1 * 60 * 60 * 1000; + +/** + * Reset the event loop delay historgram every 24h + */ +export const MONITOR_EVENT_LOOP_DELAYS_RESET = 24 * 60 * 60 * 1000; + +/** + * Start monitoring the event loop delays after 1 minute + */ +export const MONITOR_EVENT_LOOP_DELAYS_START = 1 * 60 * 1000; + +/** + * Event loop monitoring sampling rate in milliseconds. + */ +export const MONITOR_EVENT_LOOP_DELAYS_RESOLUTION = 10; diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.mocks.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.mocks.ts new file mode 100644 index 0000000000000..6b03d3cc5cbd1 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.mocks.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 moment from 'moment'; +import type { IntervalHistogram } from './event_loop_delays'; + +export const mockMonitorEnable = jest.fn(); +export const mockMonitorPercentile = jest.fn(); +export const mockMonitorReset = jest.fn(); +export const mockMonitorDisable = jest.fn(); +export const monitorEventLoopDelay = jest.fn().mockReturnValue({ + enable: mockMonitorEnable, + percentile: mockMonitorPercentile, + disable: mockMonitorDisable, + reset: mockMonitorReset, +}); + +jest.doMock('perf_hooks', () => ({ + monitorEventLoopDelay, +})); + +function createMockHistogram(overwrites: Partial = {}): IntervalHistogram { + const now = moment(); + + return { + min: 9093120, + max: 53247999, + mean: 11993238.600747818, + exceeds: 0, + stddev: 1168191.9357543814, + fromTimestamp: now.startOf('day').toISOString(), + lastUpdatedAt: now.toISOString(), + percentiles: { + '50': 12607487, + '75': 12615679, + '95': 12648447, + '99': 12713983, + }, + ...overwrites, + }; +} + +export const mocked = { + createHistogram: createMockHistogram, +}; diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.test.ts new file mode 100644 index 0000000000000..d03236a9756b3 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.test.ts @@ -0,0 +1,135 @@ +/* + * Copyright 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 { Subject } from 'rxjs'; + +import { + mockMonitorEnable, + mockMonitorPercentile, + monitorEventLoopDelay, + mockMonitorReset, + mockMonitorDisable, +} from './event_loop_delays.mocks'; +import { savedObjectsRepositoryMock } from '../../../../../core/server/mocks'; +import { startTrackingEventLoopDelaysUsage, EventLoopDelaysCollector } from './event_loop_delays'; + +describe('EventLoopDelaysCollector', () => { + jest.useFakeTimers('modern'); + const mockNow = jest.getRealSystemTime(); + jest.setSystemTime(mockNow); + + beforeEach(() => jest.clearAllMocks()); + afterAll(() => jest.useRealTimers()); + + test('#constructor enables monitoring', () => { + new EventLoopDelaysCollector(); + expect(monitorEventLoopDelay).toBeCalledWith({ resolution: 10 }); + expect(mockMonitorEnable).toBeCalledTimes(1); + }); + + test('#collect returns event loop delays histogram', () => { + const eventLoopDelaysCollector = new EventLoopDelaysCollector(); + const histogramData = eventLoopDelaysCollector.collect(); + expect(mockMonitorPercentile).toHaveBeenNthCalledWith(1, 50); + expect(mockMonitorPercentile).toHaveBeenNthCalledWith(2, 75); + expect(mockMonitorPercentile).toHaveBeenNthCalledWith(3, 95); + expect(mockMonitorPercentile).toHaveBeenNthCalledWith(4, 99); + + expect(Object.keys(histogramData)).toMatchInlineSnapshot(` + Array [ + "min", + "max", + "mean", + "exceeds", + "stddev", + "fromTimestamp", + "lastUpdatedAt", + "percentiles", + ] + `); + }); + test('#reset resets histogram data', () => { + const eventLoopDelaysCollector = new EventLoopDelaysCollector(); + eventLoopDelaysCollector.reset(); + expect(mockMonitorReset).toBeCalledTimes(1); + }); + test('#stop disables monitoring event loop delays', () => { + const eventLoopDelaysCollector = new EventLoopDelaysCollector(); + eventLoopDelaysCollector.stop(); + expect(mockMonitorDisable).toBeCalledTimes(1); + }); +}); + +describe('startTrackingEventLoopDelaysUsage', () => { + const mockInternalRepository = savedObjectsRepositoryMock.create(); + const stopMonitoringEventLoop$ = new Subject(); + + beforeAll(() => jest.useFakeTimers('modern')); + beforeEach(() => jest.clearAllMocks()); + afterEach(() => stopMonitoringEventLoop$.next()); + + it('initializes EventLoopDelaysCollector and starts timer', () => { + const collectionStartDelay = 1000; + startTrackingEventLoopDelaysUsage( + mockInternalRepository, + stopMonitoringEventLoop$, + collectionStartDelay + ); + + expect(monitorEventLoopDelay).toBeCalledTimes(1); + expect(mockMonitorPercentile).toBeCalledTimes(0); + jest.advanceTimersByTime(collectionStartDelay); + expect(mockMonitorPercentile).toBeCalled(); + }); + + it('stores event loop delays every collectionInterval duration', () => { + const collectionStartDelay = 100; + const collectionInterval = 1000; + startTrackingEventLoopDelaysUsage( + mockInternalRepository, + stopMonitoringEventLoop$, + collectionStartDelay, + collectionInterval + ); + + expect(mockInternalRepository.create).toBeCalledTimes(0); + jest.advanceTimersByTime(collectionStartDelay); + expect(mockInternalRepository.create).toBeCalledTimes(1); + jest.advanceTimersByTime(collectionInterval); + expect(mockInternalRepository.create).toBeCalledTimes(2); + jest.advanceTimersByTime(collectionInterval); + expect(mockInternalRepository.create).toBeCalledTimes(3); + }); + + it('resets histogram every histogramReset duration', () => { + const collectionStartDelay = 0; + const collectionInterval = 1000; + const histogramReset = 5000; + startTrackingEventLoopDelaysUsage( + mockInternalRepository, + stopMonitoringEventLoop$, + collectionStartDelay, + collectionInterval, + histogramReset + ); + + expect(mockMonitorReset).toBeCalledTimes(0); + jest.advanceTimersByTime(collectionInterval * 5); + expect(mockMonitorReset).toBeCalledTimes(1); + jest.advanceTimersByTime(collectionInterval * 5); + expect(mockMonitorReset).toBeCalledTimes(2); + }); + + it('stops monitoring event loop delays once stopMonitoringEventLoop$.next is called', () => { + startTrackingEventLoopDelaysUsage(mockInternalRepository, stopMonitoringEventLoop$); + + expect(mockMonitorDisable).toBeCalledTimes(0); + stopMonitoringEventLoop$.next(); + expect(mockMonitorDisable).toBeCalledTimes(1); + }); +}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.ts new file mode 100644 index 0000000000000..655cba580fc5d --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { EventLoopDelayMonitor } from 'perf_hooks'; +import { monitorEventLoopDelay } from 'perf_hooks'; +import { takeUntil, finalize, map } from 'rxjs/operators'; +import { Observable, timer } from 'rxjs'; +import type { ISavedObjectsRepository } from 'kibana/server'; +import { + MONITOR_EVENT_LOOP_DELAYS_START, + MONITOR_EVENT_LOOP_DELAYS_INTERVAL, + MONITOR_EVENT_LOOP_DELAYS_RESET, + MONITOR_EVENT_LOOP_DELAYS_RESOLUTION, +} from './constants'; +import { storeHistogram } from './saved_objects'; + +export interface IntervalHistogram { + fromTimestamp: string; + lastUpdatedAt: string; + min: number; + max: number; + mean: number; + exceeds: number; + stddev: number; + percentiles: { + 50: number; + 75: number; + 95: number; + 99: number; + }; +} + +export class EventLoopDelaysCollector { + private readonly loopMonitor: EventLoopDelayMonitor; + private fromTimestamp: Date; + + constructor() { + const monitor = monitorEventLoopDelay({ + resolution: MONITOR_EVENT_LOOP_DELAYS_RESOLUTION, + }); + monitor.enable(); + this.fromTimestamp = new Date(); + this.loopMonitor = monitor; + } + + public collect(): IntervalHistogram { + const { min, max, mean, exceeds, stddev } = this.loopMonitor; + + return { + min, + max, + mean, + exceeds, + stddev, + fromTimestamp: this.fromTimestamp.toISOString(), + lastUpdatedAt: new Date().toISOString(), + percentiles: { + 50: this.loopMonitor.percentile(50), + 75: this.loopMonitor.percentile(75), + 95: this.loopMonitor.percentile(95), + 99: this.loopMonitor.percentile(99), + }, + }; + } + + public reset() { + this.loopMonitor.reset(); + this.fromTimestamp = new Date(); + } + + public stop() { + this.loopMonitor.disable(); + } +} + +/** + * The monitoring of the event loop starts immediately. + * The first collection of the histogram happens after 1 minute. + * The daily histogram data is updated every 1 hour. + */ +export function startTrackingEventLoopDelaysUsage( + internalRepository: ISavedObjectsRepository, + stopMonitoringEventLoop$: Observable, + collectionStartDelay = MONITOR_EVENT_LOOP_DELAYS_START, + collectionInterval = MONITOR_EVENT_LOOP_DELAYS_INTERVAL, + histogramReset = MONITOR_EVENT_LOOP_DELAYS_RESET +) { + const eventLoopDelaysCollector = new EventLoopDelaysCollector(); + + const resetOnCount = Math.ceil(histogramReset / collectionInterval); + timer(collectionStartDelay, collectionInterval) + .pipe( + map((i) => (i + 1) % resetOnCount === 0), + takeUntil(stopMonitoringEventLoop$), + finalize(() => eventLoopDelaysCollector.stop()) + ) + .subscribe(async (shouldReset) => { + const histogram = eventLoopDelaysCollector.collect(); + if (shouldReset) { + eventLoopDelaysCollector.reset(); + } + await storeHistogram(histogram, internalRepository); + }); +} diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.test.ts new file mode 100644 index 0000000000000..06c51f6afa3a8 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.test.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { + Collector, + createUsageCollectionSetupMock, + createCollectorFetchContextMock, +} from '../../../../usage_collection/server/mocks'; +import { registerEventLoopDelaysCollector } from './event_loop_delays_usage_collector'; +import { loggingSystemMock, savedObjectsRepositoryMock } from '../../../../../core/server/mocks'; +import type { SavedObjectsFindResponse } from '../../../../../core/server'; + +const logger = loggingSystemMock.createLogger(); + +describe('registerEventLoopDelaysCollector', () => { + let collector: Collector; + const mockRegisterType = jest.fn(); + const mockInternalRepository = savedObjectsRepositoryMock.create(); + const mockGetSavedObjectsClient = () => mockInternalRepository; + + const usageCollectionMock = createUsageCollectionSetupMock(); + usageCollectionMock.makeUsageCollector.mockImplementation((config) => { + collector = new Collector(logger, config); + return createUsageCollectionSetupMock().makeUsageCollector(config); + }); + + const collectorFetchContext = createCollectorFetchContextMock(); + + beforeAll(() => { + registerEventLoopDelaysCollector( + logger, + usageCollectionMock, + mockRegisterType, + mockGetSavedObjectsClient + ); + }); + + it('registers event_loop_delays collector', () => { + expect(collector).not.toBeUndefined(); + expect(collector.type).toBe('event_loop_delays'); + }); + + it('registers savedObjectType "event_loop_delays_daily"', () => { + expect(mockRegisterType).toBeCalledTimes(1); + expect(mockRegisterType).toBeCalledWith( + expect.objectContaining({ + name: 'event_loop_delays_daily', + }) + ); + }); + + it('returns objects from event_loop_delays_daily from fetch function', async () => { + const mockFind = jest.fn().mockResolvedValue(({ + saved_objects: [{ attributes: { test: 1 } }], + } as unknown) as SavedObjectsFindResponse); + mockInternalRepository.find = mockFind; + const fetchResult = await collector.fetch(collectorFetchContext); + + expect(fetchResult).toMatchInlineSnapshot(` + Object { + "daily": Array [ + Object { + "test": 1, + }, + ], + } + `); + expect(mockFind).toBeCalledTimes(1); + expect(mockFind.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "sortField": "updated_at", + "sortOrder": "desc", + "type": "event_loop_delays_daily", + }, + ] + `); + }); +}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.ts new file mode 100644 index 0000000000000..774e021d7a549 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/event_loop_delays_usage_collector.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 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 { timer } from 'rxjs'; +import { SavedObjectsServiceSetup, ISavedObjectsRepository, Logger } from 'kibana/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { rollDailyData } from './rollups'; +import { registerSavedObjectTypes, EventLoopDelaysDaily } from './saved_objects'; +import { eventLoopDelaysUsageSchema, EventLoopDelaysUsageReport } from './schema'; +import { SAVED_OBJECTS_DAILY_TYPE } from './saved_objects'; +import { ROLL_DAILY_INDICES_INTERVAL, ROLL_INDICES_START } from './constants'; + +export function registerEventLoopDelaysCollector( + logger: Logger, + usageCollection: UsageCollectionSetup, + registerType: SavedObjectsServiceSetup['registerType'], + getSavedObjectsClient: () => ISavedObjectsRepository | undefined +) { + registerSavedObjectTypes(registerType); + + timer(ROLL_INDICES_START, ROLL_DAILY_INDICES_INTERVAL).subscribe(() => + rollDailyData(logger, getSavedObjectsClient()) + ); + + const collector = usageCollection.makeUsageCollector({ + type: 'event_loop_delays', + isReady: () => typeof getSavedObjectsClient() !== 'undefined', + schema: eventLoopDelaysUsageSchema, + fetch: async () => { + const internalRepository = getSavedObjectsClient(); + if (!internalRepository) { + return { daily: [] }; + } + + const { saved_objects: savedObjects } = await internalRepository.find({ + type: SAVED_OBJECTS_DAILY_TYPE, + sortField: 'updated_at', + sortOrder: 'desc', + }); + + return { + daily: savedObjects.map((savedObject) => savedObject.attributes), + }; + }, + }); + + usageCollection.registerCollector(collector); +} diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/index.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/index.ts new file mode 100644 index 0000000000000..693b173c2759e --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/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 { startTrackingEventLoopDelaysUsage } from './event_loop_delays'; +export { registerEventLoopDelaysCollector } from './event_loop_delays_usage_collector'; +export { SAVED_OBJECTS_DAILY_TYPE } from './saved_objects'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.test.ts new file mode 100644 index 0000000000000..cb59d6a44b07e --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.test.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 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 { rollDailyData } from './daily'; +import { loggingSystemMock, savedObjectsRepositoryMock } from '../../../../../../core/server/mocks'; +import type { SavedObjectsFindResponse } from '../../../../../../core/server'; + +describe('rollDailyData', () => { + const logger = loggingSystemMock.createLogger(); + const mockSavedObjectsClient = savedObjectsRepositoryMock.create(); + + beforeEach(() => jest.clearAllMocks()); + + it('returns false if no savedObjectsClient', async () => { + await rollDailyData(logger, undefined); + expect(mockSavedObjectsClient.find).toBeCalledTimes(0); + }); + + it('calls delete on documents older than 3 days', async () => { + mockSavedObjectsClient.find.mockResolvedValueOnce({ + saved_objects: [{ id: 'test_id_1' }, { id: 'test_id_2' }], + } as SavedObjectsFindResponse); + + await rollDailyData(logger, mockSavedObjectsClient); + + expect(mockSavedObjectsClient.find).toHaveBeenCalledTimes(1); + expect(mockSavedObjectsClient.delete).toBeCalledTimes(2); + expect(mockSavedObjectsClient.delete).toHaveBeenNthCalledWith( + 1, + 'event_loop_delays_daily', + 'test_id_1' + ); + expect(mockSavedObjectsClient.delete).toHaveBeenNthCalledWith( + 2, + 'event_loop_delays_daily', + 'test_id_2' + ); + }); + + it('calls logger.debug on repository find error', async () => { + const mockError = new Error('find error'); + mockSavedObjectsClient.find.mockRejectedValueOnce(mockError); + + await rollDailyData(logger, mockSavedObjectsClient); + expect(logger.debug).toBeCalledTimes(2); + expect(logger.debug).toHaveBeenNthCalledWith( + 1, + 'Failed to rollup transactional to daily entries' + ); + expect(logger.debug).toHaveBeenNthCalledWith(2, mockError); + }); + + it('settles all deletes before logging failures', async () => { + const mockError1 = new Error('delete error 1'); + const mockError2 = new Error('delete error 2'); + mockSavedObjectsClient.find.mockResolvedValueOnce({ + saved_objects: [{ id: 'test_id_1' }, { id: 'test_id_2' }, { id: 'test_id_3' }], + } as SavedObjectsFindResponse); + + mockSavedObjectsClient.delete.mockRejectedValueOnce(mockError1); + mockSavedObjectsClient.delete.mockResolvedValueOnce(true); + mockSavedObjectsClient.delete.mockRejectedValueOnce(mockError2); + + await rollDailyData(logger, mockSavedObjectsClient); + expect(mockSavedObjectsClient.delete).toBeCalledTimes(3); + expect(logger.debug).toBeCalledTimes(2); + expect(logger.debug).toHaveBeenNthCalledWith( + 1, + 'Failed to rollup transactional to daily entries' + ); + expect(logger.debug).toHaveBeenNthCalledWith(2, [ + { reason: mockError1, status: 'rejected' }, + { reason: mockError2, status: 'rejected' }, + ]); + }); +}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.ts new file mode 100644 index 0000000000000..29072335d272b --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/daily.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 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 { Logger } from '@kbn/logging'; +import { ISavedObjectsRepository } from '../../../../../../core/server'; +import { deleteHistogramSavedObjects } from '../saved_objects'; + +/** + * daily rollup function. Deletes histogram saved objects older than 3 days + * @param logger + * @param savedObjectsClient + */ +export async function rollDailyData( + logger: Logger, + savedObjectsClient?: ISavedObjectsRepository +): Promise { + if (!savedObjectsClient) { + return; + } + try { + const settledDeletes = await deleteHistogramSavedObjects(savedObjectsClient); + const failedDeletes = settledDeletes.filter(({ status }) => status !== 'fulfilled'); + if (failedDeletes.length) { + throw failedDeletes; + } + } catch (err) { + logger.debug(`Failed to rollup transactional to daily entries`); + logger.debug(err); + } +} diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/index.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/index.ts new file mode 100644 index 0000000000000..4523069a820e7 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/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 { rollDailyData } from './daily'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts new file mode 100644 index 0000000000000..8c227f260da6e --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts @@ -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 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 { Logger, ISavedObjectsRepository } from '../../../../../../../core/server'; +import { + createTestServers, + TestElasticsearchUtils, + TestKibanaUtils, + createRootWithCorePlugins, +} from '../../../../../../../core/test_helpers/kbn_server'; +import { rollDailyData } from '../daily'; +import { mocked } from '../../event_loop_delays.mocks'; + +import { + SAVED_OBJECTS_DAILY_TYPE, + serializeSavedObjectId, + EventLoopDelaysDaily, +} from '../../saved_objects'; +import moment from 'moment'; + +const { startES } = createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), +}); + +function createRawObject(date: moment.MomentInput) { + const pid = Math.round(Math.random() * 10000); + return { + type: SAVED_OBJECTS_DAILY_TYPE, + id: serializeSavedObjectId({ pid, date }), + attributes: { + ...mocked.createHistogram({ + fromTimestamp: moment(date).startOf('day').toISOString(), + lastUpdatedAt: moment(date).toISOString(), + }), + processId: pid, + }, + }; +} + +const rawEventLoopDelaysDaily = [ + createRawObject(moment.now()), + createRawObject(moment.now()), + createRawObject(moment().subtract(1, 'days')), + createRawObject(moment().subtract(3, 'days')), +]; + +const outdatedRawEventLoopDelaysDaily = [ + createRawObject(moment().subtract(5, 'days')), + createRawObject(moment().subtract(7, 'days')), +]; + +describe('daily rollups integration test', () => { + let esServer: TestElasticsearchUtils; + let root: TestKibanaUtils['root']; + let internalRepository: ISavedObjectsRepository; + let logger: Logger; + + beforeAll(async () => { + esServer = await startES(); + root = createRootWithCorePlugins(); + + await root.setup(); + const start = await root.start(); + logger = root.logger.get('test dailt rollups'); + internalRepository = start.savedObjects.createInternalRepository([SAVED_OBJECTS_DAILY_TYPE]); + + await internalRepository.bulkCreate( + [...rawEventLoopDelaysDaily, ...outdatedRawEventLoopDelaysDaily], + { refresh: true } + ); + }); + + afterAll(async () => { + await esServer.stop(); + await root.shutdown(); + }); + + it('deletes documents older that 3 days from the saved objects repository', async () => { + await rollDailyData(logger, internalRepository); + const { + total, + saved_objects: savedObjects, + } = await internalRepository.find({ type: SAVED_OBJECTS_DAILY_TYPE }); + expect(total).toBe(rawEventLoopDelaysDaily.length); + expect(savedObjects.map(({ id, type, attributes }) => ({ id, type, attributes }))).toEqual( + rawEventLoopDelaysDaily + ); + }); +}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.test.ts new file mode 100644 index 0000000000000..022040615bd45 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.test.ts @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { + storeHistogram, + serializeSavedObjectId, + deleteHistogramSavedObjects, +} from './saved_objects'; +import { savedObjectsRepositoryMock } from '../../../../../core/server/mocks'; +import type { SavedObjectsFindResponse } from '../../../../../core/server/'; +import { mocked } from './event_loop_delays.mocks'; + +describe('serializeSavedObjectId', () => { + it('returns serialized id', () => { + const id = serializeSavedObjectId({ date: 1623233091278, pid: 123 }); + expect(id).toBe('123::09062021'); + }); +}); + +describe('storeHistogram', () => { + const mockHistogram = mocked.createHistogram(); + const mockInternalRepository = savedObjectsRepositoryMock.create(); + + jest.useFakeTimers('modern'); + const mockNow = jest.getRealSystemTime(); + jest.setSystemTime(mockNow); + + beforeEach(() => jest.clearAllMocks()); + afterAll(() => jest.useRealTimers()); + + it('stores histogram data in a savedObject', async () => { + await storeHistogram(mockHistogram, mockInternalRepository); + const pid = process.pid; + const id = serializeSavedObjectId({ date: mockNow, pid }); + + expect(mockInternalRepository.create).toBeCalledWith( + 'event_loop_delays_daily', + { ...mockHistogram, processId: pid }, + { id, overwrite: true } + ); + }); +}); + +describe('deleteHistogramSavedObjects', () => { + const mockInternalRepository = savedObjectsRepositoryMock.create(); + + beforeEach(() => { + jest.clearAllMocks(); + mockInternalRepository.find.mockResolvedValue({ + saved_objects: [{ id: 'test_obj_1' }, { id: 'test_obj_1' }], + } as SavedObjectsFindResponse); + }); + + it('builds filter query based on time range passed in days', async () => { + await deleteHistogramSavedObjects(mockInternalRepository); + await deleteHistogramSavedObjects(mockInternalRepository, 20); + expect(mockInternalRepository.find.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "filter": "event_loop_delays_daily.attributes.lastUpdatedAt < \\"now-3d/d\\"", + "type": "event_loop_delays_daily", + }, + ], + Array [ + Object { + "filter": "event_loop_delays_daily.attributes.lastUpdatedAt < \\"now-20d/d\\"", + "type": "event_loop_delays_daily", + }, + ], + ] + `); + }); + + it('loops over saved objects and deletes them', async () => { + mockInternalRepository.delete.mockImplementation(async (type, id) => { + return id; + }); + + const results = await deleteHistogramSavedObjects(mockInternalRepository); + expect(results).toMatchInlineSnapshot(` + Array [ + Object { + "status": "fulfilled", + "value": "test_obj_1", + }, + Object { + "status": "fulfilled", + "value": "test_obj_1", + }, + ] + `); + }); + + it('settles all promises even if some of the deletes fail.', async () => { + mockInternalRepository.delete.mockImplementationOnce(async (type, id) => { + throw new Error('Intentional failure'); + }); + mockInternalRepository.delete.mockImplementationOnce(async (type, id) => { + return id; + }); + + const results = await deleteHistogramSavedObjects(mockInternalRepository); + expect(results).toMatchInlineSnapshot(` + Array [ + Object { + "reason": [Error: Intentional failure], + "status": "rejected", + }, + Object { + "status": "fulfilled", + "value": "test_obj_1", + }, + ] + `); + }); +}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.ts new file mode 100644 index 0000000000000..610a6697da364 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/saved_objects.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 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 { + SavedObjectAttributes, + SavedObjectsServiceSetup, + ISavedObjectsRepository, +} from 'kibana/server'; +import moment from 'moment'; +import type { IntervalHistogram } from './event_loop_delays'; + +export const SAVED_OBJECTS_DAILY_TYPE = 'event_loop_delays_daily'; + +export interface EventLoopDelaysDaily extends SavedObjectAttributes, IntervalHistogram { + processId: number; +} + +export function registerSavedObjectTypes(registerType: SavedObjectsServiceSetup['registerType']) { + registerType({ + name: SAVED_OBJECTS_DAILY_TYPE, + hidden: true, + namespaceType: 'agnostic', + mappings: { + dynamic: false, + properties: { + // This type requires `lastUpdatedAt` to be indexed so we can use it when rolling up totals (lastUpdatedAt < now-90d) + lastUpdatedAt: { type: 'date' }, + }, + }, + }); +} + +export function serializeSavedObjectId({ date, pid }: { date: moment.MomentInput; pid: number }) { + const formattedDate = moment(date).format('DDMMYYYY'); + + return `${pid}::${formattedDate}`; +} + +export async function deleteHistogramSavedObjects( + internalRepository: ISavedObjectsRepository, + daysTimeRange = 3 +) { + const { saved_objects: savedObjects } = await internalRepository.find({ + type: SAVED_OBJECTS_DAILY_TYPE, + filter: `${SAVED_OBJECTS_DAILY_TYPE}.attributes.lastUpdatedAt < "now-${daysTimeRange}d/d"`, + }); + + return await Promise.allSettled( + savedObjects.map(async (savedObject) => { + return await internalRepository.delete(SAVED_OBJECTS_DAILY_TYPE, savedObject.id); + }) + ); +} + +export async function storeHistogram( + histogram: IntervalHistogram, + internalRepository: ISavedObjectsRepository +) { + const pid = process.pid; + const id = serializeSavedObjectId({ date: histogram.lastUpdatedAt, pid }); + + return await internalRepository.create( + SAVED_OBJECTS_DAILY_TYPE, + { ...histogram, processId: pid }, + { id, overwrite: true } + ); +} diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/schema.ts new file mode 100644 index 0000000000000..319e8c77438b8 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/schema.ts @@ -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 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 { MakeSchemaFrom } from 'src/plugins/usage_collection/server'; + +export interface EventLoopDelaysUsageReport { + daily: Array<{ + processId: number; + lastUpdatedAt: string; + fromTimestamp: string; + min: number; + max: number; + mean: number; + exceeds: number; + stddev: number; + percentiles: { + '50': number; + '75': number; + '95': number; + '99': number; + }; + }>; +} + +export const eventLoopDelaysUsageSchema: MakeSchemaFrom = { + daily: { + type: 'array', + items: { + processId: { + type: 'long', + _meta: { + description: 'The process id of the monitored kibana instance.', + }, + }, + fromTimestamp: { + type: 'date', + _meta: { + description: 'Timestamp at which the histogram started monitoring.', + }, + }, + lastUpdatedAt: { + type: 'date', + _meta: { + description: 'Latest timestamp this histogram object was updated this day.', + }, + }, + min: { + type: 'long', + _meta: { + description: 'The minimum recorded event loop delay.', + }, + }, + max: { + type: 'long', + _meta: { + description: 'The maximum recorded event loop delay.', + }, + }, + mean: { + type: 'long', + _meta: { + description: 'The mean of the recorded event loop delays.', + }, + }, + exceeds: { + type: 'long', + _meta: { + description: + 'The number of times the event loop delay exceeded the maximum 1 hour eventloop delay threshold.', + }, + }, + stddev: { + type: 'long', + _meta: { + description: 'The standard deviation of the recorded event loop delays.', + }, + }, + percentiles: { + '50': { + type: 'long', + _meta: { + description: 'The 50th accumulated percentile distribution', + }, + }, + '75': { + type: 'long', + _meta: { + description: 'The 75th accumulated percentile distribution', + }, + }, + '95': { + type: 'long', + _meta: { + description: 'The 95th accumulated percentile distribution', + }, + }, + '99': { + type: 'long', + _meta: { + description: 'The 99th accumulated percentile distribution', + }, + }, + }, + }, + }, +}; diff --git a/src/plugins/kibana_usage_collection/server/collectors/index.ts b/src/plugins/kibana_usage_collection/server/collectors/index.ts index 761989938e56d..e4ed24611bfa8 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/index.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/index.ts @@ -28,3 +28,4 @@ export { registerUsageCountersRollups, registerUsageCountersUsageCollector, } from './usage_counters'; +export { registerEventLoopDelaysCollector } from './event_loop_delays'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts.ts b/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts.ts index 9927b27da6c8f..eeaeed67e753f 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/saved_objects_counts/get_saved_object_counts.ts @@ -26,6 +26,6 @@ export async function getSavedObjectsCounts( }, }; const { body } = await esClient.search(savedObjectCountSearchParams); - // @ts-expect-error @elastic/elasticsearch Aggregate does not include `buckets` + // @ts-expect-error declare type for aggregations explicitly return body.aggregations?.types?.buckets || []; } diff --git a/src/plugins/kibana_usage_collection/server/plugin.test.ts b/src/plugins/kibana_usage_collection/server/plugin.test.ts index 2100b9bbb918b..1584366a42dc1 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.test.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.test.ts @@ -16,7 +16,6 @@ import { createUsageCollectionSetupMock, } from '../../usage_collection/server/mocks'; import { cloudDetailsMock } from './mocks'; - import { plugin } from './'; describe('kibana_usage_collection', () => { @@ -105,6 +104,10 @@ describe('kibana_usage_collection', () => { "isReady": true, "type": "localization", }, + Object { + "isReady": false, + "type": "event_loop_delays", + }, ] `); }); diff --git a/src/plugins/kibana_usage_collection/server/plugin.ts b/src/plugins/kibana_usage_collection/server/plugin.ts index da6445ce957d8..4ec717c48610e 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.ts @@ -22,6 +22,10 @@ import type { CoreUsageDataStart, } from 'src/core/server'; import { SavedObjectsClient } from '../../../core/server'; +import { + startTrackingEventLoopDelaysUsage, + SAVED_OBJECTS_DAILY_TYPE, +} from './collectors/event_loop_delays'; import { registerApplicationUsageCollector, registerKibanaUsageCollector, @@ -39,6 +43,7 @@ import { registerUsageCountersRollups, registerUsageCountersUsageCollector, registerSavedObjectsCountUsageCollector, + registerEventLoopDelaysCollector, } from './collectors'; interface KibanaUsageCollectionPluginsDepsSetup { @@ -54,46 +59,46 @@ export class KibanaUsageCollectionPlugin implements Plugin { private uiSettingsClient?: IUiSettingsClient; private metric$: Subject; private coreUsageData?: CoreUsageDataStart; - private stopUsingUiCounterIndicies$: Subject; + private pluginStop$: Subject; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); this.legacyConfig$ = initializerContext.config.legacy.globalConfig$; this.metric$ = new Subject(); - this.stopUsingUiCounterIndicies$ = new Subject(); + this.pluginStop$ = new Subject(); } public setup(coreSetup: CoreSetup, { usageCollection }: KibanaUsageCollectionPluginsDepsSetup) { usageCollection.createUsageCounter('uiCounters'); - this.registerUsageCollectors( usageCollection, coreSetup, this.metric$, - this.stopUsingUiCounterIndicies$, + this.pluginStop$, coreSetup.savedObjects.registerType.bind(coreSetup.savedObjects) ); } public start(core: CoreStart) { const { savedObjects, uiSettings } = core; - this.savedObjectsClient = savedObjects.createInternalRepository(); + this.savedObjectsClient = savedObjects.createInternalRepository([SAVED_OBJECTS_DAILY_TYPE]); const savedObjectsClient = new SavedObjectsClient(this.savedObjectsClient); this.uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); core.metrics.getOpsMetrics$().subscribe(this.metric$); this.coreUsageData = core.coreUsageData; + startTrackingEventLoopDelaysUsage(this.savedObjectsClient, this.pluginStop$.asObservable()); } public stop() { this.metric$.complete(); - this.stopUsingUiCounterIndicies$.complete(); + this.pluginStop$.complete(); } private registerUsageCollectors( usageCollection: UsageCollectionSetup, coreSetup: CoreSetup, metric$: Subject, - stopUsingUiCounterIndicies$: Subject, + pluginStop$: Subject, registerType: SavedObjectsRegisterType ) { const getSavedObjectsClient = () => this.savedObjectsClient; @@ -101,12 +106,8 @@ export class KibanaUsageCollectionPlugin implements Plugin { const getCoreUsageDataService = () => this.coreUsageData!; registerUiCounterSavedObjectType(coreSetup.savedObjects); - registerUiCountersRollups( - this.logger.get('ui-counters'), - stopUsingUiCounterIndicies$, - getSavedObjectsClient - ); - registerUiCountersUsageCollector(usageCollection, stopUsingUiCounterIndicies$); + registerUiCountersRollups(this.logger.get('ui-counters'), pluginStop$, getSavedObjectsClient); + registerUiCountersUsageCollector(usageCollection, pluginStop$); registerUsageCountersRollups(this.logger.get('usage-counters-rollup'), getSavedObjectsClient); registerUsageCountersUsageCollector(usageCollection); @@ -127,5 +128,11 @@ export class KibanaUsageCollectionPlugin implements Plugin { registerCoreUsageCollector(usageCollection, getCoreUsageDataService); registerConfigUsageCollector(usageCollection, getCoreUsageDataService); registerLocalizationUsageCollector(usageCollection, coreSetup.i18n); + registerEventLoopDelaysCollector( + this.logger.get('event-loop-delays'), + usageCollection, + registerType, + getSavedObjectsClient + ); } } diff --git a/src/plugins/management/common/index.ts b/src/plugins/management/common/index.ts new file mode 100644 index 0000000000000..c701ba846bcac --- /dev/null +++ b/src/plugins/management/common/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 { ManagementAppLocator } from './locator'; diff --git a/src/plugins/management/common/locator.test.ts b/src/plugins/management/common/locator.test.ts index dda393a4203ec..20773b9732782 100644 --- a/src/plugins/management/common/locator.test.ts +++ b/src/plugins/management/common/locator.test.ts @@ -7,16 +7,16 @@ */ import { MANAGEMENT_APP_ID } from './contants'; -import { ManagementAppLocator, MANAGEMENT_APP_LOCATOR } from './locator'; +import { ManagementAppLocatorDefinition, MANAGEMENT_APP_LOCATOR } from './locator'; test('locator has the right ID', () => { - const locator = new ManagementAppLocator(); + const locator = new ManagementAppLocatorDefinition(); expect(locator.id).toBe(MANAGEMENT_APP_LOCATOR); }); test('returns management app ID', async () => { - const locator = new ManagementAppLocator(); + const locator = new ManagementAppLocatorDefinition(); const location = await locator.getLocation({ sectionId: 'a', appId: 'b', @@ -28,26 +28,26 @@ test('returns management app ID', async () => { }); test('returns Kibana location for section ID and app ID pair', async () => { - const locator = new ManagementAppLocator(); + const locator = new ManagementAppLocatorDefinition(); const location = await locator.getLocation({ sectionId: 'ingest', appId: 'index', }); expect(location).toMatchObject({ - route: '/ingest/index', + path: '/ingest/index', state: {}, }); }); test('when app ID is not provided, returns path to just the section ID', async () => { - const locator = new ManagementAppLocator(); + const locator = new ManagementAppLocatorDefinition(); const location = await locator.getLocation({ sectionId: 'data', }); expect(location).toMatchObject({ - route: '/data', + path: '/data', state: {}, }); }); diff --git a/src/plugins/management/common/locator.ts b/src/plugins/management/common/locator.ts index 4a4a50f468adc..7dbf5e2888011 100644 --- a/src/plugins/management/common/locator.ts +++ b/src/plugins/management/common/locator.ts @@ -7,7 +7,7 @@ */ import { SerializableState } from 'src/plugins/kibana_utils/common'; -import { LocatorDefinition } from 'src/plugins/share/common'; +import { LocatorDefinition, LocatorPublic } from 'src/plugins/share/common'; import { MANAGEMENT_APP_ID } from './contants'; export const MANAGEMENT_APP_LOCATOR = 'MANAGEMENT_APP_LOCATOR'; @@ -17,15 +17,18 @@ export interface ManagementAppLocatorParams extends SerializableState { appId?: string; } -export class ManagementAppLocator implements LocatorDefinition { +export type ManagementAppLocator = LocatorPublic; + +export class ManagementAppLocatorDefinition + implements LocatorDefinition { public readonly id = MANAGEMENT_APP_LOCATOR; public readonly getLocation = async (params: ManagementAppLocatorParams) => { - const route = `/${params.sectionId}${params.appId ? '/' + params.appId : ''}`; + const path = `/${params.sectionId}${params.appId ? '/' + params.appId : ''}`; return { app: MANAGEMENT_APP_ID, - route, + path, state: {}, }; }; diff --git a/src/plugins/management/public/mocks/index.ts b/src/plugins/management/public/mocks/index.ts index 70d853f32dfcc..b06e41502e9df 100644 --- a/src/plugins/management/public/mocks/index.ts +++ b/src/plugins/management/public/mocks/index.ts @@ -33,9 +33,11 @@ const createSetupContract = (): ManagementSetup => ({ locator: { getLocation: jest.fn(async () => ({ app: 'MANAGEMENT', - route: '', + path: '', state: {}, })), + getUrl: jest.fn(), + useUrl: jest.fn(), navigate: jest.fn(), }, }); diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index 3289b2f6f5446..34719fb5070e1 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -25,7 +25,7 @@ import { } from '../../../core/public'; import { MANAGEMENT_APP_ID } from '../common/contants'; -import { ManagementAppLocator } from '../common/locator'; +import { ManagementAppLocatorDefinition } from '../common/locator'; import { ManagementSectionsService, getSectionsServiceStartPrivate, @@ -74,7 +74,7 @@ export class ManagementPlugin public setup(core: CoreSetup, { home, share }: ManagementSetupDependencies) { const kibanaVersion = this.initializerContext.env.packageInfo.version; - const locator = share.url.locators.create(new ManagementAppLocator()); + const locator = share.url.locators.create(new ManagementAppLocatorDefinition()); if (home) { home.featureCatalogue.register({ diff --git a/src/plugins/management/server/plugin.ts b/src/plugins/management/server/plugin.ts index 349cab6206bab..cc3798d855c59 100644 --- a/src/plugins/management/server/plugin.ts +++ b/src/plugins/management/server/plugin.ts @@ -9,7 +9,7 @@ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from 'kibana/server'; import { LocatorPublic } from 'src/plugins/share/common'; import type { SharePluginSetup } from 'src/plugins/share/server'; -import { ManagementAppLocator, ManagementAppLocatorParams } from '../common/locator'; +import { ManagementAppLocatorDefinition, ManagementAppLocatorParams } from '../common/locator'; import { capabilitiesProvider } from './capabilities_provider'; interface ManagementSetupDependencies { @@ -31,7 +31,7 @@ export class ManagementServerPlugin public setup(core: CoreSetup, { share }: ManagementSetupDependencies) { this.logger.debug('management: Setup'); - const locator = share.url.locators.create(new ManagementAppLocator()); + const locator = share.url.locators.create(new ManagementAppLocatorDefinition()); core.capabilities.registerProvider(capabilitiesProvider); 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 ( - onClose()} headerZindexLocation="below"> - - - -

    - - - - - {strings.getTitleLabel()} - -

    -
    - - -

    {strings.getDescriptionMessage()}

    -
    -
    - - - - {footer} -
    -
    + + + +

    + + + + + {strings.getTitleLabel()} + +

    +
    + + +

    {strings.getDescriptionMessage()}

    +
    +
    + + + + {footer} +
    ); }; 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`] = `
    -
    - +
    - 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. - -
    +
    + + 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. + +
    +
    +
    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`] =
    -
    - - The index pattern associated with this object no longer exists. - -
    -
    - +
    - If you know what this error means, go ahead and fix it — otherwise click the delete button above. - -
    +
    + + The index pattern associated with this object no longer exists. + +
    +
    + + If you know what this error means, go ahead and fix it — otherwise click the delete button above. + +
    +
    +
    @@ -128,29 +138,39 @@ exports[`NotFoundErrors component renders correctly for index-pattern-field type
    -
    - - A field associated with this object no longer exists in the index pattern. - -
    -
    - +
    - If you know what this error means, go ahead and fix it — otherwise click the delete button above. - -
    +
    + + A field associated with this object no longer exists in the index pattern. + +
    +
    + + If you know what this error means, go ahead and fix it — otherwise click the delete button above. + +
    +
    +
    @@ -207,29 +227,39 @@ exports[`NotFoundErrors component renders correctly for search type 1`] = `
    -
    - - The saved search associated with this object no longer exists. - -
    -
    - +
    - If you know what this error means, go ahead and fix it — otherwise click the delete button above. - -
    +
    + + The saved search associated with this object no longer exists. + +
    +
    + + If you know what this error means, go ahead and fix it — otherwise click the delete button above. + +
    +
    +
    @@ -286,21 +316,31 @@ exports[`NotFoundErrors component renders correctly for unknown type 1`] = `
    -
    -
    - +
    - If you know what this error means, go ahead and fix it — otherwise click the delete button above. - -
    +
    +
    + + If you know what this error means, go ahead and fix it — otherwise click the delete button above. + +
    +
    +
    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`] = ` @@ -277,6 +278,7 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = ` exports[`Flyout legacy conflicts should allow conflict resolution 1`] = ` @@ -548,6 +550,7 @@ Array [ exports[`Flyout should render import step 1`] = ` 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 { } return ( - +

    diff --git a/src/plugins/security_oss/server/check_cluster_data.test.ts b/src/plugins/security_oss/server/check_cluster_data.test.ts index 9e9459a68754c..6aa1cc9a28c39 100644 --- a/src/plugins/security_oss/server/check_cluster_data.test.ts +++ b/src/plugins/security_oss/server/check_cluster_data.test.ts @@ -27,20 +27,19 @@ describe('checkClusterForUserData', () => { it('returns false if data only exists in system indices', async () => { const esClient = elasticsearchServiceMock.createElasticsearchClient(); esClient.cat.indices.mockResolvedValue( - // @ts-expect-error @elastic/elasticsearch ES types don't support array response format elasticsearchServiceMock.createApiResponse({ body: [ { index: '.kibana', - 'docs.count': 500, + 'docs.count': '500', }, { index: 'kibana_sample_ecommerce_data', - 'docs.count': 20, + 'docs.count': '20', }, { index: '.somethingElse', - 'docs.count': 20, + 'docs.count': '20', }, ], }) @@ -56,16 +55,15 @@ describe('checkClusterForUserData', () => { it('returns true if data exists in non-system indices', async () => { const esClient = elasticsearchServiceMock.createElasticsearchClient(); esClient.cat.indices.mockResolvedValue( - // @ts-expect-error @elastic/elasticsearch ES types don't support array response format elasticsearchServiceMock.createApiResponse({ body: [ { index: '.kibana', - 'docs.count': 500, + 'docs.count': '500', }, { index: 'some_real_index', - 'docs.count': 20, + 'docs.count': '20', }, ], }) @@ -87,23 +85,21 @@ describe('checkClusterForUserData', () => { ) .mockRejectedValueOnce(new Error('something terrible happened')) .mockResolvedValueOnce( - // @ts-expect-error @elastic/elasticsearch ES types don't support array response format elasticsearchServiceMock.createApiResponse({ body: [ { index: '.kibana', - 'docs.count': 500, + 'docs.count': '500', }, ], }) ) .mockResolvedValueOnce( - // @ts-expect-error @elastic/elasticsearch ES types don't support array response format elasticsearchServiceMock.createApiResponse({ body: [ { index: 'some_real_index', - 'docs.count': 20, + 'docs.count': '20', }, ], }) diff --git a/src/plugins/share/common/index.ts b/src/plugins/share/common/index.ts index 8b5d8d4557194..e724117f5b7f7 100644 --- a/src/plugins/share/common/index.ts +++ b/src/plugins/share/common/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { LocatorDefinition, LocatorPublic } from './url_service'; +export { LocatorDefinition, LocatorPublic, useLocatorUrl } from './url_service'; diff --git a/src/plugins/share/common/url_service/__tests__/locators.test.ts b/src/plugins/share/common/url_service/__tests__/locators.test.ts index 45d727df7de48..93ba76c7399f4 100644 --- a/src/plugins/share/common/url_service/__tests__/locators.test.ts +++ b/src/plugins/share/common/url_service/__tests__/locators.test.ts @@ -53,7 +53,7 @@ describe('locators', () => { expect(location).toEqual({ app: 'test_app', - route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=21', + path: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=21', state: { isFlyoutOpen: true }, }); }); @@ -97,7 +97,7 @@ describe('locators', () => { expect(deps.navigate).toHaveBeenCalledWith( { app: 'test_app', - route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1', + path: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1', state: { isFlyoutOpen: false, }, @@ -130,7 +130,7 @@ describe('locators', () => { expect(deps.navigate).toHaveBeenCalledWith( { app: 'test_app', - route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1', + path: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=1', state: { isFlyoutOpen: false, }, @@ -153,7 +153,7 @@ describe('locators', () => { expect(deps.navigate).toHaveBeenCalledWith( { app: 'test_app', - route: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=2', + path: '/my-object/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?page=2', state: { isFlyoutOpen: false, }, diff --git a/src/plugins/share/common/url_service/__tests__/setup.ts b/src/plugins/share/common/url_service/__tests__/setup.ts index ad13bb8d8d216..fea3e1b945f99 100644 --- a/src/plugins/share/common/url_service/__tests__/setup.ts +++ b/src/plugins/share/common/url_service/__tests__/setup.ts @@ -21,7 +21,7 @@ export const testLocator: LocatorDefinition = { getLocation: async ({ savedObjectId, pageNumber, showFlyout }) => { return { app: 'test_app', - route: `/my-object/${savedObjectId}?page=${pageNumber}`, + path: `/my-object/${savedObjectId}?page=${pageNumber}`, state: { isFlyoutOpen: showFlyout, }, @@ -34,6 +34,9 @@ export const urlServiceTestSetup = (partialDeps: Partial navigate: async () => { throw new Error('not implemented'); }, + getUrl: async () => { + throw new Error('not implemented'); + }, ...partialDeps, }; const service = new UrlService(deps); diff --git a/src/plugins/share/common/url_service/locators/index.ts b/src/plugins/share/common/url_service/locators/index.ts index f9f87215eb4db..7ab3938984f23 100644 --- a/src/plugins/share/common/url_service/locators/index.ts +++ b/src/plugins/share/common/url_service/locators/index.ts @@ -9,3 +9,4 @@ export * from './types'; export * from './locator'; export * from './locator_client'; +export { useLocatorUrl } from './use_locator_url'; diff --git a/src/plugins/share/common/url_service/locators/locator.ts b/src/plugins/share/common/url_service/locators/locator.ts index 68c3b05a7f411..680fb2231fc48 100644 --- a/src/plugins/share/common/url_service/locators/locator.ts +++ b/src/plugins/share/common/url_service/locators/locator.ts @@ -7,16 +7,27 @@ */ import type { SavedObjectReference } from 'kibana/server'; +import { DependencyList } from 'react'; import type { PersistableState, SerializableState } from 'src/plugins/kibana_utils/common'; +import { useLocatorUrl } from './use_locator_url'; import type { LocatorDefinition, LocatorPublic, KibanaLocation, LocatorNavigationParams, + LocatorGetUrlParams, } from './types'; export interface LocatorDependencies { + /** + * Navigate without reloading the page to a KibanaLocation. + */ navigate: (location: KibanaLocation, params?: LocatorNavigationParams) => Promise; + + /** + * Resolve a Kibana URL given KibanaLocation. + */ + getUrl: (location: KibanaLocation, getUrlParams: LocatorGetUrlParams) => Promise; } export class Locator

    implements PersistableState

    , LocatorPublic

    { @@ -57,13 +68,29 @@ export class Locator

    implements PersistableState

    return await this.definition.getLocation(params); } + public async getUrl(params: P, { absolute = false }: LocatorGetUrlParams = {}): Promise { + const location = await this.getLocation(params); + const url = this.deps.getUrl(location, { absolute }); + + return url; + } + public async navigate( params: P, { replace = false }: LocatorNavigationParams = {} ): Promise { const location = await this.getLocation(params); + await this.deps.navigate(location, { replace, }); } + + /* eslint-disable react-hooks/rules-of-hooks */ + public readonly useUrl = ( + params: P, + getUrlParams?: LocatorGetUrlParams, + deps: DependencyList = [] + ): string => useLocatorUrl

    (this, params, getUrlParams, deps); + /* eslint-enable react-hooks/rules-of-hooks */ } diff --git a/src/plugins/share/common/url_service/locators/types.ts b/src/plugins/share/common/url_service/locators/types.ts index d811ae0fd4aa2..870eaa3718d3f 100644 --- a/src/plugins/share/common/url_service/locators/types.ts +++ b/src/plugins/share/common/url_service/locators/types.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { DependencyList } from 'react'; import { PersistableState, SerializableState } from 'src/plugins/kibana_utils/common'; /** @@ -51,23 +52,57 @@ export interface LocatorDefinition

    */ export interface LocatorPublic

    { /** - * Returns a relative URL to the client-side redirect endpoint using this - * locator. (This method is necessary for compatibility with URL generators.) + * Returns a reference to a Kibana client-side location. + * + * @param params URL locator parameters. */ getLocation(params: P): Promise; + /** + * Returns a URL as a string. + * + * @param params URL locator parameters. + * @param getUrlParams URL construction parameters. + */ + getUrl(params: P, getUrlParams?: LocatorGetUrlParams): Promise; + /** * Navigate using the `core.application.navigateToApp()` method to a Kibana * location generated by this locator. This method is available only on the * browser. + * + * @param params URL locator parameters. + * @param navigationParams Navigation parameters. */ navigate(params: P, navigationParams?: LocatorNavigationParams): Promise; + + /** + * React hook which returns a URL string given locator parameters. Returns + * empty string if URL is being loaded or an error happened. + */ + useUrl: (params: P, getUrlParams?: LocatorGetUrlParams, deps?: DependencyList) => string; } +/** + * Parameters used when navigating on client-side using browser history object. + */ export interface LocatorNavigationParams { + /** + * Whether to replace a navigation entry in history queue or push a new entry. + */ replace?: boolean; } +/** + * Parameters used when constructing a string URL. + */ +export interface LocatorGetUrlParams { + /** + * Whether to return an absolute long URL or relative short URL. + */ + absolute?: boolean; +} + /** * This interface represents a location in Kibana to which one can navigate * using the `core.application.navigateToApp()` method. @@ -79,9 +114,9 @@ export interface KibanaLocation { app: string; /** - * A URL route within a Kibana application. + * A relative URL path within a Kibana application. */ - route: string; + path: string; /** * A serializable location state object, which the app can use to determine diff --git a/src/plugins/share/common/url_service/locators/use_locator_url.ts b/src/plugins/share/common/url_service/locators/use_locator_url.ts new file mode 100644 index 0000000000000..a84c712e16248 --- /dev/null +++ b/src/plugins/share/common/url_service/locators/use_locator_url.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 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 { DependencyList, useEffect, useState } from 'react'; +import useMountedState from 'react-use/lib/useMountedState'; +import { SerializableState } from 'src/plugins/kibana_utils/common'; +import { LocatorGetUrlParams, LocatorPublic } from '../../../common/url_service'; + +export const useLocatorUrl =

    ( + locator: LocatorPublic

    | null | undefined, + params: P, + getUrlParams?: LocatorGetUrlParams, + deps: DependencyList = [] +): string => { + const [url, setUrl] = useState(''); + const isMounted = useMountedState(); + + /* eslint-disable react-hooks/exhaustive-deps */ + useEffect(() => { + if (!locator) { + setUrl(''); + return; + } + + locator + .getUrl(params, getUrlParams) + .then((result: string) => { + if (!isMounted()) return; + setUrl(result); + }) + .catch((error) => { + if (!isMounted()) return; + // eslint-disable-next-line no-console + console.error('useLocatorUrl', error); + setUrl(''); + }); + }, [locator, ...deps]); + /* eslint-enable react-hooks/exhaustive-deps */ + + return url; +}; diff --git a/src/plugins/share/common/url_service/url_service.ts b/src/plugins/share/common/url_service/url_service.ts index 0c3a0aabb750b..5daba1500cdfd 100644 --- a/src/plugins/share/common/url_service/url_service.ts +++ b/src/plugins/share/common/url_service/url_service.ts @@ -17,7 +17,9 @@ export class UrlService { /** * Client to work with locators. */ - locators: LocatorClient = new LocatorClient(this.deps); + public readonly locators: LocatorClient; - constructor(protected readonly deps: UrlServiceDependencies) {} + constructor(protected readonly deps: UrlServiceDependencies) { + this.locators = new LocatorClient(deps); + } } diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts index d13bb15f8c72c..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'; @@ -29,6 +30,8 @@ export { UrlGeneratorsService, } from './url_generators'; +export { useLocatorUrl } from '../common/url_service/locators/use_locator_url'; + import { SharePlugin } from './plugin'; export { KibanaURL } from './kibana_url'; diff --git a/src/plugins/share/public/plugin.ts b/src/plugins/share/public/plugin.ts index eb7c46cdaef86..893108b56bcfa 100644 --- a/src/plugins/share/public/plugin.ts +++ b/src/plugins/share/public/plugin.ts @@ -68,14 +68,22 @@ export class SharePlugin implements Plugin { core.application.register(createShortUrlRedirectApp(core, window.location)); this.url = new UrlService({ - navigate: async (location, { replace = false } = {}) => { + navigate: async ({ app, path, state }, { replace = false } = {}) => { const [start] = await core.getStartServices(); - await start.application.navigateToApp(location.app, { - path: location.route, - state: location.state, + await start.application.navigateToApp(app, { + path, + state, replace, }); }, + getUrl: async ({ app, path }, { absolute }) => { + const start = await core.getStartServices(); + const url = start[0].application.getUrlForApp(app, { + path, + absolute, + }); + return url; + }, }); return { diff --git a/src/plugins/share/server/plugin.ts b/src/plugins/share/server/plugin.ts index 6e3c68935f77b..76e10372cdb67 100644 --- a/src/plugins/share/server/plugin.ts +++ b/src/plugins/share/server/plugin.ts @@ -32,7 +32,10 @@ export class SharePlugin implements Plugin { public setup(core: CoreSetup) { this.url = new UrlService({ navigate: async () => { - throw new Error('Locator .navigate() does not work on server.'); + throw new Error('Locator .navigate() currently is not supported on the server.'); + }, + getUrl: async () => { + throw new Error('Locator .getUrl() currently is not supported on the server.'); }, }); diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 6ab550389a12d..99c6dcb40e57d 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -7900,6 +7900,93 @@ } } }, + "event_loop_delays": { + "properties": { + "daily": { + "type": "array", + "items": { + "properties": { + "processId": { + "type": "long", + "_meta": { + "description": "The process id of the monitored kibana instance." + } + }, + "fromTimestamp": { + "type": "date", + "_meta": { + "description": "Timestamp at which the histogram started monitoring." + } + }, + "lastUpdatedAt": { + "type": "date", + "_meta": { + "description": "Latest timestamp this histogram object was updated this day." + } + }, + "min": { + "type": "long", + "_meta": { + "description": "The minimum recorded event loop delay." + } + }, + "max": { + "type": "long", + "_meta": { + "description": "The maximum recorded event loop delay." + } + }, + "mean": { + "type": "long", + "_meta": { + "description": "The mean of the recorded event loop delays." + } + }, + "exceeds": { + "type": "long", + "_meta": { + "description": "The number of times the event loop delay exceeded the maximum 1 hour eventloop delay threshold." + } + }, + "stddev": { + "type": "long", + "_meta": { + "description": "The standard deviation of the recorded event loop delays." + } + }, + "percentiles": { + "properties": { + "50": { + "type": "long", + "_meta": { + "description": "The 50th accumulated percentile distribution" + } + }, + "75": { + "type": "long", + "_meta": { + "description": "The 75th accumulated percentile distribution" + } + }, + "95": { + "type": "long", + "_meta": { + "description": "The 95th accumulated percentile distribution" + } + }, + "99": { + "type": "long", + "_meta": { + "description": "The 99th accumulated percentile distribution" + } + } + } + } + } + } + } + } + }, "localization": { "properties": { "locale": { 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({ ) : ( - - - + + + + + + + + + + )} 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(); 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(); 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[1]; -export function replaceVars(str, args = {}, vars = {}) { +export function replaceVars( + str: string, + args: Record = {}, + vars: Record = {}, + compileOptions: Partial = {} +) { 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/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" />

    { await PageObjects.settings.clickEditFieldFormat(); await a11y.testAppSnapshot(); + await PageObjects.settings.clickCloseEditFieldFormatFlyout(); }); it('Advanced settings', async () => { 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/_discover_fields_api.ts b/test/functional/apps/discover/_discover_fields_api.ts index 614a0794ffb3b..42e2a94b36462 100644 --- a/test/functional/apps/discover/_discover_fields_api.ts +++ b/test/functional/apps/discover/_discover_fields_api.ts @@ -11,6 +11,7 @@ import { FtrProviderContext } from './ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); + const docTable = getService('docTable'); const retry = getService('retry'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); @@ -58,5 +59,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await PageObjects.discover.getDocHeader()).not.to.have.string('_score'); expect(await PageObjects.discover.getDocHeader()).to.have.string('Document'); }); + + it('displays _source viewer in doc viewer', async function () { + await docTable.clickRowToggle({ rowIndex: 0 }); + + await PageObjects.discover.isShowingDocViewer(); + await PageObjects.discover.clickDocViewerTab(1); + await PageObjects.discover.expectSourceViewerToExist(); + }); }); } 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/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/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/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 41c4441a1c95d..65b899d2e2fb0 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -289,6 +289,14 @@ export class DiscoverPageObject extends FtrService { return await this.testSubjects.exists('kbnDocViewer'); } + public async clickDocViewerTab(index: number) { + return await this.find.clickByCssSelector(`#kbn_doc_viewer_tab_${index}`); + } + + public async expectSourceViewerToExist() { + return await this.find.byClassName('monaco-editor'); + } + public async getMarks() { const table = await this.docTable.getTable(); const marks = await table.findAllByTagName('mark'); 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/time_to_visualize_page.ts b/test/functional/page_objects/time_to_visualize_page.ts index 287b03ec60d88..57a22103f6409 100644 --- a/test/functional/page_objects/time_to_visualize_page.ts +++ b/test/functional/page_objects/time_to_visualize_page.ts @@ -51,7 +51,10 @@ export class TimeToVisualizePageObject extends FtrService { vizName: string, { saveAsNew, redirectToOrigin, addToDashboard, dashboardId, saveToLibrary }: SaveModalArgs = {} ) { - await this.testSubjects.setValue('savedObjectTitle', vizName); + await this.testSubjects.setValue('savedObjectTitle', vizName, { + typeCharByChar: true, + clearWithKeyboard: true, + }); const hasSaveAsNew = await this.testSubjects.exists('saveAsNewCheckbox'); if (hasSaveAsNew && saveAsNew !== undefined) { 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 { 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 { 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/esaggs_timeshift.ts b/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts index c750602f735bd..8fc09ce2d7342 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/esaggs_timeshift.ts @@ -71,6 +71,21 @@ export default function ({ expect(getCell(result, 0, 2)).to.be(4618); }); + it('shifts multiple metrics with relative time range and previous', async () => { + const expression = ` + kibana_context timeRange={timerange from='${timeRange.from}' to='now'} + | esaggs index={indexPatternLoad id='logstash-*'} + aggs={aggCount id="1" enabled=true schema="metric"} + aggs={aggCount id="2" enabled=true schema="metric" timeShift="previous"} + `; + const result = await expectExpression( + 'esaggs_shift_multi_metric_previous', + expression + ).getResponse(); + expect(getCell(result, 0, 0)).to.be(9247); + expect(getCell(result, 0, 1)).to.be(4763); + }); + it('shifts single percentile', async () => { const expression = ` kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} @@ -137,7 +152,7 @@ export default function ({ customMetric={aggAvg id="3" field="bytes" enabled=true - schema="metric" + schema="metric" } enabled=true schema="metric" @@ -154,7 +169,7 @@ export default function ({ customMetric={aggAvg id="5" field="bytes" enabled=true - schema="metric" + schema="metric" } enabled=true schema="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 (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/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(); + + 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 & { 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 => { + const resp = await supertest.get('/api/status?v8format=true'); + + return resp.body.status.plugins[pluginName]; + }; + + const setStatus = async (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/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 84fd5ba081d8f..1397a3da81072 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -27,11 +27,6 @@ }, "devDependencies": { "@kbn/plugin-helpers": "link:../packages/kbn-plugin-helpers", - "@kbn/storybook": "link:../packages/kbn-storybook", "@kbn/test": "link:../packages/kbn-test" - }, - "dependencies": { - "@kbn/interpreter": "link:../packages/kbn-interpreter", - "@kbn/ui-framework": "link:../packages/kbn-ui-framework" } } \ 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 `.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 `.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.

    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.

    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.

    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 3b91b07eb30f4..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', }, @@ -1676,6 +1676,70 @@ describe('execute()', () => { name: 'my name', }, }); + + await expect( + actionsClient.execute({ + actionId, + params: { + name: 'my name', + }, + relatedSavedObjects: [ + { + id: 'some-id', + typeId: 'some-type-id', + type: 'some-type', + }, + ], + }) + ).resolves.toMatchObject({ status: 'ok', actionId }); + + expect(actionExecutor.execute).toHaveBeenCalledWith({ + actionId, + request, + params: { + name: 'my name', + }, + relatedSavedObjects: [ + { + id: 'some-id', + typeId: 'some-type-id', + type: 'some-type', + }, + ], + }); + + await expect( + actionsClient.execute({ + actionId, + params: { + name: 'my name', + }, + relatedSavedObjects: [ + { + id: 'some-id', + typeId: 'some-type-id', + type: 'some-type', + namespace: 'some-namespace', + }, + ], + }) + ).resolves.toMatchObject({ status: 'ok', actionId }); + + expect(actionExecutor.execute).toHaveBeenCalledWith({ + actionId, + request, + params: { + name: 'my name', + }, + relatedSavedObjects: [ + { + id: 'some-id', + typeId: 'some-type-id', + type: 'some-type', + namespace: 'some-namespace', + }, + ], + }); }); }); diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 449d218ed5ae0..f8d13cdafa755 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -469,6 +469,7 @@ export class ActionsClient { actionId, params, source, + relatedSavedObjects, }: Omit): Promise> { if ( (await getAuthorizationModeBySource(this.unsecuredSavedObjectsClient, source)) === @@ -476,7 +477,13 @@ export class ActionsClient { ) { await this.authorization.ensureAuthorized('execute'); } - return this.actionExecutor.execute({ actionId, params, source, request: this.request }); + return this.actionExecutor.execute({ + actionId, + params, + source, + request: this.request, + relatedSavedObjects, + }); } public async enqueueExecution(options: EnqueueExecutionOptions): Promise { 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; -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 = {}; 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 = {}; - 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; + +describe('api', () => { + let externalService: jest.Mocked; + + 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 => { + 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; + +const mappingKeysToIncidentKeys: Record = { + 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).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> { + 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 => { + 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; + +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 = { + '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 => { + 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 => { + try { + const mappingConfig = mappings as MappingConfigType; + const data = getBodyForEventAction(appId, mappingConfig, params.incident, params.incidentId); + + const res = await request({ + 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; +export type SwimlaneSecretConfigurationType = TypeOf; + +export type MappingConfigType = TypeOf; +export type ExecutorParams = TypeOf; +export type ExecutorSubActionPushParams = TypeOf; + +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; + createRecord: (params: CreateRecordParams) => Promise; + updateRecord: (params: UpdateRecordParams) => Promise; +} + +export type Incident = Omit; + +export interface ExternalServiceApiHandlerArgs { + externalService: ExternalService; +} + +export interface GetApplicationHandlerArgs { + externalService: ExternalService; +} + +export interface PushToServiceResponse extends ExternalServiceIncidentResponse { + comments?: ExternalServiceCommentResponse[]; +} + +export interface ExternalServiceApi { + pushToService: (args: PushToServiceApiHandlerArgs) => Promise; +} + +export type SwimlaneExecutorResultData = ExternalServiceIncidentResponse; +export type SwimlaneDataValues = Record; +export interface SwimlaneComment { + fieldId: string; + message: string | number; + createdDate: string; + isRichText: boolean; +} +export type SwimlaneDataComments = Record; + +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 = { - 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/constants/event_log.ts b/x-pack/plugins/actions/server/constants/event_log.ts index 508709c8783ab..9163a0d105ce8 100644 --- a/x-pack/plugins/actions/server/constants/event_log.ts +++ b/x-pack/plugins/actions/server/constants/event_log.ts @@ -8,5 +8,6 @@ export const EVENT_LOG_PROVIDER = 'actions'; export const EVENT_LOG_ACTIONS = { execute: 'execute', + executeStart: 'execute-start', executeViaHttp: 'execute-via-http', }; diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts index 4cacba6dc880a..ee8064d2aadc5 100644 --- a/x-pack/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_execute_function.test.ts @@ -83,6 +83,62 @@ describe('execute()', () => { }); }); + test('schedules the action with all given parameters and relatedSavedObjects', async () => { + const actionTypeRegistry = actionTypeRegistryMock.create(); + const executeFn = createExecutionEnqueuerFunction({ + taskManager: mockTaskManager, + actionTypeRegistry, + isESOCanEncrypt: true, + preconfiguredActions: [], + }); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + type: 'action', + attributes: { + actionTypeId: 'mock-action', + }, + references: [], + }); + savedObjectsClient.create.mockResolvedValueOnce({ + id: '234', + type: 'action_task_params', + attributes: {}, + references: [], + }); + await executeFn(savedObjectsClient, { + id: '123', + params: { baz: false }, + spaceId: 'default', + apiKey: Buffer.from('123:abc').toString('base64'), + source: asHttpRequestExecutionSource(request), + relatedSavedObjects: [ + { + id: 'some-id', + namespace: 'some-namespace', + type: 'some-type', + typeId: 'some-typeId', + }, + ], + }); + expect(savedObjectsClient.create).toHaveBeenCalledWith( + 'action_task_params', + { + actionId: '123', + params: { baz: false }, + apiKey: Buffer.from('123:abc').toString('base64'), + relatedSavedObjects: [ + { + id: 'some-id', + namespace: 'some-namespace', + type: 'some-type', + typeId: 'some-typeId', + }, + ], + }, + {} + ); + }); + test('schedules the action with all given parameters with a preconfigured action', async () => { const executeFn = createExecutionEnqueuerFunction({ taskManager: mockTaskManager, diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index 4f3ffbef36c6e..7dcd66c711bdd 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -11,6 +11,7 @@ import { RawAction, ActionTypeRegistryContract, PreConfiguredAction } from './ty import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from './constants/saved_objects'; import { ExecuteOptions as ActionExecutorOptions } from './lib/action_executor'; import { isSavedObjectExecutionSource } from './lib'; +import { RelatedSavedObjects } from './lib/related_saved_objects'; interface CreateExecuteFunctionOptions { taskManager: TaskManagerStartContract; @@ -23,6 +24,7 @@ export interface ExecuteOptions extends Pick = { if ( customHostSettings.find( (customHostSchema: CustomHostSettings) => - !!customHostSchema.tls && !!customHostSchema.tls.rejectUnauthorized + !!customHostSchema.ssl && !!customHostSchema.ssl.rejectUnauthorized ) ) { addDeprecation({ message: - `"xpack.actions.customHostSettings[].tls.rejectUnauthorized" is deprecated.` + - `Use "xpack.actions.customHostSettings[].tls.verificationMode" instead, ` + + `"xpack.actions.customHostSettings[].ssl.rejectUnauthorized" is deprecated.` + + `Use "xpack.actions.customHostSettings[].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[].tls.rejectUnauthorized" from your kibana configs.`, - `Use "xpack.actions.customHostSettings[].tls.verificationMode" ` + + `Remove "xpack.actions.customHostSettings[].ssl.rejectUnauthorized" from your kibana configs.`, + `Use "xpack.actions.customHostSettings[].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 8ec94c4d4a552..37d461d6b2a50 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -23,6 +23,7 @@ const services = actionsMock.createServices(); const actionsClient = actionsClientMock.create(); const encryptedSavedObjectsClient = encryptedSavedObjectsMock.createClient(); const actionTypeRegistry = actionTypeRegistryMock.create(); +const eventLogger = eventLoggerMock.create(); const executeParams = { actionId: '1', @@ -42,7 +43,7 @@ actionExecutor.initialize({ getActionsClientWithRequest, actionTypeRegistry, encryptedSavedObjectsClient, - eventLogger: eventLoggerMock.create(), + eventLogger, preconfiguredActions: [], }); @@ -379,6 +380,50 @@ test('logs a warning when alert executor returns invalid status', async () => { ); }); +test('writes to event log for execute and execute start', async () => { + const executorMock = setupActionExecutorMock(); + executorMock.mockResolvedValue({ + actionId: '1', + status: 'ok', + }); + await actionExecutor.execute(executeParams); + expect(eventLogger.logEvent).toHaveBeenCalledTimes(2); + expect(eventLogger.logEvent.mock.calls[0][0]).toMatchObject({ + event: { + action: 'execute-start', + }, + kibana: { + saved_objects: [ + { + rel: 'primary', + type: 'action', + id: '1', + type_id: 'test', + namespace: 'some-namespace', + }, + ], + }, + message: 'action started: test:1: action-1', + }); + expect(eventLogger.logEvent.mock.calls[1][0]).toMatchObject({ + event: { + action: 'execute', + }, + kibana: { + saved_objects: [ + { + rel: 'primary', + type: 'action', + id: '1', + type_id: 'test', + namespace: 'some-namespace', + }, + ], + }, + message: 'action executed: test:1: action-1', + }); +}); + function setupActionExecutorMock() { const actionType: jest.Mocked = { id: 'test', diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index 0737e0ce3f071..e9e7b17288611 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -7,6 +7,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { Logger, KibanaRequest } from 'src/core/server'; +import { cloneDeep } from 'lodash'; import { withSpan } from '@kbn/apm-utils'; import { validateParams, validateConfig, validateSecrets } from './validate_with_schema'; import { @@ -22,6 +23,7 @@ import { EVENT_LOG_ACTIONS } from '../constants/event_log'; import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server'; import { ActionsClient } from '../actions_client'; import { ActionExecutionSource } from './action_execution_source'; +import { RelatedSavedObjects } from './related_saved_objects'; export interface ActionExecutorContext { logger: Logger; @@ -42,6 +44,7 @@ export interface ExecuteOptions { request: KibanaRequest; params: Record; source?: ActionExecutionSource; + relatedSavedObjects?: RelatedSavedObjects; } export type ActionExecutorContract = PublicMethodsOf; @@ -68,6 +71,7 @@ export class ActionExecutor { params, request, source, + relatedSavedObjects, }: ExecuteOptions): Promise> { if (!this.isInitialized) { throw new Error('ActionExecutor not initialized'); @@ -154,7 +158,28 @@ export class ActionExecutor { }, }; + for (const relatedSavedObject of relatedSavedObjects || []) { + event.kibana?.saved_objects?.push({ + rel: SAVED_OBJECT_REL_PRIMARY, + type: relatedSavedObject.type, + id: relatedSavedObject.id, + type_id: relatedSavedObject.typeId, + namespace: relatedSavedObject.namespace, + }); + } + eventLogger.startTiming(event); + + const startEvent = cloneDeep({ + ...event, + event: { + ...event.event, + action: EVENT_LOG_ACTIONS.executeStart, + }, + message: `action started: ${actionLabel}`, + }); + eventLogger.logEvent(startEvent); + let rawResult: ActionTypeExecutorResult; try { rawResult = await actionType.executor({ 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/related_saved_objects.test.ts b/x-pack/plugins/actions/server/lib/related_saved_objects.test.ts new file mode 100644 index 0000000000000..8fd13d1375697 --- /dev/null +++ b/x-pack/plugins/actions/server/lib/related_saved_objects.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { validatedRelatedSavedObjects } from './related_saved_objects'; +import { loggingSystemMock } from '../../../../../src/core/server/mocks'; +import { Logger } from '../../../../../src/core/server'; + +const loggerMock = loggingSystemMock.createLogger(); + +describe('related_saved_objects', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('validates valid objects', () => { + ensureValid(loggerMock, undefined); + ensureValid(loggerMock, []); + ensureValid(loggerMock, [ + { + id: 'some-id', + type: 'some-type', + }, + ]); + ensureValid(loggerMock, [ + { + id: 'some-id', + type: 'some-type', + typeId: 'some-type-id', + }, + ]); + ensureValid(loggerMock, [ + { + id: 'some-id', + type: 'some-type', + namespace: 'some-namespace', + }, + ]); + ensureValid(loggerMock, [ + { + id: 'some-id', + type: 'some-type', + typeId: 'some-type-id', + namespace: 'some-namespace', + }, + ]); + ensureValid(loggerMock, [ + { + id: 'some-id', + type: 'some-type', + }, + { + id: 'some-id-2', + type: 'some-type-2', + }, + ]); + }); +}); + +it('handles invalid objects', () => { + ensureInvalid(loggerMock, 42); + ensureInvalid(loggerMock, {}); + ensureInvalid(loggerMock, [{}]); + ensureInvalid(loggerMock, [{ id: 'some-id' }]); + ensureInvalid(loggerMock, [{ id: 42 }]); + ensureInvalid(loggerMock, [{ id: 'some-id', type: 'some-type', x: 42 }]); +}); + +function ensureValid(logger: Logger, savedObjects: unknown) { + const result = validatedRelatedSavedObjects(logger, savedObjects); + expect(result).toEqual(savedObjects === undefined ? [] : savedObjects); + expect(loggerMock.warn).not.toHaveBeenCalled(); +} + +function ensureInvalid(logger: Logger, savedObjects: unknown) { + const result = validatedRelatedSavedObjects(logger, savedObjects); + expect(result).toEqual([]); + + const message = loggerMock.warn.mock.calls[0][0]; + expect(message).toMatch( + /ignoring invalid related saved objects: expected value of type \[array\] but got/ + ); +} diff --git a/x-pack/plugins/actions/server/lib/related_saved_objects.ts b/x-pack/plugins/actions/server/lib/related_saved_objects.ts new file mode 100644 index 0000000000000..160587a3a9a8b --- /dev/null +++ b/x-pack/plugins/actions/server/lib/related_saved_objects.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 { schema, TypeOf } from '@kbn/config-schema'; +import { Logger } from '../../../../../src/core/server'; + +export type RelatedSavedObjects = TypeOf; + +const RelatedSavedObjectsSchema = schema.arrayOf( + schema.object({ + namespace: schema.maybe(schema.string({ minLength: 1 })), + id: schema.string({ minLength: 1 }), + type: schema.string({ minLength: 1 }), + // optional; for SO types like action/alert that have type id's + typeId: schema.maybe(schema.string({ minLength: 1 })), + }), + { defaultValue: [] } +); + +export function validatedRelatedSavedObjects(logger: Logger, data: unknown): RelatedSavedObjects { + try { + return RelatedSavedObjectsSchema.validate(data); + } catch (err) { + logger.warn(`ignoring invalid related saved objects: ${err.message}`); + return []; + } +} 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 229324c1f0df3..2292994e3ccfd 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 @@ -126,6 +126,7 @@ test('executes the task by calling the executor with proper parameters', async ( expect(mockedActionExecutor.execute).toHaveBeenCalledWith({ actionId: '2', params: { baz: true }, + relatedSavedObjects: [], request: expect.objectContaining({ headers: { // base64 encoded "123:abc" @@ -247,6 +248,7 @@ test('uses API key when provided', async () => { expect(mockedActionExecutor.execute).toHaveBeenCalledWith({ actionId: '2', params: { baz: true }, + relatedSavedObjects: [], request: expect.objectContaining({ headers: { // base64 encoded "123:abc" @@ -262,6 +264,79 @@ test('uses API key when provided', async () => { ); }); +test('uses relatedSavedObjects when provided', async () => { + const taskRunner = taskRunnerFactory.create({ + taskInstance: mockedTaskInstance, + }); + + mockedActionExecutor.execute.mockResolvedValueOnce({ status: 'ok', actionId: '2' }); + spaceIdToNamespace.mockReturnValueOnce('namespace-test'); + mockedEncryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ + id: '3', + type: 'action_task_params', + attributes: { + actionId: '2', + params: { baz: true }, + apiKey: Buffer.from('123:abc').toString('base64'), + relatedSavedObjects: [{ id: 'some-id', type: 'some-type' }], + }, + references: [], + }); + + await taskRunner.run(); + + expect(mockedActionExecutor.execute).toHaveBeenCalledWith({ + actionId: '2', + params: { baz: true }, + relatedSavedObjects: [ + { + id: 'some-id', + type: 'some-type', + }, + ], + request: expect.objectContaining({ + headers: { + // base64 encoded "123:abc" + authorization: 'ApiKey MTIzOmFiYw==', + }, + }), + }); +}); + +test('sanitizes invalid relatedSavedObjects when provided', async () => { + const taskRunner = taskRunnerFactory.create({ + taskInstance: mockedTaskInstance, + }); + + mockedActionExecutor.execute.mockResolvedValueOnce({ status: 'ok', actionId: '2' }); + spaceIdToNamespace.mockReturnValueOnce('namespace-test'); + mockedEncryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ + id: '3', + type: 'action_task_params', + attributes: { + actionId: '2', + params: { baz: true }, + apiKey: Buffer.from('123:abc').toString('base64'), + relatedSavedObjects: [{ Xid: 'some-id', type: 'some-type' }], + }, + references: [], + }); + + await taskRunner.run(); + + expect(mockedActionExecutor.execute).toHaveBeenCalledWith({ + actionId: '2', + params: { baz: true }, + relatedSavedObjects: [], + request: expect.objectContaining({ + headers: { + // base64 encoded "123:abc" + authorization: 'ApiKey MTIzOmFiYw==', + }, + }), + }); +}); + test(`doesn't use API key when not provided`, async () => { const factory = new TaskRunnerFactory(mockedActionExecutor); factory.initialize(taskRunnerFactoryInitializerParams); @@ -284,6 +359,7 @@ test(`doesn't use API key when not provided`, async () => { expect(mockedActionExecutor.execute).toHaveBeenCalledWith({ actionId: '2', params: { baz: true }, + relatedSavedObjects: [], request: expect.objectContaining({ headers: {}, }), 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 cf4b1576f2778..0515963ab82f4 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts @@ -30,6 +30,7 @@ import { } from '../types'; import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '../constants/saved_objects'; import { asSavedObjectExecutionSource } from './action_execution_source'; +import { validatedRelatedSavedObjects } from './related_saved_objects'; export interface TaskRunnerContext { logger: Logger; @@ -77,7 +78,7 @@ export class TaskRunnerFactory { const namespace = spaceIdToNamespace(spaceId); const { - attributes: { actionId, params, apiKey }, + attributes: { actionId, params, apiKey, relatedSavedObjects }, references, } = await encryptedSavedObjectsClient.getDecryptedAsInternalUser( ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, @@ -117,6 +118,7 @@ export class TaskRunnerFactory { actionId, request: fakeRequest, ...getSourceFromReferences(references), + relatedSavedObjects: validatedRelatedSavedObjects(logger, relatedSavedObjects), }); } catch (e) { if (e instanceof ActionTypeDisabledError) { diff --git a/x-pack/plugins/actions/server/routes/execute.test.ts b/x-pack/plugins/actions/server/routes/execute.test.ts index 4b12bf3111c1f..54e10698e5af9 100644 --- a/x-pack/plugins/actions/server/routes/execute.test.ts +++ b/x-pack/plugins/actions/server/routes/execute.test.ts @@ -65,6 +65,7 @@ describe('executeActionRoute', () => { someData: 'data', }, source: asHttpRequestExecutionSource(req), + relatedSavedObjects: [], }); expect(res.ok).toHaveBeenCalled(); @@ -101,6 +102,7 @@ describe('executeActionRoute', () => { expect(actionsClient.execute).toHaveBeenCalledWith({ actionId: '1', params: {}, + relatedSavedObjects: [], source: asHttpRequestExecutionSource(req), }); diff --git a/x-pack/plugins/actions/server/routes/execute.ts b/x-pack/plugins/actions/server/routes/execute.ts index 377fe1215b3fb..7e8110365e87a 100644 --- a/x-pack/plugins/actions/server/routes/execute.ts +++ b/x-pack/plugins/actions/server/routes/execute.ts @@ -53,6 +53,7 @@ export const executeActionRoute = ( params, actionId: id, source: asHttpRequestExecutionSource(req), + relatedSavedObjects: [], }); return body ? res.ok({ diff --git a/x-pack/plugins/actions/server/routes/legacy/execute.test.ts b/x-pack/plugins/actions/server/routes/legacy/execute.test.ts index 2ac53ddaaedf6..05b71819911a3 100644 --- a/x-pack/plugins/actions/server/routes/legacy/execute.test.ts +++ b/x-pack/plugins/actions/server/routes/legacy/execute.test.ts @@ -63,6 +63,7 @@ describe('executeActionRoute', () => { someData: 'data', }, source: asHttpRequestExecutionSource(req), + relatedSavedObjects: [], }); expect(res.ok).toHaveBeenCalled(); @@ -100,6 +101,7 @@ describe('executeActionRoute', () => { actionId: '1', params: {}, source: asHttpRequestExecutionSource(req), + relatedSavedObjects: [], }); expect(res.ok).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/actions/server/routes/legacy/execute.ts b/x-pack/plugins/actions/server/routes/legacy/execute.ts index f6ddec1d01c20..d7ed8d2e15604 100644 --- a/x-pack/plugins/actions/server/routes/legacy/execute.ts +++ b/x-pack/plugins/actions/server/routes/legacy/execute.ts @@ -48,6 +48,7 @@ export const executeActionRoute = ( params, actionId: id, source: asHttpRequestExecutionSource(req), + relatedSavedObjects: [], }); return body ? res.ok({ diff --git a/x-pack/plugins/actions/server/saved_objects/mappings.json b/x-pack/plugins/actions/server/saved_objects/mappings.json index c598b96ba2451..57f801ae9a075 100644 --- a/x-pack/plugins/actions/server/saved_objects/mappings.json +++ b/x-pack/plugins/actions/server/saved_objects/mappings.json @@ -35,6 +35,10 @@ }, "apiKey": { "type": "binary" + }, + "relatedSavedObjects": { + "enabled": false, + "type": "object" } } } 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 = Pick>; export type GetServicesFunction = (request: KibanaRequest) => Services; export type ActionTypeRegistryContract = PublicMethodsOf; @@ -142,7 +142,7 @@ export interface ProxySettings { proxyBypassHosts: Set | undefined; proxyOnlyHosts: Set | undefined; proxyHeaders?: Record; - 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['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/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index c81fa7927ef7d..53d888967c431 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts @@ -63,7 +63,7 @@ import { parseDuration } from '../../common/parse_duration'; import { retryIfConflicts } from '../lib/retry_if_conflicts'; import { partiallyUpdateAlert } from '../saved_objects'; import { markApiKeyForInvalidation } from '../invalidate_pending_api_keys/mark_api_key_for_invalidation'; -import { alertAuditEvent, AlertAuditAction } from './audit_events'; +import { ruleAuditEvent, RuleAuditAction } from './audit_events'; import { KueryNode, nodeBuilder } from '../../../../../src/plugins/data/common'; import { mapSortField } from './lib'; import { getAlertExecutionStatusPending } from '../lib/alert_execution_status'; @@ -253,8 +253,8 @@ export class AlertsClient { }); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.CREATE, + ruleAuditEvent({ + action: RuleAuditAction.CREATE, savedObject: { type: 'alert', id }, error, }) @@ -305,8 +305,8 @@ export class AlertsClient { }; this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.CREATE, + ruleAuditEvent({ + action: RuleAuditAction.CREATE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -375,8 +375,8 @@ export class AlertsClient { }); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.GET, + ruleAuditEvent({ + action: RuleAuditAction.GET, savedObject: { type: 'alert', id }, error, }) @@ -384,8 +384,8 @@ export class AlertsClient { throw error; } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.GET, + ruleAuditEvent({ + action: RuleAuditAction.GET, savedObject: { type: 'alert', id }, }) ); @@ -467,8 +467,8 @@ export class AlertsClient { ); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.FIND, + ruleAuditEvent({ + action: RuleAuditAction.FIND, error, }) ); @@ -508,8 +508,8 @@ export class AlertsClient { ); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.FIND, + ruleAuditEvent({ + action: RuleAuditAction.FIND, savedObject: { type: 'alert', id }, error, }) @@ -525,8 +525,8 @@ export class AlertsClient { authorizedData.forEach(({ id }) => this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.FIND, + ruleAuditEvent({ + action: RuleAuditAction.FIND, savedObject: { type: 'alert', id }, }) ) @@ -620,8 +620,8 @@ export class AlertsClient { }); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.DELETE, + ruleAuditEvent({ + action: RuleAuditAction.DELETE, savedObject: { type: 'alert', id }, error, }) @@ -630,8 +630,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.DELETE, + ruleAuditEvent({ + action: RuleAuditAction.DELETE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -694,8 +694,8 @@ export class AlertsClient { }); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UPDATE, + ruleAuditEvent({ + action: RuleAuditAction.UPDATE, savedObject: { type: 'alert', id }, error, }) @@ -704,8 +704,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UPDATE, + ruleAuditEvent({ + action: RuleAuditAction.UPDATE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -870,8 +870,8 @@ export class AlertsClient { } } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UPDATE_API_KEY, + ruleAuditEvent({ + action: RuleAuditAction.UPDATE_API_KEY, savedObject: { type: 'alert', id }, error, }) @@ -900,8 +900,8 @@ export class AlertsClient { }); this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UPDATE_API_KEY, + ruleAuditEvent({ + action: RuleAuditAction.UPDATE_API_KEY, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -976,8 +976,8 @@ export class AlertsClient { } } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.ENABLE, + ruleAuditEvent({ + action: RuleAuditAction.ENABLE, savedObject: { type: 'alert', id }, error, }) @@ -986,8 +986,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.ENABLE, + ruleAuditEvent({ + action: RuleAuditAction.ENABLE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -1090,8 +1090,8 @@ export class AlertsClient { }); } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.DISABLE, + ruleAuditEvent({ + action: RuleAuditAction.DISABLE, savedObject: { type: 'alert', id }, error, }) @@ -1100,8 +1100,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.DISABLE, + ruleAuditEvent({ + action: RuleAuditAction.DISABLE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -1167,8 +1167,8 @@ export class AlertsClient { } } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.MUTE, + ruleAuditEvent({ + action: RuleAuditAction.MUTE, savedObject: { type: 'alert', id }, error, }) @@ -1177,8 +1177,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.MUTE, + ruleAuditEvent({ + action: RuleAuditAction.MUTE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -1229,8 +1229,8 @@ export class AlertsClient { } } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UNMUTE, + ruleAuditEvent({ + action: RuleAuditAction.UNMUTE, savedObject: { type: 'alert', id }, error, }) @@ -1239,8 +1239,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UNMUTE, + ruleAuditEvent({ + action: RuleAuditAction.UNMUTE, outcome: 'unknown', savedObject: { type: 'alert', id }, }) @@ -1291,8 +1291,8 @@ export class AlertsClient { } } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.MUTE_INSTANCE, + ruleAuditEvent({ + action: RuleAuditAction.MUTE_ALERT, savedObject: { type: 'alert', id: alertId }, error, }) @@ -1301,8 +1301,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.MUTE_INSTANCE, + ruleAuditEvent({ + action: RuleAuditAction.MUTE_ALERT, outcome: 'unknown', savedObject: { type: 'alert', id: alertId }, }) @@ -1358,8 +1358,8 @@ export class AlertsClient { } } catch (error) { this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UNMUTE_INSTANCE, + ruleAuditEvent({ + action: RuleAuditAction.UNMUTE_ALERT, savedObject: { type: 'alert', id: alertId }, error, }) @@ -1368,8 +1368,8 @@ export class AlertsClient { } this.auditLogger?.log( - alertAuditEvent({ - action: AlertAuditAction.UNMUTE_INSTANCE, + ruleAuditEvent({ + action: RuleAuditAction.UNMUTE_ALERT, outcome: 'unknown', savedObject: { type: 'alert', id: alertId }, }) diff --git a/x-pack/plugins/alerting/server/alerts_client/audit_events.test.ts b/x-pack/plugins/alerting/server/alerts_client/audit_events.test.ts index 4ccb69832cd26..781b8fe1f4715 100644 --- a/x-pack/plugins/alerting/server/alerts_client/audit_events.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/audit_events.test.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { AlertAuditAction, alertAuditEvent } from './audit_events'; +import { RuleAuditAction, ruleAuditEvent } from './audit_events'; -describe('#alertAuditEvent', () => { +describe('#ruleAuditEvent', () => { test('creates event with `unknown` outcome', () => { expect( - alertAuditEvent({ - action: AlertAuditAction.CREATE, + ruleAuditEvent({ + action: RuleAuditAction.CREATE, outcome: 'unknown', savedObject: { type: 'alert', id: 'ALERT_ID' }, }) @@ -19,7 +19,7 @@ describe('#alertAuditEvent', () => { Object { "error": undefined, "event": Object { - "action": "alert_create", + "action": "rule_create", "category": Array [ "database", ], @@ -34,22 +34,22 @@ describe('#alertAuditEvent', () => { "type": "alert", }, }, - "message": "User is creating alert [id=ALERT_ID]", + "message": "User is creating rule [id=ALERT_ID]", } `); }); test('creates event with `success` outcome', () => { expect( - alertAuditEvent({ - action: AlertAuditAction.CREATE, + ruleAuditEvent({ + action: RuleAuditAction.CREATE, savedObject: { type: 'alert', id: 'ALERT_ID' }, }) ).toMatchInlineSnapshot(` Object { "error": undefined, "event": Object { - "action": "alert_create", + "action": "rule_create", "category": Array [ "database", ], @@ -64,15 +64,15 @@ describe('#alertAuditEvent', () => { "type": "alert", }, }, - "message": "User has created alert [id=ALERT_ID]", + "message": "User has created rule [id=ALERT_ID]", } `); }); test('creates event with `failure` outcome', () => { expect( - alertAuditEvent({ - action: AlertAuditAction.CREATE, + ruleAuditEvent({ + action: RuleAuditAction.CREATE, savedObject: { type: 'alert', id: 'ALERT_ID' }, error: new Error('ERROR_MESSAGE'), }) @@ -83,7 +83,7 @@ describe('#alertAuditEvent', () => { "message": "ERROR_MESSAGE", }, "event": Object { - "action": "alert_create", + "action": "rule_create", "category": Array [ "database", ], @@ -98,7 +98,7 @@ describe('#alertAuditEvent', () => { "type": "alert", }, }, - "message": "Failed attempt to create alert [id=ALERT_ID]", + "message": "Failed attempt to create rule [id=ALERT_ID]", } `); }); diff --git a/x-pack/plugins/alerting/server/alerts_client/audit_events.ts b/x-pack/plugins/alerting/server/alerts_client/audit_events.ts index 93cca255d6ebc..f04b7c3701974 100644 --- a/x-pack/plugins/alerting/server/alerts_client/audit_events.ts +++ b/x-pack/plugins/alerting/server/alerts_client/audit_events.ts @@ -8,67 +8,67 @@ import { EcsEventOutcome, EcsEventType } from 'src/core/server'; import { AuditEvent } from '../../../security/server'; -export enum AlertAuditAction { - CREATE = 'alert_create', - GET = 'alert_get', - UPDATE = 'alert_update', - UPDATE_API_KEY = 'alert_update_api_key', - ENABLE = 'alert_enable', - DISABLE = 'alert_disable', - DELETE = 'alert_delete', - FIND = 'alert_find', - MUTE = 'alert_mute', - UNMUTE = 'alert_unmute', - MUTE_INSTANCE = 'alert_instance_mute', - UNMUTE_INSTANCE = 'alert_instance_unmute', +export enum RuleAuditAction { + CREATE = 'rule_create', + GET = 'rule_get', + UPDATE = 'rule_update', + UPDATE_API_KEY = 'rule_update_api_key', + ENABLE = 'rule_enable', + DISABLE = 'rule_disable', + DELETE = 'rule_delete', + FIND = 'rule_find', + MUTE = 'rule_mute', + UNMUTE = 'rule_unmute', + MUTE_ALERT = 'rule_alert_mute', + UNMUTE_ALERT = 'rule_alert_unmute', } type VerbsTuple = [string, string, string]; -const eventVerbs: Record = { - alert_create: ['create', 'creating', 'created'], - alert_get: ['access', 'accessing', 'accessed'], - alert_update: ['update', 'updating', 'updated'], - alert_update_api_key: ['update API key of', 'updating API key of', 'updated API key of'], - alert_enable: ['enable', 'enabling', 'enabled'], - alert_disable: ['disable', 'disabling', 'disabled'], - alert_delete: ['delete', 'deleting', 'deleted'], - alert_find: ['access', 'accessing', 'accessed'], - alert_mute: ['mute', 'muting', 'muted'], - alert_unmute: ['unmute', 'unmuting', 'unmuted'], - alert_instance_mute: ['mute instance of', 'muting instance of', 'muted instance of'], - alert_instance_unmute: ['unmute instance of', 'unmuting instance of', 'unmuted instance of'], +const eventVerbs: Record = { + rule_create: ['create', 'creating', 'created'], + rule_get: ['access', 'accessing', 'accessed'], + rule_update: ['update', 'updating', 'updated'], + rule_update_api_key: ['update API key of', 'updating API key of', 'updated API key of'], + rule_enable: ['enable', 'enabling', 'enabled'], + rule_disable: ['disable', 'disabling', 'disabled'], + rule_delete: ['delete', 'deleting', 'deleted'], + rule_find: ['access', 'accessing', 'accessed'], + rule_mute: ['mute', 'muting', 'muted'], + rule_unmute: ['unmute', 'unmuting', 'unmuted'], + rule_alert_mute: ['mute alert of', 'muting alert of', 'muted alert of'], + rule_alert_unmute: ['unmute alert of', 'unmuting alert of', 'unmuted alert of'], }; -const eventTypes: Record = { - alert_create: 'creation', - alert_get: 'access', - alert_update: 'change', - alert_update_api_key: 'change', - alert_enable: 'change', - alert_disable: 'change', - alert_delete: 'deletion', - alert_find: 'access', - alert_mute: 'change', - alert_unmute: 'change', - alert_instance_mute: 'change', - alert_instance_unmute: 'change', +const eventTypes: Record = { + rule_create: 'creation', + rule_get: 'access', + rule_update: 'change', + rule_update_api_key: 'change', + rule_enable: 'change', + rule_disable: 'change', + rule_delete: 'deletion', + rule_find: 'access', + rule_mute: 'change', + rule_unmute: 'change', + rule_alert_mute: 'change', + rule_alert_unmute: 'change', }; -export interface AlertAuditEventParams { - action: AlertAuditAction; +export interface RuleAuditEventParams { + action: RuleAuditAction; outcome?: EcsEventOutcome; savedObject?: NonNullable['saved_object']; error?: Error; } -export function alertAuditEvent({ +export function ruleAuditEvent({ action, savedObject, outcome, error, -}: AlertAuditEventParams): AuditEvent { - const doc = savedObject ? `alert [id=${savedObject.id}]` : 'an alert'; +}: RuleAuditEventParams): AuditEvent { + const doc = savedObject ? `rule [id=${savedObject.id}]` : 'a rule'; const [present, progressive, past] = eventVerbs[action]; const message = error ? `Failed attempt to ${present} ${doc}` diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts index a2d5a5e0386c4..793357215d382 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts @@ -226,7 +226,7 @@ describe('create()', () => { }); describe('auditLogger', () => { - test('logs audit event when creating an alert', async () => { + test('logs audit event when creating a rule', async () => { const data = getMockData({ enabled: false, actions: [], @@ -241,7 +241,7 @@ describe('create()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_create', + action: 'rule_create', outcome: 'unknown', }), kibana: { saved_object: { id: 'mock-saved-object-id', type: 'alert' } }, @@ -249,7 +249,7 @@ describe('create()', () => { ); }); - test('logs audit event when not authorised to create an alert', async () => { + test('logs audit event when not authorised to create a rule', async () => { authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); await expect( @@ -263,7 +263,7 @@ describe('create()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_create', + action: 'rule_create', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts index 0f9d91d829854..ca0f0cf0fb5a6 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/delete.test.ts @@ -258,12 +258,12 @@ describe('delete()', () => { }); describe('auditLogger', () => { - test('logs audit event when deleting an alert', async () => { + test('logs audit event when deleting a rule', async () => { await alertsClient.delete({ id: '1' }); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_delete', + action: 'rule_delete', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -271,14 +271,14 @@ describe('delete()', () => { ); }); - test('logs audit event when not authorised to delete an alert', async () => { + test('logs audit event when not authorised to delete a rule', async () => { authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); await expect(alertsClient.delete({ id: '1' })).rejects.toThrow(); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_delete', + action: 'rule_delete', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts index 7eb107c2f4dec..da1c5ea8bfe8d 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/disable.test.ts @@ -126,12 +126,12 @@ describe('disable()', () => { }); describe('auditLogger', () => { - test('logs audit event when disabling an alert', async () => { + test('logs audit event when disabling a rule', async () => { await alertsClient.disable({ id: '1' }); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_disable', + action: 'rule_disable', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -139,14 +139,14 @@ describe('disable()', () => { ); }); - test('logs audit event when not authorised to disable an alert', async () => { + test('logs audit event when not authorised to disable a rule', async () => { authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); await expect(alertsClient.disable({ id: '1' })).rejects.toThrow(); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_disable', + action: 'rule_disable', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts index 8329e52d7444a..b3c8d3bd83980 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/enable.test.ts @@ -165,12 +165,12 @@ describe('enable()', () => { }); describe('auditLogger', () => { - test('logs audit event when enabling an alert', async () => { + test('logs audit event when enabling a rule', async () => { await alertsClient.enable({ id: '1' }); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_enable', + action: 'rule_enable', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -178,14 +178,14 @@ describe('enable()', () => { ); }); - test('logs audit event when not authorised to enable an alert', async () => { + test('logs audit event when not authorised to enable a rule', async () => { authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); await expect(alertsClient.enable({ id: '1' })).rejects.toThrow(); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_enable', + action: 'rule_enable', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts index 8fa8ae7ae38b0..fe788cd43bc2b 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/find.test.ts @@ -277,13 +277,13 @@ describe('find()', () => { }); describe('auditLogger', () => { - test('logs audit event when searching alerts', async () => { + test('logs audit event when searching rules', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); await alertsClient.find(); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_find', + action: 'rule_find', outcome: 'success', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -291,7 +291,7 @@ describe('find()', () => { ); }); - test('logs audit event when not authorised to search alerts', async () => { + test('logs audit event when not authorised to search rules', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); authorization.getFindAuthorizationFilter.mockRejectedValue(new Error('Unauthorized')); @@ -299,7 +299,7 @@ describe('find()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_find', + action: 'rule_find', outcome: 'failure', }), error: { @@ -310,7 +310,7 @@ describe('find()', () => { ); }); - test('logs audit event when not authorised to search alert type', async () => { + test('logs audit event when not authorised to search rule type', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); authorization.getFindAuthorizationFilter.mockResolvedValue({ ensureRuleTypeIsAuthorized: jest.fn(() => { @@ -323,7 +323,7 @@ describe('find()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_find', + action: 'rule_find', outcome: 'failure', }), kibana: { saved_object: { id: '1', type: 'alert' } }, diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts index a958ea4061ae5..1be9d3e3ba2c9 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/get.test.ts @@ -226,13 +226,13 @@ describe('get()', () => { }); }); - test('logs audit event when getting an alert', async () => { + test('logs audit event when getting a rule', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); await alertsClient.get({ id: '1' }); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_get', + action: 'rule_get', outcome: 'success', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -240,7 +240,7 @@ describe('get()', () => { ); }); - test('logs audit event when not authorised to get an alert', async () => { + test('logs audit event when not authorised to get a rule', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); @@ -248,7 +248,7 @@ describe('get()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_get', + action: 'rule_get', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts index 6734ec9b99600..43f43b539ebf2 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/mute_all.test.ts @@ -155,7 +155,7 @@ describe('muteAll()', () => { }); describe('auditLogger', () => { - test('logs audit event when muting an alert', async () => { + test('logs audit event when muting a rule', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -181,7 +181,7 @@ describe('muteAll()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_mute', + action: 'rule_mute', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -189,7 +189,7 @@ describe('muteAll()', () => { ); }); - test('logs audit event when not authorised to mute an alert', async () => { + test('logs audit event when not authorised to mute a rule', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -217,7 +217,7 @@ describe('muteAll()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_mute', + action: 'rule_mute', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts index bc0b7288e952f..e2e4aff61866b 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/mute_instance.test.ts @@ -189,7 +189,7 @@ describe('muteInstance()', () => { }); describe('auditLogger', () => { - test('logs audit event when muting an alert instance', async () => { + test('logs audit event when muting an alert', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -209,7 +209,7 @@ describe('muteInstance()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_instance_mute', + action: 'rule_alert_mute', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -217,7 +217,7 @@ describe('muteInstance()', () => { ); }); - test('logs audit event when not authorised to mute an alert instance', async () => { + test('logs audit event when not authorised to mute an alert', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -241,7 +241,7 @@ describe('muteInstance()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_instance_mute', + action: 'rule_alert_mute', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts index c061bc7840fb6..02439d3cd6bad 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_all.test.ts @@ -155,7 +155,7 @@ describe('unmuteAll()', () => { }); describe('auditLogger', () => { - test('logs audit event when unmuting an alert', async () => { + test('logs audit event when unmuting a rule', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -181,7 +181,7 @@ describe('unmuteAll()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_unmute', + action: 'rule_unmute', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -189,7 +189,7 @@ describe('unmuteAll()', () => { ); }); - test('logs audit event when not authorised to unmute an alert', async () => { + test('logs audit event when not authorised to unmute a rule', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -217,7 +217,7 @@ describe('unmuteAll()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_unmute', + action: 'rule_unmute', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts index 4da83b6441a8d..3f3ec697a9478 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/unmute_instance.test.ts @@ -187,7 +187,7 @@ describe('unmuteInstance()', () => { }); describe('auditLogger', () => { - test('logs audit event when unmuting an alert instance', async () => { + test('logs audit event when unmuting an alert', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -207,7 +207,7 @@ describe('unmuteInstance()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_instance_unmute', + action: 'rule_alert_unmute', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -215,7 +215,7 @@ describe('unmuteInstance()', () => { ); }); - test('logs audit event when not authorised to unmute an alert instance', async () => { + test('logs audit event when not authorised to unmute an alert', async () => { const alertsClient = new AlertsClient({ ...alertsClientParams, auditLogger }); unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -239,7 +239,7 @@ describe('unmuteInstance()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_instance_unmute', + action: 'rule_alert_unmute', outcome: 'failure', }), kibana: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts index c743312ef2c4b..350c9ed31298f 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts @@ -1476,7 +1476,7 @@ describe('update()', () => { }); }); - test('logs audit event when updating an alert', async () => { + test('logs audit event when updating a rule', async () => { await alertsClient.update({ id: '1', data: { @@ -1495,7 +1495,7 @@ describe('update()', () => { expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_update', + action: 'rule_update', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -1503,7 +1503,7 @@ describe('update()', () => { ); }); - test('logs audit event when not authorised to update an alert', async () => { + test('logs audit event when not authorised to update a rule', async () => { authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); await expect( @@ -1526,7 +1526,7 @@ describe('update()', () => { expect.objectContaining({ event: expect.objectContaining({ outcome: 'failure', - action: 'alert_update', + action: 'rule_update', }), kibana: { saved_object: { diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts index 4215f14b4a560..15aa0dbc64eb8 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/update_api_key.test.ts @@ -295,13 +295,13 @@ describe('updateApiKey()', () => { }); describe('auditLogger', () => { - test('logs audit event when updating the API key of an alert', async () => { + test('logs audit event when updating the API key of a rule', async () => { await alertsClient.updateApiKey({ id: '1' }); expect(auditLogger.log).toHaveBeenCalledWith( expect.objectContaining({ event: expect.objectContaining({ - action: 'alert_update_api_key', + action: 'rule_update_api_key', outcome: 'unknown', }), kibana: { saved_object: { id: '1', type: 'alert' } }, @@ -309,7 +309,7 @@ describe('updateApiKey()', () => { ); }); - test('logs audit event when not authorised to update the API key of an alert', async () => { + test('logs audit event when not authorised to update the API key of a rule', async () => { authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); await expect(alertsClient.updateApiKey({ id: '1' })).rejects.toThrow(); @@ -317,7 +317,7 @@ describe('updateApiKey()', () => { expect.objectContaining({ event: expect.objectContaining({ outcome: 'failure', - action: 'alert_update_api_key', + action: 'rule_update_api_key', }), kibana: { saved_object: { 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 25f0656163f5d..033ffcceb6a0a 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 @@ -135,6 +135,14 @@ test('enqueues execution per selected action', async () => { "foo": true, "stateVal": "My goes here", }, + "relatedSavedObjects": Array [ + Object { + "id": "1", + "namespace": "test1", + "type": "alert", + "typeId": "test", + }, + ], "source": Object { "source": Object { "id": "1", @@ -247,6 +255,14 @@ test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () => id: '1', type: 'alert', }), + relatedSavedObjects: [ + { + id: '1', + namespace: 'test1', + type: 'alert', + typeId: 'test', + }, + ], spaceId: 'test1', apiKey: createExecutionHandlerParams.apiKey, }); @@ -327,6 +343,14 @@ test('context attribute gets parameterized', async () => { "foo": true, "stateVal": "My goes here", }, + "relatedSavedObjects": Array [ + Object { + "id": "1", + "namespace": "test1", + "type": "alert", + "typeId": "test", + }, + ], "source": Object { "source": Object { "id": "1", @@ -360,6 +384,14 @@ test('state attribute gets parameterized', async () => { "foo": true, "stateVal": "My state-val goes here", }, + "relatedSavedObjects": Array [ + Object { + "id": "1", + "namespace": "test1", + "type": "alert", + "typeId": "test", + }, + ], "source": Object { "source": Object { "id": "1", 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 c3a36297c217a..968fff540dc03 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 @@ -157,6 +157,8 @@ export function createExecutionHandler< continue; } + const namespace = spaceId === 'default' ? {} : { namespace: spaceId }; + // TODO would be nice to add the action name here, but it's not available const actionLabel = `${action.actionTypeId}:${action.id}`; const actionsClient = await actionsPlugin.getActionsClientWithRequest(request); @@ -169,10 +171,16 @@ export function createExecutionHandler< id: alertId, type: 'alert', }), + relatedSavedObjects: [ + { + id: alertId, + type: 'alert', + namespace: namespace.namespace, + typeId: alertType.id, + }, + ], }); - const namespace = spaceId === 'default' ? {} : { namespace: spaceId }; - const event: IEvent = { event: { action: EVENT_LOG_ACTIONS.executeAction, 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 39a45584631d2..8ab267a5610d3 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 @@ -352,6 +352,14 @@ describe('Task Runner', () => { "params": Object { "foo": true, }, + "relatedSavedObjects": Array [ + Object { + "id": "1", + "namespace": undefined, + "type": "alert", + "typeId": "test", + }, + ], "source": Object { "source": Object { "id": "1", @@ -1098,6 +1106,14 @@ describe('Task Runner', () => { "params": Object { "foo": true, }, + "relatedSavedObjects": Array [ + Object { + "id": "1", + "namespace": undefined, + "type": "alert", + "typeId": "test", + }, + ], "source": Object { "source": Object { "id": "1", @@ -1634,6 +1650,14 @@ describe('Task Runner', () => { "params": Object { "isResolved": true, }, + "relatedSavedObjects": Array [ + Object { + "id": "1", + "namespace": undefined, + "type": "alert", + "typeId": "test", + }, + ], "source": Object { "source": Object { "id": "1", @@ -1826,6 +1850,14 @@ describe('Task Runner', () => { "params": Object { "isResolved": true, }, + "relatedSavedObjects": Array [ + Object { + "id": "1", + "namespace": undefined, + "type": "alert", + "typeId": "test", + }, + ], "source": Object { "source": Object { "id": "1", diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index e33c410668c25..21aef379715c7 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -43,4 +43,4 @@ "ml", "observability" ] -} +} \ No newline at end of file diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx index da55f274bd77c..11926dd965f95 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx @@ -85,7 +85,7 @@ export function DetailView({ errorGroup, urlParams }: Props) { const status = error.http?.response?.status_code; return ( - +

    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 ( + + + + + + + + ); + }, + ], +}; + +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 ; +} + +export function EmptyState() { + return ( + + ); +} 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( @@ -84,7 +85,7 @@ export function ErrorDistribution({ distribution, title }: Props) { }; return ( -
    + <> {title} @@ -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) {
    -

    + ); } diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx index 0f2180721afe3..3d22c3863c100 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx @@ -146,9 +146,13 @@ export function ErrorGroupDetails({ return ( <> + + - + + + {showDetails && ( 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/error_group_overview/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx index 95ec80b1a51bc..886ef8412f35b 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx @@ -73,7 +73,7 @@ export function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) { return ( - + - +

    {i18n.translate( 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/shared/apm_header_action_menu/alerting_popover_flyout.tsx b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx index 5b4f4e24af44d..ca73f6ddd05b3 100644 --- a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx +++ b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx @@ -18,7 +18,7 @@ import { AlertType } from '../../../../common/alert_types'; import { AlertingFlyout } from '../../alerting/alerting_flyout'; const alertLabel = i18n.translate('xpack.apm.home.alertsMenu.alerts', { - defaultMessage: 'Alerts', + defaultMessage: 'Alerts and rules', }); const transactionDurationLabel = i18n.translate( 'xpack.apm.home.alertsMenu.transactionDuration', @@ -33,11 +33,11 @@ const errorCountLabel = i18n.translate('xpack.apm.home.alertsMenu.errorCount', { }); const createThresholdAlertLabel = i18n.translate( 'xpack.apm.home.alertsMenu.createThresholdAlert', - { defaultMessage: 'Create threshold alert' } + { defaultMessage: 'Create threshold rule' } ); const createAnomalyAlertAlertLabel = i18n.translate( 'xpack.apm.home.alertsMenu.createAnomalyAlert', - { defaultMessage: 'Create anomaly alert' } + { defaultMessage: 'Create anomaly rule' } ); const CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID = @@ -102,7 +102,7 @@ export function AlertingPopoverAndFlyout({ { name: i18n.translate( 'xpack.apm.home.alertsMenu.viewActiveAlerts', - { defaultMessage: 'View active alerts' } + { defaultMessage: 'Manage rules' } ), href: basePath.prepend( '/app/management/insightsAndAlerting/triggersActions/alerts' diff --git a/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts b/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts index 35c7f0dfdfd73..c122a5c406eab 100644 --- a/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts +++ b/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts @@ -12,6 +12,7 @@ import { APMPluginStartDependencies } from '../../types'; import { ExternalCallback } from '../../../../fleet/server'; import { AGENT_NAME } from '../../../common/elasticsearch_fieldnames'; import { AgentConfiguration } from '../../../common/agent_configuration/configuration_types'; +import { getPackagePolicyWithSourceMap, listArtifacts } from './source_maps'; export async function registerFleetPolicyCallbacks({ plugins, @@ -31,7 +32,7 @@ export async function registerFleetPolicyCallbacks({ // Registers a callback invoked when a policy is created to populate the APM // integration policy with pre-existing agent configurations - registerAgentConfigExternalCallback({ + registerPackagePolicyExternalCallback({ fleetPluginStart, callbackName: 'packagePolicyCreate', plugins, @@ -42,7 +43,7 @@ export async function registerFleetPolicyCallbacks({ // Registers a callback invoked when a policy is updated to populate the APM // integration policy with existing agent configurations - registerAgentConfigExternalCallback({ + registerPackagePolicyExternalCallback({ fleetPluginStart, callbackName: 'packagePolicyUpdate', plugins, @@ -53,11 +54,11 @@ export async function registerFleetPolicyCallbacks({ } type ExternalCallbackParams = Parameters; -type PackagePolicy = ExternalCallbackParams[0]; +export type PackagePolicy = ExternalCallbackParams[0]; type Context = ExternalCallbackParams[1]; type Request = ExternalCallbackParams[2]; -function registerAgentConfigExternalCallback({ +function registerPackagePolicyExternalCallback({ fleetPluginStart, callbackName, plugins, @@ -91,8 +92,9 @@ function registerAgentConfigExternalCallback({ ruleDataClient, }); const agentConfigurations = await listConfigurations({ setup }); + const artifacts = await listArtifacts({ fleetPluginStart }); return getPackagePolicyWithAgentConfigurations( - packagePolicy, + getPackagePolicyWithSourceMap({ packagePolicy, artifacts }), agentConfigurations ); }; @@ -100,7 +102,7 @@ function registerAgentConfigExternalCallback({ fleetPluginStart.registerExternalCallback(callbackName, callbackFn); } -const APM_SERVER = 'apm-server'; +export const APM_SERVER = 'apm-server'; // Immutable function applies the given package policy with a set of agent configurations export function getPackagePolicyWithAgentConfigurations( 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 new file mode 100644 index 0000000000000..61a4fa4436e69 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/fleet/source_maps.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 { + ArtifactSourceMap, + getPackagePolicyWithSourceMap, +} from './source_maps'; + +const packagePolicy = { + id: '123', + version: 'WzMxNDI2LDFd', + name: 'apm-1', + description: '', + namespace: 'default', + policy_id: '7a87c160-c961-11eb-81e2-f7327d61c92a', + enabled: true, + output_id: '', + inputs: [ + { + policy_template: 'apmserver', + streams: [], + vars: {}, + type: 'apm', + enabled: true, + compiled_input: { + 'apm-server': { + capture_personal_data: true, + max_event_size: 307200, + api_key: { limit: 100, enabled: false }, + default_service_environment: null, + host: 'localhost:8200', + kibana: { api_key: null }, + secret_token: null, + }, + }, + }, + ], + package: { name: 'apm', title: 'Elastic APM', version: '0.2.0' }, + created_at: '2021-06-16T14:54:32.195Z', + created_by: 'elastic', +}; + +const artifacts = [ + { + type: 'sourcemap', + identifier: 'service_name-1.0.0', + relative_url: '/api/fleet/artifacts/service_name-1.0.0/my-id-1', + body: { + serviceName: 'service_name', + serviceVersion: '1.0.0', + bundleFilepath: 'http://localhost:3000/static/js/main.chunk.js', + sourceMap: { + version: 3, + file: 'static/js/main.chunk.js', + sources: ['foo'], + sourcesContent: ['foo'], + mappings: 'foo', + sourceRoot: '', + }, + }, + created: '2021-06-16T15:03:55.049Z', + id: 'apm:service_name-1.0.0-my-id-1', + compressionAlgorithm: 'zlib', + decodedSha256: 'my-id-1', + decodedSize: 9440, + encodedSha256: 'sha123', + encodedSize: 2622, + encryptionAlgorithm: 'none', + packageName: 'apm', + }, + { + type: 'sourcemap', + identifier: 'service_name-2.0.0', + relative_url: '/api/fleet/artifacts/service_name-2.0.0/my-id-2', + body: { + serviceName: 'service_name', + serviceVersion: '2.0.0', + bundleFilepath: 'http://localhost:3000/static/js/main.chunk.js', + sourceMap: { + version: 3, + file: 'static/js/main.chunk.js', + sources: ['foo'], + sourcesContent: ['foo'], + mappings: 'foo', + sourceRoot: '', + }, + }, + created: '2021-06-16T15:03:55.049Z', + id: 'apm:service_name-2.0.0-my-id-2', + compressionAlgorithm: 'zlib', + decodedSha256: 'my-id-2', + decodedSize: 9440, + encodedSha256: 'sha456', + encodedSize: 2622, + encryptionAlgorithm: 'none', + packageName: 'apm', + }, +] as ArtifactSourceMap[]; + +describe('Source maps', () => { + describe('getPackagePolicyWithSourceMap', () => { + it('returns unchanged package policy when artifacts is empty', () => { + const updatedPackagePolicy = getPackagePolicyWithSourceMap({ + packagePolicy, + artifacts: [], + }); + expect(updatedPackagePolicy).toEqual(packagePolicy); + }); + it('adds source maps into the package policy', () => { + const updatedPackagePolicy = getPackagePolicyWithSourceMap({ + packagePolicy, + artifacts, + }); + expect(updatedPackagePolicy.inputs[0].config).toEqual({ + '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', + }, + ], + }, + }, + }, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/fleet/source_maps.ts b/x-pack/plugins/apm/server/lib/fleet/source_maps.ts new file mode 100644 index 0000000000000..b313fbad2806f --- /dev/null +++ b/x-pack/plugins/apm/server/lib/fleet/source_maps.ts @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 t from 'io-ts'; +import { + CoreSetup, + CoreStart, + ElasticsearchClient, + SavedObjectsClientContract, +} from 'kibana/server'; +import { promisify } from 'util'; +import { unzip } from 'zlib'; +import { Artifact } from '../../../../fleet/server'; +import { sourceMapRt } from '../../routes/source_maps'; +import { APMPluginStartDependencies } from '../../types'; +import { getApmPackgePolicies } from './get_apm_package_policies'; +import { APM_SERVER, PackagePolicy } from './register_fleet_policy_callbacks'; + +export interface ApmArtifactBody { + serviceName: string; + serviceVersion: string; + bundleFilepath: string; + sourceMap: t.TypeOf; +} +export type ArtifactSourceMap = Omit & { + body: ApmArtifactBody; +}; + +export type FleetPluginStart = NonNullable; + +const doUnzip = promisify(unzip); + +function decodeArtifacts(artifacts: Artifact[]): Promise { + return Promise.all( + artifacts.map(async (artifact) => { + const body = await doUnzip(Buffer.from(artifact.body, 'base64')); + return { + ...artifact, + body: JSON.parse(body.toString()) as ApmArtifactBody, + }; + }) + ); +} + +function getApmArtifactClient(fleetPluginStart: FleetPluginStart) { + return fleetPluginStart.createArtifactsClient('apm'); +} + +export async function listArtifacts({ + fleetPluginStart, +}: { + fleetPluginStart: FleetPluginStart; +}) { + const apmArtifactClient = getApmArtifactClient(fleetPluginStart); + const artifacts = await apmArtifactClient.listArtifacts({ + kuery: 'type: sourcemap', + }); + + return decodeArtifacts(artifacts.items); +} + +export async function createApmArtifact({ + apmArtifactBody, + fleetPluginStart, +}: { + apmArtifactBody: ApmArtifactBody; + fleetPluginStart: FleetPluginStart; +}) { + const apmArtifactClient = getApmArtifactClient(fleetPluginStart); + const identifier = `${apmArtifactBody.serviceName}-${apmArtifactBody.serviceVersion}`; + + return apmArtifactClient.createArtifact({ + type: 'sourcemap', + identifier, + content: JSON.stringify(apmArtifactBody), + }); +} + +export async function deleteApmArtifact({ + id, + fleetPluginStart, +}: { + id: string; + fleetPluginStart: FleetPluginStart; +}) { + const apmArtifactClient = getApmArtifactClient(fleetPluginStart); + return apmArtifactClient.deleteArtifact(id); +} + +export function getPackagePolicyWithSourceMap({ + packagePolicy, + artifacts, +}: { + packagePolicy: PackagePolicy; + artifacts: ArtifactSourceMap[]; +}) { + if (!artifacts.length) { + return packagePolicy; + } + const [firstInput, ...restInputs] = packagePolicy.inputs; + return { + ...packagePolicy, + inputs: [ + { + ...firstInput, + config: { + ...firstInput.config, + [APM_SERVER]: { + value: { + ...firstInput?.config?.[APM_SERVER].value, + rum: { + source_mapping: { + metadata: artifacts.map((artifact) => ({ + 'service.name': artifact.body.serviceName, + 'service.version': artifact.body.serviceVersion, + 'bundle.filepath': artifact.body.bundleFilepath, + 'sourcemap.url': artifact.relative_url, + })), + }, + }, + }, + }, + }, + }, + ...restInputs, + ], + }; +} + +export async function updateSourceMapsOnFleetPolicies({ + core, + fleetPluginStart, + savedObjectsClient, + elasticsearchClient, +}: { + core: { setup: CoreSetup; start: () => Promise }; + fleetPluginStart: FleetPluginStart; + savedObjectsClient: SavedObjectsClientContract; + elasticsearchClient: ElasticsearchClient; +}) { + const artifacts = await listArtifacts({ fleetPluginStart }); + + const apmFleetPolicies = await getApmPackgePolicies({ + core, + fleetPluginStart, + }); + + return Promise.all( + apmFleetPolicies.items.map(async (item) => { + const { + id, + revision, + updated_at: updatedAt, + updated_by: updatedBy, + ...packagePolicy + } = item; + + const updatedPackagePolicy = getPackagePolicyWithSourceMap({ + packagePolicy, + artifacts, + }); + + await fleetPluginStart.packagePolicyService.update( + savedObjectsClient, + elasticsearchClient, + id, + updatedPackagePolicy + ); + }) + ); +} diff --git a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts index c151752b4b6e0..f1c08444d2e1e 100644 --- a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts +++ b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts @@ -26,6 +26,7 @@ import { agentConfigurationRouteRepository } from './settings/agent_configuratio import { anomalyDetectionRouteRepository } from './settings/anomaly_detection'; import { apmIndicesRouteRepository } from './settings/apm_indices'; import { customLinkRouteRepository } from './settings/custom_link'; +import { sourceMapsRouteRepository } from './source_maps'; import { traceRouteRepository } from './traces'; import { transactionRouteRepository } from './transactions'; import { APMRouteHandlerResources } from './typings'; @@ -48,7 +49,8 @@ const getTypedGlobalApmServerRouteRepository = () => { .merge(agentConfigurationRouteRepository) .merge(anomalyDetectionRouteRepository) .merge(apmIndicesRouteRepository) - .merge(customLinkRouteRepository); + .merge(customLinkRouteRepository) + .merge(sourceMapsRouteRepository); return repository; }; diff --git a/x-pack/plugins/apm/server/routes/source_maps.ts b/x-pack/plugins/apm/server/routes/source_maps.ts new file mode 100644 index 0000000000000..24ea825774b0a --- /dev/null +++ b/x-pack/plugins/apm/server/routes/source_maps.ts @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import Boom from '@hapi/boom'; +import * as t from 'io-ts'; +import { SavedObjectsClientContract } from 'kibana/server'; +import { + createApmArtifact, + deleteApmArtifact, + listArtifacts, + updateSourceMapsOnFleetPolicies, +} from '../lib/fleet/source_maps'; +import { getInternalSavedObjectsClient } from '../lib/helpers/get_internal_saved_objects_client'; +import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; + +export const sourceMapRt = t.intersection([ + t.type({ + version: t.number, + sources: t.array(t.string), + mappings: t.string, + }), + t.partial({ + names: t.array(t.string), + file: t.string, + sourceRoot: t.string, + sourcesContent: t.array(t.string), + }), +]); + +const listSourceMapRoute = createApmServerRoute({ + endpoint: 'GET /api/apm/sourcemaps', + options: { tags: ['access:apm'] }, + handler: async ({ plugins, logger }) => { + try { + const fleetPluginStart = await plugins.fleet?.start(); + if (fleetPluginStart) { + const artifacts = await listArtifacts({ fleetPluginStart }); + return { artifacts }; + } + } catch (e) { + throw Boom.internal( + 'Something went wrong while fetching artifacts source maps', + e + ); + } + }, +}); + +const uploadSourceMapRoute = createApmServerRoute({ + endpoint: 'POST /api/apm/sourcemaps/{serviceName}/{serviceVersion}', + options: { tags: ['access:apm', 'access:apm_write'] }, + params: t.type({ + path: t.type({ + serviceName: t.string, + serviceVersion: t.string, + }), + body: t.type({ + bundleFilepath: t.string, + sourceMap: sourceMapRt, + }), + }), + handler: async ({ params, plugins, core }) => { + const { serviceName, serviceVersion } = params.path; + const { bundleFilepath, sourceMap } = params.body; + const fleetPluginStart = await plugins.fleet?.start(); + const coreStart = await core.start(); + const esClient = coreStart.elasticsearch.client.asInternalUser; + const savedObjectsClient = await getInternalSavedObjectsClient(core.setup); + try { + if (fleetPluginStart) { + const artifact = await createApmArtifact({ + fleetPluginStart, + apmArtifactBody: { + serviceName, + serviceVersion, + bundleFilepath, + sourceMap, + }, + }); + await updateSourceMapsOnFleetPolicies({ + core, + fleetPluginStart, + savedObjectsClient: (savedObjectsClient as unknown) as SavedObjectsClientContract, + elasticsearchClient: esClient, + }); + + return artifact; + } + } catch (e) { + throw Boom.internal( + 'Something went wrong while creating a new source map', + e + ); + } + }, +}); + +const deleteSourceMapRoute = createApmServerRoute({ + endpoint: 'DELETE /api/apm/sourcemaps/{id}', + options: { tags: ['access:apm', 'access:apm_write'] }, + params: t.type({ + path: t.type({ + id: t.string, + }), + }), + handler: async ({ context, params, plugins, core }) => { + const fleetPluginStart = await plugins.fleet?.start(); + const { id } = params.path; + const coreStart = await core.start(); + const esClient = coreStart.elasticsearch.client.asInternalUser; + const savedObjectsClient = await getInternalSavedObjectsClient(core.setup); + try { + if (fleetPluginStart) { + await deleteApmArtifact({ id, fleetPluginStart }); + await updateSourceMapsOnFleetPolicies({ + core, + fleetPluginStart, + savedObjectsClient: (savedObjectsClient as unknown) as SavedObjectsClientContract, + elasticsearchClient: esClient, + }); + } + } catch (e) { + throw Boom.internal( + `Something went wrong while deleting source map. id: ${id}`, + e + ); + } + }, +}); + +export const sourceMapsRouteRepository = createApmServerRouteRepository() + .add(listSourceMapRoute) + .add(uploadSourceMapRoute) + .add(deleteSourceMapRoute); 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 = ( {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/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx index 97b5e592552ed..fbcba9e56aef5 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx @@ -45,9 +45,12 @@ export const dropdownFilter: RendererFactory = () => ({ reuseDomNode: true, height: 50, render(domNode, config, handlers) { - const filterExpression = handlers.getFilter(); + let filterExpression = handlers.getFilter(); - if (filterExpression !== '') { + if (filterExpression === undefined || filterExpression.indexOf('exactly')) { + filterExpression = ''; + handlers.setFilter(filterExpression); + } else if (filterExpression !== '') { // NOTE: setFilter() will cause a data refresh, avoid calling unless required // compare expression and filter, update filter if needed const { changed, newAst } = syncFilterExpression(config, filterExpression, ['filterGroup']); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx index ff781bb294db4..02a36b80fa364 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx @@ -19,6 +19,8 @@ import { RendererFactory } from '../../../../types'; const { timeFilter: strings } = RendererStrings; +const defaultTimeFilterExpression = 'timefilter column=@timestamp from=now-24h to=now'; + export const timeFilterFactory: StartInitializer> = (core, plugins) => { const { uiSettings } = core; @@ -38,9 +40,12 @@ export const timeFilterFactory: StartInitializer> = ( help: strings.getHelpDescription(), reuseDomNode: true, // must be true, otherwise popovers don't work render: async (domNode: HTMLElement, config: Arguments, handlers: RendererHandlers) => { - const filterExpression = handlers.getFilter(); + let filterExpression = handlers.getFilter(); - if (filterExpression !== '') { + if (filterExpression === undefined || filterExpression.indexOf('timefilter') !== 0) { + filterExpression = defaultTimeFilterExpression; + handlers.setFilter(filterExpression); + } else if (filterExpression !== '') { // NOTE: setFilter() will cause a data refresh, avoid calling unless required // compare expression and filter, update filter if needed const { changed, newAst } = syncFilterExpression(config, filterExpression, [ diff --git a/x-pack/plugins/canvas/i18n/components.ts b/x-pack/plugins/canvas/i18n/components.ts deleted file mode 100644 index 7a23137e7ef60..0000000000000 --- a/x-pack/plugins/canvas/i18n/components.ts +++ /dev/null @@ -1,1764 +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.', - }), - }, - WorkpadCreate: { - getWorkpadCreateButtonLabel: () => - i18n.translate('xpack.canvas.workpadCreate.createButtonLabel', { - defaultMessage: 'Create workpad', - }), - }, - 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', - }), - }, - WorkpadLoader: { - getClonedWorkpadName: (workpadName: string) => - i18n.translate('xpack.canvas.workpadLoader.clonedWorkpadName', { - defaultMessage: 'Copy of {workpadName}', - values: { - workpadName, - }, - description: - 'This suffix is added to the end of the name of a cloned workpad to indicate that this ' + - 'new workpad is a copy of the original workpad. Example: "Copy of Sales Pitch"', - }), - getCloneToolTip: () => - i18n.translate('xpack.canvas.workpadLoader.cloneTooltip', { - defaultMessage: 'Clone workpad', - }), - getCreateWorkpadLoadingDescription: () => - i18n.translate('xpack.canvas.workpadLoader.createWorkpadLoadingDescription', { - defaultMessage: 'Creating workpad...', - description: - 'This message appears while the user is waiting for a new workpad to be created', - }), - getDeleteButtonAriaLabel: (numberOfWorkpads: number) => - i18n.translate('xpack.canvas.workpadLoader.deleteButtonAriaLabel', { - defaultMessage: 'Delete {numberOfWorkpads} workpads', - values: { - numberOfWorkpads, - }, - }), - getDeleteButtonLabel: (numberOfWorkpads: number) => - i18n.translate('xpack.canvas.workpadLoader.deleteButtonLabel', { - defaultMessage: 'Delete ({numberOfWorkpads})', - values: { - numberOfWorkpads, - }, - }), - getDeleteModalConfirmButtonLabel: () => - i18n.translate('xpack.canvas.workpadLoader.deleteModalConfirmButtonLabel', { - defaultMessage: 'Delete', - }), - getDeleteModalDescription: () => - i18n.translate('xpack.canvas.workpadLoader.deleteModalDescription', { - defaultMessage: `You can't recover deleted workpads.`, - }), - getDeleteMultipleWorkpadModalTitle: (numberOfWorkpads: string) => - i18n.translate('xpack.canvas.workpadLoader.deleteMultipleWorkpadsModalTitle', { - defaultMessage: 'Delete {numberOfWorkpads} workpads?', - values: { - numberOfWorkpads, - }, - }), - getDeleteSingleWorkpadModalTitle: (workpadName: string) => - i18n.translate('xpack.canvas.workpadLoader.deleteSingleWorkpadModalTitle', { - defaultMessage: `Delete workpad '{workpadName}'?`, - values: { - workpadName, - }, - }), - getEmptyPromptGettingStartedDescription: () => - i18n.translate('xpack.canvas.workpadLoader.emptyPromptGettingStartedDescription', { - defaultMessage: - 'Create a new workpad, start from a template, or import a workpad {JSON} file by dropping it here.', - values: { - JSON, - }, - }), - getEmptyPromptNewUserDescription: () => - i18n.translate('xpack.canvas.workpadLoader.emptyPromptNewUserDescription', { - defaultMessage: 'New to {CANVAS}?', - values: { - CANVAS, - }, - }), - getEmptyPromptTitle: () => - i18n.translate('xpack.canvas.workpadLoader.emptyPromptTitle', { - defaultMessage: 'Add your first workpad', - }), - getExportButtonAriaLabel: (numberOfWorkpads: number) => - i18n.translate('xpack.canvas.workpadLoader.exportButtonAriaLabel', { - defaultMessage: 'Export {numberOfWorkpads} workpads', - values: { - numberOfWorkpads, - }, - }), - getExportButtonLabel: (numberOfWorkpads: number) => - i18n.translate('xpack.canvas.workpadLoader.exportButtonLabel', { - defaultMessage: 'Export ({numberOfWorkpads})', - values: { - numberOfWorkpads, - }, - }), - getExportToolTip: () => - i18n.translate('xpack.canvas.workpadLoader.exportTooltip', { - defaultMessage: 'Export workpad', - }), - getFetchLoadingDescription: () => - i18n.translate('xpack.canvas.workpadLoader.fetchLoadingDescription', { - defaultMessage: 'Fetching workpads...', - description: - 'This message appears while the user is waiting for their list of workpads to load', - }), - getFilePickerPlaceholder: () => - i18n.translate('xpack.canvas.workpadLoader.filePickerPlaceholder', { - defaultMessage: 'Import workpad {JSON} file', - values: { - JSON, - }, - }), - getLoadWorkpadArialLabel: (workpadName: string) => - i18n.translate('xpack.canvas.workpadLoader.loadWorkpadArialLabel', { - defaultMessage: `Load workpad '{workpadName}'`, - values: { - workpadName, - }, - }), - getNoPermissionToCloneToolTip: () => - i18n.translate('xpack.canvas.workpadLoader.noPermissionToCloneToolTip', { - defaultMessage: `You don't have permission to clone workpads`, - }), - getNoPermissionToCreateToolTip: () => - i18n.translate('xpack.canvas.workpadLoader.noPermissionToCreateToolTip', { - defaultMessage: `You don't have permission to create workpads`, - }), - getNoPermissionToDeleteToolTip: () => - i18n.translate('xpack.canvas.workpadLoader.noPermissionToDeleteToolTip', { - defaultMessage: `You don't have permission to delete workpads`, - }), - getNoPermissionToUploadToolTip: () => - i18n.translate('xpack.canvas.workpadLoader.noPermissionToUploadToolTip', { - defaultMessage: `You don't have permission to upload workpads`, - }), - getSampleDataLinkLabel: () => - i18n.translate('xpack.canvas.workpadLoader.sampleDataLinkLabel', { - defaultMessage: 'Add your first workpad', - }), - getTableCreatedColumnTitle: () => - i18n.translate('xpack.canvas.workpadLoader.table.createdColumnTitle', { - defaultMessage: 'Created', - description: 'This column in the table contains the date/time the workpad was created.', - }), - getTableNameColumnTitle: () => - i18n.translate('xpack.canvas.workpadLoader.table.nameColumnTitle', { - defaultMessage: 'Workpad name', - }), - getTableUpdatedColumnTitle: () => - i18n.translate('xpack.canvas.workpadLoader.table.updatedColumnTitle', { - defaultMessage: 'Updated', - description: - 'This column in the table contains the date/time the workpad was last updated.', - }), - getTableActionsColumnTitle: () => - i18n.translate('xpack.canvas.workpadLoader.table.actionsColumnTitle', { - defaultMessage: 'Actions', - description: - 'This column in the table contains the actions that can be taken on a workpad.', - }), - }, - WorkpadManager: { - getModalTitle: () => - i18n.translate('xpack.canvas.workpadManager.modalTitle', { - defaultMessage: '{CANVAS} workpads', - values: { - CANVAS, - }, - }), - getMyWorkpadsTabLabel: () => - i18n.translate('xpack.canvas.workpadManager.myWorkpadsTabLabel', { - defaultMessage: 'My workpads', - }), - getWorkpadTemplatesTabLabel: () => - i18n.translate('xpack.canvas.workpadManager.workpadTemplatesTabLabel', { - defaultMessage: 'Templates', - description: 'The label for the tab that displays a list of designed workpad templates.', - }), - }, - WorkpadSearch: { - getWorkpadSearchPlaceholder: () => - i18n.translate('xpack.canvas.workpadSearch.searchPlaceholder', { - defaultMessage: 'Find workpad', - }), - }, - WorkpadTemplates: { - getCloneTemplateLinkAriaLabel: (templateName: string) => - i18n.translate('xpack.canvas.workpadTemplate.cloneTemplateLinkAriaLabel', { - defaultMessage: `Clone workpad template '{templateName}'`, - values: { - templateName, - }, - }), - getTableDescriptionColumnTitle: () => - i18n.translate('xpack.canvas.workpadTemplates.table.descriptionColumnTitle', { - defaultMessage: 'Description', - }), - getTableNameColumnTitle: () => - i18n.translate('xpack.canvas.workpadTemplates.table.nameColumnTitle', { - defaultMessage: 'Template name', - }), - getTableTagsColumnTitle: () => - i18n.translate('xpack.canvas.workpadTemplates.table.tagsColumnTitle', { - defaultMessage: 'Tags', - description: - 'This column contains relevant tags that indicate what type of template ' + - 'is displayed. For example: "report", "presentation", etc.', - }), - getTemplateSearchPlaceholder: () => - i18n.translate('xpack.canvas.workpadTemplate.searchPlaceholder', { - defaultMessage: 'Find template', - }), - getCreatingTemplateLabel: (templateName: string) => - i18n.translate('xpack.canvas.workpadTemplate.creatingTemplateLabel', { - defaultMessage: `Creating from template '{templateName}'`, - values: { - templateName, - }, - }), - }, -}; diff --git a/x-pack/plugins/canvas/i18n/errors.ts b/x-pack/plugins/canvas/i18n/errors.ts index 0928045119234..a55762dce2d20 100644 --- a/x-pack/plugins/canvas/i18n/errors.ts +++ b/x-pack/plugins/canvas/i18n/errors.ts @@ -6,7 +6,6 @@ */ import { i18n } from '@kbn/i18n'; -import { CANVAS, JSON } from './constants'; export const ErrorStrings = { actionsElements: { @@ -93,54 +92,10 @@ export const ErrorStrings = { }, }), }, - WorkpadFileUpload: { - getAcceptJSONOnlyErrorMessage: () => - i18n.translate('xpack.canvas.error.workpadUpload.acceptJSONOnlyErrorMessage', { - defaultMessage: 'Only {JSON} files are accepted', - values: { - JSON, - }, - }), - getFileUploadFailureWithFileNameErrorMessage: (fileName: string) => - i18n.translate('xpack.canvas.errors.workpadUpload.fileUploadFileWithFileNameErrorMessage', { - defaultMessage: `Couldn't upload '{fileName}'`, - values: { - fileName, - }, - }), - getFileUploadFailureWithoutFileNameErrorMessage: () => - i18n.translate( - 'xpack.canvas.error.workpadUpload.fileUploadFailureWithoutFileNameErrorMessage', - { - defaultMessage: `Couldn't upload file`, - } - ), - getMissingPropertiesErrorMessage: () => - i18n.translate('xpack.canvas.error.workpadUpload.missingPropertiesErrorMessage', { - defaultMessage: - 'Some properties required for a {CANVAS} workpad are missing. Edit your {JSON} file to provide the correct property values, and try again.', - values: { - CANVAS, - JSON, - }, - }), - }, - WorkpadLoader: { - getCloneFailureErrorMessage: () => - i18n.translate('xpack.canvas.error.workpadLoader.cloneFailureErrorMessage', { - defaultMessage: `Couldn't clone workpad`, - }), - getDeleteFailureErrorMessage: () => - i18n.translate('xpack.canvas.error.workpadLoader.deleteFailureErrorMessage', { - defaultMessage: `Couldn't delete all workpads`, - }), - getFindFailureErrorMessage: () => - i18n.translate('xpack.canvas.error.workpadLoader.findFailureErrorMessage', { - defaultMessage: `Couldn't find workpad`, - }), - getUploadFailureErrorMessage: () => - i18n.translate('xpack.canvas.error.workpadLoader.uploadFailureErrorMessage', { - defaultMessage: `Couldn't upload workpad`, + WorkpadDropzone: { + getTooManyFilesErrorMessage: () => + i18n.translate('xpack.canvas.error.workpadDropzone.tooManyFilesErrorMessage', { + defaultMessage: 'One one file can be uploaded at a time', }), }, workpadRoutes: { 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" />
    - -

    - Import your assets to get started -

    -
    - + Import your assets to get started +

    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" >
    40 characters remaining
    @@ -119,7 +119,7 @@ exports[`Storyshots components/Elements/CustomElementModal with description 1`] className="euiFormRow__fieldWrapper" >