diff --git a/dev_docs/key_concepts/performance.mdx b/dev_docs/key_concepts/performance.mdx new file mode 100644 index 00000000000000..f7b71b02589144 --- /dev/null +++ b/dev_docs/key_concepts/performance.mdx @@ -0,0 +1,106 @@ +--- +id: kibDevPerformance +slug: /kibana-dev-docs/performance +title: Performance +summary: Performance tips for Kibana development. +date: 2021-09-02 +tags: ['kibana', 'onboarding', 'dev', 'performance'] +--- + +## Keep Kibana fast + +*tl;dr*: Load as much code lazily as possible. Everyone loves snappy +applications with a responsive UI and hates spinners. Users deserve the +best experience whether they run Kibana locally or +in the cloud, regardless of their hardware and environment. + +There are 2 main aspects of the perceived speed of an application: loading time +and responsiveness to user actions. Kibana loads and bootstraps *all* +the plugins whenever a user lands on any page. It means that +every new application affects the overall _loading performance_, as plugin code is +loaded _eagerly_ to initialize the plugin and provide plugin API to dependent +plugins. + +However, it’s usually not necessary that the whole plugin code should be loaded +and initialized at once. The plugin could keep on loading code covering API functionality +on Kibana bootstrap, but load UI related code lazily on-demand, when an +application page or management section is mounted. +Always prefer to import UI root components lazily when possible (such as in `mount` +handlers). Even if their size may seem negligible, they are likely using +some heavy-weight libraries that will also be removed from the initial +plugin bundle, therefore, reducing its size by a significant amount. + +```ts +import type { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; +export class MyPlugin implements Plugin { + setup(core: CoreSetup, plugins: SetupDeps) { + core.application.register({ + id: 'app', + title: 'My app', + async mount(params: AppMountParameters) { + const { mountApp } = await import('./app/mount_app'); + return mountApp(await core.getStartServices(), params); + }, + }); + plugins.management.sections.section.kibana.registerApp({ + id: 'app', + title: 'My app', + order: 1, + async mount(params) { + const { mountManagementSection } = await import('./app/mount_management_section'); + return mountManagementSection(coreSetup, params); + }, + }); + return { + doSomething() {}, + }; + } +} +``` + +### Understanding plugin bundle size + +Kibana Platform plugins are pre-built with `@kbn/optimizer` +and distributed as package artifacts. This means that it is no +longer necessary for us to include the `optimizer` in the +distributable version of Kibana Every plugin artifact contains all +plugin dependencies required to run the plugin, except some +stateful dependencies shared across plugin bundles via +`@kbn/ui-shared-deps`. This means that plugin artifacts _tend to +be larger_ than they were in the legacy platform. To understand the +current size of your plugin artifact, run `@kbn/optimizer` with: + +```bash +node scripts/build_kibana_platform_plugins.js --dist --profile --focus=my_plugin +``` + +and check the output in the `target` sub-folder of your plugin folder: + +```bash +ls -lh plugins/my_plugin/target/public/ +# output +# an async chunk loaded on demand +... 262K 0.plugin.js +# eagerly loaded chunk +... 50K my_plugin.plugin.js +``` + +You might see at least one js bundle - `my_plugin.plugin.js`. This is +the _only_ artifact loaded by Kibana during bootstrap in the +browser. The rule of thumb is to keep its size as small as possible. +Other lazily loaded parts of your plugin will be present in the same folder as +separate chunks under `{number}.myplugin.js` names. If you want to +investigate what your plugin bundle consists of, you need to run +`@kbn/optimizer` with `--profile` flag to generate a +[webpack stats file](https://webpack.js.org/api/stats/). + +```bash +node scripts/build_kibana_platform_plugins.js --dist --no-examples --profile +``` + +Many OSS tools allow you to analyze the generated stats file: + +* [An official tool](https://webpack.github.io/analyse/#modules) from +Webpack authors +* [webpack-visualizer](https://chrisbateman.github.io/webpack-visualizer/) +* [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.hasheaderbanner_.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.hasheaderbanner_.md new file mode 100644 index 00000000000000..6ce0671eb52305 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.hasheaderbanner_.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeStart](./kibana-plugin-core-public.chromestart.md) > [hasHeaderBanner$](./kibana-plugin-core-public.chromestart.hasheaderbanner_.md) + +## ChromeStart.hasHeaderBanner$() method + +Get an observable of the current header banner presence state. + +Signature: + +```typescript +hasHeaderBanner$(): Observable; +``` +Returns: + +`Observable` + diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.md index 7285b4a00a0ec7..ffc77dd653c0f8 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.md @@ -57,6 +57,7 @@ core.chrome.setHelpExtension(elem => { | [getHelpExtension$()](./kibana-plugin-core-public.chromestart.gethelpextension_.md) | Get an observable of the current custom help conttent | | [getIsNavDrawerLocked$()](./kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md) | Get an observable of the current locked state of the nav drawer. | | [getIsVisible$()](./kibana-plugin-core-public.chromestart.getisvisible_.md) | Get an observable of the current visibility state of the chrome. | +| [hasHeaderBanner$()](./kibana-plugin-core-public.chromestart.hasheaderbanner_.md) | Get an observable of the current header banner presence state. | | [setBadge(badge)](./kibana-plugin-core-public.chromestart.setbadge.md) | Override the current badge | | [setBreadcrumbs(newBreadcrumbs)](./kibana-plugin-core-public.chromestart.setbreadcrumbs.md) | Override the current set of breadcrumbs | | [setBreadcrumbsAppendExtension(breadcrumbsAppendExtension)](./kibana-plugin-core-public.chromestart.setbreadcrumbsappendextension.md) | Mount an element next to the last breadcrumb | 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 908615b3220dd4..7d862f50bba18a 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,7 +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 settings: string;
readonly apm: {
readonly kibanaSettings: string;
readonly supportedServiceMaps: string;
readonly customLinks: string;
};
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 suricataModule: string;
readonly zeekModule: string;
};
readonly auditbeat: {
readonly base: string;
readonly auditdModule: string;
readonly systemModule: 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 libbeat: {
readonly getStarted: 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 rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly privileges: string;
readonly guide: string;
readonly gettingStarted: string;
readonly ml: string;
readonly ruleChangeLog: string;
readonly detectionsReq: string;
readonly networkMap: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
readonly autocompleteChanges: 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>;
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;
}>;
readonly ecs: {
readonly guide: string;
};
} | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly apm: {
readonly kibanaSettings: string;
readonly supportedServiceMaps: string;
readonly upgrading: string;
};
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 suricataModule: string;
readonly zeekModule: string;
};
readonly auditbeat: {
readonly base: string;
readonly auditdModule: string;
readonly systemModule: 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 libbeat: {
readonly getStarted: 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 rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly privileges: string;
readonly guide: string;
readonly gettingStarted: string;
readonly ml: string;
readonly ruleChangeLog: string;
readonly detectionsReq: string;
readonly networkMap: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
readonly autocompleteChanges: 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>;
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;
}>;
readonly ecs: {
readonly guide: string;
};
} | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly apm: {
readonly kibanaSettings: string;
readonly supportedServiceMaps: string;
readonly metaData: string;
};
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 suricataModule: string;
readonly zeekModule: string;
};
readonly auditbeat: {
readonly base: string;
readonly auditdModule: string;
readonly systemModule: 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 libbeat: {
readonly getStarted: 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 rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly privileges: string;
readonly guide: string;
readonly gettingStarted: string;
readonly ml: string;
readonly ruleChangeLog: string;
readonly detectionsReq: string;
readonly networkMap: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
readonly autocompleteChanges: 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>;
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;
}>;
readonly ecs: {
readonly guide: string;
};
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly apm: {
readonly kibanaSettings: string;
readonly supportedServiceMaps: string;
readonly customLinks: string;
readonly droppedTransactionSpans: string;
readonly upgrading: string;
readonly metaData: string;
};
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 suricataModule: string;
readonly zeekModule: string;
};
readonly auditbeat: {
readonly base: string;
readonly auditdModule: string;
readonly systemModule: 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 libbeat: {
readonly getStarted: 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 rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly privileges: string;
readonly guide: string;
readonly gettingStarted: string;
readonly ml: string;
readonly ruleChangeLog: string;
readonly detectionsReq: string;
readonly networkMap: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
readonly autocompleteChanges: 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>;
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;
}>;
readonly ecs: {
readonly guide: string;
};
} | | diff --git a/docs/maps/maps-getting-started.asciidoc b/docs/maps/maps-getting-started.asciidoc index 548a5742934035..64ab6fca0714ef 100644 --- a/docs/maps/maps-getting-started.asciidoc +++ b/docs/maps/maps-getting-started.asciidoc @@ -50,13 +50,13 @@ and lighter shades will symbolize countries with less traffic. . In **Statistics source**, set: ** **Index pattern** to **kibana_sample_data_logs** -** **Join field** to **geo.src** +** **Join field** to **geo.dest** . Click **Add layer**. . In **Layer settings**, set: -** **Name** to `Total Requests by Country` +** **Name** to `Total Requests by Destination` ** **Opacity** to 50% . Add a Tooltip field: diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index 347b81abf6d512..a92179ca9283cc 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -54,6 +54,7 @@ const createStartContractMock = () => { getCustomNavLink$: jest.fn(), setCustomNavLink: jest.fn(), setHeaderBanner: jest.fn(), + hasHeaderBanner$: jest.fn(), getBodyClasses$: jest.fn(), }; startContract.navLinks.getAll.mockReturnValue([]); @@ -65,6 +66,7 @@ const createStartContractMock = () => { startContract.getHelpExtension$.mockReturnValue(new BehaviorSubject(undefined)); startContract.getIsNavDrawerLocked$.mockReturnValue(new BehaviorSubject(false)); startContract.getBodyClasses$.mockReturnValue(new BehaviorSubject([])); + startContract.hasHeaderBanner$.mockReturnValue(new BehaviorSubject(false)); return startContract; }; diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index 8df8d76a13c46a..b3815c02674e1d 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -390,6 +390,19 @@ describe('start', () => { }); }); + describe('header banner', () => { + it('updates/emits the state of the header banner', async () => { + const { chrome, service } = await start(); + const promise = chrome.hasHeaderBanner$().pipe(toArray()).toPromise(); + + chrome.setHeaderBanner({ content: () => () => undefined }); + chrome.setHeaderBanner(undefined); + service.stop(); + + await expect(promise).resolves.toEqual([false, true, false]); + }); + }); + describe('erase chrome fields', () => { it('while switching an app', async () => { const startDeps = defaultStartDeps([new FakeApp('alpha')]); diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 5740e1739280ad..8c8b264b094ccf 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -273,6 +273,13 @@ export class ChromeService { headerBanner$.next(headerBanner); }, + hasHeaderBanner$: () => { + return headerBanner$.pipe( + takeUntil(this.stop$), + map((banner) => Boolean(banner)) + ); + }, + getBodyClasses$: () => bodyClasses$.pipe(takeUntil(this.stop$)), }; } diff --git a/src/core/public/chrome/types.ts b/src/core/public/chrome/types.ts index 813f385fc94d20..98987678d64cd3 100644 --- a/src/core/public/chrome/types.ts +++ b/src/core/public/chrome/types.ts @@ -168,6 +168,11 @@ export interface ChromeStart { * @remarks Using `undefined` when invoking this API will remove the banner. */ setHeaderBanner(headerBanner?: ChromeUserBanner): void; + + /** + * Get an observable of the current header banner presence state. + */ + hasHeaderBanner$(): Observable; } /** @internal */ diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index b26d1c4cbce446..554e6704657f08 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -355,6 +355,7 @@ export interface ChromeStart { getHelpExtension$(): Observable; getIsNavDrawerLocked$(): Observable; getIsVisible$(): Observable; + hasHeaderBanner$(): Observable; navControls: ChromeNavControls; navLinks: ChromeNavLinks; recentlyAccessed: ChromeRecentlyAccessed; diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/batch_size_bytes.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/batch_size_bytes.test.ts index e96aeb6a93b659..8e79e6342c0d52 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/batch_size_bytes.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/batch_size_bytes.test.ts @@ -67,7 +67,9 @@ describe('migration v2', () => { await root.setup(); await expect(root.start()).resolves.toBeTruthy(); - await new Promise((resolve) => setTimeout(resolve, 1000)); + // After plugins start, some saved objects are deleted/recreated, so we + // wait a bit for the count to settle. + await new Promise((resolve) => setTimeout(resolve, 5000)); const esClient: ElasticsearchClient = esServer.es.getClient(); const migratedIndexResponse = await esClient.count({ diff --git a/src/core/server/saved_objects/migrationsv2/test_helpers/retry.test.ts b/src/core/server/saved_objects/migrationsv2/test_helpers/retry.test.ts index ff5bf3d01c641a..8294a61caae6e9 100644 --- a/src/core/server/saved_objects/migrationsv2/test_helpers/retry.test.ts +++ b/src/core/server/saved_objects/migrationsv2/test_helpers/retry.test.ts @@ -8,8 +8,7 @@ import { retryAsync } from './retry_async'; -// FLAKY: https://github.com/elastic/kibana/issues/110970 -describe.skip('retry', () => { +describe('retry', () => { it('retries throwing functions until they succeed', async () => { let i = 0; await expect( @@ -53,6 +52,8 @@ describe.skip('retry', () => { }, { retryAttempts: 3, retryDelayMs: 100 } ); - expect(Date.now() - now).toBeGreaterThanOrEqual(200); + // Would expect it to take 200ms but seems like timing inaccuracies + // sometimes causes the duration to be measured as 199ms + expect(Date.now() - now).toBeGreaterThanOrEqual(199); }); }); diff --git a/src/core/server/saved_objects/routes/index.ts b/src/core/server/saved_objects/routes/index.ts index 889edfb66a20f6..3ab870c276d29f 100644 --- a/src/core/server/saved_objects/routes/index.ts +++ b/src/core/server/saved_objects/routes/index.ts @@ -20,7 +20,6 @@ import { registerUpdateRoute } from './update'; import { registerBulkGetRoute } from './bulk_get'; import { registerBulkCreateRoute } from './bulk_create'; import { registerBulkUpdateRoute } from './bulk_update'; -import { registerLogLegacyImportRoute } from './log_legacy_import'; import { registerExportRoute } from './export'; import { registerImportRoute } from './import'; import { registerResolveImportErrorsRoute } from './resolve_import_errors'; @@ -50,7 +49,6 @@ export function registerRoutes({ registerBulkGetRoute(router, { coreUsageData }); registerBulkCreateRoute(router, { coreUsageData }); registerBulkUpdateRoute(router, { coreUsageData }); - registerLogLegacyImportRoute(router, logger); registerExportRoute(router, { config, coreUsageData }); registerImportRoute(router, { config, coreUsageData }); registerResolveImportErrorsRoute(router, { config, coreUsageData }); diff --git a/src/core/server/saved_objects/routes/integration_tests/log_legacy_import.test.ts b/src/core/server/saved_objects/routes/integration_tests/log_legacy_import.test.ts deleted file mode 100644 index 38e94112f63e72..00000000000000 --- a/src/core/server/saved_objects/routes/integration_tests/log_legacy_import.test.ts +++ /dev/null @@ -1,50 +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 supertest from 'supertest'; -import { UnwrapPromise } from '@kbn/utility-types'; -import { registerLogLegacyImportRoute } from '../log_legacy_import'; -import { loggingSystemMock } from '../../../logging/logging_system.mock'; -import { setupServer } from '../test_utils'; - -type SetupServerReturn = UnwrapPromise>; - -describe('POST /api/saved_objects/_log_legacy_import', () => { - let server: SetupServerReturn['server']; - let httpSetup: SetupServerReturn['httpSetup']; - let logger: ReturnType; - - beforeEach(async () => { - ({ server, httpSetup } = await setupServer()); - logger = loggingSystemMock.createLogger(); - - const router = httpSetup.createRouter('/api/saved_objects/'); - registerLogLegacyImportRoute(router, logger); - - await server.start(); - }); - - afterEach(async () => { - await server.stop(); - }); - - it('logs a warning when called', async () => { - const result = await supertest(httpSetup.server.listener) - .post('/api/saved_objects/_log_legacy_import') - .expect(200); - - expect(result.body).toEqual({ success: true }); - expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(` - Array [ - Array [ - "Importing saved objects from a .json file has been deprecated", - ], - ] - `); - }); -}); diff --git a/src/core/server/saved_objects/routes/log_legacy_import.ts b/src/core/server/saved_objects/routes/log_legacy_import.ts deleted file mode 100644 index 0c0b04b49b7b8f..00000000000000 --- a/src/core/server/saved_objects/routes/log_legacy_import.ts +++ /dev/null @@ -1,23 +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 { IRouter } from '../../http'; -import { Logger } from '../../logging'; - -export const registerLogLegacyImportRoute = (router: IRouter, logger: Logger) => { - router.post( - { - path: '/_log_legacy_import', - validate: false, - }, - async (context, req, res) => { - logger.warn('Importing saved objects from a .json file has been deprecated'); - return res.ok({ body: { success: true } }); - } - ); -}; diff --git a/src/dev/build/tasks/create_empty_dirs_and_files_task.ts b/src/dev/build/tasks/create_empty_dirs_and_files_task.ts index 06b402c5801512..26ed25e8014759 100644 --- a/src/dev/build/tasks/create_empty_dirs_and_files_task.ts +++ b/src/dev/build/tasks/create_empty_dirs_and_files_task.ts @@ -12,9 +12,6 @@ export const CreateEmptyDirsAndFiles: Task = { description: 'Creating some empty directories and files to prevent file-permission issues', async run(config, log, build) { - await Promise.all([ - mkdirp(build.resolvePath('plugins')), - mkdirp(build.resolvePath('data/optimize')), - ]); + await Promise.all([mkdirp(build.resolvePath('plugins')), mkdirp(build.resolvePath('data'))]); }, }; diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts index 7bf390b0bee5ad..1b24062ccd9b5f 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts @@ -127,6 +127,7 @@ export const useDashboardAppState = ({ savedDashboards, kbnUrlStateStorage, initializerContext, + savedObjectsTagging, isEmbeddedExternally, dashboardCapabilities, dispatchDashboardStateChange, diff --git a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.test.ts b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.test.ts index 17f1802a47327a..554aca6ddb8f16 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.test.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.test.ts @@ -27,6 +27,7 @@ describe('getStateDefaults', () => { "default_column", ], "filters": undefined, + "hideChart": undefined, "index": "index-pattern-with-timefield-id", "interval": "auto", "query": undefined, @@ -54,6 +55,7 @@ describe('getStateDefaults', () => { "default_column", ], "filters": undefined, + "hideChart": undefined, "index": "the-index-pattern-id", "interval": "auto", "query": undefined, diff --git a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts index fc835d4d3dd162..4061d9a61f0a39 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts @@ -46,6 +46,7 @@ export function getStateDefaults({ index: indexPattern!.id, interval: 'auto', filters: cloneDeep(searchSource.getOwnField('filter')), + hideChart: undefined, } as AppState; if (savedSearch.grid) { defaultState.grid = savedSearch.grid; diff --git a/src/plugins/embeddable/kibana.json b/src/plugins/embeddable/kibana.json index 25e95061ed2ace..1f4b6ff7b7f371 100644 --- a/src/plugins/embeddable/kibana.json +++ b/src/plugins/embeddable/kibana.json @@ -7,6 +7,7 @@ "name": "App Services", "githubTeam": "kibana-app-services" }, + "description": "Adds embeddables service to Kibana", "requiredPlugins": ["inspector", "uiActions"], "extraPublicDirs": ["public/lib/test_samples", "common"], "requiredBundles": ["savedObjects", "kibanaReact", "kibanaUtils"] diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx index 59888b00fc5762..919058bcbbc32c 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx @@ -31,8 +31,8 @@ interface HelloWorldContainerInput extends ContainerInput { } interface HelloWorldContainerOptions { - getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; - panelComponent: EmbeddableStart['EmbeddablePanel']; + getEmbeddableFactory?: EmbeddableStart['getEmbeddableFactory']; + panelComponent?: EmbeddableStart['EmbeddablePanel']; } export class HelloWorldContainer extends Container { @@ -42,7 +42,7 @@ export class HelloWorldContainer extends Container, private readonly options: HelloWorldContainerOptions ) { - super(input, { embeddableLoaded: {} }, options.getEmbeddableFactory); + super(input, { embeddableLoaded: {} }, options.getEmbeddableFactory || (() => undefined)); } public getInheritedInput(id: string) { @@ -56,10 +56,14 @@ export class HelloWorldContainer extends Container - + {this.options.panelComponent ? ( + + ) : ( +
Panel component not provided.
+ )} , node ); diff --git a/src/plugins/expressions/kibana.json b/src/plugins/expressions/kibana.json index 46e6ef8b4ea751..82b16d4f003695 100644 --- a/src/plugins/expressions/kibana.json +++ b/src/plugins/expressions/kibana.json @@ -7,6 +7,7 @@ "name": "App Services", "githubTeam": "kibana-app-services" }, + "description": "Adds expression runtime to Kibana", "extraPublicDirs": ["common", "common/fonts"], "requiredBundles": ["kibanaUtils", "inspector"] } diff --git a/src/plugins/home/server/services/sample_data/data_sets/logs/logs.json.gz b/src/plugins/home/server/services/sample_data/data_sets/logs/logs.json.gz index 0b0ecf3a2a4099..241b5cecb71c90 100644 Binary files a/src/plugins/home/server/services/sample_data/data_sets/logs/logs.json.gz and b/src/plugins/home/server/services/sample_data/data_sets/logs/logs.json.gz differ diff --git a/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts index b8b8adba307dbe..f7e8c824030bdf 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts @@ -43,7 +43,7 @@ export const getSavedObjects = (): SavedObject[] => [ }, attributes: { title: i18n.translate('home.sampleData.logsSpec.heatmapTitle', { - defaultMessage: '[Logs] Unique Visitor Heatmap', + defaultMessage: '[Logs] Unique Destination Heatmap', }), description: '', kibanaSavedObjectMeta: { @@ -52,7 +52,7 @@ export const getSavedObjects = (): SavedObject[] => [ uiStateJSON: '{}', version: 1, visState: - '{"title":"[Logs] Unique Visitor Heatmap","type":"vega","aggs":[],"params":{"spec":"{\\n $schema: https://vega.github.io/schema/vega-lite/v5.json\\n data: {\\n url: {\\n %context%: true\\n %timefield%: @timestamp\\n index: kibana_sample_data_logs\\n body: {\\n aggs: {\\n countries: {\\n terms: {\\n field: geo.src\\n size: 25\\n }\\n aggs: {\\n hours: {\\n histogram: {\\n field: hour_of_day\\n interval: 1\\n }\\n aggs: {\\n unique: {\\n cardinality: {\\n field: clientip\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n size: 0\\n }\\n }\\n format: {property: \\"aggregations.countries.buckets\\"}\\n }\\n \\n transform: [\\n {\\n flatten: [\\"hours.buckets\\"],\\n as: [\\"buckets\\"]\\n },\\n {\\n filter: \\"datum.buckets.unique.value > 0\\"\\n }\\n ]\\n\\n mark: {\\n type: rect\\n tooltip: {\\n expr: \\"{\\\\\\"Unique Visitors\\\\\\": datum.buckets.unique.value,\\\\\\"geo.src\\\\\\": datum.key,\\\\\\"Hour\\\\\\": datum.buckets.key}\\"\\n }\\n }\\n\\n encoding: {\\n x: {\\n field: buckets.key\\n type: nominal\\n scale: {\\n domain: {\\n expr: \\"sequence(0, 24)\\"\\n }\\n }\\n axis: {\\n title: false\\n labelAngle: 0\\n }\\n }\\n y: {\\n field: key\\n type: nominal\\n sort: {\\n field: -buckets.unique.value\\n }\\n axis: {title: false}\\n }\\n color: {\\n field: buckets.unique.value\\n type: quantitative\\n axis: {title: false}\\n scale: {\\n scheme: blues\\n }\\n }\\n }\\n}\\n"}}', + '{"title":"[Logs] Unique Destination Heatmap","type":"vega","aggs":[],"params":{"spec":"{\\n $schema: https://vega.github.io/schema/vega-lite/v5.json\\n data: {\\n url: {\\n %context%: true\\n %timefield%: @timestamp\\n index: kibana_sample_data_logs\\n body: {\\n aggs: {\\n countries: {\\n terms: {\\n field: geo.dest\\n size: 25\\n }\\n aggs: {\\n hours: {\\n histogram: {\\n field: hour_of_day\\n interval: 1\\n }\\n aggs: {\\n unique: {\\n cardinality: {\\n field: clientip\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n size: 0\\n }\\n }\\n format: {property: \\"aggregations.countries.buckets\\"}\\n }\\n \\n transform: [\\n {\\n flatten: [\\"hours.buckets\\"],\\n as: [\\"buckets\\"]\\n },\\n {\\n filter: \\"datum.buckets.unique.value > 0\\"\\n }\\n ]\\n\\n mark: {\\n type: rect\\n tooltip: {\\n expr: \\"{\\\\\\"Unique Visitors\\\\\\": datum.buckets.unique.value,\\\\\\"geo.src\\\\\\": datum.key,\\\\\\"Hour\\\\\\": datum.buckets.key}\\"\\n }\\n }\\n\\n encoding: {\\n x: {\\n field: buckets.key\\n type: nominal\\n scale: {\\n domain: {\\n expr: \\"sequence(0, 24)\\"\\n }\\n }\\n axis: {\\n title: false\\n labelAngle: 0\\n }\\n }\\n y: {\\n field: key\\n type: nominal\\n sort: {\\n field: -buckets.unique.value\\n }\\n axis: {title: false}\\n }\\n color: {\\n field: buckets.unique.value\\n type: quantitative\\n axis: {title: false}\\n scale: {\\n scheme: blues\\n }\\n }\\n }\\n}\\n"}}', }, references: [], }, @@ -116,10 +116,10 @@ export const getSavedObjects = (): SavedObject[] => [ migrationVersion: {}, attributes: { title: i18n.translate('home.sampleData.logsSpec.sourceAndDestinationSankeyChartTitle', { - defaultMessage: '[Logs] Source and Destination Sankey Chart', + defaultMessage: '[Logs] Machine OS and Destination Sankey Chart', }), visState: - '{"title":"[Logs] Source and Destination Sankey Chart","type":"vega","params":{"spec":"{ \\n $schema: https://vega.github.io/schema/vega/v5.json\\n data: [\\n\\t{\\n \\t// query ES based on the currently selected time range and filter string\\n \\tname: rawData\\n \\turl: {\\n \\t%context%: true\\n \\t%timefield%: timestamp\\n \\tindex: kibana_sample_data_logs\\n \\tbody: {\\n \\tsize: 0\\n \\taggs: {\\n \\ttable: {\\n \\tcomposite: {\\n \\tsize: 10000\\n \\tsources: [\\n \\t{\\n \\tstk1: {\\n \\tterms: {field: \\"geo.src\\"}\\n \\t}\\n \\t}\\n \\t{\\n \\tstk2: {\\n \\tterms: {field: \\"geo.dest\\"}\\n \\t}\\n \\t}\\n \\t]\\n \\t}\\n \\t}\\n \\t}\\n \\t}\\n \\t}\\n \\t// From the result, take just the data we are interested in\\n \\tformat: {property: \\"aggregations.table.buckets\\"}\\n \\t// Convert key.stk1 -> stk1 for simpler access below\\n \\ttransform: [\\n \\t{type: \\"formula\\", expr: \\"datum.key.stk1\\", as: \\"stk1\\"}\\n \\t{type: \\"formula\\", expr: \\"datum.key.stk2\\", as: \\"stk2\\"}\\n \\t{type: \\"formula\\", expr: \\"datum.doc_count\\", as: \\"size\\"}\\n \\t]\\n\\t}\\n\\t{\\n \\tname: nodes\\n \\tsource: rawData\\n \\ttransform: [\\n \\t// when a country is selected, filter out unrelated data\\n \\t{\\n \\ttype: filter\\n \\texpr: !groupSelector || groupSelector.stk1 == datum.stk1 || groupSelector.stk2 == datum.stk2\\n \\t}\\n \\t// Set new key for later lookups - identifies each node\\n \\t{type: \\"formula\\", expr: \\"datum.stk1+datum.stk2\\", as: \\"key\\"}\\n \\t// instead of each table row, create two new rows,\\n \\t// one for the source (stack=stk1) and one for destination node (stack=stk2).\\n \\t// The country code stored in stk1 and stk2 fields is placed into grpId field.\\n \\t{\\n \\ttype: fold\\n \\tfields: [\\"stk1\\", \\"stk2\\"]\\n \\tas: [\\"stack\\", \\"grpId\\"]\\n \\t}\\n \\t// Create a sortkey, different for stk1 and stk2 stacks.\\n \\t{\\n \\ttype: formula\\n \\texpr: datum.stack == \'stk1\' ? datum.stk1+datum.stk2 : datum.stk2+datum.stk1\\n \\tas: sortField\\n \\t}\\n \\t// Calculate y0 and y1 positions for stacking nodes one on top of the other,\\n \\t// independently for each stack, and ensuring they are in the proper order,\\n \\t// alphabetical from the top (reversed on the y axis)\\n \\t{\\n \\ttype: stack\\n \\tgroupby: [\\"stack\\"]\\n \\tsort: {field: \\"sortField\\", order: \\"descending\\"}\\n \\tfield: size\\n \\t}\\n \\t// calculate vertical center point for each node, used to draw edges\\n \\t{type: \\"formula\\", expr: \\"(datum.y0+datum.y1)/2\\", as: \\"yc\\"}\\n \\t]\\n\\t}\\n\\t{\\n \\tname: groups\\n \\tsource: nodes\\n \\ttransform: [\\n \\t// combine all nodes into country groups, summing up the doc counts\\n \\t{\\n \\ttype: aggregate\\n \\tgroupby: [\\"stack\\", \\"grpId\\"]\\n \\tfields: [\\"size\\"]\\n \\tops: [\\"sum\\"]\\n \\tas: [\\"total\\"]\\n \\t}\\n \\t// re-calculate the stacking y0,y1 values\\n \\t{\\n \\ttype: stack\\n \\tgroupby: [\\"stack\\"]\\n \\tsort: {field: \\"grpId\\", order: \\"descending\\"}\\n \\tfield: total\\n \\t}\\n \\t// project y0 and y1 values to screen coordinates\\n \\t// doing it once here instead of doing it several times in marks\\n \\t{type: \\"formula\\", expr: \\"scale(\'y\', datum.y0)\\", as: \\"scaledY0\\"}\\n \\t{type: \\"formula\\", expr: \\"scale(\'y\', datum.y1)\\", as: \\"scaledY1\\"}\\n \\t// boolean flag if the label should be on the right of the stack\\n \\t{type: \\"formula\\", expr: \\"datum.stack == \'stk1\'\\", as: \\"rightLabel\\"}\\n \\t// Calculate traffic percentage for this country using \\"y\\" scale\\n \\t// domain upper bound, which represents the total traffic\\n \\t{\\n \\ttype: formula\\n \\texpr: datum.total/domain(\'y\')[1]\\n \\tas: percentage\\n \\t}\\n \\t]\\n\\t}\\n\\t{\\n \\t// This is a temp lookup table with all the \'stk2\' stack nodes\\n \\tname: destinationNodes\\n \\tsource: nodes\\n \\ttransform: [\\n \\t{type: \\"filter\\", expr: \\"datum.stack == \'stk2\'\\"}\\n \\t]\\n\\t}\\n\\t{\\n \\tname: edges\\n \\tsource: nodes\\n \\ttransform: [\\n \\t// we only want nodes from the left stack\\n \\t{type: \\"filter\\", expr: \\"datum.stack == \'stk1\'\\"}\\n \\t// find corresponding node from the right stack, keep it as \\"target\\"\\n \\t{\\n \\ttype: lookup\\n \\tfrom: destinationNodes\\n \\tkey: key\\n \\tfields: [\\"key\\"]\\n \\tas: [\\"target\\"]\\n \\t}\\n \\t// calculate SVG link path between stk1 and stk2 stacks for the node pair\\n \\t{\\n \\ttype: linkpath\\n \\torient: horizontal\\n \\tshape: diagonal\\n \\tsourceY: {expr: \\"scale(\'y\', datum.yc)\\"}\\n \\tsourceX: {expr: \\"scale(\'x\', \'stk1\') + bandwidth(\'x\')\\"}\\n \\ttargetY: {expr: \\"scale(\'y\', datum.target.yc)\\"}\\n \\ttargetX: {expr: \\"scale(\'x\', \'stk2\')\\"}\\n \\t}\\n \\t// A little trick to calculate the thickness of the line.\\n \\t// The value needs to be the same as the hight of the node, but scaling\\n \\t// size to screen\'s height gives inversed value because screen\'s Y\\n \\t// coordinate goes from the top to the bottom, whereas the graph\'s Y=0\\n \\t// is at the bottom. So subtracting scaled doc count from screen height\\n \\t// (which is the \\"lower\\" bound of the \\"y\\" scale) gives us the right value\\n \\t{\\n \\ttype: formula\\n \\texpr: range(\'y\')[0]-scale(\'y\', datum.size)\\n \\tas: strokeWidth\\n \\t}\\n \\t// Tooltip needs individual link\'s percentage of all traffic\\n \\t{\\n \\ttype: formula\\n \\texpr: datum.size/domain(\'y\')[1]\\n \\tas: percentage\\n \\t}\\n \\t]\\n\\t}\\n ]\\n scales: [\\n\\t{\\n \\t// calculates horizontal stack positioning\\n \\tname: x\\n \\ttype: band\\n \\trange: width\\n \\tdomain: [\\"stk1\\", \\"stk2\\"]\\n \\tpaddingOuter: 0.05\\n \\tpaddingInner: 0.95\\n\\t}\\n\\t{\\n \\t// this scale goes up as high as the highest y1 value of all nodes\\n \\tname: y\\n \\ttype: linear\\n \\trange: height\\n \\tdomain: {data: \\"nodes\\", field: \\"y1\\"}\\n\\t}\\n\\t{\\n \\t// use rawData to ensure the colors stay the same when clicking.\\n \\tname: color\\n \\ttype: ordinal\\n \\trange: category\\n \\tdomain: {data: \\"rawData\\", field: \\"stk1\\"}\\n\\t}\\n\\t{\\n \\t// this scale is used to map internal ids (stk1, stk2) to stack names\\n \\tname: stackNames\\n \\ttype: ordinal\\n \\trange: [\\"Source\\", \\"Destination\\"]\\n \\tdomain: [\\"stk1\\", \\"stk2\\"]\\n\\t}\\n ]\\n axes: [\\n\\t{\\n \\t// x axis should use custom label formatting to print proper stack names\\n \\torient: bottom\\n \\tscale: x\\n \\tencode: {\\n \\tlabels: {\\n \\tupdate: {\\n \\ttext: {scale: \\"stackNames\\", field: \\"value\\"}\\n \\t}\\n \\t}\\n \\t}\\n\\t}\\n\\t{orient: \\"left\\", scale: \\"y\\"}\\n ]\\n marks: [\\n\\t{\\n \\t// draw the connecting line between stacks\\n \\ttype: path\\n \\tname: edgeMark\\n \\tfrom: {data: \\"edges\\"}\\n \\t// this prevents some autosizing issues with large strokeWidth for paths\\n \\tclip: true\\n \\tencode: {\\n \\tupdate: {\\n \\t// By default use color of the left node, except when showing traffic\\n \\t// from just one country, in which case use destination color.\\n \\tstroke: [\\n \\t{\\n \\ttest: groupSelector && groupSelector.stack==\'stk1\'\\n \\tscale: color\\n \\tfield: stk2\\n \\t}\\n \\t{scale: \\"color\\", field: \\"stk1\\"}\\n \\t]\\n \\tstrokeWidth: {field: \\"strokeWidth\\"}\\n \\tpath: {field: \\"path\\"}\\n \\t// when showing all traffic, and hovering over a country,\\n \\t// highlight the traffic from that country.\\n \\tstrokeOpacity: {\\n \\tsignal: !groupSelector && (groupHover.stk1 == datum.stk1 || groupHover.stk2 == datum.stk2) ? 0.9 : 0.3\\n \\t}\\n \\t// Ensure that the hover-selected edges show on top\\n \\tzindex: {\\n \\tsignal: !groupSelector && (groupHover.stk1 == datum.stk1 || groupHover.stk2 == datum.stk2) ? 1 : 0\\n \\t}\\n \\t// format tooltip string\\n \\ttooltip: {\\n \\tsignal: datum.stk1 + \' → \' + datum.stk2 + \'\\t\' + format(datum.size, \',.0f\') + \' (\' + format(datum.percentage, \'.1%\') + \')\'\\n \\t}\\n \\t}\\n \\t// Simple mouseover highlighting of a single line\\n \\thover: {\\n \\tstrokeOpacity: {value: 1}\\n \\t}\\n \\t}\\n\\t}\\n\\t{\\n \\t// draw stack groups (countries)\\n \\ttype: rect\\n \\tname: groupMark\\n \\tfrom: {data: \\"groups\\"}\\n \\tencode: {\\n \\tenter: {\\n \\tfill: {scale: \\"color\\", field: \\"grpId\\"}\\n \\twidth: {scale: \\"x\\", band: 1}\\n \\t}\\n \\tupdate: {\\n \\tx: {scale: \\"x\\", field: \\"stack\\"}\\n \\ty: {field: \\"scaledY0\\"}\\n \\ty2: {field: \\"scaledY1\\"}\\n \\tfillOpacity: {value: 0.6}\\n \\ttooltip: {\\n \\tsignal: datum.grpId + \' \' + format(datum.total, \',.0f\') + \' (\' + format(datum.percentage, \'.1%\') + \')\'\\n \\t}\\n \\t}\\n \\thover: {\\n \\tfillOpacity: {value: 1}\\n \\t}\\n \\t}\\n\\t}\\n\\t{\\n \\t// draw country code labels on the inner side of the stack\\n \\ttype: text\\n \\tfrom: {data: \\"groups\\"}\\n \\t// don\'t process events for the labels - otherwise line mouseover is unclean\\n \\tinteractive: false\\n \\tencode: {\\n \\tupdate: {\\n \\t// depending on which stack it is, position x with some padding\\n \\tx: {\\n \\tsignal: scale(\'x\', datum.stack) + (datum.rightLabel ? bandwidth(\'x\') + 8 : -8)\\n \\t}\\n \\t// middle of the group\\n \\tyc: {signal: \\"(datum.scaledY0 + datum.scaledY1)/2\\"}\\n \\talign: {signal: \\"datum.rightLabel ? \'left\' : \'right\'\\"}\\n \\tbaseline: {value: \\"middle\\"}\\n \\tfontWeight: {value: \\"bold\\"}\\n \\t// only show text label if the group\'s height is large enough\\n \\ttext: {signal: \\"abs(datum.scaledY0-datum.scaledY1) > 13 ? datum.grpId : \'\'\\"}\\n \\t}\\n \\t}\\n\\t}\\n\\t{\\n \\t// Create a \\"show all\\" button. Shown only when a country is selected.\\n \\ttype: group\\n \\tdata: [\\n \\t// We need to make the button show only when groupSelector signal is true.\\n \\t// Each mark is drawn as many times as there are elements in the backing data.\\n \\t// Which means that if values list is empty, it will not be drawn.\\n \\t// Here I create a data source with one empty object, and filter that list\\n \\t// based on the signal value. This can only be done in a group.\\n \\t{\\n \\tname: dataForShowAll\\n \\tvalues: [{}]\\n \\ttransform: [{type: \\"filter\\", expr: \\"groupSelector\\"}]\\n \\t}\\n \\t]\\n \\t// Set button size and positioning\\n \\tencode: {\\n \\tenter: {\\n \\txc: {signal: \\"width/2\\"}\\n \\ty: {value: 30}\\n \\twidth: {value: 80}\\n \\theight: {value: 30}\\n \\t}\\n \\t}\\n \\tmarks: [\\n \\t{\\n \\t// This group is shown as a button with rounded corners.\\n \\ttype: group\\n \\t// mark name allows signal capturing\\n \\tname: groupReset\\n \\t// Only shows button if dataForShowAll has values.\\n \\tfrom: {data: \\"dataForShowAll\\"}\\n \\tencode: {\\n \\tenter: {\\n \\tcornerRadius: {value: 6}\\n \\tfill: {value: \\"#F5F7FA\\"}\\n \\tstroke: {value: \\"#c1c1c1\\"}\\n \\tstrokeWidth: {value: 2}\\n \\t// use parent group\'s size\\n \\theight: {\\n \\tfield: {group: \\"height\\"}\\n \\t}\\n \\twidth: {\\n \\tfield: {group: \\"width\\"}\\n \\t}\\n \\t}\\n \\tupdate: {\\n \\t// groups are transparent by default\\n \\topacity: {value: 1}\\n \\t}\\n \\thover: {\\n \\topacity: {value: 0.7}\\n \\t}\\n \\t}\\n \\tmarks: [\\n \\t{\\n \\ttype: text\\n \\t// if true, it will prevent clicking on the button when over text.\\n \\tinteractive: false\\n \\tencode: {\\n \\tenter: {\\n \\t// center text in the paren group\\n \\txc: {\\n \\tfield: {group: \\"width\\"}\\n \\tmult: 0.5\\n \\t}\\n \\tyc: {\\n \\tfield: {group: \\"height\\"}\\n \\tmult: 0.5\\n \\toffset: 2\\n \\t}\\n \\talign: {value: \\"center\\"}\\n \\tbaseline: {value: \\"middle\\"}\\n \\tfontWeight: {value: \\"bold\\"}\\n \\ttext: {value: \\"Show All\\"}\\n \\t}\\n \\t}\\n \\t}\\n \\t]\\n \\t}\\n \\t]\\n\\t}\\n ]\\n signals: [\\n\\t{\\n \\t// used to highlight traffic to/from the same country\\n \\tname: groupHover\\n \\tvalue: {}\\n \\ton: [\\n \\t{\\n \\tevents: @groupMark:mouseover\\n \\tupdate: \\"{stk1:datum.stack==\'stk1\' && datum.grpId, stk2:datum.stack==\'stk2\' && datum.grpId}\\"\\n \\t}\\n \\t{events: \\"mouseout\\", update: \\"{}\\"}\\n \\t]\\n\\t}\\n\\t// used to filter only the data related to the selected country\\n\\t{\\n \\tname: groupSelector\\n \\tvalue: false\\n \\ton: [\\n \\t{\\n \\t// Clicking groupMark sets this signal to the filter values\\n \\tevents: @groupMark:click!\\n \\tupdate: \\"{stack:datum.stack, stk1:datum.stack==\'stk1\' && datum.grpId, stk2:datum.stack==\'stk2\' && datum.grpId}\\"\\n \\t}\\n \\t{\\n \\t// Clicking \\"show all\\" button, or double-clicking anywhere resets it\\n \\tevents: [\\n \\t{type: \\"click\\", markname: \\"groupReset\\"}\\n \\t{type: \\"dblclick\\"}\\n \\t]\\n \\tupdate: \\"false\\"\\n \\t}\\n \\t]\\n\\t}\\n ]\\n}\\n"},"aggs":[]}', + '{"title":"[Logs] Machine OS and Destination Sankey Chart","type":"vega","params":{"spec":"{ \\n $schema: https://vega.github.io/schema/vega/v5.json\\n data: [\\n\\t{\\n \\t// query ES based on the currently selected time range and filter string\\n \\tname: rawData\\n \\turl: {\\n \\t%context%: true\\n \\t%timefield%: timestamp\\n \\tindex: kibana_sample_data_logs\\n \\tbody: {\\n \\tsize: 0\\n \\taggs: {\\n \\ttable: {\\n \\tcomposite: {\\n \\tsize: 10000\\n \\tsources: [\\n \\t{\\n \\tstk1: {\\n \\tterms: {field: \\"machine.os.keyword\\"}\\n \\t}\\n \\t}\\n \\t{\\n \\tstk2: {\\n \\tterms: {field: \\"geo.dest\\"}\\n \\t}\\n \\t}\\n \\t]\\n \\t}\\n \\t}\\n \\t}\\n \\t}\\n \\t}\\n \\t// From the result, take just the data we are interested in\\n \\tformat: {property: \\"aggregations.table.buckets\\"}\\n \\t// Convert key.stk1 -> stk1 for simpler access below\\n \\ttransform: [\\n \\t{type: \\"formula\\", expr: \\"datum.key.stk1\\", as: \\"stk1\\"}\\n \\t{type: \\"formula\\", expr: \\"datum.key.stk2\\", as: \\"stk2\\"}\\n \\t{type: \\"formula\\", expr: \\"datum.doc_count\\", as: \\"size\\"}\\n \\t]\\n\\t}\\n\\t{\\n \\tname: nodes\\n \\tsource: rawData\\n \\ttransform: [\\n \\t// when a country is selected, filter out unrelated data\\n \\t{\\n \\ttype: filter\\n \\texpr: !groupSelector || groupSelector.stk1 == datum.stk1 || groupSelector.stk2 == datum.stk2\\n \\t}\\n \\t// Set new key for later lookups - identifies each node\\n \\t{type: \\"formula\\", expr: \\"datum.stk1+datum.stk2\\", as: \\"key\\"}\\n \\t// instead of each table row, create two new rows,\\n \\t// one for the source (stack=stk1) and one for destination node (stack=stk2).\\n \\t// The country code stored in stk1 and stk2 fields is placed into grpId field.\\n \\t{\\n \\ttype: fold\\n \\tfields: [\\"stk1\\", \\"stk2\\"]\\n \\tas: [\\"stack\\", \\"grpId\\"]\\n \\t}\\n \\t// Create a sortkey, different for stk1 and stk2 stacks.\\n \\t{\\n \\ttype: formula\\n \\texpr: datum.stack == \'stk1\' ? datum.stk1+datum.stk2 : datum.stk2+datum.stk1\\n \\tas: sortField\\n \\t}\\n \\t// Calculate y0 and y1 positions for stacking nodes one on top of the other,\\n \\t// independently for each stack, and ensuring they are in the proper order,\\n \\t// alphabetical from the top (reversed on the y axis)\\n \\t{\\n \\ttype: stack\\n \\tgroupby: [\\"stack\\"]\\n \\tsort: {field: \\"sortField\\", order: \\"descending\\"}\\n \\tfield: size\\n \\t}\\n \\t// calculate vertical center point for each node, used to draw edges\\n \\t{type: \\"formula\\", expr: \\"(datum.y0+datum.y1)/2\\", as: \\"yc\\"}\\n \\t]\\n\\t}\\n\\t{\\n \\tname: groups\\n \\tsource: nodes\\n \\ttransform: [\\n \\t// combine all nodes into country groups, summing up the doc counts\\n \\t{\\n \\ttype: aggregate\\n \\tgroupby: [\\"stack\\", \\"grpId\\"]\\n \\tfields: [\\"size\\"]\\n \\tops: [\\"sum\\"]\\n \\tas: [\\"total\\"]\\n \\t}\\n \\t// re-calculate the stacking y0,y1 values\\n \\t{\\n \\ttype: stack\\n \\tgroupby: [\\"stack\\"]\\n \\tsort: {field: \\"grpId\\", order: \\"descending\\"}\\n \\tfield: total\\n \\t}\\n \\t// project y0 and y1 values to screen coordinates\\n \\t// doing it once here instead of doing it several times in marks\\n \\t{type: \\"formula\\", expr: \\"scale(\'y\', datum.y0)\\", as: \\"scaledY0\\"}\\n \\t{type: \\"formula\\", expr: \\"scale(\'y\', datum.y1)\\", as: \\"scaledY1\\"}\\n \\t// boolean flag if the label should be on the right of the stack\\n \\t{type: \\"formula\\", expr: \\"datum.stack == \'stk1\'\\", as: \\"rightLabel\\"}\\n \\t// Calculate traffic percentage for this country using \\"y\\" scale\\n \\t// domain upper bound, which represents the total traffic\\n \\t{\\n \\ttype: formula\\n \\texpr: datum.total/domain(\'y\')[1]\\n \\tas: percentage\\n \\t}\\n \\t]\\n\\t}\\n\\t{\\n \\t// This is a temp lookup table with all the \'stk2\' stack nodes\\n \\tname: destinationNodes\\n \\tsource: nodes\\n \\ttransform: [\\n \\t{type: \\"filter\\", expr: \\"datum.stack == \'stk2\'\\"}\\n \\t]\\n\\t}\\n\\t{\\n \\tname: edges\\n \\tsource: nodes\\n \\ttransform: [\\n \\t// we only want nodes from the left stack\\n \\t{type: \\"filter\\", expr: \\"datum.stack == \'stk1\'\\"}\\n \\t// find corresponding node from the right stack, keep it as \\"target\\"\\n \\t{\\n \\ttype: lookup\\n \\tfrom: destinationNodes\\n \\tkey: key\\n \\tfields: [\\"key\\"]\\n \\tas: [\\"target\\"]\\n \\t}\\n \\t// calculate SVG link path between stk1 and stk2 stacks for the node pair\\n \\t{\\n \\ttype: linkpath\\n \\torient: horizontal\\n \\tshape: diagonal\\n \\tsourceY: {expr: \\"scale(\'y\', datum.yc)\\"}\\n \\tsourceX: {expr: \\"scale(\'x\', \'stk1\') + bandwidth(\'x\')\\"}\\n \\ttargetY: {expr: \\"scale(\'y\', datum.target.yc)\\"}\\n \\ttargetX: {expr: \\"scale(\'x\', \'stk2\')\\"}\\n \\t}\\n \\t// A little trick to calculate the thickness of the line.\\n \\t// The value needs to be the same as the hight of the node, but scaling\\n \\t// size to screen\'s height gives inversed value because screen\'s Y\\n \\t// coordinate goes from the top to the bottom, whereas the graph\'s Y=0\\n \\t// is at the bottom. So subtracting scaled doc count from screen height\\n \\t// (which is the \\"lower\\" bound of the \\"y\\" scale) gives us the right value\\n \\t{\\n \\ttype: formula\\n \\texpr: range(\'y\')[0]-scale(\'y\', datum.size)\\n \\tas: strokeWidth\\n \\t}\\n \\t// Tooltip needs individual link\'s percentage of all traffic\\n \\t{\\n \\ttype: formula\\n \\texpr: datum.size/domain(\'y\')[1]\\n \\tas: percentage\\n \\t}\\n \\t]\\n\\t}\\n ]\\n scales: [\\n\\t{\\n \\t// calculates horizontal stack positioning\\n \\tname: x\\n \\ttype: band\\n \\trange: width\\n \\tdomain: [\\"stk1\\", \\"stk2\\"]\\n \\tpaddingOuter: 0.05\\n \\tpaddingInner: 0.95\\n\\t}\\n\\t{\\n \\t// this scale goes up as high as the highest y1 value of all nodes\\n \\tname: y\\n \\ttype: linear\\n \\trange: height\\n \\tdomain: {data: \\"nodes\\", field: \\"y1\\"}\\n\\t}\\n\\t{\\n \\t// use rawData to ensure the colors stay the same when clicking.\\n \\tname: color\\n \\ttype: ordinal\\n \\trange: category\\n \\tdomain: {data: \\"rawData\\", field: \\"stk1\\"}\\n\\t}\\n\\t{\\n \\t// this scale is used to map internal ids (stk1, stk2) to stack names\\n \\tname: stackNames\\n \\ttype: ordinal\\n \\trange: [\\"Source\\", \\"Destination\\"]\\n \\tdomain: [\\"stk1\\", \\"stk2\\"]\\n\\t}\\n ]\\n axes: [\\n\\t{\\n \\t// x axis should use custom label formatting to print proper stack names\\n \\torient: bottom\\n \\tscale: x\\n \\tencode: {\\n \\tlabels: {\\n \\tupdate: {\\n \\ttext: {scale: \\"stackNames\\", field: \\"value\\"}\\n \\t}\\n \\t}\\n \\t}\\n\\t}\\n\\t{orient: \\"left\\", scale: \\"y\\"}\\n ]\\n marks: [\\n\\t{\\n \\t// draw the connecting line between stacks\\n \\ttype: path\\n \\tname: edgeMark\\n \\tfrom: {data: \\"edges\\"}\\n \\t// this prevents some autosizing issues with large strokeWidth for paths\\n \\tclip: true\\n \\tencode: {\\n \\tupdate: {\\n \\t// By default use color of the left node, except when showing traffic\\n \\t// from just one country, in which case use destination color.\\n \\tstroke: [\\n \\t{\\n \\ttest: groupSelector && groupSelector.stack==\'stk1\'\\n \\tscale: color\\n \\tfield: stk2\\n \\t}\\n \\t{scale: \\"color\\", field: \\"stk1\\"}\\n \\t]\\n \\tstrokeWidth: {field: \\"strokeWidth\\"}\\n \\tpath: {field: \\"path\\"}\\n \\t// when showing all traffic, and hovering over a country,\\n \\t// highlight the traffic from that country.\\n \\tstrokeOpacity: {\\n \\tsignal: !groupSelector && (groupHover.stk1 == datum.stk1 || groupHover.stk2 == datum.stk2) ? 0.9 : 0.3\\n \\t}\\n \\t// Ensure that the hover-selected edges show on top\\n \\tzindex: {\\n \\tsignal: !groupSelector && (groupHover.stk1 == datum.stk1 || groupHover.stk2 == datum.stk2) ? 1 : 0\\n \\t}\\n \\t// format tooltip string\\n \\ttooltip: {\\n \\tsignal: datum.stk1 + \' → \' + datum.stk2 + \'\\t\' + format(datum.size, \',.0f\') + \' (\' + format(datum.percentage, \'.1%\') + \')\'\\n \\t}\\n \\t}\\n \\t// Simple mouseover highlighting of a single line\\n \\thover: {\\n \\tstrokeOpacity: {value: 1}\\n \\t}\\n \\t}\\n\\t}\\n\\t{\\n \\t// draw stack groups (countries)\\n \\ttype: rect\\n \\tname: groupMark\\n \\tfrom: {data: \\"groups\\"}\\n \\tencode: {\\n \\tenter: {\\n \\tfill: {scale: \\"color\\", field: \\"grpId\\"}\\n \\twidth: {scale: \\"x\\", band: 1}\\n \\t}\\n \\tupdate: {\\n \\tx: {scale: \\"x\\", field: \\"stack\\"}\\n \\ty: {field: \\"scaledY0\\"}\\n \\ty2: {field: \\"scaledY1\\"}\\n \\tfillOpacity: {value: 0.6}\\n \\ttooltip: {\\n \\tsignal: datum.grpId + \' \' + format(datum.total, \',.0f\') + \' (\' + format(datum.percentage, \'.1%\') + \')\'\\n \\t}\\n \\t}\\n \\thover: {\\n \\tfillOpacity: {value: 1}\\n \\t}\\n \\t}\\n\\t}\\n\\t{\\n \\t// draw country code labels on the inner side of the stack\\n \\ttype: text\\n \\tfrom: {data: \\"groups\\"}\\n \\t// don\'t process events for the labels - otherwise line mouseover is unclean\\n \\tinteractive: false\\n \\tencode: {\\n \\tupdate: {\\n \\t// depending on which stack it is, position x with some padding\\n \\tx: {\\n \\tsignal: scale(\'x\', datum.stack) + (datum.rightLabel ? bandwidth(\'x\') + 8 : -8)\\n \\t}\\n \\t// middle of the group\\n \\tyc: {signal: \\"(datum.scaledY0 + datum.scaledY1)/2\\"}\\n \\talign: {signal: \\"datum.rightLabel ? \'left\' : \'right\'\\"}\\n \\tbaseline: {value: \\"middle\\"}\\n \\tfontWeight: {value: \\"bold\\"}\\n \\t// only show text label if the group\'s height is large enough\\n \\ttext: {signal: \\"abs(datum.scaledY0-datum.scaledY1) > 13 ? datum.grpId : \'\'\\"}\\n \\t}\\n \\t}\\n\\t}\\n\\t{\\n \\t// Create a \\"show all\\" button. Shown only when a country is selected.\\n \\ttype: group\\n \\tdata: [\\n \\t// We need to make the button show only when groupSelector signal is true.\\n \\t// Each mark is drawn as many times as there are elements in the backing data.\\n \\t// Which means that if values list is empty, it will not be drawn.\\n \\t// Here I create a data source with one empty object, and filter that list\\n \\t// based on the signal value. This can only be done in a group.\\n \\t{\\n \\tname: dataForShowAll\\n \\tvalues: [{}]\\n \\ttransform: [{type: \\"filter\\", expr: \\"groupSelector\\"}]\\n \\t}\\n \\t]\\n \\t// Set button size and positioning\\n \\tencode: {\\n \\tenter: {\\n \\txc: {signal: \\"width/2\\"}\\n \\ty: {value: 30}\\n \\twidth: {value: 80}\\n \\theight: {value: 30}\\n \\t}\\n \\t}\\n \\tmarks: [\\n \\t{\\n \\t// This group is shown as a button with rounded corners.\\n \\ttype: group\\n \\t// mark name allows signal capturing\\n \\tname: groupReset\\n \\t// Only shows button if dataForShowAll has values.\\n \\tfrom: {data: \\"dataForShowAll\\"}\\n \\tencode: {\\n \\tenter: {\\n \\tcornerRadius: {value: 6}\\n \\tfill: {value: \\"#F5F7FA\\"}\\n \\tstroke: {value: \\"#c1c1c1\\"}\\n \\tstrokeWidth: {value: 2}\\n \\t// use parent group\'s size\\n \\theight: {\\n \\tfield: {group: \\"height\\"}\\n \\t}\\n \\twidth: {\\n \\tfield: {group: \\"width\\"}\\n \\t}\\n \\t}\\n \\tupdate: {\\n \\t// groups are transparent by default\\n \\topacity: {value: 1}\\n \\t}\\n \\thover: {\\n \\topacity: {value: 0.7}\\n \\t}\\n \\t}\\n \\tmarks: [\\n \\t{\\n \\ttype: text\\n \\t// if true, it will prevent clicking on the button when over text.\\n \\tinteractive: false\\n \\tencode: {\\n \\tenter: {\\n \\t// center text in the paren group\\n \\txc: {\\n \\tfield: {group: \\"width\\"}\\n \\tmult: 0.5\\n \\t}\\n \\tyc: {\\n \\tfield: {group: \\"height\\"}\\n \\tmult: 0.5\\n \\toffset: 2\\n \\t}\\n \\talign: {value: \\"center\\"}\\n \\tbaseline: {value: \\"middle\\"}\\n \\tfontWeight: {value: \\"bold\\"}\\n \\ttext: {value: \\"Show All\\"}\\n \\t}\\n \\t}\\n \\t}\\n \\t]\\n \\t}\\n \\t]\\n\\t}\\n ]\\n signals: [\\n\\t{\\n \\t// used to highlight traffic to/from the same country\\n \\tname: groupHover\\n \\tvalue: {}\\n \\ton: [\\n \\t{\\n \\tevents: @groupMark:mouseover\\n \\tupdate: \\"{stk1:datum.stack==\'stk1\' && datum.grpId, stk2:datum.stack==\'stk2\' && datum.grpId}\\"\\n \\t}\\n \\t{events: \\"mouseout\\", update: \\"{}\\"}\\n \\t]\\n\\t}\\n\\t// used to filter only the data related to the selected country\\n\\t{\\n \\tname: groupSelector\\n \\tvalue: false\\n \\ton: [\\n \\t{\\n \\t// Clicking groupMark sets this signal to the filter values\\n \\tevents: @groupMark:click!\\n \\tupdate: \\"{stack:datum.stack, stk1:datum.stack==\'stk1\' && datum.grpId, stk2:datum.stack==\'stk2\' && datum.grpId}\\"\\n \\t}\\n \\t{\\n \\t// Clicking \\"show all\\" button, or double-clicking anywhere resets it\\n \\tevents: [\\n \\t{type: \\"click\\", markname: \\"groupReset\\"}\\n \\t{type: \\"dblclick\\"}\\n \\t]\\n \\tupdate: \\"false\\"\\n \\t}\\n \\t]\\n\\t}\\n ]\\n}\\n"},"aggs":[]}', uiStateJSON: '{}', description: '', version: 1, diff --git a/src/plugins/saved_objects_management/public/lib/import_legacy_file.test.ts b/src/plugins/saved_objects_management/public/lib/import_legacy_file.test.ts deleted file mode 100644 index 2554c78514de3a..00000000000000 --- a/src/plugins/saved_objects_management/public/lib/import_legacy_file.test.ts +++ /dev/null @@ -1,26 +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 { importLegacyFile } from './import_legacy_file'; - -describe('importFile', () => { - it('should import a file with valid json format', async () => { - const file = new File([`{"text": "foo"}`], 'file.json'); - - const imported = await importLegacyFile(file); - expect(imported).toEqual({ text: 'foo' }); - }); - - it('should throw errors when file content is not parseable', async () => { - const file = new File([`not_parseable`], 'file.json'); - - await expect(importLegacyFile(file)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Unexpected token o in JSON at position 1"` - ); - }); -}); diff --git a/src/plugins/saved_objects_management/public/lib/import_legacy_file.ts b/src/plugins/saved_objects_management/public/lib/import_legacy_file.ts deleted file mode 100644 index 3e605154f5339d..00000000000000 --- a/src/plugins/saved_objects_management/public/lib/import_legacy_file.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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 async function importLegacyFile(file: File) { - return new Promise((resolve, reject) => { - const fr = new FileReader(); - fr.onload = (event) => { - const result = event.target!.result as string; - try { - resolve(JSON.parse(result)); - } catch (e) { - reject(e); - } - }; - fr.readAsText(file); - }); -} diff --git a/src/plugins/saved_objects_management/public/lib/index.ts b/src/plugins/saved_objects_management/public/lib/index.ts index df1485bedfc695..aefa5b614f618e 100644 --- a/src/plugins/saved_objects_management/public/lib/index.ts +++ b/src/plugins/saved_objects_management/public/lib/index.ts @@ -13,17 +13,8 @@ export { getRelationships } from './get_relationships'; export { getSavedObjectCounts } from './get_saved_object_counts'; export { getSavedObjectLabel } from './get_saved_object_label'; export { importFile } from './import_file'; -export { importLegacyFile } from './import_legacy_file'; export { parseQuery } from './parse_query'; export { resolveImportErrors } from './resolve_import_errors'; -export { - resolveIndexPatternConflicts, - resolveSavedObjects, - resolveSavedSearches, - saveObject, - saveObjects, -} from './resolve_saved_objects'; -export { logLegacyImport } from './log_legacy_import'; export { processImportResponse, ProcessedImportResponse, diff --git a/src/plugins/saved_objects_management/public/lib/log_legacy_import.ts b/src/plugins/saved_objects_management/public/lib/log_legacy_import.ts deleted file mode 100644 index 70120f887afef4..00000000000000 --- a/src/plugins/saved_objects_management/public/lib/log_legacy_import.ts +++ /dev/null @@ -1,13 +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 { HttpStart } from 'src/core/public'; - -export async function logLegacyImport(http: HttpStart) { - return http.post('/api/saved_objects/_log_legacy_import'); -} diff --git a/src/plugins/saved_objects_management/public/lib/process_import_response.ts b/src/plugins/saved_objects_management/public/lib/process_import_response.ts index d7da441ce92309..67c66bce1b3311 100644 --- a/src/plugins/saved_objects_management/public/lib/process_import_response.ts +++ b/src/plugins/saved_objects_management/public/lib/process_import_response.ts @@ -40,8 +40,6 @@ export interface ProcessedImportResponse { unmatchedReferences: UnmatchedReference[]; status: 'success' | 'idle'; importCount: number; - conflictedSavedObjectsLinkedToSavedSearches: undefined; - conflictedSearchDocs: undefined; importWarnings: SavedObjectsImportWarning[]; } @@ -87,8 +85,6 @@ export function processImportResponse( ? 'success' : 'idle', importCount: response.successCount, - conflictedSavedObjectsLinkedToSavedSearches: undefined, - conflictedSearchDocs: undefined, importWarnings: response.warnings, }; } diff --git a/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.test.ts b/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.test.ts deleted file mode 100644 index b1c052e56ef226..00000000000000 --- a/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.test.ts +++ /dev/null @@ -1,399 +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 { - resolveSavedObjects, - resolveIndexPatternConflicts, - saveObjects, - saveObject, -} from './resolve_saved_objects'; -import { SavedObject, SavedObjectLoader } from '../../../saved_objects/public'; -import { IndexPatternsContract } from '../../../data/public'; -import { dataPluginMock } from '../../../data/public/mocks'; - -class SavedObjectNotFound extends Error { - constructor(options: Record) { - super(); - for (const option in options) { - if (options.hasOwnProperty(option)) { - (this as any)[option] = options[option]; - } - } - } -} - -const openModalMock = jest.fn(); - -const createObj = (props: Partial): SavedObject => - ({ - ...props, - } as SavedObject); - -describe('resolveSavedObjects', () => { - describe('resolveSavedObjects', () => { - it('should take in saved objects and spit out conflicts', async () => { - const savedObjects = [ - { - _type: 'search', - }, - { - _type: 'index-pattern', - _id: '1', - _source: { - title: 'pattern', - timeFieldName: '@timestamp', - }, - }, - { - _type: 'dashboard', - }, - { - _type: 'visualization', - }, - ]; - - const indexPatterns = ({ - get: async () => { - return { - create: () => '2', - }; - }, - create: async () => { - return '2'; - }, - cache: { - clear: () => {}, - }, - } as unknown) as IndexPatternsContract; - - const services = ([ - { - type: 'search', - get: async () => { - return { - applyESResp: async () => {}, - save: async () => { - throw new SavedObjectNotFound({ - savedObjectType: 'index-pattern', - }); - }, - }; - }, - }, - { - type: 'dashboard', - get: async () => { - return { - applyESResp: async () => {}, - save: async () => { - throw new SavedObjectNotFound({ - savedObjectType: 'index-pattern', - }); - }, - }; - }, - }, - { - type: 'visualization', - get: async () => { - return { - applyESResp: async () => {}, - save: async () => { - throw new SavedObjectNotFound({ - savedObjectType: 'index-pattern', - }); - }, - }; - }, - }, - ] as unknown) as SavedObjectLoader[]; - - const overwriteAll = false; - - const result = await resolveSavedObjects( - savedObjects, - overwriteAll, - services, - indexPatterns, - openModalMock - ); - - expect(result.conflictedIndexPatterns.length).toBe(3); - expect(result.conflictedSavedObjectsLinkedToSavedSearches.length).toBe(0); - expect(result.conflictedSearchDocs.length).toBe(0); - }); - - it('should bucket conflicts based on the type', async () => { - const savedObjects = [ - { - _type: 'search', - }, - { - _type: 'index-pattern', - _id: '1', - _source: { - title: 'pattern', - timeFieldName: '@timestamp', - }, - }, - { - _type: 'dashboard', - }, - { - _type: 'visualization', - }, - ]; - - const indexPatterns = ({ - get: async () => { - return { - create: () => '2', - }; - }, - create: async () => { - return '2'; - }, - cache: { - clear: () => {}, - }, - } as unknown) as IndexPatternsContract; - - const services = ([ - { - type: 'search', - get: async () => { - return { - applyESResp: async () => {}, - save: async () => { - throw new SavedObjectNotFound({ - savedObjectType: 'search', - }); - }, - }; - }, - }, - { - type: 'dashboard', - get: async () => { - return { - applyESResp: async () => {}, - save: async () => { - throw new SavedObjectNotFound({ - savedObjectType: 'index-pattern', - }); - }, - }; - }, - }, - { - type: 'visualization', - get: async () => { - return { - savedSearchId: '1', - applyESResp: async () => {}, - save: async () => { - throw new SavedObjectNotFound({ - savedObjectType: 'index-pattern', - }); - }, - }; - }, - }, - ] as unknown) as SavedObjectLoader[]; - - const overwriteAll = false; - - const result = await resolveSavedObjects( - savedObjects, - overwriteAll, - services, - indexPatterns, - openModalMock - ); - - expect(result.conflictedIndexPatterns.length).toBe(1); - expect(result.conflictedSavedObjectsLinkedToSavedSearches.length).toBe(1); - expect(result.conflictedSearchDocs.length).toBe(1); - }); - }); - - describe('resolveIndexPatternConflicts', () => { - let dependencies: Parameters[3]; - - beforeEach(() => { - const search = dataPluginMock.createStartContract().search; - - dependencies = { - indexPatterns: ({ - get: (id: string) => Promise.resolve({ id }), - } as unknown) as IndexPatternsContract, - search, - }; - }); - - it('should resave resolutions', async () => { - const save = jest.fn(); - - const conflictedIndexPatterns = ([ - { - obj: { - save, - }, - doc: { - _source: { - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: '1', - }), - }, - }, - }, - }, - { - obj: { - save, - }, - doc: { - _source: { - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: '3', - }), - }, - }, - }, - }, - ] as unknown) as Array<{ obj: SavedObject; doc: any }>; - - const resolutions = [ - { - oldId: '1', - newId: '2', - }, - { - oldId: '3', - newId: '4', - }, - { - oldId: '5', - newId: '5', - }, - ]; - - const overwriteAll = false; - - await resolveIndexPatternConflicts( - resolutions, - conflictedIndexPatterns, - overwriteAll, - dependencies - ); - - expect(save.mock.calls.length).toBe(2); - expect(save).toHaveBeenCalledWith({ confirmOverwrite: !overwriteAll }); - }); - - it('should resolve filter index conflicts', async () => { - const save = jest.fn(); - - const conflictedIndexPatterns = ([ - { - obj: { - save, - }, - doc: { - _source: { - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: '1', - filter: [{ meta: { index: 'filterIndex' } }], - }), - }, - }, - }, - }, - { - obj: { - save, - }, - doc: { - _source: { - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: '3', - }), - }, - }, - }, - }, - ] as unknown) as Array<{ obj: SavedObject; doc: any }>; - - const resolutions = [ - { - oldId: '1', - newId: '2', - }, - { - oldId: '3', - newId: '4', - }, - { - oldId: 'filterIndex', - newId: 'newFilterIndex', - }, - ]; - - const overwriteAll = false; - - await resolveIndexPatternConflicts( - resolutions, - conflictedIndexPatterns, - overwriteAll, - dependencies - ); - - expect(save.mock.calls.length).toBe(2); - }); - }); - - describe('saveObjects', () => { - it('should save every object', async () => { - const save = jest.fn(); - - const objs = [ - createObj({ - save, - }), - createObj({ - save, - }), - ]; - - const overwriteAll = false; - - await saveObjects(objs, overwriteAll); - expect(save.mock.calls.length).toBe(2); - expect(save).toHaveBeenCalledWith({ confirmOverwrite: !overwriteAll }); - }); - }); - - describe('saveObject', () => { - it('should save the object', async () => { - const save = jest.fn(); - const obj = createObj({ - save, - }); - - const overwriteAll = false; - - await saveObject(obj, overwriteAll); - expect(save.mock.calls.length).toBe(1); - expect(save).toHaveBeenCalledWith({ confirmOverwrite: !overwriteAll }); - }); - }); -}); diff --git a/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts b/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts deleted file mode 100644 index 95bd41745553d9..00000000000000 --- a/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts +++ /dev/null @@ -1,353 +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 { i18n } from '@kbn/i18n'; -import { cloneDeep } from 'lodash'; -import { OverlayStart, SavedObjectReference } from 'src/core/public'; -import { SavedObject, SavedObjectLoader } from '../../../saved_objects/public'; -import { - DataPublicPluginStart, - IndexPatternsContract, - injectSearchSourceReferences, - IndexPatternSpec, -} from '../../../data/public'; -import { FailedImport } from './process_import_response'; -import { DuplicateDataViewError, IndexPattern } from '../../../data/public'; - -type SavedObjectsRawDoc = Record; - -async function getSavedObject(doc: SavedObjectsRawDoc, services: SavedObjectLoader[]) { - const service = services.find((s) => s.type === doc._type); - if (!service) { - return; - } - - const obj = await service.get(); - obj.id = doc._id; - obj.migrationVersion = doc._migrationVersion; - return obj; -} - -function addJsonFieldToIndexPattern( - target: Record, - sourceString: string, - fieldName: string, - indexName: string -) { - if (sourceString) { - try { - target[fieldName] = JSON.parse(sourceString); - } catch (error) { - throw new Error( - i18n.translate('savedObjectsManagement.parsingFieldErrorMessage', { - defaultMessage: - 'Error encountered parsing {fieldName} for index pattern {indexName}: {errorMessage}', - values: { - fieldName, - indexName, - errorMessage: error.message, - }, - }) - ); - } - } -} -async function importIndexPattern( - doc: SavedObjectsRawDoc, - indexPatterns: IndexPatternsContract, - overwriteAll: boolean = false, - openConfirm: OverlayStart['openConfirm'] -) { - // TODO: consolidate this is the code in create_index_pattern_wizard.js - const { - title, - timeFieldName, - fields, - fieldFormatMap, - sourceFilters, - type, - typeMeta, - } = doc._source; - const indexPatternSpec: IndexPatternSpec = { - id: doc._id, - title, - timeFieldName, - }; - let emptyPattern: IndexPattern; - if (type) { - indexPatternSpec.type = type; - } - addJsonFieldToIndexPattern(indexPatternSpec, fields, 'fields', title); - addJsonFieldToIndexPattern(indexPatternSpec, fieldFormatMap, 'fieldFormatMap', title); - addJsonFieldToIndexPattern(indexPatternSpec, sourceFilters, 'sourceFilters', title); - addJsonFieldToIndexPattern(indexPatternSpec, typeMeta, 'typeMeta', title); - try { - emptyPattern = await indexPatterns.createAndSave(indexPatternSpec, overwriteAll, true); - } catch (err) { - if (err instanceof DuplicateDataViewError) { - // We can override and we want to prompt for confirmation - const isConfirmed = await openConfirm( - i18n.translate('savedObjectsManagement.indexPattern.confirmOverwriteLabel', { - values: { title }, - defaultMessage: "Are you sure you want to overwrite '{title}'?", - }), - { - title: i18n.translate('savedObjectsManagement.indexPattern.confirmOverwriteTitle', { - defaultMessage: 'Overwrite {type}?', - values: { type }, - }), - confirmButtonText: i18n.translate( - 'savedObjectsManagement.indexPattern.confirmOverwriteButton', - { - defaultMessage: 'Overwrite', - } - ), - } - ); - - if (isConfirmed) { - emptyPattern = await indexPatterns.createAndSave(indexPatternSpec, true, true); - } else { - return; - } - } - } - - indexPatterns.clearCache(emptyPattern!.id); - return emptyPattern!.id; -} - -async function importDocument(obj: SavedObject, doc: SavedObjectsRawDoc, overwriteAll: boolean) { - await obj.applyESResp({ - references: doc._references || [], - ...cloneDeep(doc), - }); - return await obj.save({ confirmOverwrite: !overwriteAll }); -} - -function groupByType(docs: SavedObjectsRawDoc[]): Record { - const defaultDocTypes = { - searches: [], - indexPatterns: [], - other: [], - } as Record; - - return docs.reduce((types, doc) => { - switch (doc._type) { - case 'search': - types.searches.push(doc); - break; - case 'index-pattern': - types.indexPatterns.push(doc); - break; - default: - types.other.push(doc); - } - return types; - }, defaultDocTypes); -} - -export async function resolveIndexPatternConflicts( - resolutions: Array<{ oldId: string; newId: string }>, - conflictedIndexPatterns: any[], - overwriteAll: boolean, - dependencies: { - indexPatterns: IndexPatternsContract; - search: DataPublicPluginStart['search']; - } -) { - let importCount = 0; - - for (const { obj, doc } of conflictedIndexPatterns) { - const serializedSearchSource = JSON.parse( - doc._source.kibanaSavedObjectMeta?.searchSourceJSON || '{}' - ); - const oldIndexId = serializedSearchSource.index; - let allResolved = true; - const inlineResolution = resolutions.find(({ oldId }) => oldId === oldIndexId); - if (inlineResolution) { - serializedSearchSource.index = inlineResolution.newId; - } else { - allResolved = false; - } - - // Resolve filter index reference: - const filter = (serializedSearchSource.filter || []).map((f: any) => { - if (!(f.meta && f.meta.index)) { - return f; - } - - const resolution = resolutions.find(({ oldId }) => oldId === f.meta.index); - return resolution ? { ...f, ...{ meta: { ...f.meta, index: resolution.newId } } } : f; - }); - - if (filter.length > 0) { - serializedSearchSource.filter = filter; - } - - const replacedReferences = (doc._references || []).map((reference: SavedObjectReference) => { - const resolution = resolutions.find(({ oldId }) => oldId === reference.id); - if (resolution) { - return { ...reference, id: resolution.newId }; - } else { - allResolved = false; - } - - return reference; - }); - - const serializedSearchSourceWithInjectedReferences = injectSearchSourceReferences( - serializedSearchSource, - replacedReferences - ); - - if (!allResolved) { - // The user decided to skip this conflict so do nothing - continue; - } - obj.searchSource = await dependencies.search.searchSource.create( - serializedSearchSourceWithInjectedReferences - ); - if (await saveObject(obj, overwriteAll)) { - importCount++; - } - } - return importCount; -} - -export async function saveObjects(objs: SavedObject[], overwriteAll: boolean) { - let importCount = 0; - for (const obj of objs) { - if (await saveObject(obj, overwriteAll)) { - importCount++; - } - } - return importCount; -} - -export async function saveObject(obj: SavedObject, overwriteAll: boolean) { - return await obj.save({ confirmOverwrite: !overwriteAll }); -} - -export async function resolveSavedSearches( - savedSearches: any[], - services: SavedObjectLoader[], - indexPatterns: IndexPatternsContract, - overwriteAll: boolean -) { - let importCount = 0; - for (const searchDoc of savedSearches) { - const obj = await getSavedObject(searchDoc, services); - if (!obj) { - // Just ignore? - continue; - } - if (await importDocument(obj, searchDoc, overwriteAll)) { - importCount++; - } - } - return importCount; -} - -export async function resolveSavedObjects( - savedObjects: SavedObjectsRawDoc[], - overwriteAll: boolean, - services: SavedObjectLoader[], - indexPatterns: IndexPatternsContract, - confirmModalPromise: OverlayStart['openConfirm'] -) { - const docTypes = groupByType(savedObjects); - - // Keep track of how many we actually import because the user - // can cancel an override - let importedObjectCount = 0; - const failedImports: FailedImport[] = []; - // Start with the index patterns since everything is dependent on them - // As the confirmation opens a modal, and as we only allow one modal at a time - // (opening a new one close the previous with a rejection) - // we can't do that in parallel - for (const indexPatternDoc of docTypes.indexPatterns) { - try { - const importedIndexPatternId = await importIndexPattern( - indexPatternDoc, - indexPatterns, - overwriteAll, - confirmModalPromise - ); - if (importedIndexPatternId) { - importedObjectCount++; - } - } catch (error) { - failedImports.push({ obj: indexPatternDoc as any, error }); - } - } - - // We want to do the same for saved searches, but we want to keep them separate because they need - // to be applied _first_ because other saved objects can be dependent on those saved searches existing - const conflictedSearchDocs: any[] = []; - // Keep a record of the index patterns assigned to our imported saved objects that do not - // exist. We will provide a way for the user to manually select a new index pattern for those - // saved objects. - const conflictedIndexPatterns: any[] = []; - // Keep a record of any objects which fail to import for unknown reasons. - - // It's possible to have saved objects that link to saved searches which then link to index patterns - // and those could error out, but the error comes as an index pattern not found error. We can't resolve - // those the same as way as normal index pattern not found errors, but when those are fixed, it's very - // likely that these saved objects will work once resaved so keep them around to resave them. - const conflictedSavedObjectsLinkedToSavedSearches: any[] = []; - - for (const searchDoc of docTypes.searches) { - const obj = await getSavedObject(searchDoc, services); - - try { - if (await importDocument(obj, searchDoc, overwriteAll)) { - importedObjectCount++; - } - } catch (error) { - if (error.constructor.name === 'SavedObjectNotFound') { - if (error.savedObjectType === 'index-pattern') { - conflictedIndexPatterns.push({ obj, doc: searchDoc }); - } else { - conflictedSearchDocs.push(searchDoc); - } - } else { - failedImports.push({ obj, error }); - } - } - } - - for (const otherDoc of docTypes.other) { - const obj = await getSavedObject(otherDoc, services); - - try { - if (await importDocument(obj, otherDoc, overwriteAll)) { - importedObjectCount++; - } - } catch (error) { - const isIndexPatternNotFound = - error.constructor.name === 'SavedObjectNotFound' && - error.savedObjectType === 'index-pattern'; - if (isIndexPatternNotFound && obj.savedSearchId) { - conflictedSavedObjectsLinkedToSavedSearches.push(obj); - } else if (isIndexPatternNotFound) { - conflictedIndexPatterns.push({ obj, doc: otherDoc }); - } else { - failedImports.push({ obj, error }); - } - } - } - - return { - conflictedIndexPatterns, - conflictedSavedObjectsLinkedToSavedSearches, - conflictedSearchDocs, - importedObjectCount, - failedImports, - }; -} 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 015c7068d72b6a..1affbd4d964633 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 @@ -195,9 +195,6 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = ` "put": [MockFunction], }, "state": Object { - "conflictedIndexPatterns": undefined, - "conflictedSavedObjectsLinkedToSavedSearches": undefined, - "conflictedSearchDocs": undefined, "conflictingRecord": undefined, "error": undefined, "failedImports": Array [ diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.mocks.ts b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.mocks.ts index 7b716e1b813c98..78f12d0753b26a 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.mocks.ts +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.mocks.ts @@ -15,14 +15,3 @@ export const resolveImportErrorsMock = jest.fn(); jest.doMock('../../../lib/resolve_import_errors', () => ({ resolveImportErrors: resolveImportErrorsMock, })); - -export const resolveSavedObjectsMock = jest.fn(); -export const resolveSavedSearchesMock = jest.fn(); -export const resolveIndexPatternConflictsMock = jest.fn(); -export const saveObjectsMock = jest.fn(); -jest.doMock('../../../lib/resolve_saved_objects', () => ({ - resolveSavedObjects: resolveSavedObjectsMock, - resolveSavedSearches: resolveSavedSearchesMock, - resolveIndexPatternConflicts: resolveIndexPatternConflictsMock, - saveObjects: saveObjectsMock, -})); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx index 28190e6bd872fd..f055817f691050 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx @@ -33,7 +33,7 @@ describe('Flyout', () => { }; beforeEach(() => { - const { http, overlays } = coreMock.createStart(); + const { http } = coreMock.createStart(); const search = dataPluginMock.createStartContract().search; const basePath = httpServiceMock.createBasePath(); @@ -47,7 +47,6 @@ describe('Flyout', () => { { id: '2', attributes: {} }, ]), } as any, - overlays, http, allowedTypes: ['search', 'index-pattern', 'visualization'], serviceRegistry: serviceRegistryMock.create(), @@ -140,9 +139,6 @@ describe('Flyout', () => { overwrite: true, }); expect(component.state()).toMatchObject({ - conflictedIndexPatterns: undefined, - conflictedSavedObjectsLinkedToSavedSearches: undefined, - conflictedSearchDocs: undefined, importCount: 0, status: 'idle', error: undefined, 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 aca229b9a70ed3..26de8c5f8b25a8 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 @@ -31,7 +31,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { OverlayStart, HttpStart, IBasePath } from 'src/core/public'; +import { HttpStart, IBasePath } from 'src/core/public'; import { IndexPatternsContract, IndexPattern, @@ -59,16 +59,12 @@ export interface FlyoutProps { done: () => void; newIndexPatternUrl: string; indexPatterns: IndexPatternsContract; - overlays: OverlayStart; http: HttpStart; basePath: IBasePath; search: DataPublicPluginStart['search']; } export interface FlyoutState { - conflictedIndexPatterns?: any[]; - conflictedSavedObjectsLinkedToSavedSearches?: any[]; - conflictedSearchDocs?: any[]; unmatchedReferences?: ProcessedImportResponse['unmatchedReferences']; unmatchedReferencesTablePagination: { pageIndex: number; pageSize: number }; failedImports?: ProcessedImportResponse['failedImports']; @@ -105,9 +101,6 @@ export class Flyout extends Component { super(props); this.state = { - conflictedIndexPatterns: undefined, - conflictedSavedObjectsLinkedToSavedSearches: undefined, - conflictedSearchDocs: undefined, unmatchedReferences: undefined, unmatchedReferencesTablePagination: { pageIndex: 0, diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index 5ea433f91d1a6e..d4067cc21c2bee 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -544,7 +544,6 @@ export class SavedObjectsTable extends Component diff --git a/src/plugins/share/kibana.json b/src/plugins/share/kibana.json index 5580b723a095aa..2616b299da28d8 100644 --- a/src/plugins/share/kibana.json +++ b/src/plugins/share/kibana.json @@ -7,6 +7,7 @@ "name": "App Services", "githubTeam": "kibana-app-services" }, + "description": "Adds URL Service and sharing capabilities to Kibana", "requiredBundles": ["kibanaUtils"], "optionalPlugins": ["securityOss"] } diff --git a/src/plugins/ui_actions/kibana.json b/src/plugins/ui_actions/kibana.json index d112f6310a1fe9..ceeadc05ee91ee 100644 --- a/src/plugins/ui_actions/kibana.json +++ b/src/plugins/ui_actions/kibana.json @@ -7,5 +7,6 @@ "name": "App Services", "githubTeam": "kibana-app-services" }, + "description": "Adds UI Actions service to Kibana", "requiredBundles": ["kibanaUtils", "kibanaReact"] } diff --git a/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js b/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js index 913db9aee9c0f8..a3ae8f55135b5e 100644 --- a/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js +++ b/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js @@ -84,8 +84,7 @@ export default function ({ getService }) { .then(ensureFieldsAreSorted); }); - // https://github.com/elastic/kibana/issues/79813 - it.skip('always returns a field for all passed meta fields', async () => { + it('always returns a field for all passed meta fields', async () => { await supertest .get('/api/index_patterns/_fields_for_wildcard') .query({ @@ -95,7 +94,7 @@ export default function ({ getService }) { .expect(200, { fields: [ { - aggregatable: true, + aggregatable: false, name: '_id', esTypes: ['_id'], readFromDocValues: false, diff --git a/test/functional/apps/discover/_data_grid_doc_navigation.ts b/test/functional/apps/discover/_data_grid_doc_navigation.ts index cf5532aa6d7625..73077dcc9749a8 100644 --- a/test/functional/apps/discover/_data_grid_doc_navigation.ts +++ b/test/functional/apps/discover/_data_grid_doc_navigation.ts @@ -20,9 +20,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const defaultSettings = { defaultIndex: 'logstash-*', 'doc_table:legacy': false }; describe('discover data grid doc link', function () { - beforeEach(async function () { + before(async () => { await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/discover'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); + }); + + after(async () => { + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + }); + + beforeEach(async function () { await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await kibanaServer.uiSettings.update(defaultSettings); await PageObjects.common.navigateToApp('discover'); diff --git a/test/functional/apps/discover/_doc_navigation.ts b/test/functional/apps/discover/_doc_navigation.ts index 8d156cb305586b..19f61851ef9615 100644 --- a/test/functional/apps/discover/_doc_navigation.ts +++ b/test/functional/apps/discover/_doc_navigation.ts @@ -22,7 +22,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('doc link in discover', function contextSize() { before(async () => { await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/discover'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await kibanaServer.uiSettings.update({ 'doc_table:legacy': true, @@ -30,6 +30,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); after(async () => { + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); await kibanaServer.uiSettings.replace({}); }); diff --git a/test/functional/apps/discover/_runtime_fields_editor.ts b/test/functional/apps/discover/_runtime_fields_editor.ts index a77bc4c77568a9..fa42b0ac49617b 100644 --- a/test/functional/apps/discover/_runtime_fields_editor.ts +++ b/test/functional/apps/discover/_runtime_fields_editor.ts @@ -32,13 +32,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('discover integration with runtime fields editor', function describeIndexTests() { before(async function () { - await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); await kibanaServer.uiSettings.replace(defaultSettings); await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await PageObjects.common.navigateToApp('discover'); }); + after(async () => { + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + await kibanaServer.savedObjects.clean({ types: ['saved-search'] }); + }); + it('allows adding custom label to existing fields', async function () { const customLabel = 'megabytes'; await PageObjects.discover.editField('bytes'); diff --git a/test/functional/apps/discover/_sidebar.ts b/test/functional/apps/discover/_sidebar.ts index d8701261126c46..a74f4367e657b5 100644 --- a/test/functional/apps/discover/_sidebar.ts +++ b/test/functional/apps/discover/_sidebar.ts @@ -17,7 +17,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('discover sidebar', function describeIndexTests() { before(async function () { await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/discover'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*', }); @@ -25,6 +25,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToApp('discover'); }); + after(async () => { + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + }); + describe('field filtering', function () { it('should reveal and hide the filter form when the toggle is clicked', async function () { await PageObjects.discover.openSidebarFieldFilter(); diff --git a/test/functional/apps/getting_started/_shakespeare.ts b/test/functional/apps/getting_started/_shakespeare.ts index ae6841b85c98dd..98eeed7bcf53ee 100644 --- a/test/functional/apps/getting_started/_shakespeare.ts +++ b/test/functional/apps/getting_started/_shakespeare.ts @@ -65,6 +65,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async () => { await security.testUser.restoreDefaults(); + await esArchiver.unload('test/functional/fixtures/es_archiver/getting_started/shakespeare'); + await kibanaServer.uiSettings.replace({}); }); it('should create shakespeare index pattern', async function () { diff --git a/test/functional/apps/home/_navigation.ts b/test/functional/apps/home/_navigation.ts index a57ce0596abac9..016cead53f0c43 100644 --- a/test/functional/apps/home/_navigation.ts +++ b/test/functional/apps/home/_navigation.ts @@ -14,11 +14,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'header', 'home', 'timePicker']); const appsMenu = getService('appsMenu'); const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); describe('Kibana browser back navigation should work', function describeIndexTests() { before(async () => { - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/discover'); - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await esArchiver.load('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); + }); + + after(async () => { + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + await kibanaServer.uiSettings.replace({}); }); it('detect navigate back issues', async () => { diff --git a/test/functional/config.js b/test/functional/config.js index f477b250864317..844ebc5a90f601 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -286,7 +286,7 @@ export default async function ({ readConfigFile }) { cluster: [], indices: [ { - names: ['message_with_newline'], + names: ['newline-test'], privileges: ['read', 'view_index_metadata'], field_security: { grant: ['*'], except: [] }, }, diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 210c8f61b23915..3d2ba53e7ba985 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -284,9 +284,11 @@ export class DashboardPageObject extends FtrService { } public async clickQuickSave() { - await this.expectQuickSaveButtonEnabled(); - this.log.debug('clickQuickSave'); - await this.testSubjects.click('dashboardQuickSaveMenuItem'); + await this.retry.try(async () => { + await this.expectQuickSaveButtonEnabled(); + this.log.debug('clickQuickSave'); + await this.testSubjects.click('dashboardQuickSaveMenuItem'); + }); } public async clickNewDashboard(continueEditing = false) { @@ -392,10 +394,11 @@ export class DashboardPageObject extends FtrService { */ public async saveDashboard( dashboardName: string, - saveOptions: SaveDashboardOptions = { waitDialogIsClosed: true, exitFromEditMode: true } + saveOptions: SaveDashboardOptions = { waitDialogIsClosed: true, exitFromEditMode: true }, + clickMenuItem = true ) { await this.retry.try(async () => { - await this.enterDashboardTitleAndClickSave(dashboardName, saveOptions); + await this.enterDashboardTitleAndClickSave(dashboardName, saveOptions, clickMenuItem); if (saveOptions.needsConfirm) { await this.ensureDuplicateTitleCallout(); @@ -435,9 +438,12 @@ export class DashboardPageObject extends FtrService { */ public async enterDashboardTitleAndClickSave( dashboardTitle: string, - saveOptions: SaveDashboardOptions = { waitDialogIsClosed: true } + saveOptions: SaveDashboardOptions = { waitDialogIsClosed: true }, + clickMenuItem = true ) { - await this.testSubjects.click('dashboardSaveMenuItem'); + if (clickMenuItem) { + await this.testSubjects.click('dashboardSaveMenuItem'); + } const modalDialog = await this.testSubjects.find('savedObjectSaveModal'); this.log.debug('entering new title'); diff --git a/x-pack/plugins/apm/common/search_strategies/constants.ts b/x-pack/plugins/apm/common/search_strategies/constants.ts new file mode 100644 index 00000000000000..b1bd321e1c9145 --- /dev/null +++ b/x-pack/plugins/apm/common/search_strategies/constants.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const APM_SEARCH_STRATEGIES = { + APM_FAILED_TRANSACTIONS_CORRELATIONS: 'apmFailedTransactionsCorrelations', + APM_LATENCY_CORRELATIONS: 'apmLatencyCorrelations', +} as const; +export type ApmSearchStrategies = typeof APM_SEARCH_STRATEGIES[keyof typeof APM_SEARCH_STRATEGIES]; + +export const DEFAULT_PERCENTILE_THRESHOLD = 95; diff --git a/x-pack/plugins/apm/common/search_strategies/correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/correlations/types.ts deleted file mode 100644 index 886c5fd6161d8d..00000000000000 --- a/x-pack/plugins/apm/common/search_strategies/correlations/types.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export interface HistogramItem { - key: number; - doc_count: number; -} - -export interface ResponseHitSource { - [s: string]: unknown; -} - -export interface ResponseHit { - _source: ResponseHitSource; -} - -export interface SearchServiceParams { - environment: string; - kuery: string; - serviceName?: string; - transactionName?: string; - transactionType?: string; - start?: string; - end?: string; - percentileThreshold?: number; - analyzeCorrelations?: boolean; -} - -export interface SearchServiceFetchParams extends SearchServiceParams { - index: string; - includeFrozen?: boolean; -} - -export interface SearchServiceValue { - histogram: HistogramItem[]; - value: string; - field: string; - correlation: number; - ksTest: number; - duplicatedFields?: string[]; -} - -export interface AsyncSearchProviderProgress { - started: number; - loadedHistogramStepsize: number; - loadedOverallHistogram: number; - loadedFieldCanditates: number; - loadedFieldValuePairs: number; - loadedHistograms: number; -} - -export interface SearchServiceRawResponse { - ccsWarning: boolean; - log: string[]; - overallHistogram?: HistogramItem[]; - percentileThresholdValue?: number; - took: number; - values: SearchServiceValue[]; -} diff --git a/x-pack/plugins/apm/common/search_strategies/failure_correlations/constants.ts b/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/constants.ts similarity index 86% rename from x-pack/plugins/apm/common/search_strategies/failure_correlations/constants.ts rename to x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/constants.ts index a80918f0e399e2..09e3e22a1d352f 100644 --- a/x-pack/plugins/apm/common/search_strategies/failure_correlations/constants.ts +++ b/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/constants.ts @@ -7,9 +7,6 @@ import { i18n } from '@kbn/i18n'; -export const FAILED_TRANSACTIONS_CORRELATION_SEARCH_STRATEGY = - 'apmFailedTransactionsCorrelationsSearchStrategy'; - export const FAILED_TRANSACTIONS_IMPACT_THRESHOLD = { HIGH: i18n.translate( 'xpack.apm.correlations.failedTransactions.highImpactText', diff --git a/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts new file mode 100644 index 00000000000000..dca07e52107e70 --- /dev/null +++ b/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + FieldValuePair, + RawResponseBase, + SearchStrategyClientParams, +} from '../types'; + +import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from './constants'; + +export interface FailedTransactionsCorrelation extends FieldValuePair { + doc_count: number; + bg_count: number; + score: number; + pValue: number | null; + normalizedScore: number; + failurePercentage: number; + successPercentage: number; +} + +export type FailedTransactionsCorrelationsImpactThreshold = typeof FAILED_TRANSACTIONS_IMPACT_THRESHOLD[keyof typeof FAILED_TRANSACTIONS_IMPACT_THRESHOLD]; + +export type FailedTransactionsCorrelationsParams = SearchStrategyClientParams; + +export interface FailedTransactionsCorrelationsRawResponse + extends RawResponseBase { + log: string[]; + failedTransactionsCorrelations: FailedTransactionsCorrelation[]; +} diff --git a/x-pack/plugins/apm/common/search_strategies/failure_correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/failure_correlations/types.ts deleted file mode 100644 index 2b0d2b5642e0c4..00000000000000 --- a/x-pack/plugins/apm/common/search_strategies/failure_correlations/types.ts +++ /dev/null @@ -1,28 +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 { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from './constants'; - -export interface FailedTransactionsCorrelationValue { - key: string; - doc_count: number; - bg_count: number; - score: number; - pValue: number | null; - fieldName: string; - fieldValue: string; - normalizedScore: number; - failurePercentage: number; - successPercentage: number; -} - -export type FailureCorrelationImpactThreshold = typeof FAILED_TRANSACTIONS_IMPACT_THRESHOLD[keyof typeof FAILED_TRANSACTIONS_IMPACT_THRESHOLD]; - -export interface CorrelationsTerm { - fieldName: string; - fieldValue: string; -} diff --git a/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts new file mode 100644 index 00000000000000..29f419da350efe --- /dev/null +++ b/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + FieldValuePair, + HistogramItem, + RawResponseBase, + SearchStrategyClientParams, +} from '../types'; + +export interface LatencyCorrelation extends FieldValuePair { + correlation: number; + histogram: HistogramItem[]; + ksTest: number; +} + +export interface LatencyCorrelationSearchServiceProgress { + started: number; + loadedHistogramStepsize: number; + loadedOverallHistogram: number; + loadedFieldCandidates: number; + loadedFieldValuePairs: number; + loadedHistograms: number; +} + +export interface LatencyCorrelationsParams extends SearchStrategyClientParams { + percentileThreshold: number; + analyzeCorrelations: boolean; +} + +export interface LatencyCorrelationsRawResponse extends RawResponseBase { + log: string[]; + overallHistogram?: HistogramItem[]; + percentileThresholdValue?: number; + latencyCorrelations?: LatencyCorrelation[]; +} diff --git a/x-pack/plugins/apm/common/search_strategies/types.ts b/x-pack/plugins/apm/common/search_strategies/types.ts new file mode 100644 index 00000000000000..d7c6eab1f07c19 --- /dev/null +++ b/x-pack/plugins/apm/common/search_strategies/types.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface FieldValuePair { + fieldName: string; + // For dynamic fieldValues we only identify fields as `string`, + // but for example `http.response.status_code` which is part of + // of the list of predefined field candidates is of type long/number. + fieldValue: string | number; +} + +export interface HistogramItem { + key: number; + doc_count: number; +} + +export interface ResponseHitSource { + [s: string]: unknown; +} + +export interface ResponseHit { + _source: ResponseHitSource; +} + +export interface RawResponseBase { + ccsWarning: boolean; + took: number; +} + +export interface SearchStrategyClientParams { + environment: string; + kuery: string; + serviceName?: string; + transactionName?: string; + transactionType?: string; + start?: string; + end?: string; +} + +export interface SearchStrategyServerParams { + index: string; + includeFrozen?: boolean; +} + +export type SearchStrategyParams = SearchStrategyClientParams & + SearchStrategyServerParams; diff --git a/x-pack/plugins/apm/dev_docs/local_setup.md b/x-pack/plugins/apm/dev_docs/local_setup.md index d977f444451485..ea6741f572bad0 100644 --- a/x-pack/plugins/apm/dev_docs/local_setup.md +++ b/x-pack/plugins/apm/dev_docs/local_setup.md @@ -15,7 +15,7 @@ To access an elasticsearch instance that has live data you have two options: #### A. Connect to Elasticsearch on Cloud (internal devs only) -Find the credentials for the cluster [here](https://github.com/elastic/apm-dev/blob/master/docs/credentials/apm-ui-clusters.md#apmelstcco) +Find the credentials for the cluster [here](https://github.com/elastic/observability-dev/blob/master/docs/observability-clusters.md) #### B. Start Elastic Stack and APM data generators diff --git a/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx b/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx index f7e62b76a61c06..c700533ca4f457 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx @@ -14,28 +14,23 @@ import type { Criteria } from '@elastic/eui/src/components/basic_table/basic_tab import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { useUiTracker } from '../../../../../observability/public'; import { useTheme } from '../../../hooks/use_theme'; -import type { CorrelationsTerm } from '../../../../common/search_strategies/failure_correlations/types'; +import type { FieldValuePair } from '../../../../common/search_strategies/types'; const PAGINATION_SIZE_OPTIONS = [5, 10, 20, 50]; -export type SelectedCorrelationTerm = Pick< - T, - 'fieldName' | 'fieldValue' ->; - -interface Props { +interface CorrelationsTableProps { significantTerms?: T[]; status: FETCH_STATUS; percentageColumnName?: string; setSelectedSignificantTerm: (term: T | null) => void; - selectedTerm?: { fieldName: string; fieldValue: string }; + selectedTerm?: FieldValuePair; onFilter?: () => void; columns: Array>; onTableChange: (c: Criteria) => void; sorting?: EuiTableSortingType; } -export function CorrelationsTable({ +export function CorrelationsTable({ significantTerms, status, setSelectedSignificantTerm, @@ -43,7 +38,7 @@ export function CorrelationsTable({ selectedTerm, onTableChange, sorting, -}: Props) { +}: CorrelationsTableProps) { const euiTheme = useTheme(); const trackApmEvent = useUiTracker({ app: 'apm' }); const trackSelectSignificantCorrelationTerm = useCallback( diff --git a/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx index 4fb7bf5d6fcfb1..fc1d9a3324b247 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx @@ -6,6 +6,9 @@ */ import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import { orderBy } from 'lodash'; + import { EuiBasicTableColumn, EuiFlexGroup, @@ -18,33 +21,35 @@ import { EuiBadge, EuiToolTip, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { useHistory } from 'react-router-dom'; -import { orderBy } from 'lodash'; import type { EuiTableSortingType } from '@elastic/eui/src/components/basic_table/table_types'; import type { Direction } from '@elastic/eui/src/services/sort/sort_direction'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; + +import { i18n } from '@kbn/i18n'; +import { + enableInspectEsQueries, + useUiTracker, +} from '../../../../../observability/public'; + +import { asPercent } from '../../../../common/utils/formatters'; +import { FailedTransactionsCorrelation } from '../../../../common/search_strategies/failed_transactions_correlations/types'; +import { APM_SEARCH_STRATEGIES } from '../../../../common/search_strategies/constants'; + import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { useSearchStrategy } from '../../../hooks/use_search_strategy'; + +import { ImpactBar } from '../../shared/ImpactBar'; +import { createHref, push } from '../../shared/Links/url_helpers'; +import { Summary } from '../../shared/Summary'; + import { CorrelationsTable } from './correlations_table'; -import { enableInspectEsQueries } from '../../../../../observability/public'; -import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { FailedTransactionsCorrelationsHelpPopover } from './failed_transactions_correlations_help_popover'; -import { ImpactBar } from '../../shared/ImpactBar'; import { isErrorMessage } from './utils/is_error_message'; -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { getFailedTransactionsCorrelationImpactLabel } from './utils/get_failed_transactions_correlation_impact_label'; -import { createHref, push } from '../../shared/Links/url_helpers'; -import { useUiTracker } from '../../../../../observability/public'; -import { useFailedTransactionsCorrelationsFetcher } from '../../../hooks/use_failed_transactions_correlations_fetcher'; -import { useApmParams } from '../../../hooks/use_apm_params'; import { CorrelationsLog } from './correlations_log'; import { CorrelationsEmptyStatePrompt } from './empty_state_prompt'; import { CrossClusterSearchCompatibilityWarning } from './cross_cluster_search_warning'; import { CorrelationsProgressControls } from './progress_controls'; -import type { FailedTransactionsCorrelationValue } from '../../../../common/search_strategies/failure_correlations/types'; -import { Summary } from '../../shared/Summary'; -import { asPercent } from '../../../../common/utils/formatters'; -import { useTimeRange } from '../../../hooks/use_time_range'; export function FailedTransactionsCorrelations({ onFilter, @@ -56,78 +61,28 @@ export function FailedTransactionsCorrelations({ } = useApmPluginContext(); const trackApmEvent = useUiTracker({ app: 'apm' }); - const { serviceName, transactionType } = useApmServiceContext(); - - const { - query: { kuery, environment, rangeFrom, rangeTo }, - } = useApmParams('/services/:serviceName'); - - const { urlParams } = useUrlParams(); - const { transactionName } = urlParams; - - const { start, end } = useTimeRange({ rangeFrom, rangeTo }); - const inspectEnabled = uiSettings.get(enableInspectEsQueries); - const result = useFailedTransactionsCorrelationsFetcher(); - - const { - ccsWarning, - log, - error, - isRunning, - progress, - startFetch, - cancelFetch, - } = result; - - const startFetchHandler = useCallback(() => { - startFetch({ - environment, - kuery, - serviceName, - transactionName, - transactionType, - start, - end, - }); - }, [ - startFetch, - environment, - serviceName, - transactionName, - transactionType, - kuery, - start, - end, - ]); - - useEffect(() => { - startFetchHandler(); - return cancelFetch; - }, [cancelFetch, startFetchHandler]); + const { progress, response, startFetch, cancelFetch } = useSearchStrategy( + APM_SEARCH_STRATEGIES.APM_FAILED_TRANSACTIONS_CORRELATIONS + ); + const progressNormalized = progress.loaded / progress.total; const [ selectedSignificantTerm, setSelectedSignificantTerm, - ] = useState(null); + ] = useState(null); - const selectedTerm = useMemo(() => { - if (selectedSignificantTerm) return selectedSignificantTerm; - return result?.values && - Array.isArray(result.values) && - result.values.length > 0 - ? result?.values[0] - : undefined; - }, [selectedSignificantTerm, result]); + const selectedTerm = + selectedSignificantTerm ?? response.failedTransactionsCorrelations?.[0]; const history = useHistory(); const failedTransactionsCorrelationsColumns: Array< - EuiBasicTableColumn + EuiBasicTableColumn > = useMemo(() => { const percentageColumns: Array< - EuiBasicTableColumn + EuiBasicTableColumn > = inspectEnabled ? [ { @@ -159,7 +114,7 @@ export function FailedTransactionsCorrelations({ ), - render: (failurePercentage: number) => + render: (_, { failurePercentage }) => asPercent(failurePercentage, 1), sortable: true, }, @@ -193,7 +148,7 @@ export function FailedTransactionsCorrelations({ ), - render: (successPercentage: number) => + render: (_, { successPercentage }) => asPercent(successPercentage, 1), sortable: true, }, @@ -213,7 +168,7 @@ export function FailedTransactionsCorrelations({ )} ), - render: (normalizedScore: number) => { + render: (_, { normalizedScore }) => { return ( <> @@ -235,7 +190,7 @@ export function FailedTransactionsCorrelations({ )} ), - render: (pValue: number) => { + render: (_, { pValue }) => { const label = getFailedTransactionsCorrelationImpactLabel(pValue); return label ? ( {label.impact} @@ -252,12 +207,12 @@ export function FailedTransactionsCorrelations({ sortable: true, }, { - field: 'key', + field: 'fieldValue', name: i18n.translate( 'xpack.apm.correlations.failedTransactions.correlationsTable.fieldValueLabel', { defaultMessage: 'Field value' } ), - render: (fieldValue: string) => String(fieldValue).slice(0, 50), + render: (_, { fieldValue }) => String(fieldValue).slice(0, 50), sortable: true, }, ...percentageColumns, @@ -275,7 +230,7 @@ export function FailedTransactionsCorrelations({ ), icon: 'plusInCircle', type: 'icon', - onClick: (term: FailedTransactionsCorrelationValue) => { + onClick: (term: FailedTransactionsCorrelation) => { push(history, { query: { kuery: `${term.fieldName}:"${term.fieldValue}"`, @@ -296,7 +251,7 @@ export function FailedTransactionsCorrelations({ ), icon: 'minusInCircle', type: 'icon', - onClick: (term: FailedTransactionsCorrelationValue) => { + onClick: (term: FailedTransactionsCorrelation) => { push(history, { query: { kuery: `not ${term.fieldName}:"${term.fieldValue}"`, @@ -311,13 +266,13 @@ export function FailedTransactionsCorrelations({ 'xpack.apm.correlations.correlationsTable.actionsLabel', { defaultMessage: 'Filter' } ), - render: (_: unknown, term: FailedTransactionsCorrelationValue) => { + render: (_, { fieldName, fieldValue }) => { return ( <> @@ -327,7 +282,7 @@ export function FailedTransactionsCorrelations({ @@ -337,11 +292,11 @@ export function FailedTransactionsCorrelations({ ); }, }, - ] as Array>; + ] as Array>; }, [history, onFilter, trackApmEvent, inspectEnabled]); useEffect(() => { - if (isErrorMessage(error)) { + if (isErrorMessage(progress.error)) { notifications.toasts.addDanger({ title: i18n.translate( 'xpack.apm.correlations.failedTransactions.errorTitle', @@ -350,13 +305,13 @@ export function FailedTransactionsCorrelations({ 'An error occurred performing correlations on failed transactions', } ), - text: error.toString(), + text: progress.error.toString(), }); } - }, [error, notifications.toasts]); + }, [progress.error, notifications.toasts]); const [sortField, setSortField] = useState< - keyof FailedTransactionsCorrelationValue + keyof FailedTransactionsCorrelation >('normalizedScore'); const [sortDirection, setSortDirection] = useState('desc'); @@ -367,28 +322,32 @@ export function FailedTransactionsCorrelations({ setSortDirection(currentSortDirection); }, []); - const { sorting, correlationTerms } = useMemo(() => { - if (!Array.isArray(result.values)) { - return { correlationTerms: [], sorting: undefined }; - } - const orderedTerms = orderBy( - result.values, - // The smaller the p value the higher the impact - // So we want to sort by the normalized score here - // which goes from 0 -> 1 - sortField === 'pValue' ? 'normalizedScore' : sortField, - sortDirection - ); - return { - correlationTerms: orderedTerms, - sorting: { - sort: { - field: sortField, - direction: sortDirection, - }, - } as EuiTableSortingType, - }; - }, [result?.values, sortField, sortDirection]); + const sorting: EuiTableSortingType = { + sort: { field: sortField, direction: sortDirection }, + }; + + const correlationTerms = useMemo( + () => + orderBy( + response.failedTransactionsCorrelations, + // The smaller the p value the higher the impact + // So we want to sort by the normalized score here + // which goes from 0 -> 1 + sortField === 'pValue' ? 'normalizedScore' : sortField, + sortDirection + ), + [response.failedTransactionsCorrelations, sortField, sortDirection] + ); + + const showCorrelationsTable = + progress.isRunning || correlationTerms.length > 0; + + const showCorrelationsEmptyStatePrompt = + correlationTerms.length < 1 && + (progressNormalized === 1 || !progress.isRunning); + + const showSummaryBadge = + inspectEnabled && (progress.isRunning || correlationTerms.length > 0); return (
@@ -456,54 +415,53 @@ export function FailedTransactionsCorrelations({ - {ccsWarning && ( + {response.ccsWarning && ( <> + {/* Failed transactions correlations uses ES aggs that are available since 7.15 */} )} - {inspectEnabled && - selectedTerm?.pValue != null && - (isRunning || correlationTerms.length > 0) ? ( + {showSummaryBadge && selectedTerm?.pValue && ( <> - {`${selectedTerm.fieldName}: ${selectedTerm.key}`} + {`${selectedTerm.fieldName}: ${selectedTerm.fieldValue}`} , <>{`p-value: ${selectedTerm.pValue.toPrecision(3)}`}, ]} /> - ) : null} + )}
- {(isRunning || correlationTerms.length > 0) && ( - + {showCorrelationsTable && ( + columns={failedTransactionsCorrelationsColumns} significantTerms={correlationTerms} - status={isRunning ? FETCH_STATUS.LOADING : FETCH_STATUS.SUCCESS} + status={ + progress.isRunning ? FETCH_STATUS.LOADING : FETCH_STATUS.SUCCESS + } setSelectedSignificantTerm={setSelectedSignificantTerm} selectedTerm={selectedTerm} onTableChange={onTableChange} sorting={sorting} /> )} - {correlationTerms.length < 1 && (progress === 1 || !isRunning) && ( - - )} + {showCorrelationsEmptyStatePrompt && }
- {inspectEnabled && } + {inspectEnabled && }
); } diff --git a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx index b0da5b6d60d74c..c1fb1beb1918e1 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx @@ -18,7 +18,7 @@ import { dataPluginMock } from 'src/plugins/data/public/mocks'; import type { IKibanaSearchResponse } from 'src/plugins/data/public'; import { EuiThemeProvider } from 'src/plugins/kibana_react/common'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; -import type { SearchServiceRawResponse } from '../../../../common/search_strategies/correlations/types'; +import type { LatencyCorrelationsRawResponse } from '../../../../common/search_strategies/latency_correlations/types'; import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; import { @@ -34,7 +34,7 @@ function Wrapper({ dataSearchResponse, }: { children?: ReactNode; - dataSearchResponse: IKibanaSearchResponse; + dataSearchResponse: IKibanaSearchResponse; }) { const mockDataSearch = jest.fn(() => of(dataSearchResponse)); @@ -97,7 +97,12 @@ describe('correlations', () => { @@ -115,7 +120,12 @@ describe('correlations', () => { diff --git a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx index ad8a56a3ac6f91..1a769adb621df0 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx @@ -7,6 +7,8 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useHistory } from 'react-router-dom'; +import { orderBy } from 'lodash'; + import { EuiIcon, EuiBasicTableColumn, @@ -16,103 +18,61 @@ import { EuiTitle, EuiToolTip, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { Direction } from '@elastic/eui/src/services/sort/sort_direction'; -import { orderBy } from 'lodash'; import { EuiTableSortingType } from '@elastic/eui/src/components/basic_table/table_types'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { useTransactionLatencyCorrelationsFetcher } from '../../../hooks/use_transaction_latency_correlations_fetcher'; -import { TransactionDistributionChart } from '../../shared/charts/transaction_distribution_chart'; -import { CorrelationsTable } from './correlations_table'; -import { push } from '../../shared/Links/url_helpers'; + +import { i18n } from '@kbn/i18n'; + import { enableInspectEsQueries, useUiTracker, } from '../../../../../observability/public'; + import { asPreciseDecimal } from '../../../../common/utils/formatters'; -import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; +import { + APM_SEARCH_STRATEGIES, + DEFAULT_PERCENTILE_THRESHOLD, +} from '../../../../common/search_strategies/constants'; +import { LatencyCorrelation } from '../../../../common/search_strategies/latency_correlations/types'; + +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { useSearchStrategy } from '../../../hooks/use_search_strategy'; + +import { TransactionDistributionChart } from '../../shared/charts/transaction_distribution_chart'; +import { push } from '../../shared/Links/url_helpers'; + +import { CorrelationsTable } from './correlations_table'; import { LatencyCorrelationsHelpPopover } from './latency_correlations_help_popover'; -import { useApmParams } from '../../../hooks/use_apm_params'; import { isErrorMessage } from './utils/is_error_message'; +import { getOverallHistogram } from './utils/get_overall_histogram'; import { CorrelationsLog } from './correlations_log'; import { CorrelationsEmptyStatePrompt } from './empty_state_prompt'; import { CrossClusterSearchCompatibilityWarning } from './cross_cluster_search_warning'; import { CorrelationsProgressControls } from './progress_controls'; -import { useTimeRange } from '../../../hooks/use_time_range'; - -const DEFAULT_PERCENTILE_THRESHOLD = 95; - -interface MlCorrelationsTerms { - correlation: number; - ksTest: number; - fieldName: string; - fieldValue: string; - duplicatedFields?: string[]; -} export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { const { core: { notifications, uiSettings }, } = useApmPluginContext(); - const { serviceName, transactionType } = useApmServiceContext(); - - const { - query: { kuery, environment, rangeFrom, rangeTo }, - } = useApmParams('/services/:serviceName/transactions/view'); - - const { urlParams } = useUrlParams(); - - const { transactionName } = urlParams; - - const { start, end } = useTimeRange({ rangeFrom, rangeTo }); - const displayLog = uiSettings.get(enableInspectEsQueries); - const { - ccsWarning, - log, - error, - histograms, - percentileThresholdValue, - isRunning, - progress, - startFetch, - cancelFetch, - overallHistogram, - } = useTransactionLatencyCorrelationsFetcher(); - - const startFetchHandler = useCallback(() => { - startFetch({ - environment, - kuery, - serviceName, - transactionName, - transactionType, - start, - end, + const { progress, response, startFetch, cancelFetch } = useSearchStrategy( + APM_SEARCH_STRATEGIES.APM_LATENCY_CORRELATIONS, + { percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, - }); - }, [ - startFetch, - environment, - serviceName, - transactionName, - transactionType, - kuery, - start, - end, - ]); - - useEffect(() => { - startFetchHandler(); - return cancelFetch; - }, [cancelFetch, startFetchHandler]); + analyzeCorrelations: true, + } + ); + const progressNormalized = progress.loaded / progress.total; + const { overallHistogram, hasData, status } = getOverallHistogram( + response, + progress.isRunning + ); useEffect(() => { - if (isErrorMessage(error)) { + if (isErrorMessage(progress.error)) { notifications.toasts.addDanger({ title: i18n.translate( 'xpack.apm.correlations.latencyCorrelations.errorTitle', @@ -120,34 +80,31 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { defaultMessage: 'An error occurred fetching correlations', } ), - text: error.toString(), + text: progress.error.toString(), }); } - }, [error, notifications.toasts]); + }, [progress.error, notifications.toasts]); const [ selectedSignificantTerm, setSelectedSignificantTerm, - ] = useState(null); - - const selectedHistogram = useMemo(() => { - let selected = histograms.length > 0 ? histograms[0] : undefined; + ] = useState(null); - if (histograms.length > 0 && selectedSignificantTerm !== null) { - selected = histograms.find( + const selectedHistogram = useMemo( + () => + response.latencyCorrelations?.find( (h) => - h.field === selectedSignificantTerm.fieldName && - h.value === selectedSignificantTerm.fieldValue - ); - } - return selected; - }, [histograms, selectedSignificantTerm]); + h.fieldName === selectedSignificantTerm?.fieldName && + h.fieldValue === selectedSignificantTerm?.fieldValue + ) ?? response.latencyCorrelations?.[0], + [response.latencyCorrelations, selectedSignificantTerm] + ); const history = useHistory(); const trackApmEvent = useUiTracker({ app: 'apm' }); const mlCorrelationColumns: Array< - EuiBasicTableColumn + EuiBasicTableColumn > = useMemo( () => [ { @@ -179,7 +136,7 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { ), - render: (correlation: number) => { + render: (_, { correlation }) => { return
{asPreciseDecimal(correlation, 2)}
; }, sortable: true, @@ -198,7 +155,7 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { 'xpack.apm.correlations.latencyCorrelations.correlationsTable.fieldValueLabel', { defaultMessage: 'Field value' } ), - render: (fieldValue: string) => String(fieldValue).slice(0, 50), + render: (_, { fieldValue }) => String(fieldValue).slice(0, 50), sortable: true, }, { @@ -215,7 +172,7 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { ), icon: 'plusInCircle', type: 'icon', - onClick: (term: MlCorrelationsTerms) => { + onClick: (term: LatencyCorrelation) => { push(history, { query: { kuery: `${term.fieldName}:"${term.fieldValue}"`, @@ -236,7 +193,7 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { ), icon: 'minusInCircle', type: 'icon', - onClick: (term: MlCorrelationsTerms) => { + onClick: (term: LatencyCorrelation) => { push(history, { query: { kuery: `not ${term.fieldName}:"${term.fieldValue}"`, @@ -256,7 +213,7 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { [history, onFilter, trackApmEvent] ); - const [sortField, setSortField] = useState( + const [sortField, setSortField] = useState( 'correlation' ); const [sortDirection, setSortDirection] = useState('desc'); @@ -268,34 +225,19 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { setSortDirection(currentSortDirection); }, []); - const { histogramTerms, sorting } = useMemo(() => { - if (!Array.isArray(histograms)) { - return { histogramTerms: [], sorting: undefined }; - } - const orderedTerms = orderBy( - histograms.map((d) => { - return { - fieldName: d.field, - fieldValue: d.value, - ksTest: d.ksTest, - correlation: d.correlation, - duplicatedFields: d.duplicatedFields, - }; - }), - sortField, - sortDirection - ); - - return { - histogramTerms: orderedTerms, - sorting: { - sort: { - field: sortField, - direction: sortDirection, - }, - } as EuiTableSortingType, - }; - }, [histograms, sortField, sortDirection]); + const sorting: EuiTableSortingType = { + sort: { field: sortField, direction: sortDirection }, + }; + + const histogramTerms = useMemo( + () => orderBy(response.latencyCorrelations ?? [], sortField, sortDirection), + [response.latencyCorrelations, sortField, sortDirection] + ); + + const showCorrelationsTable = progress.isRunning || histogramTerms.length > 0; + const showCorrelationsEmptyStatePrompt = + histogramTerms.length < 1 && + (progressNormalized === 1 || !progress.isRunning); return (
@@ -321,9 +263,11 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { @@ -342,15 +286,16 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) { - {ccsWarning && ( + {response.ccsWarning && ( <> + {/* Latency correlations uses ES aggs that are available since 7.14 */} )} @@ -358,29 +303,22 @@ export function LatencyCorrelations({ onFilter }: { onFilter: () => void }) {
- {(isRunning || histogramTerms.length > 0) && ( - + {showCorrelationsTable && ( + columns={mlCorrelationColumns} significantTerms={histogramTerms} - status={isRunning ? FETCH_STATUS.LOADING : FETCH_STATUS.SUCCESS} - setSelectedSignificantTerm={setSelectedSignificantTerm} - selectedTerm={ - selectedHistogram !== undefined - ? { - fieldName: selectedHistogram.field, - fieldValue: selectedHistogram.value, - } - : undefined + status={ + progress.isRunning ? FETCH_STATUS.LOADING : FETCH_STATUS.SUCCESS } + setSelectedSignificantTerm={setSelectedSignificantTerm} + selectedTerm={selectedHistogram} onTableChange={onTableChange} sorting={sorting} /> )} - {histogramTerms.length < 1 && (progress === 1 || !isRunning) && ( - - )} + {showCorrelationsEmptyStatePrompt && }
- {displayLog && } + {displayLog && }
); } diff --git a/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.test.ts b/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.test.ts index edb7c8c16e2675..e4c08b42b24202 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.test.ts +++ b/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.test.ts @@ -6,7 +6,7 @@ */ import { getFailedTransactionsCorrelationImpactLabel } from './get_failed_transactions_correlation_impact_label'; -import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from '../../../../../common/search_strategies/failure_correlations/constants'; +import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from '../../../../../common/search_strategies/failed_transactions_correlations/constants'; const EXPECTED_RESULT = { HIGH: { diff --git a/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.ts b/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.ts index 5a806aba5371ea..cbfaee88ff6f4c 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.ts +++ b/x-pack/plugins/apm/public/components/app/correlations/utils/get_failed_transactions_correlation_impact_label.ts @@ -5,12 +5,22 @@ * 2.0. */ -import { FailureCorrelationImpactThreshold } from '../../../../../common/search_strategies/failure_correlations/types'; -import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from '../../../../../common/search_strategies/failure_correlations/constants'; +import { + FailedTransactionsCorrelation, + FailedTransactionsCorrelationsImpactThreshold, +} from '../../../../../common/search_strategies/failed_transactions_correlations/types'; +import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from '../../../../../common/search_strategies/failed_transactions_correlations/constants'; export function getFailedTransactionsCorrelationImpactLabel( - pValue: number -): { impact: FailureCorrelationImpactThreshold; color: string } | null { + pValue: FailedTransactionsCorrelation['pValue'] +): { + impact: FailedTransactionsCorrelationsImpactThreshold; + color: string; +} | null { + if (pValue === null) { + return null; + } + // The lower the p value, the higher the impact if (pValue >= 0 && pValue < 1e-6) return { diff --git a/x-pack/plugins/apm/public/components/app/correlations/utils/get_overall_histogram.test.ts b/x-pack/plugins/apm/public/components/app/correlations/utils/get_overall_histogram.test.ts new file mode 100644 index 00000000000000..c323b69594013e --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/utils/get_overall_histogram.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { LatencyCorrelationsRawResponse } from '../../../../../common/search_strategies/latency_correlations/types'; + +import { getOverallHistogram } from './get_overall_histogram'; + +describe('getOverallHistogram', () => { + it('returns "loading" when undefined and running', () => { + const { overallHistogram, hasData, status } = getOverallHistogram( + {} as LatencyCorrelationsRawResponse, + true + ); + expect(overallHistogram).toStrictEqual(undefined); + expect(hasData).toBe(false); + expect(status).toBe('loading'); + }); + + it('returns "success" when undefined and not running', () => { + const { overallHistogram, hasData, status } = getOverallHistogram( + {} as LatencyCorrelationsRawResponse, + false + ); + expect(overallHistogram).toStrictEqual([]); + expect(hasData).toBe(false); + expect(status).toBe('success'); + }); + + it('returns "success" when not undefined and still running', () => { + const { overallHistogram, hasData, status } = getOverallHistogram( + { + overallHistogram: [{ key: 1, doc_count: 1234 }], + } as LatencyCorrelationsRawResponse, + true + ); + expect(overallHistogram).toStrictEqual([{ key: 1, doc_count: 1234 }]); + expect(hasData).toBe(true); + expect(status).toBe('success'); + }); + + it('returns "success" when not undefined and not running', () => { + const { overallHistogram, hasData, status } = getOverallHistogram( + { + overallHistogram: [{ key: 1, doc_count: 1234 }], + } as LatencyCorrelationsRawResponse, + false + ); + expect(overallHistogram).toStrictEqual([{ key: 1, doc_count: 1234 }]); + expect(hasData).toBe(true); + expect(status).toBe('success'); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/correlations/utils/get_overall_histogram.ts b/x-pack/plugins/apm/public/components/app/correlations/utils/get_overall_histogram.ts new file mode 100644 index 00000000000000..3a90eb4b891235 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/utils/get_overall_histogram.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { LatencyCorrelationsRawResponse } from '../../../../../common/search_strategies/latency_correlations/types'; + +import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; + +// `isRunning` refers to the search strategy as whole which might still be in the process +// of fetching more data such as correlation results. That's why we have to determine +// the `status` of the data for the latency chart separately. +export function getOverallHistogram( + data: LatencyCorrelationsRawResponse, + isRunning: boolean +) { + const overallHistogram = + data.overallHistogram === undefined && !isRunning + ? [] + : data.overallHistogram; + const hasData = + Array.isArray(overallHistogram) && overallHistogram.length > 0; + const status = Array.isArray(overallHistogram) + ? FETCH_STATUS.SUCCESS + : FETCH_STATUS.LOADING; + + return { overallHistogram, hasData, status }; +} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx index 5a9977b373c336..9a38e0fcf6289e 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx @@ -16,7 +16,7 @@ import { dataPluginMock } from 'src/plugins/data/public/mocks'; import type { IKibanaSearchResponse } from 'src/plugins/data/public'; import { EuiThemeProvider } from 'src/plugins/kibana_react/common'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; -import type { SearchServiceRawResponse } from '../../../../../common/search_strategies/correlations/types'; +import type { LatencyCorrelationsRawResponse } from '../../../../../common/search_strategies/latency_correlations/types'; import { MockUrlParamsContextProvider } from '../../../../context/url_params_context/mock_url_params_context_provider'; import { ApmPluginContextValue } from '../../../../context/apm_plugin/apm_plugin_context'; import { @@ -32,7 +32,7 @@ function Wrapper({ dataSearchResponse, }: { children?: ReactNode; - dataSearchResponse: IKibanaSearchResponse; + dataSearchResponse: IKibanaSearchResponse; }) { const mockDataSearch = jest.fn(() => of(dataSearchResponse)); @@ -101,18 +101,22 @@ describe('transaction_details/distribution', () => { describe('TransactionDistribution', () => { it('shows loading indicator when the service is running and returned no results yet', async () => { - const onHasData = jest.fn(); render( ); @@ -120,23 +124,26 @@ describe('transaction_details/distribution', () => { await waitFor(() => { expect(screen.getByTestId('apmCorrelationsChart')).toBeInTheDocument(); expect(screen.getByTestId('loading')).toBeInTheDocument(); - expect(onHasData).toHaveBeenLastCalledWith(false); }); }); it("doesn't show loading indicator when the service isn't running", async () => { - const onHasData = jest.fn(); render( ); @@ -144,7 +151,6 @@ describe('transaction_details/distribution', () => { await waitFor(() => { expect(screen.getByTestId('apmCorrelationsChart')).toBeInTheDocument(); expect(screen.queryByTestId('loading')).toBeNull(); // it doesn't exist - expect(onHasData).toHaveBeenLastCalledWith(false); }); }); }); diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx index 2da61bc0fc555d..acd8c5f4d57d3c 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { BrushEndListener, XYBrushArea } from '@elastic/charts'; import { EuiBadge, @@ -18,20 +18,24 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { getDurationFormatter } from '../../../../../common/utils/formatters'; -import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; -import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; -import { useTransactionDistributionFetcher } from '../../../../hooks/use_transaction_distribution_fetcher'; import { - OnHasData, - TransactionDistributionChart, -} from '../../../shared/charts/transaction_distribution_chart'; + APM_SEARCH_STRATEGIES, + DEFAULT_PERCENTILE_THRESHOLD, +} from '../../../../../common/search_strategies/constants'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { useSearchStrategy } from '../../../../hooks/use_search_strategy'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; + +import { TransactionDistributionChart } from '../../../shared/charts/transaction_distribution_chart'; import { useUiTracker } from '../../../../../../observability/public'; -import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; -import { useApmParams } from '../../../../hooks/use_apm_params'; import { isErrorMessage } from '../../correlations/utils/is_error_message'; -import { useTimeRange } from '../../../../hooks/use_time_range'; +import { getOverallHistogram } from '../../correlations/utils/get_overall_histogram'; + +import type { TabContentProps } from '../types'; +import { useWaterfallFetcher } from '../use_waterfall_fetcher'; +import { WaterfallWithSummary } from '../waterfall_with_summary'; -const DEFAULT_PERCENTILE_THRESHOLD = 95; // Enforce min height so it's consistent across all tabs on the same level // to prevent "flickering" behavior const MIN_TAB_TITLE_HEIGHT = 56; @@ -51,45 +55,32 @@ export function getFormattedSelection(selection: Selection): string { } interface TransactionDistributionProps { - markerCurrentTransaction?: number; onChartSelection: BrushEndListener; onClearSelection: () => void; - onHasData: OnHasData; selection?: Selection; + traceSamples: TabContentProps['traceSamples']; } export function TransactionDistribution({ - markerCurrentTransaction, onChartSelection, onClearSelection, - onHasData, selection, + traceSamples, }: TransactionDistributionProps) { const { core: { notifications }, } = useApmPluginContext(); - const { serviceName, transactionType } = useApmServiceContext(); - - const { - query: { kuery, environment, rangeFrom, rangeTo }, - } = useApmParams('/services/:serviceName/transactions/view'); - - const { start, end } = useTimeRange({ rangeFrom, rangeTo }); - const { urlParams } = useUrlParams(); - const { transactionName } = urlParams; - - const [showSelection, setShowSelection] = useState(false); + const { + waterfall, + exceedsMax, + status: waterfallStatus, + } = useWaterfallFetcher(); - const onTransactionDistributionHasData: OnHasData = useCallback( - (hasData) => { - setShowSelection(hasData); - onHasData(hasData); - }, - [onHasData] - ); + const markerCurrentTransaction = + waterfall.entryWaterfallTransaction?.doc.transaction.duration.us; const emptySelectionText = i18n.translate( 'xpack.apm.transactionDetails.emptySelectionText', @@ -105,43 +96,20 @@ export function TransactionDistribution({ } ); - const { - error, - percentileThresholdValue, - startFetch, - cancelFetch, - transactionDistribution, - } = useTransactionDistributionFetcher(); - - const startFetchHandler = useCallback(() => { - startFetch({ - environment, - kuery, - serviceName, - transactionName, - transactionType, - start, - end, + const { progress, response } = useSearchStrategy( + APM_SEARCH_STRATEGIES.APM_LATENCY_CORRELATIONS, + { percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, - }); - }, [ - startFetch, - environment, - serviceName, - transactionName, - transactionType, - kuery, - start, - end, - ]); - - useEffect(() => { - startFetchHandler(); - return cancelFetch; - }, [cancelFetch, startFetchHandler]); + analyzeCorrelations: false, + } + ); + const { overallHistogram, hasData, status } = getOverallHistogram( + response, + progress.isRunning + ); useEffect(() => { - if (isErrorMessage(error)) { + if (isErrorMessage(progress.error)) { notifications.toasts.addDanger({ title: i18n.translate( 'xpack.apm.transactionDetails.distribution.errorTitle', @@ -149,10 +117,10 @@ export function TransactionDistribution({ defaultMessage: 'An error occurred fetching the distribution', } ), - text: error.toString(), + text: progress.error.toString(), }); } - }, [error, notifications.toasts]); + }, [progress.error, notifications.toasts]); const trackApmEvent = useUiTracker({ app: 'apm' }); @@ -183,7 +151,7 @@ export function TransactionDistribution({ - {showSelection && !selection && ( + {hasData && !selection && ( )} - {showSelection && selection && ( + {hasData && selection && ( + + {hasData && ( + <> + + + + + )} ); } diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/trace_samples_tab.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/trace_samples_tab.tsx index ea02cfea5a941d..ad629b7a2d132f 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/trace_samples_tab.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/trace_samples_tab.tsx @@ -5,19 +5,12 @@ * 2.0. */ -import React, { useState } from 'react'; - -import { EuiSpacer } from '@elastic/eui'; +import React from 'react'; import { i18n } from '@kbn/i18n'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; - import { TransactionDistribution } from './distribution'; -import { useWaterfallFetcher } from './use_waterfall_fetcher'; import type { TabContentProps } from './types'; -import { WaterfallWithSummary } from './waterfall_with_summary'; function TraceSamplesTab({ selectSampleFromChartSelection, @@ -26,49 +19,17 @@ function TraceSamplesTab({ sampleRangeTo, traceSamples, }: TabContentProps) { - const { urlParams } = useUrlParams(); - - const { - waterfall, - exceedsMax, - status: waterfallStatus, - } = useWaterfallFetcher(); - - const [ - transactionDistributionHasData, - setTransactionDistributionHasData, - ] = useState(false); - return ( - <> - - - {transactionDistributionHasData && ( - <> - - - - - )} - + ); } diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx index a58a2887b15768..ee5ae0d4dc840e 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useEffect, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { AnnotationDomainType, AreaSeries, @@ -30,31 +30,28 @@ import { i18n } from '@kbn/i18n'; import { useChartTheme } from '../../../../../../observability/public'; import { getDurationFormatter } from '../../../../../common/utils/formatters'; -import { HistogramItem } from '../../../../../common/search_strategies/correlations/types'; +import type { + FieldValuePair, + HistogramItem, +} from '../../../../../common/search_strategies/types'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { useTheme } from '../../../../hooks/use_theme'; -import { ChartContainer, ChartContainerProps } from '../chart_container'; - -export type TransactionDistributionChartLoadingState = Pick< - ChartContainerProps, - 'hasData' | 'status' ->; - -export type OnHasData = (hasData: boolean) => void; +import { ChartContainer } from '../chart_container'; interface TransactionDistributionChartProps { - field?: string; - value?: string; + fieldName?: FieldValuePair['fieldName']; + fieldValue?: FieldValuePair['fieldValue']; + hasData: boolean; histogram?: HistogramItem[]; markerCurrentTransaction?: number; markerValue: number; markerPercentile: number; overallHistogram?: HistogramItem[]; onChartSelection?: BrushEndListener; - onHasData?: OnHasData; selection?: [number, number]; + status: FETCH_STATUS; } const getAnnotationsStyle = (color = 'gray'): LineAnnotationStyle => ({ @@ -103,16 +100,17 @@ const xAxisTickFormat: TickFormatter = (d) => getDurationFormatter(d, 0.9999)(d).formatted; export function TransactionDistributionChart({ - field: fieldName, - value: fieldValue, + fieldName, + fieldValue, + hasData, histogram: originalHistogram, markerCurrentTransaction, markerValue, markerPercentile, overallHistogram, onChartSelection, - onHasData, selection, + status, }: TransactionDistributionChartProps) { const chartTheme = useChartTheme(); const euiTheme = useTheme(); @@ -163,34 +161,12 @@ export function TransactionDistributionChart({ ] : undefined; - const chartLoadingState: TransactionDistributionChartLoadingState = useMemo( - () => ({ - hasData: - Array.isArray(patchedOverallHistogram) && - patchedOverallHistogram.length > 0, - status: Array.isArray(patchedOverallHistogram) - ? FETCH_STATUS.SUCCESS - : FETCH_STATUS.LOADING, - }), - [patchedOverallHistogram] - ); - - useEffect(() => { - if (onHasData) { - onHasData(chartLoadingState.hasData); - } - }, [chartLoadingState, onHasData]); - return (
- + { - const { - services: { data }, - } = useKibana(); - - const [ - fetchState, - setFetchState, - ] = useState({ - isComplete: false, - isRunning: false, - loaded: 0, - ccsWarning: false, - values: [], - log: [], - total: 100, - }); - - const abortCtrl = useRef(new AbortController()); - const searchSubscription$ = useRef(); - - function setResponse(response: IKibanaSearchResponse) { - setFetchState((prevState) => ({ - ...prevState, - isRunning: response.isRunning || false, - ccsWarning: response.rawResponse?.ccsWarning ?? false, - values: response.rawResponse?.values ?? [], - log: response.rawResponse?.log ?? [], - loaded: response.loaded!, - total: response.total!, - timeTook: response.rawResponse.took, - })); - } - - const startFetch = useCallback( - (params: SearchServiceParams) => { - setFetchState((prevState) => ({ - ...prevState, - error: undefined, - isComplete: false, - })); - searchSubscription$.current?.unsubscribe(); - abortCtrl.current.abort(); - abortCtrl.current = new AbortController(); - - const req = { params }; - - // Submit the search request using the `data.search` service. - searchSubscription$.current = data.search - .search>(req, { - strategy: FAILED_TRANSACTIONS_CORRELATION_SEARCH_STRATEGY, - abortSignal: abortCtrl.current.signal, - }) - .subscribe({ - next: (res: IKibanaSearchResponse) => { - setResponse(res); - if (isCompleteResponse(res)) { - searchSubscription$.current?.unsubscribe(); - setFetchState((prevState) => ({ - ...prevState, - isRunnning: false, - isComplete: true, - })); - } else if (isErrorResponse(res)) { - searchSubscription$.current?.unsubscribe(); - setFetchState((prevState) => ({ - ...prevState, - error: (res as unknown) as Error, - isRunning: false, - })); - } - }, - error: (error: Error) => { - setFetchState((prevState) => ({ - ...prevState, - error, - isRunning: false, - })); - }, - }); - }, - [data.search, setFetchState] - ); - - const cancelFetch = useCallback(() => { - searchSubscription$.current?.unsubscribe(); - searchSubscription$.current = undefined; - abortCtrl.current.abort(); - setFetchState((prevState) => ({ - ...prevState, - isRunning: false, - })); - }, [setFetchState]); - - return { - ...fetchState, - progress: fetchState.loaded / fetchState.total, - startFetch, - cancelFetch, - }; -}; diff --git a/x-pack/plugins/apm/public/hooks/use_search_strategy.ts b/x-pack/plugins/apm/public/hooks/use_search_strategy.ts new file mode 100644 index 00000000000000..6f6c9bf151c003 --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_search_strategy.ts @@ -0,0 +1,208 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useEffect, useReducer, useRef } from 'react'; +import type { Subscription } from 'rxjs'; + +import { + IKibanaSearchRequest, + IKibanaSearchResponse, + isCompleteResponse, + isErrorResponse, +} from '../../../../../src/plugins/data/public'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; + +import type { SearchStrategyClientParams } from '../../common/search_strategies/types'; +import type { RawResponseBase } from '../../common/search_strategies/types'; +import type { LatencyCorrelationsRawResponse } from '../../common/search_strategies/latency_correlations/types'; +import type { FailedTransactionsCorrelationsRawResponse } from '../../common/search_strategies/failed_transactions_correlations/types'; +import { + ApmSearchStrategies, + APM_SEARCH_STRATEGIES, +} from '../../common/search_strategies/constants'; +import { useApmServiceContext } from '../context/apm_service/use_apm_service_context'; +import { useUrlParams } from '../context/url_params_context/use_url_params'; + +import { ApmPluginStartDeps } from '../plugin'; + +import { useApmParams } from './use_apm_params'; +import { useTimeRange } from './use_time_range'; + +interface SearchStrategyProgress { + error?: Error; + isRunning: boolean; + loaded: number; + total: number; +} + +const getInitialRawResponse = < + TRawResponse extends RawResponseBase +>(): TRawResponse => + ({ + ccsWarning: false, + took: 0, + } as TRawResponse); + +const getInitialProgress = (): SearchStrategyProgress => ({ + isRunning: false, + loaded: 0, + total: 100, +}); + +const getReducer = () => (prev: T, update: Partial): T => ({ + ...prev, + ...update, +}); + +interface SearchStrategyReturnBase { + progress: SearchStrategyProgress; + startFetch: () => void; + cancelFetch: () => void; +} + +// Function overload for Latency Correlations +export function useSearchStrategy( + searchStrategyName: typeof APM_SEARCH_STRATEGIES.APM_LATENCY_CORRELATIONS, + options: { + percentileThreshold: number; + analyzeCorrelations: boolean; + } +): { + response: LatencyCorrelationsRawResponse; +} & SearchStrategyReturnBase; + +// Function overload for Failed Transactions Correlations +export function useSearchStrategy( + searchStrategyName: typeof APM_SEARCH_STRATEGIES.APM_FAILED_TRANSACTIONS_CORRELATIONS +): { + response: FailedTransactionsCorrelationsRawResponse; +} & SearchStrategyReturnBase; + +export function useSearchStrategy< + TRawResponse extends RawResponseBase, + TOptions = unknown +>(searchStrategyName: ApmSearchStrategies, options?: TOptions): unknown { + const { + services: { data }, + } = useKibana(); + + const { serviceName, transactionType } = useApmServiceContext(); + const { + query: { kuery, environment, rangeFrom, rangeTo }, + } = useApmParams('/services/:serviceName/transactions/view'); + const { start, end } = useTimeRange({ rangeFrom, rangeTo }); + const { urlParams } = useUrlParams(); + const { transactionName } = urlParams; + + const [rawResponse, setRawResponse] = useReducer( + getReducer(), + getInitialRawResponse() + ); + + const [fetchState, setFetchState] = useReducer( + getReducer(), + getInitialProgress() + ); + + const abortCtrl = useRef(new AbortController()); + const searchSubscription$ = useRef(); + const optionsRef = useRef(options); + + const startFetch = useCallback(() => { + searchSubscription$.current?.unsubscribe(); + abortCtrl.current.abort(); + abortCtrl.current = new AbortController(); + setFetchState({ + ...getInitialProgress(), + error: undefined, + }); + + const request = { + params: { + environment, + serviceName, + transactionName, + transactionType, + kuery, + start, + end, + ...(optionsRef.current ? { ...optionsRef.current } : {}), + }, + }; + + // Submit the search request using the `data.search` service. + searchSubscription$.current = data.search + .search< + IKibanaSearchRequest, + IKibanaSearchResponse + >(request, { + strategy: searchStrategyName, + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response: IKibanaSearchResponse) => { + setRawResponse(response.rawResponse); + setFetchState({ + isRunning: response.isRunning || false, + loaded: response.loaded, + total: response.total, + }); + + if (isCompleteResponse(response)) { + searchSubscription$.current?.unsubscribe(); + setFetchState({ + isRunning: false, + }); + } else if (isErrorResponse(response)) { + searchSubscription$.current?.unsubscribe(); + setFetchState({ + error: (response as unknown) as Error, + isRunning: false, + }); + } + }, + error: (error: Error) => { + setFetchState({ + error, + isRunning: false, + }); + }, + }); + }, [ + searchStrategyName, + data.search, + environment, + serviceName, + transactionName, + transactionType, + kuery, + start, + end, + ]); + + const cancelFetch = useCallback(() => { + searchSubscription$.current?.unsubscribe(); + searchSubscription$.current = undefined; + abortCtrl.current.abort(); + setFetchState({ + isRunning: false, + }); + }, []); + + // auto-update + useEffect(() => { + startFetch(); + return cancelFetch; + }, [startFetch, cancelFetch]); + + return { + progress: fetchState, + response: rawResponse, + startFetch, + cancelFetch, + }; +} diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts deleted file mode 100644 index 2ff1b83ef17829..00000000000000 --- a/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts +++ /dev/null @@ -1,160 +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 { useCallback, useRef, useState } from 'react'; -import type { Subscription } from 'rxjs'; -import { - IKibanaSearchRequest, - IKibanaSearchResponse, - isCompleteResponse, - isErrorResponse, -} from '../../../../../src/plugins/data/public'; -import type { - SearchServiceParams, - SearchServiceRawResponse, -} from '../../common/search_strategies/correlations/types'; -import { useKibana } from '../../../../../src/plugins/kibana_react/public'; -import { ApmPluginStartDeps } from '../plugin'; - -interface TransactionDistributionFetcherState { - error?: Error; - isComplete: boolean; - isRunning: boolean; - loaded: number; - ccsWarning: SearchServiceRawResponse['ccsWarning']; - log: SearchServiceRawResponse['log']; - transactionDistribution?: SearchServiceRawResponse['overallHistogram']; - percentileThresholdValue?: SearchServiceRawResponse['percentileThresholdValue']; - timeTook?: number; - total: number; -} - -export function useTransactionDistributionFetcher() { - const { - services: { data }, - } = useKibana(); - - const [ - fetchState, - setFetchState, - ] = useState({ - isComplete: false, - isRunning: false, - loaded: 0, - ccsWarning: false, - log: [], - total: 100, - }); - - const abortCtrl = useRef(new AbortController()); - const searchSubscription$ = useRef(); - - function setResponse( - response: IKibanaSearchResponse - ) { - setFetchState((prevState) => ({ - ...prevState, - isRunning: response.isRunning || false, - ccsWarning: response.rawResponse?.ccsWarning ?? false, - histograms: response.rawResponse?.values ?? [], - log: response.rawResponse?.log ?? [], - loaded: response.loaded!, - total: response.total!, - timeTook: response.rawResponse.took, - // only set percentileThresholdValue and overallHistogram once it's repopulated on a refresh, - // otherwise the consuming chart would flicker with an empty state on reload. - ...(response.rawResponse?.percentileThresholdValue !== undefined && - response.rawResponse?.overallHistogram !== undefined - ? { - transactionDistribution: response.rawResponse?.overallHistogram, - percentileThresholdValue: - response.rawResponse?.percentileThresholdValue, - } - : {}), - // if loading is done but didn't return any data for the overall histogram, - // set it to an empty array so the consuming chart component knows loading is done. - ...(!response.isRunning && - response.rawResponse?.overallHistogram === undefined - ? { transactionDistribution: [] } - : {}), - })); - } - - const startFetch = useCallback( - (params: Omit) => { - setFetchState((prevState) => ({ - ...prevState, - error: undefined, - isComplete: false, - })); - searchSubscription$.current?.unsubscribe(); - abortCtrl.current.abort(); - abortCtrl.current = new AbortController(); - - const searchServiceParams: SearchServiceParams = { - ...params, - analyzeCorrelations: false, - }; - const req = { params: searchServiceParams }; - - // Submit the search request using the `data.search` service. - searchSubscription$.current = data.search - .search< - IKibanaSearchRequest, - IKibanaSearchResponse - >(req, { - strategy: 'apmCorrelationsSearchStrategy', - abortSignal: abortCtrl.current.signal, - }) - .subscribe({ - next: (res: IKibanaSearchResponse) => { - setResponse(res); - if (isCompleteResponse(res)) { - searchSubscription$.current?.unsubscribe(); - setFetchState((prevState) => ({ - ...prevState, - isRunnning: false, - isComplete: true, - })); - } else if (isErrorResponse(res)) { - searchSubscription$.current?.unsubscribe(); - setFetchState((prevState) => ({ - ...prevState, - error: (res as unknown) as Error, - isRunning: false, - })); - } - }, - error: (error: Error) => { - setFetchState((prevState) => ({ - ...prevState, - error, - isRunning: false, - })); - }, - }); - }, - [data.search, setFetchState] - ); - - const cancelFetch = useCallback(() => { - searchSubscription$.current?.unsubscribe(); - searchSubscription$.current = undefined; - abortCtrl.current.abort(); - setFetchState((prevState) => ({ - ...prevState, - isRunning: false, - })); - }, [setFetchState]); - - return { - ...fetchState, - progress: fetchState.loaded / fetchState.total, - startFetch, - cancelFetch, - }; -} diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_latency_correlations_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_latency_correlations_fetcher.ts deleted file mode 100644 index 0b035c6af23549..00000000000000 --- a/x-pack/plugins/apm/public/hooks/use_transaction_latency_correlations_fetcher.ts +++ /dev/null @@ -1,167 +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 { useCallback, useRef, useState } from 'react'; -import type { Subscription } from 'rxjs'; -import { - IKibanaSearchRequest, - IKibanaSearchResponse, - isCompleteResponse, - isErrorResponse, -} from '../../../../../src/plugins/data/public'; -import type { - SearchServiceParams, - SearchServiceRawResponse, -} from '../../common/search_strategies/correlations/types'; -import { useKibana } from '../../../../../src/plugins/kibana_react/public'; -import { ApmPluginStartDeps } from '../plugin'; - -interface TransactionLatencyCorrelationsFetcherState { - error?: Error; - isComplete: boolean; - isRunning: boolean; - loaded: number; - ccsWarning: SearchServiceRawResponse['ccsWarning']; - histograms: SearchServiceRawResponse['values']; - log: SearchServiceRawResponse['log']; - overallHistogram?: SearchServiceRawResponse['overallHistogram']; - percentileThresholdValue?: SearchServiceRawResponse['percentileThresholdValue']; - timeTook?: number; - total: number; -} - -export const useTransactionLatencyCorrelationsFetcher = () => { - const { - services: { data }, - } = useKibana(); - - const [ - fetchState, - setFetchState, - ] = useState({ - isComplete: false, - isRunning: false, - loaded: 0, - ccsWarning: false, - histograms: [], - log: [], - total: 100, - }); - - const abortCtrl = useRef(new AbortController()); - const searchSubscription$ = useRef(); - - function setResponse( - response: IKibanaSearchResponse - ) { - setFetchState((prevState) => ({ - ...prevState, - isRunning: response.isRunning || false, - ccsWarning: response.rawResponse?.ccsWarning ?? false, - histograms: response.rawResponse?.values ?? [], - log: response.rawResponse?.log ?? [], - loaded: response.loaded!, - total: response.total!, - timeTook: response.rawResponse.took, - // only set percentileThresholdValue and overallHistogram once it's repopulated on a refresh, - // otherwise the consuming chart would flicker with an empty state on reload. - ...(response.rawResponse?.percentileThresholdValue !== undefined && - response.rawResponse?.overallHistogram !== undefined - ? { - overallHistogram: response.rawResponse?.overallHistogram, - percentileThresholdValue: - response.rawResponse?.percentileThresholdValue, - } - : {}), - // if loading is done but didn't return any data for the overall histogram, - // set it to an empty array so the consuming chart component knows loading is done. - ...(!response.isRunning && - response.rawResponse?.overallHistogram === undefined - ? { overallHistogram: [] } - : {}), - })); - } - - const startFetch = useCallback( - (params: Omit) => { - setFetchState((prevState) => ({ - ...prevState, - error: undefined, - isComplete: false, - })); - searchSubscription$.current?.unsubscribe(); - abortCtrl.current.abort(); - abortCtrl.current = new AbortController(); - - const searchServiceParams: SearchServiceParams = { - ...params, - analyzeCorrelations: true, - }; - const req = { params: searchServiceParams }; - - // Submit the search request using the `data.search` service. - searchSubscription$.current = data.search - .search< - IKibanaSearchRequest, - IKibanaSearchResponse - >(req, { - strategy: 'apmCorrelationsSearchStrategy', - abortSignal: abortCtrl.current.signal, - }) - .subscribe({ - next: (res: IKibanaSearchResponse) => { - setResponse(res); - if (isCompleteResponse(res)) { - searchSubscription$.current?.unsubscribe(); - setFetchState((prevState) => ({ - ...prevState, - isRunnning: false, - isComplete: true, - })); - } else if (isErrorResponse(res)) { - searchSubscription$.current?.unsubscribe(); - setFetchState((prevState) => ({ - ...prevState, - error: (res as unknown) as Error, - isRunning: false, - })); - } - }, - error: (error: Error) => { - setFetchState((prevState) => ({ - ...prevState, - error, - isRunning: false, - })); - }, - }); - }, - [data.search, setFetchState] - ); - - const cancelFetch = useCallback(() => { - searchSubscription$.current?.unsubscribe(); - searchSubscription$.current = undefined; - abortCtrl.current.abort(); - setFetchState((prevState) => ({ - ...prevState, - // If we didn't receive data for the overall histogram yet - // set it to an empty array to indicate loading stopped. - ...(prevState.overallHistogram === undefined - ? { overallHistogram: [] } - : {}), - isRunning: false, - })); - }, [setFetchState]); - - return { - ...fetchState, - progress: fetchState.loaded / fetchState.total, - startFetch, - cancelFetch, - }; -}; diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts index 28f3041d65d703..1ea9ae6b65ac53 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts @@ -9,6 +9,7 @@ import { Logger } from 'kibana/server'; import uuid from 'uuid/v4'; import { snakeCase } from 'lodash'; import Boom from '@hapi/boom'; +import moment from 'moment'; import { ML_ERRORS } from '../../../common/anomaly_detection'; import { ProcessorEvent } from '../../../common/processor_event'; import { environmentQuery } from '../../../common/utils/environment_query'; @@ -87,6 +88,7 @@ async function createAnomalyDetectionJob({ groups: [APM_ML_JOB_GROUP], indexPatternName, applyToAllSpaces: true, + start: moment().subtract(4, 'weeks').valueOf(), query: { bool: { filter: [ diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/constants.ts b/x-pack/plugins/apm/server/lib/search_strategies/constants.ts similarity index 97% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/constants.ts rename to x-pack/plugins/apm/server/lib/search_strategies/constants.ts index 6b96b6b9d21314..5500e336c35425 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/constants.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/constants.ts @@ -79,3 +79,5 @@ export const SIGNIFICANT_VALUE_DIGITS = 3; export const CORRELATION_THRESHOLD = 0.3; export const KS_TEST_THRESHOLD = 0.1; + +export const ERROR_CORRELATION_THRESHOLD = 0.02; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_log.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_log.test.ts deleted file mode 100644 index bbeb8435e61bfb..00000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_log.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { asyncSearchServiceLogProvider } from './async_search_service_log'; - -describe('async search service', () => { - describe('asyncSearchServiceLogProvider', () => { - it('adds and retrieves messages from the log', async () => { - const { addLogMessage, getLogMessages } = asyncSearchServiceLogProvider(); - - const mockDate = new Date(1392202800000); - // @ts-ignore ignore the mockImplementation callback error - const spy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate); - - addLogMessage('the first message'); - addLogMessage('the second message'); - - expect(getLogMessages()).toEqual([ - '2014-02-12T11:00:00.000Z: the first message', - '2014-02-12T11:00:00.000Z: the second message', - ]); - - spy.mockRestore(); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.ts deleted file mode 100644 index 7f67147a755803..00000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.ts +++ /dev/null @@ -1,130 +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 uuid from 'uuid'; -import { of } from 'rxjs'; - -import type { ISearchStrategy } from '../../../../../../../src/plugins/data/server'; -import { - IKibanaSearchRequest, - IKibanaSearchResponse, -} from '../../../../../../../src/plugins/data/common'; - -import type { - SearchServiceParams, - SearchServiceRawResponse, - SearchServiceValue, -} from '../../../../common/search_strategies/correlations/types'; - -import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; - -import { asyncSearchServiceProvider } from './async_search_service'; - -export type PartialSearchRequest = IKibanaSearchRequest; -export type PartialSearchResponse = IKibanaSearchResponse<{ - values: SearchServiceValue[]; -}>; - -export const apmCorrelationsSearchStrategyProvider = ( - getApmIndices: () => Promise, - includeFrozen: boolean -): ISearchStrategy => { - const asyncSearchServiceMap = new Map< - string, - ReturnType - >(); - - return { - search: (request, options, deps) => { - if (request.params === undefined) { - throw new Error('Invalid request parameters.'); - } - - // The function to fetch the current state of the async search service. - // This will be either an existing service for a follow up fetch or a new one for new requests. - let getAsyncSearchServiceState: ReturnType< - typeof asyncSearchServiceProvider - >; - - // If the request includes an ID, we require that the async search service already exists - // otherwise we throw an error. The client should never poll a service that's been cancelled or finished. - // This also avoids instantiating async search services when the service gets called with random IDs. - if (typeof request.id === 'string') { - const existingGetAsyncSearchServiceState = asyncSearchServiceMap.get( - request.id - ); - - if (typeof existingGetAsyncSearchServiceState === 'undefined') { - throw new Error( - `AsyncSearchService with ID '${request.id}' does not exist.` - ); - } - - getAsyncSearchServiceState = existingGetAsyncSearchServiceState; - } else { - getAsyncSearchServiceState = asyncSearchServiceProvider( - deps.esClient.asCurrentUser, - getApmIndices, - request.params, - includeFrozen - ); - } - - // Reuse the request's id or create a new one. - const id = request.id ?? uuid(); - - const { - ccsWarning, - error, - log, - isRunning, - loaded, - started, - total, - values, - percentileThresholdValue, - overallHistogram, - } = getAsyncSearchServiceState(); - - if (error instanceof Error) { - asyncSearchServiceMap.delete(id); - throw error; - } else if (isRunning) { - asyncSearchServiceMap.set(id, getAsyncSearchServiceState); - } else { - asyncSearchServiceMap.delete(id); - } - - const took = Date.now() - started; - - const rawResponse: SearchServiceRawResponse = { - ccsWarning, - log, - took, - values, - percentileThresholdValue, - overallHistogram, - }; - - return of({ - id, - loaded, - total, - isRunning, - isPartial: isRunning, - rawResponse, - }); - }, - cancel: async (id, options, deps) => { - const getAsyncSearchServiceState = asyncSearchServiceMap.get(id); - if (getAsyncSearchServiceState !== undefined) { - getAsyncSearchServiceState().cancel(); - asyncSearchServiceMap.delete(id); - } - }, - }; -}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/current_time_as_string.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/current_time_as_string.test.ts deleted file mode 100644 index 2034c29b01d946..00000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/current_time_as_string.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { currentTimeAsString } from './current_time_as_string'; - -describe('aggregation utils', () => { - describe('currentTimeAsString', () => { - it('returns the current time as a string', () => { - const mockDate = new Date(1392202800000); - // @ts-ignore ignore the mockImplementation callback error - const spy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate); - - const timeString = currentTimeAsString(); - - expect(timeString).toEqual('2014-02-12T11:00:00.000Z'); - - spy.mockRestore(); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/current_time_as_string.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/current_time_as_string.ts deleted file mode 100644 index f454b8c8274f17..00000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/current_time_as_string.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const currentTimeAsString = () => new Date().toISOString(); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/async_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts similarity index 60% rename from x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/async_search_service.ts rename to x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts index 89fcda926d547b..12f7902b514880 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/async_search_service.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts @@ -5,32 +5,58 @@ * 2.0. */ -import type { ElasticsearchClient } from 'src/core/server'; import { chunk } from 'lodash'; -import type { SearchServiceParams } from '../../../../common/search_strategies/correlations/types'; -import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; -import { asyncSearchServiceLogProvider } from '../correlations/async_search_service_log'; -import { asyncErrorCorrelationsSearchServiceStateProvider } from './async_search_service_state'; -import { fetchTransactionDurationFieldCandidates } from '../correlations/queries'; -import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; -import { fetchFailedTransactionsCorrelationPValues } from './queries/query_failure_correlation'; -import { ERROR_CORRELATION_THRESHOLD } from './constants'; + +import type { ElasticsearchClient } from 'src/core/server'; + +import type { ISearchStrategy } from '../../../../../../../src/plugins/data/server'; +import { + IKibanaSearchRequest, + IKibanaSearchResponse, +} from '../../../../../../../src/plugins/data/common'; + import { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames'; +import type { SearchStrategyParams } from '../../../../common/search_strategies/types'; +import type { + FailedTransactionsCorrelationsParams, + FailedTransactionsCorrelationsRawResponse, +} from '../../../../common/search_strategies/failed_transactions_correlations/types'; +import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; +import { searchServiceLogProvider } from '../search_service_log'; +import { + fetchFailedTransactionsCorrelationPValues, + fetchTransactionDurationFieldCandidates, +} from '../queries'; +import type { SearchServiceProvider } from '../search_strategy_provider'; + +import { failedTransactionsCorrelationsSearchServiceStateProvider } from './failed_transactions_correlations_search_service_state'; -export const asyncErrorCorrelationSearchServiceProvider = ( +import { ERROR_CORRELATION_THRESHOLD } from '../constants'; + +export type FailedTransactionsCorrelationsSearchServiceProvider = SearchServiceProvider< + FailedTransactionsCorrelationsParams, + FailedTransactionsCorrelationsRawResponse +>; + +export type FailedTransactionsCorrelationsSearchStrategy = ISearchStrategy< + IKibanaSearchRequest, + IKibanaSearchResponse +>; + +export const failedTransactionsCorrelationsSearchServiceProvider: FailedTransactionsCorrelationsSearchServiceProvider = ( esClient: ElasticsearchClient, getApmIndices: () => Promise, - searchServiceParams: SearchServiceParams, + searchServiceParams: FailedTransactionsCorrelationsParams, includeFrozen: boolean ) => { - const { addLogMessage, getLogMessages } = asyncSearchServiceLogProvider(); + const { addLogMessage, getLogMessages } = searchServiceLogProvider(); - const state = asyncErrorCorrelationsSearchServiceStateProvider(); + const state = failedTransactionsCorrelationsSearchServiceStateProvider(); async function fetchErrorCorrelations() { try { const indices = await getApmIndices(); - const params: SearchServiceFetchParams = { + const params: SearchStrategyParams = { ...searchServiceParams, index: indices['apm_oss.transactionIndices'], includeFrozen, @@ -63,7 +89,7 @@ export const asyncErrorCorrelationSearchServiceProvider = ( results.forEach((result, idx) => { if (result.status === 'fulfilled') { - state.addValues( + state.addFailedTransactionsCorrelations( result.value.filter( (record) => record && @@ -87,7 +113,7 @@ export const asyncErrorCorrelationSearchServiceProvider = ( } finally { fieldCandidatesFetchedCount += batches[i].length; state.setProgress({ - loadedErrorCorrelations: + loadedFailedTransactionsCorrelations: fieldCandidatesFetchedCount / fieldCandidates.length, }); } @@ -103,7 +129,7 @@ export const asyncErrorCorrelationSearchServiceProvider = ( addLogMessage( `Identified ${ - state.getState().values.length + state.getState().failedTransactionsCorrelations.length } significant correlations relating to failed transactions.` ); @@ -116,18 +142,23 @@ export const asyncErrorCorrelationSearchServiceProvider = ( const { ccsWarning, error, isRunning, progress } = state.getState(); return { - ccsWarning, - error, - log: getLogMessages(), - isRunning, - loaded: Math.round(state.getOverallProgress() * 100), - started: progress.started, - total: 100, - values: state.getValuesSortedByScore(), cancel: () => { addLogMessage(`Service cancelled.`); state.setIsCancelled(true); }, + error, + meta: { + loaded: Math.round(state.getOverallProgress() * 100), + total: 100, + isRunning, + isPartial: isRunning, + }, + rawResponse: { + ccsWarning, + log: getLogMessages(), + took: Date.now() - progress.started, + failedTransactionsCorrelations: state.getFailedTransactionsCorrelationsSortedByScore(), + }, }; }; }; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/async_search_service_state.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service_state.ts similarity index 52% rename from x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/async_search_service_state.ts rename to x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service_state.ts index fb0c6fea4879a6..13cf7526185372 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/async_search_service_state.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service_state.ts @@ -5,14 +5,14 @@ * 2.0. */ -import { FailedTransactionsCorrelationValue } from '../../../../common/search_strategies/failure_correlations/types'; +import { FailedTransactionsCorrelation } from '../../../../common/search_strategies/failed_transactions_correlations/types'; interface Progress { started: number; loadedFieldCandidates: number; - loadedErrorCorrelations: number; + loadedFailedTransactionsCorrelations: number; } -export const asyncErrorCorrelationsSearchServiceStateProvider = () => { +export const failedTransactionsCorrelationsSearchServiceStateProvider = () => { let ccsWarning = false; function setCcsWarning(d: boolean) { ccsWarning = d; @@ -36,12 +36,12 @@ export const asyncErrorCorrelationsSearchServiceStateProvider = () => { let progress: Progress = { started: Date.now(), loadedFieldCandidates: 0, - loadedErrorCorrelations: 0, + loadedFailedTransactionsCorrelations: 0, }; function getOverallProgress() { return ( progress.loadedFieldCandidates * 0.025 + - progress.loadedErrorCorrelations * (1 - 0.025) + progress.loadedFailedTransactionsCorrelations * (1 - 0.025) ); } function setProgress(d: Partial>) { @@ -51,16 +51,18 @@ export const asyncErrorCorrelationsSearchServiceStateProvider = () => { }; } - const values: FailedTransactionsCorrelationValue[] = []; - function addValue(d: FailedTransactionsCorrelationValue) { - values.push(d); + const failedTransactionsCorrelations: FailedTransactionsCorrelation[] = []; + function addFailedTransactionsCorrelation(d: FailedTransactionsCorrelation) { + failedTransactionsCorrelations.push(d); } - function addValues(d: FailedTransactionsCorrelationValue[]) { - values.push(...d); + function addFailedTransactionsCorrelations( + d: FailedTransactionsCorrelation[] + ) { + failedTransactionsCorrelations.push(...d); } - function getValuesSortedByScore() { - return values.sort((a, b) => b.score - a.score); + function getFailedTransactionsCorrelationsSortedByScore() { + return failedTransactionsCorrelations.sort((a, b) => b.score - a.score); } function getState() { @@ -70,16 +72,16 @@ export const asyncErrorCorrelationsSearchServiceStateProvider = () => { isCancelled, isRunning, progress, - values, + failedTransactionsCorrelations, }; } return { - addValue, - addValues, + addFailedTransactionsCorrelation, + addFailedTransactionsCorrelations, getOverallProgress, getState, - getValuesSortedByScore, + getFailedTransactionsCorrelationsSortedByScore, setCcsWarning, setError, setIsCancelled, @@ -88,6 +90,6 @@ export const asyncErrorCorrelationsSearchServiceStateProvider = () => { }; }; -export type AsyncSearchServiceState = ReturnType< - typeof asyncErrorCorrelationsSearchServiceStateProvider +export type FailedTransactionsCorrelationsSearchServiceState = ReturnType< + typeof failedTransactionsCorrelationsSearchServiceStateProvider >; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts index f7e24ac6e1335e..ec91165cb481b9 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts @@ -5,5 +5,8 @@ * 2.0. */ -export { apmFailedTransactionsCorrelationsSearchStrategyProvider } from './search_strategy'; -export { FAILED_TRANSACTIONS_CORRELATION_SEARCH_STRATEGY } from '../../../../common/search_strategies/failure_correlations/constants'; +export { + failedTransactionsCorrelationsSearchServiceProvider, + FailedTransactionsCorrelationsSearchServiceProvider, + FailedTransactionsCorrelationsSearchStrategy, +} from './failed_transactions_correlations_search_service'; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/search_strategy.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/search_strategy.ts deleted file mode 100644 index 415f19e892741b..00000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/search_strategy.ts +++ /dev/null @@ -1,120 +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 uuid from 'uuid'; -import { of } from 'rxjs'; - -import type { ISearchStrategy } from '../../../../../../../src/plugins/data/server'; -import { - IKibanaSearchRequest, - IKibanaSearchResponse, -} from '../../../../../../../src/plugins/data/common'; - -import type { SearchServiceParams } from '../../../../common/search_strategies/correlations/types'; -import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; - -import { asyncErrorCorrelationSearchServiceProvider } from './async_search_service'; -import { FailedTransactionsCorrelationValue } from '../../../../common/search_strategies/failure_correlations/types'; - -export type PartialSearchRequest = IKibanaSearchRequest; -export type PartialSearchResponse = IKibanaSearchResponse<{ - values: FailedTransactionsCorrelationValue[]; -}>; - -export const apmFailedTransactionsCorrelationsSearchStrategyProvider = ( - getApmIndices: () => Promise, - includeFrozen: boolean -): ISearchStrategy => { - const asyncSearchServiceMap = new Map< - string, - ReturnType - >(); - - return { - search: (request, options, deps) => { - if (request.params === undefined) { - throw new Error('Invalid request parameters.'); - } - - // The function to fetch the current state of the async search service. - // This will be either an existing service for a follow up fetch or a new one for new requests. - let getAsyncSearchServiceState: ReturnType< - typeof asyncErrorCorrelationSearchServiceProvider - >; - - // If the request includes an ID, we require that the async search service already exists - // otherwise we throw an error. The client should never poll a service that's been cancelled or finished. - // This also avoids instantiating async search services when the service gets called with random IDs. - if (typeof request.id === 'string') { - const existingGetAsyncSearchServiceState = asyncSearchServiceMap.get( - request.id - ); - - if (typeof existingGetAsyncSearchServiceState === 'undefined') { - throw new Error( - `AsyncSearchService with ID '${request.id}' does not exist.` - ); - } - - getAsyncSearchServiceState = existingGetAsyncSearchServiceState; - } else { - getAsyncSearchServiceState = asyncErrorCorrelationSearchServiceProvider( - deps.esClient.asCurrentUser, - getApmIndices, - request.params, - includeFrozen - ); - } - - // Reuse the request's id or create a new one. - const id = request.id ?? uuid(); - - const { - ccsWarning, - error, - log, - isRunning, - loaded, - started, - total, - values, - } = getAsyncSearchServiceState(); - - if (error instanceof Error) { - asyncSearchServiceMap.delete(id); - throw error; - } else if (isRunning) { - asyncSearchServiceMap.set(id, getAsyncSearchServiceState); - } else { - asyncSearchServiceMap.delete(id); - } - - const took = Date.now() - started; - - return of({ - id, - loaded, - total, - isRunning, - isPartial: isRunning, - rawResponse: { - ccsWarning, - log, - took, - values, - }, - }); - }, - cancel: async (id, options, deps) => { - const getAsyncSearchServiceState = asyncSearchServiceMap.get(id); - if (getAsyncSearchServiceState !== undefined) { - getAsyncSearchServiceState().cancel(); - asyncSearchServiceMap.delete(id); - } - }, - }; -}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/constants.ts b/x-pack/plugins/apm/server/lib/search_strategies/index.ts similarity index 77% rename from x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/constants.ts rename to x-pack/plugins/apm/server/lib/search_strategies/index.ts index 711c5f736d7745..b4668138eefabc 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/constants.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export const ERROR_CORRELATION_THRESHOLD = 0.02; +export { registerSearchStrategies } from './register_search_strategies'; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/index.ts b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/index.ts similarity index 58% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/index.ts rename to x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/index.ts index 5ba7b4d7c957ac..073bb122896ffd 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/index.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/index.ts @@ -5,4 +5,8 @@ * 2.0. */ -export { apmCorrelationsSearchStrategyProvider } from './search_strategy'; +export { + latencyCorrelationsSearchServiceProvider, + LatencyCorrelationsSearchServiceProvider, + LatencyCorrelationsSearchStrategy, +} from './latency_correlations_search_service'; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts similarity index 72% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts rename to x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts index e9986bd9f0cf59..b623f6c73f896c 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts @@ -7,11 +7,21 @@ import { range } from 'lodash'; import type { ElasticsearchClient } from 'src/core/server'; + +import type { ISearchStrategy } from '../../../../../../../src/plugins/data/server'; +import { + IKibanaSearchRequest, + IKibanaSearchResponse, +} from '../../../../../../../src/plugins/data/common'; + +import type { SearchStrategyServerParams } from '../../../../common/search_strategies/types'; import type { - SearchServiceParams, - SearchServiceFetchParams, -} from '../../../../common/search_strategies/correlations/types'; + LatencyCorrelationsParams, + LatencyCorrelationsRawResponse, +} from '../../../../common/search_strategies/latency_correlations/types'; + import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; + import { fetchTransactionDurationFieldCandidates, fetchTransactionDurationFieldValuePairs, @@ -20,23 +30,37 @@ import { fetchTransactionDurationHistograms, fetchTransactionDurationHistogramRangeSteps, fetchTransactionDurationRanges, -} from './queries'; -import { computeExpectationsAndRanges } from './utils'; -import { asyncSearchServiceLogProvider } from './async_search_service_log'; -import { asyncSearchServiceStateProvider } from './async_search_service_state'; +} from '../queries'; +import { computeExpectationsAndRanges } from '../utils'; +import { searchServiceLogProvider } from '../search_service_log'; +import type { SearchServiceProvider } from '../search_strategy_provider'; + +import { latencyCorrelationsSearchServiceStateProvider } from './latency_correlations_search_service_state'; + +export type LatencyCorrelationsSearchServiceProvider = SearchServiceProvider< + LatencyCorrelationsParams, + LatencyCorrelationsRawResponse +>; + +export type LatencyCorrelationsSearchStrategy = ISearchStrategy< + IKibanaSearchRequest, + IKibanaSearchResponse +>; -export const asyncSearchServiceProvider = ( +export const latencyCorrelationsSearchServiceProvider: LatencyCorrelationsSearchServiceProvider = ( esClient: ElasticsearchClient, getApmIndices: () => Promise, - searchServiceParams: SearchServiceParams, + searchServiceParams: LatencyCorrelationsParams, includeFrozen: boolean ) => { - const { addLogMessage, getLogMessages } = asyncSearchServiceLogProvider(); + const { addLogMessage, getLogMessages } = searchServiceLogProvider(); - const state = asyncSearchServiceStateProvider(); + const state = latencyCorrelationsSearchServiceStateProvider(); async function fetchCorrelations() { - let params: SearchServiceFetchParams | undefined; + let params: + | (LatencyCorrelationsParams & SearchStrategyServerParams) + | undefined; try { const indices = await getApmIndices(); @@ -71,7 +95,7 @@ export const asyncSearchServiceProvider = ( state.setProgress({ loadedHistogramStepsize: 1, loadedOverallHistogram: 1, - loadedFieldCanditates: 1, + loadedFieldCandidates: 1, loadedFieldValuePairs: 1, loadedHistograms: 1, }); @@ -115,7 +139,7 @@ export const asyncSearchServiceProvider = ( state.setProgress({ loadedHistogramStepsize: 1, loadedOverallHistogram: 1, - loadedFieldCanditates: 1, + loadedFieldCandidates: 1, loadedFieldValuePairs: 1, loadedHistograms: 1, }); @@ -148,7 +172,7 @@ export const asyncSearchServiceProvider = ( addLogMessage(`Identified ${fieldCandidates.length} fieldCandidates.`); - state.setProgress({ loadedFieldCanditates: 1 }); + state.setProgress({ loadedFieldCandidates: 1 }); const fieldValuePairs = await fetchTransactionDurationFieldValuePairs( esClient, @@ -190,7 +214,7 @@ export const asyncSearchServiceProvider = ( fieldValuePairs )) { if (item !== undefined) { - state.addValue(item); + state.addLatencyCorrelation(item); } loadedHistograms++; state.setProgress({ @@ -200,7 +224,7 @@ export const asyncSearchServiceProvider = ( addLogMessage( `Identified ${ - state.getState().values.length + state.getState().latencyCorrelations.length } significant correlations out of ${ fieldValuePairs.length } field/value pairs.` @@ -216,6 +240,11 @@ export const asyncSearchServiceProvider = ( state.setIsRunning(false); } + function cancel() { + addLogMessage(`Service cancelled.`); + state.setIsCancelled(true); + } + fetchCorrelations(); return () => { @@ -229,19 +258,21 @@ export const asyncSearchServiceProvider = ( } = state.getState(); return { - ccsWarning, + cancel, error, - log: getLogMessages(), - isRunning, - loaded: Math.round(state.getOverallProgress() * 100), - overallHistogram, - started: progress.started, - total: 100, - values: state.getValuesSortedByCorrelation(), - percentileThresholdValue, - cancel: () => { - addLogMessage(`Service cancelled.`); - state.setIsCancelled(true); + meta: { + loaded: Math.round(state.getOverallProgress() * 100), + total: 100, + isRunning, + isPartial: isRunning, + }, + rawResponse: { + ccsWarning, + log: getLogMessages(), + took: Date.now() - progress.started, + latencyCorrelations: state.getLatencyCorrelationsSortedByCorrelation(), + percentileThresholdValue, + overallHistogram, }, }; }; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_state.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service_state.test.ts similarity index 82% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_state.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service_state.test.ts index cfa1bf2a5ad716..ce9014004f4b0a 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_state.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service_state.test.ts @@ -5,12 +5,12 @@ * 2.0. */ -import { asyncSearchServiceStateProvider } from './async_search_service_state'; +import { latencyCorrelationsSearchServiceStateProvider } from './latency_correlations_search_service_state'; -describe('async search service', () => { - describe('asyncSearchServiceStateProvider', () => { +describe('search service', () => { + describe('latencyCorrelationsSearchServiceStateProvider', () => { it('initializes with default state', () => { - const state = asyncSearchServiceStateProvider(); + const state = latencyCorrelationsSearchServiceStateProvider(); const defaultState = state.getState(); const defaultProgress = state.getOverallProgress(); @@ -19,7 +19,7 @@ describe('async search service', () => { expect(defaultState.isCancelled).toBe(false); expect(defaultState.isRunning).toBe(true); expect(defaultState.overallHistogram).toBe(undefined); - expect(defaultState.progress.loadedFieldCanditates).toBe(0); + expect(defaultState.progress.loadedFieldCandidates).toBe(0); expect(defaultState.progress.loadedFieldValuePairs).toBe(0); expect(defaultState.progress.loadedHistogramStepsize).toBe(0); expect(defaultState.progress.loadedHistograms).toBe(0); @@ -30,7 +30,7 @@ describe('async search service', () => { }); it('returns updated state', () => { - const state = asyncSearchServiceStateProvider(); + const state = latencyCorrelationsSearchServiceStateProvider(); state.setCcsWarning(true); state.setError(new Error('the-error-message')); @@ -49,7 +49,7 @@ describe('async search service', () => { expect(updatedState.overallHistogram).toEqual([ { key: 1392202800000, doc_count: 1234 }, ]); - expect(updatedState.progress.loadedFieldCanditates).toBe(0); + expect(updatedState.progress.loadedFieldCandidates).toBe(0); expect(updatedState.progress.loadedFieldValuePairs).toBe(0); expect(updatedState.progress.loadedHistogramStepsize).toBe(0); expect(updatedState.progress.loadedHistograms).toBe(0.5); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_state.ts b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service_state.ts similarity index 64% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_state.ts rename to x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service_state.ts index d0aac8987e0703..53f357ed1135fd 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_state.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service_state.ts @@ -5,14 +5,13 @@ * 2.0. */ +import type { HistogramItem } from '../../../../common/search_strategies/types'; import type { - AsyncSearchProviderProgress, - SearchServiceValue, -} from '../../../../common/search_strategies/correlations/types'; + LatencyCorrelationSearchServiceProgress, + LatencyCorrelation, +} from '../../../../common/search_strategies/latency_correlations/types'; -import { HistogramItem } from './queries'; - -export const asyncSearchServiceStateProvider = () => { +export const latencyCorrelationsSearchServiceStateProvider = () => { let ccsWarning = false; function setCcsWarning(d: boolean) { ccsWarning = d; @@ -46,11 +45,11 @@ export const asyncSearchServiceStateProvider = () => { percentileThresholdValue = d; } - let progress: AsyncSearchProviderProgress = { + let progress: LatencyCorrelationSearchServiceProgress = { started: Date.now(), loadedHistogramStepsize: 0, loadedOverallHistogram: 0, - loadedFieldCanditates: 0, + loadedFieldCandidates: 0, loadedFieldValuePairs: 0, loadedHistograms: 0, }; @@ -58,13 +57,13 @@ export const asyncSearchServiceStateProvider = () => { return ( progress.loadedHistogramStepsize * 0.025 + progress.loadedOverallHistogram * 0.025 + - progress.loadedFieldCanditates * 0.025 + + progress.loadedFieldCandidates * 0.025 + progress.loadedFieldValuePairs * 0.025 + progress.loadedHistograms * 0.9 ); } function setProgress( - d: Partial> + d: Partial> ) { progress = { ...progress, @@ -72,13 +71,13 @@ export const asyncSearchServiceStateProvider = () => { }; } - const values: SearchServiceValue[] = []; - function addValue(d: SearchServiceValue) { - values.push(d); + const latencyCorrelations: LatencyCorrelation[] = []; + function addLatencyCorrelation(d: LatencyCorrelation) { + latencyCorrelations.push(d); } - function getValuesSortedByCorrelation() { - return values.sort((a, b) => b.correlation - a.correlation); + function getLatencyCorrelationsSortedByCorrelation() { + return latencyCorrelations.sort((a, b) => b.correlation - a.correlation); } function getState() { @@ -90,16 +89,16 @@ export const asyncSearchServiceStateProvider = () => { overallHistogram, percentileThresholdValue, progress, - values, + latencyCorrelations, }; } return { - addValue, + addLatencyCorrelation, getIsCancelled, getOverallProgress, getState, - getValuesSortedByCorrelation, + getLatencyCorrelationsSortedByCorrelation, setCcsWarning, setError, setIsCancelled, @@ -110,6 +109,6 @@ export const asyncSearchServiceStateProvider = () => { }; }; -export type AsyncSearchServiceState = ReturnType< - typeof asyncSearchServiceStateProvider +export type LatencyCorrelationsSearchServiceState = ReturnType< + typeof latencyCorrelationsSearchServiceStateProvider >; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_prioritized_field_value_pairs.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_prioritized_field_value_pairs.test.ts similarity index 64% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_prioritized_field_value_pairs.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/get_prioritized_field_value_pairs.test.ts index dc11b4860a8b6e..cb1500e70babc4 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_prioritized_field_value_pairs.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_prioritized_field_value_pairs.test.ts @@ -11,13 +11,13 @@ describe('correlations', () => { describe('getPrioritizedFieldValuePairs', () => { it('returns fields without prioritization in the same order', () => { const fieldValuePairs = [ - { field: 'the-field-1', value: 'the-value-1' }, - { field: 'the-field-2', value: 'the-value-2' }, + { fieldName: 'the-field-1', fieldValue: 'the-value-1' }, + { fieldName: 'the-field-2', fieldValue: 'the-value-2' }, ]; const prioritziedFieldValuePairs = getPrioritizedFieldValuePairs( fieldValuePairs ); - expect(prioritziedFieldValuePairs.map((d) => d.field)).toEqual([ + expect(prioritziedFieldValuePairs.map((d) => d.fieldName)).toEqual([ 'the-field-1', 'the-field-2', ]); @@ -25,13 +25,13 @@ describe('correlations', () => { it('returns fields with already sorted prioritization in the same order', () => { const fieldValuePairs = [ - { field: 'service.version', value: 'the-value-1' }, - { field: 'the-field-2', value: 'the-value-2' }, + { fieldName: 'service.version', fieldValue: 'the-value-1' }, + { fieldName: 'the-field-2', fieldValue: 'the-value-2' }, ]; const prioritziedFieldValuePairs = getPrioritizedFieldValuePairs( fieldValuePairs ); - expect(prioritziedFieldValuePairs.map((d) => d.field)).toEqual([ + expect(prioritziedFieldValuePairs.map((d) => d.fieldName)).toEqual([ 'service.version', 'the-field-2', ]); @@ -39,13 +39,13 @@ describe('correlations', () => { it('returns fields with unsorted prioritization in the corrected order', () => { const fieldValuePairs = [ - { field: 'the-field-1', value: 'the-value-1' }, - { field: 'service.version', value: 'the-value-2' }, + { fieldName: 'the-field-1', fieldValue: 'the-value-1' }, + { fieldName: 'service.version', fieldValue: 'the-value-2' }, ]; const prioritziedFieldValuePairs = getPrioritizedFieldValuePairs( fieldValuePairs ); - expect(prioritziedFieldValuePairs.map((d) => d.field)).toEqual([ + expect(prioritziedFieldValuePairs.map((d) => d.fieldName)).toEqual([ 'service.version', 'the-field-1', ]); @@ -53,14 +53,14 @@ describe('correlations', () => { it('considers prefixes when sorting', () => { const fieldValuePairs = [ - { field: 'the-field-1', value: 'the-value-1' }, - { field: 'service.version', value: 'the-value-2' }, - { field: 'cloud.the-field-3', value: 'the-value-3' }, + { fieldName: 'the-field-1', fieldValue: 'the-value-1' }, + { fieldName: 'service.version', fieldValue: 'the-value-2' }, + { fieldName: 'cloud.the-field-3', fieldValue: 'the-value-3' }, ]; const prioritziedFieldValuePairs = getPrioritizedFieldValuePairs( fieldValuePairs ); - expect(prioritziedFieldValuePairs.map((d) => d.field)).toEqual([ + expect(prioritziedFieldValuePairs.map((d) => d.fieldName)).toEqual([ 'service.version', 'cloud.the-field-3', 'the-field-1', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_prioritized_field_value_pairs.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_prioritized_field_value_pairs.ts similarity index 67% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_prioritized_field_value_pairs.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/get_prioritized_field_value_pairs.ts index ddfd87c83f9f38..6338422b022daf 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_prioritized_field_value_pairs.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_prioritized_field_value_pairs.ts @@ -8,19 +8,19 @@ import { FIELDS_TO_ADD_AS_CANDIDATE } from '../constants'; import { hasPrefixToInclude } from '../utils'; -import type { FieldValuePairs } from './query_field_value_pairs'; +import type { FieldValuePair } from '../../../../common/search_strategies/types'; export const getPrioritizedFieldValuePairs = ( - fieldValuePairs: FieldValuePairs + fieldValuePairs: FieldValuePair[] ) => { const prioritizedFields = [...FIELDS_TO_ADD_AS_CANDIDATE]; return fieldValuePairs.sort((a, b) => { - const hasPrefixA = hasPrefixToInclude(a.field); - const hasPrefixB = hasPrefixToInclude(b.field); + const hasPrefixA = hasPrefixToInclude(a.fieldName); + const hasPrefixB = hasPrefixToInclude(b.fieldName); - const includesA = prioritizedFields.includes(a.field); - const includesB = prioritizedFields.includes(b.field); + const includesA = prioritizedFields.includes(a.fieldName); + const includesB = prioritizedFields.includes(b.fieldName); if ((includesA || hasPrefixA) && !includesB && !hasPrefixB) { return -1; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.test.ts similarity index 97% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.test.ts index 3be3438b2d18fe..b8bce753209427 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { getQueryWithParams } from './get_query_with_params'; describe('correlations', () => { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts similarity index 72% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts index 8bd9f3d4e582c9..445f432f2d5ad2 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts @@ -10,22 +10,25 @@ import { getOrElse } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import * as t from 'io-ts'; import { failure } from 'io-ts/lib/PathReporter'; -import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; -import { rangeRt } from '../../../../routes/default_api_types'; -import { getCorrelationsFilters } from '../../../correlations/get_filters'; -import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; +import type { + FieldValuePair, + SearchStrategyParams, +} from '../../../../common/search_strategies/types'; +import { rangeRt } from '../../../routes/default_api_types'; +import { getCorrelationsFilters } from '../../correlations/get_filters'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; export const getTermsQuery = ( - fieldName: string | undefined, - fieldValue: string | undefined + fieldName: FieldValuePair['fieldName'] | undefined, + fieldValue: FieldValuePair['fieldValue'] | undefined ) => { return fieldName && fieldValue ? [{ term: { [fieldName]: fieldValue } }] : []; }; interface QueryParams { - params: SearchServiceFetchParams; - fieldName?: string; - fieldValue?: string; + params: SearchStrategyParams; + fieldName?: FieldValuePair['fieldName']; + fieldValue?: FieldValuePair['fieldValue']; } export const getQueryWithParams = ({ params, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_request_base.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.test.ts similarity index 92% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_request_base.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.test.ts index b95db6d2691f1f..fd5f52207d4c5b 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_request_base.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { getRequestBase } from './get_request_base'; describe('correlations', () => { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_request_base.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.ts similarity index 76% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_request_base.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.ts index e2cdbab830e0da..fb1639b5d5f4ad 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_request_base.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.ts @@ -5,12 +5,12 @@ * 2.0. */ -import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; +import type { SearchStrategyParams } from '../../../../common/search_strategies/types'; export const getRequestBase = ({ index, includeFrozen, -}: SearchServiceFetchParams) => ({ +}: SearchStrategyParams) => ({ index, // matches APM's event client settings ignore_throttled: includeFrozen === undefined ? true : !includeFrozen, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/index.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/index.ts similarity index 84% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/index.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/index.ts index c33b131d9cbd7a..e691b81e4adcf1 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/index.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +export { fetchFailedTransactionsCorrelationPValues } from './query_failure_correlation'; export { fetchTransactionDurationFieldCandidates } from './query_field_candidates'; export { fetchTransactionDurationFieldValuePairs } from './query_field_value_pairs'; export { fetchTransactionDurationFractions } from './query_fractions'; @@ -12,4 +13,4 @@ export { fetchTransactionDurationPercentiles } from './query_percentiles'; export { fetchTransactionDurationCorrelation } from './query_correlation'; export { fetchTransactionDurationHistograms } from './query_histograms_generator'; export { fetchTransactionDurationHistogramRangeSteps } from './query_histogram_range_steps'; -export { fetchTransactionDurationRanges, HistogramItem } from './query_ranges'; +export { fetchTransactionDurationRanges } from './query_ranges'; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_correlation.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.test.ts similarity index 97% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_correlation.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.test.ts index 5245af6cdadcd3..d3d14260df65c8 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_correlation.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.test.ts @@ -8,7 +8,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { fetchTransactionDurationCorrelation, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_correlation.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.ts similarity index 84% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_correlation.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.ts index 823abe936e2236..6e2981032d67d1 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_correlation.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.ts @@ -9,24 +9,16 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { TRANSACTION_DURATION } from '../../../../../common/elasticsearch_fieldnames'; -import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; +import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; +import type { + FieldValuePair, + ResponseHit, + SearchStrategyParams, +} from '../../../../common/search_strategies/types'; import { getQueryWithParams } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; -export interface HistogramItem { - key: number; - doc_count: number; -} - -interface ResponseHitSource { - [s: string]: unknown; -} -interface ResponseHit { - _source: ResponseHitSource; -} - export interface BucketCorrelation { buckets_path: string; function: { @@ -41,13 +33,13 @@ export interface BucketCorrelation { } export const getTransactionDurationCorrelationRequest = ( - params: SearchServiceFetchParams, + params: SearchStrategyParams, expectations: number[], ranges: estypes.AggregationsAggregationRange[], fractions: number[], totalDocCount: number, - fieldName?: string, - fieldValue?: string + fieldName?: FieldValuePair['fieldName'], + fieldValue?: FieldValuePair['fieldValue'] ): estypes.SearchRequest => { const query = getQueryWithParams({ params, fieldName, fieldValue }); @@ -96,13 +88,13 @@ export const getTransactionDurationCorrelationRequest = ( export const fetchTransactionDurationCorrelation = async ( esClient: ElasticsearchClient, - params: SearchServiceFetchParams, + params: SearchStrategyParams, expectations: number[], ranges: estypes.AggregationsAggregationRange[], fractions: number[], totalDocCount: number, - fieldName?: string, - fieldValue?: string + fieldName?: FieldValuePair['fieldName'], + fieldValue?: FieldValuePair['fieldValue'] ): Promise<{ ranges: unknown[]; correlation: number | null; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/queries/query_failure_correlation.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_failure_correlation.ts similarity index 76% rename from x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/queries/query_failure_correlation.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/query_failure_correlation.ts index 81fe6697d1fb18..bc8ab4be97c113 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/queries/query_failure_correlation.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_failure_correlation.ts @@ -6,17 +6,14 @@ */ import { estypes } from '@elastic/elasticsearch'; import { ElasticsearchClient } from 'kibana/server'; -import { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; -import { - getQueryWithParams, - getTermsQuery, -} from '../../correlations/queries/get_query_with_params'; -import { getRequestBase } from '../../correlations/queries/get_request_base'; -import { EVENT_OUTCOME } from '../../../../../common/elasticsearch_fieldnames'; -import { EventOutcome } from '../../../../../common/event_outcome'; +import { SearchStrategyParams } from '../../../../common/search_strategies/types'; +import { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames'; +import { EventOutcome } from '../../../../common/event_outcome'; +import { getQueryWithParams, getTermsQuery } from './get_query_with_params'; +import { getRequestBase } from './get_request_base'; export const getFailureCorrelationRequest = ( - params: SearchServiceFetchParams, + params: SearchStrategyParams, fieldName: string ): estypes.SearchRequest => { const query = getQueryWithParams({ @@ -62,7 +59,7 @@ export const getFailureCorrelationRequest = ( export const fetchFailedTransactionsCorrelationPValues = async ( esClient: ElasticsearchClient, - params: SearchServiceFetchParams, + params: SearchStrategyParams, fieldName: string ) => { const resp = await esClient.search( @@ -77,26 +74,26 @@ export const fetchFailedTransactionsCorrelationPValues = async ( const overallResult = resp.body.aggregations .failure_p_value as estypes.AggregationsSignificantTermsAggregate<{ - key: string; + key: string | number; doc_count: number; bg_count: number; score: number; }>; const result = overallResult.buckets.map((bucket) => { - const score = bucket.score; - // Scale the score into a value from 0 - 1 // using a concave piecewise linear function in -log(p-value) const normalizedScore = - 0.5 * Math.min(Math.max((score - 3.912) / 2.995, 0), 1) + - 0.25 * Math.min(Math.max((score - 6.908) / 6.908, 0), 1) + - 0.25 * Math.min(Math.max((score - 13.816) / 101.314, 0), 1); + 0.5 * Math.min(Math.max((bucket.score - 3.912) / 2.995, 0), 1) + + 0.25 * Math.min(Math.max((bucket.score - 6.908) / 6.908, 0), 1) + + 0.25 * Math.min(Math.max((bucket.score - 13.816) / 101.314, 0), 1); return { - ...bucket, fieldName, fieldValue: bucket.key, - pValue: Math.exp(-score), + doc_count: bucket.doc_count, + bg_count: bucket.doc_count, + score: bucket.score, + pValue: Math.exp(-bucket.score), normalizedScore, // Percentage of time the term appears in failed transactions failurePercentage: bucket.doc_count / overallResult.doc_count, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_candidates.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.test.ts similarity index 96% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_candidates.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.test.ts index 688af72e8f6d39..150348e2a7aa27 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_candidates.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.test.ts @@ -8,9 +8,9 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; -import { hasPrefixToInclude } from '../utils/has_prefix_to_include'; +import { hasPrefixToInclude } from '../utils'; import { fetchTransactionDurationFieldCandidates, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_candidates.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.ts similarity index 90% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_candidates.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.ts index aeb67a4d6884b1..390243295c4f09 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_candidates.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.ts @@ -9,7 +9,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; +import type { SearchStrategyParams } from '../../../../common/search_strategies/types'; import { FIELD_PREFIX_TO_EXCLUDE_AS_CANDIDATE, @@ -21,7 +21,6 @@ import { hasPrefixToInclude } from '../utils'; import { getQueryWithParams } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; -import type { FieldName } from './query_field_value_pairs'; export const shouldBeExcluded = (fieldName: string) => { return ( @@ -33,7 +32,7 @@ export const shouldBeExcluded = (fieldName: string) => { }; export const getRandomDocsRequest = ( - params: SearchServiceFetchParams + params: SearchStrategyParams ): estypes.SearchRequest => ({ ...getRequestBase(params), body: { @@ -52,8 +51,8 @@ export const getRandomDocsRequest = ( export const fetchTransactionDurationFieldCandidates = async ( esClient: ElasticsearchClient, - params: SearchServiceFetchParams -): Promise<{ fieldCandidates: FieldName[] }> => { + params: SearchStrategyParams +): Promise<{ fieldCandidates: string[] }> => { const { index } = params; // Get all fields with keyword mapping const respMapping = await esClient.fieldCaps({ diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_value_pairs.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.test.ts similarity index 72% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_value_pairs.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.test.ts index a20720944f19b4..1fff8cde5bbb30 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_value_pairs.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.test.ts @@ -8,10 +8,10 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; -import { asyncSearchServiceLogProvider } from '../async_search_service_log'; -import { asyncSearchServiceStateProvider } from '../async_search_service_state'; +import { searchServiceLogProvider } from '../search_service_log'; +import { latencyCorrelationsSearchServiceStateProvider } from '../latency_correlations/latency_correlations_search_service_state'; import { fetchTransactionDurationFieldValuePairs, @@ -62,8 +62,8 @@ describe('query_field_value_pairs', () => { search: esClientSearchMock, } as unknown) as ElasticsearchClient; - const { addLogMessage, getLogMessages } = asyncSearchServiceLogProvider(); - const state = asyncSearchServiceStateProvider(); + const { addLogMessage, getLogMessages } = searchServiceLogProvider(); + const state = latencyCorrelationsSearchServiceStateProvider(); const resp = await fetchTransactionDurationFieldValuePairs( esClientMock, @@ -77,12 +77,12 @@ describe('query_field_value_pairs', () => { expect(progress.loadedFieldValuePairs).toBe(1); expect(resp).toEqual([ - { field: 'myFieldCandidate1', value: 'myValue1' }, - { field: 'myFieldCandidate1', value: 'myValue2' }, - { field: 'myFieldCandidate2', value: 'myValue1' }, - { field: 'myFieldCandidate2', value: 'myValue2' }, - { field: 'myFieldCandidate3', value: 'myValue1' }, - { field: 'myFieldCandidate3', value: 'myValue2' }, + { fieldName: 'myFieldCandidate1', fieldValue: 'myValue1' }, + { fieldName: 'myFieldCandidate1', fieldValue: 'myValue2' }, + { fieldName: 'myFieldCandidate2', fieldValue: 'myValue1' }, + { fieldName: 'myFieldCandidate2', fieldValue: 'myValue2' }, + { fieldName: 'myFieldCandidate3', fieldValue: 'myValue1' }, + { fieldName: 'myFieldCandidate3', fieldValue: 'myValue2' }, ]); expect(esClientSearchMock).toHaveBeenCalledTimes(3); expect(getLogMessages()).toEqual([]); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_value_pairs.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.ts similarity index 72% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_value_pairs.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.ts index 33adff4af7a52f..aa7d9f341a3458 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_value_pairs.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.ts @@ -9,26 +9,21 @@ import type { ElasticsearchClient } from 'src/core/server'; import type { estypes } from '@elastic/elasticsearch'; -import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; +import type { + FieldValuePair, + SearchStrategyParams, +} from '../../../../common/search_strategies/types'; -import type { AsyncSearchServiceLog } from '../async_search_service_log'; -import type { AsyncSearchServiceState } from '../async_search_service_state'; +import type { SearchServiceLog } from '../search_service_log'; +import type { LatencyCorrelationsSearchServiceState } from '../latency_correlations/latency_correlations_search_service_state'; import { TERMS_SIZE } from '../constants'; import { getQueryWithParams } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; -export type FieldName = string; - -interface FieldValuePair { - field: FieldName; - value: string; -} -export type FieldValuePairs = FieldValuePair[]; - export const getTermsAggRequest = ( - params: SearchServiceFetchParams, - fieldName: FieldName + params: SearchStrategyParams, + fieldName: string ): estypes.SearchRequest => ({ ...getRequestBase(params), body: { @@ -47,10 +42,10 @@ export const getTermsAggRequest = ( const fetchTransactionDurationFieldTerms = async ( esClient: ElasticsearchClient, - params: SearchServiceFetchParams, + params: SearchStrategyParams, fieldName: string, - addLogMessage: AsyncSearchServiceLog['addLogMessage'] -): Promise => { + addLogMessage: SearchServiceLog['addLogMessage'] +): Promise => { try { const resp = await esClient.search(getTermsAggRequest(params, fieldName)); @@ -67,8 +62,8 @@ const fetchTransactionDurationFieldTerms = async ( }>)?.buckets; if (buckets?.length >= 1) { return buckets.map((d) => ({ - field: fieldName, - value: d.key, + fieldName, + fieldValue: d.key, })); } } catch (e) { @@ -82,8 +77,8 @@ const fetchTransactionDurationFieldTerms = async ( }; async function fetchInSequence( - fieldCandidates: FieldName[], - fn: (fieldCandidate: string) => Promise + fieldCandidates: string[], + fn: (fieldCandidate: string) => Promise ) { const results = []; @@ -96,11 +91,11 @@ async function fetchInSequence( export const fetchTransactionDurationFieldValuePairs = async ( esClient: ElasticsearchClient, - params: SearchServiceFetchParams, - fieldCandidates: FieldName[], - state: AsyncSearchServiceState, - addLogMessage: AsyncSearchServiceLog['addLogMessage'] -): Promise => { + params: SearchStrategyParams, + fieldCandidates: string[], + state: LatencyCorrelationsSearchServiceState, + addLogMessage: SearchServiceLog['addLogMessage'] +): Promise => { let fieldValuePairsProgress = 1; return await fetchInSequence( diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_fractions.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.test.ts similarity index 96% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_fractions.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.test.ts index 73df48a0d8170a..fdf383453e17f1 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_fractions.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.test.ts @@ -8,7 +8,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { fetchTransactionDurationFractions, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_fractions.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.ts similarity index 87% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_fractions.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.ts index 35e59054ad01f6..25e5f62564b044 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_fractions.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.ts @@ -8,14 +8,14 @@ import { ElasticsearchClient } from 'kibana/server'; import { estypes } from '@elastic/elasticsearch'; -import { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; -import { TRANSACTION_DURATION } from '../../../../../common/elasticsearch_fieldnames'; +import { SearchStrategyParams } from '../../../../common/search_strategies/types'; +import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; import { getQueryWithParams } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; export const getTransactionDurationRangesRequest = ( - params: SearchServiceFetchParams, + params: SearchStrategyParams, ranges: estypes.AggregationsAggregationRange[] ): estypes.SearchRequest => ({ ...getRequestBase(params), @@ -38,7 +38,7 @@ export const getTransactionDurationRangesRequest = ( */ export const fetchTransactionDurationFractions = async ( esClient: ElasticsearchClient, - params: SearchServiceFetchParams, + params: SearchStrategyParams, ranges: estypes.AggregationsAggregationRange[] ): Promise<{ fractions: number[]; totalDocCount: number }> => { const resp = await esClient.search( diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.test.ts similarity index 97% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.test.ts index 9b2a4807d48633..e6faeb16247fba 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.test.ts @@ -8,7 +8,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { fetchTransactionDurationHistogram, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.ts similarity index 78% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.ts index 18fc18af1472e3..1dac98d785f3c1 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.ts @@ -9,21 +9,22 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { TRANSACTION_DURATION } from '../../../../../common/elasticsearch_fieldnames'; +import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; import type { + FieldValuePair, HistogramItem, ResponseHit, - SearchServiceFetchParams, -} from '../../../../../common/search_strategies/correlations/types'; + SearchStrategyParams, +} from '../../../../common/search_strategies/types'; import { getQueryWithParams } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; export const getTransactionDurationHistogramRequest = ( - params: SearchServiceFetchParams, + params: SearchStrategyParams, interval: number, - fieldName?: string, - fieldValue?: string + fieldName?: FieldValuePair['fieldName'], + fieldValue?: FieldValuePair['fieldValue'] ): estypes.SearchRequest => ({ ...getRequestBase(params), body: { @@ -39,10 +40,10 @@ export const getTransactionDurationHistogramRequest = ( export const fetchTransactionDurationHistogram = async ( esClient: ElasticsearchClient, - params: SearchServiceFetchParams, + params: SearchStrategyParams, interval: number, - fieldName?: string, - fieldValue?: string + fieldName?: FieldValuePair['fieldName'], + fieldValue?: FieldValuePair['fieldValue'] ): Promise => { const resp = await esClient.search( getTransactionDurationHistogramRequest( diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_interval.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_interval.test.ts similarity index 97% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_interval.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_interval.test.ts index bb76769fe94b5e..7b0d00d0d9b578 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_interval.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_interval.test.ts @@ -8,7 +8,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { fetchTransactionDurationHistogramInterval, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_interval.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_interval.ts similarity index 85% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_interval.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_interval.ts index cc50c8d4d860af..7a8752e45c6f26 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_interval.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_interval.ts @@ -9,8 +9,8 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { TRANSACTION_DURATION } from '../../../../../common/elasticsearch_fieldnames'; -import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; +import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; +import type { SearchStrategyParams } from '../../../../common/search_strategies/types'; import { getQueryWithParams } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; @@ -18,7 +18,7 @@ import { getRequestBase } from './get_request_base'; const HISTOGRAM_INTERVALS = 1000; export const getHistogramIntervalRequest = ( - params: SearchServiceFetchParams + params: SearchStrategyParams ): estypes.SearchRequest => ({ ...getRequestBase(params), body: { @@ -33,7 +33,7 @@ export const getHistogramIntervalRequest = ( export const fetchTransactionDurationHistogramInterval = async ( esClient: ElasticsearchClient, - params: SearchServiceFetchParams + params: SearchStrategyParams ): Promise => { const resp = await esClient.search(getHistogramIntervalRequest(params)); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_range_steps.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.test.ts similarity index 97% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_range_steps.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.test.ts index 52cfe6168232d4..88d4f1a57adebc 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_range_steps.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.test.ts @@ -8,7 +8,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { fetchTransactionDurationHistogramRangeSteps, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_range_steps.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.ts similarity index 88% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_range_steps.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.ts index 116b5d16456015..31ab7392155bc0 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_range_steps.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.ts @@ -11,8 +11,8 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { TRANSACTION_DURATION } from '../../../../../common/elasticsearch_fieldnames'; -import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; +import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; +import type { SearchStrategyParams } from '../../../../common/search_strategies/types'; import { getQueryWithParams } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; @@ -26,7 +26,7 @@ const getHistogramRangeSteps = (min: number, max: number, steps: number) => { }; export const getHistogramIntervalRequest = ( - params: SearchServiceFetchParams + params: SearchStrategyParams ): estypes.SearchRequest => ({ ...getRequestBase(params), body: { @@ -41,7 +41,7 @@ export const getHistogramIntervalRequest = ( export const fetchTransactionDurationHistogramRangeSteps = async ( esClient: ElasticsearchClient, - params: SearchServiceFetchParams + params: SearchStrategyParams ): Promise => { const steps = 100; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histograms_generator.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.test.ts similarity index 82% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histograms_generator.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.test.ts index 22876684bec7eb..c80b2533d7e329 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histograms_generator.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.test.ts @@ -8,10 +8,10 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; -import { asyncSearchServiceLogProvider } from '../async_search_service_log'; -import { asyncSearchServiceStateProvider } from '../async_search_service_state'; +import { searchServiceLogProvider } from '../search_service_log'; +import { latencyCorrelationsSearchServiceStateProvider } from '../latency_correlations/latency_correlations_search_service_state'; import { fetchTransactionDurationHistograms } from './query_histograms_generator'; @@ -30,9 +30,9 @@ const totalDocCount = 1234; const histogramRangeSteps = [1, 2, 4, 5]; const fieldValuePairs = [ - { field: 'the-field-name-1', value: 'the-field-value-1' }, - { field: 'the-field-name-2', value: 'the-field-value-2' }, - { field: 'the-field-name-2', value: 'the-field-value-3' }, + { fieldName: 'the-field-name-1', fieldValue: 'the-field-value-1' }, + { fieldName: 'the-field-name-2', fieldValue: 'the-field-value-2' }, + { fieldName: 'the-field-name-2', fieldValue: 'the-field-value-3' }, ]; describe('query_histograms_generator', () => { @@ -50,8 +50,8 @@ describe('query_histograms_generator', () => { search: esClientSearchMock, } as unknown) as ElasticsearchClient; - const state = asyncSearchServiceStateProvider(); - const { addLogMessage, getLogMessages } = asyncSearchServiceLogProvider(); + const state = latencyCorrelationsSearchServiceStateProvider(); + const { addLogMessage, getLogMessages } = searchServiceLogProvider(); let loadedHistograms = 0; const items = []; @@ -104,8 +104,8 @@ describe('query_histograms_generator', () => { search: esClientSearchMock, } as unknown) as ElasticsearchClient; - const state = asyncSearchServiceStateProvider(); - const { addLogMessage, getLogMessages } = asyncSearchServiceLogProvider(); + const state = latencyCorrelationsSearchServiceStateProvider(); + const { addLogMessage, getLogMessages } = searchServiceLogProvider(); let loadedHistograms = 0; const items = []; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histograms_generator.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.ts similarity index 78% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histograms_generator.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.ts index c4869aac187c65..a07abd356db6da 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histograms_generator.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.ts @@ -9,29 +9,30 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; +import type { + FieldValuePair, + SearchStrategyParams, +} from '../../../../common/search_strategies/types'; -import type { AsyncSearchServiceLog } from '../async_search_service_log'; -import type { AsyncSearchServiceState } from '../async_search_service_state'; +import type { SearchServiceLog } from '../search_service_log'; +import type { LatencyCorrelationsSearchServiceState } from '../latency_correlations/latency_correlations_search_service_state'; import { CORRELATION_THRESHOLD, KS_TEST_THRESHOLD } from '../constants'; import { getPrioritizedFieldValuePairs } from './get_prioritized_field_value_pairs'; import { fetchTransactionDurationCorrelation } from './query_correlation'; import { fetchTransactionDurationRanges } from './query_ranges'; -import type { FieldValuePairs } from './query_field_value_pairs'; - export async function* fetchTransactionDurationHistograms( esClient: ElasticsearchClient, - addLogMessage: AsyncSearchServiceLog['addLogMessage'], - params: SearchServiceFetchParams, - state: AsyncSearchServiceState, + addLogMessage: SearchServiceLog['addLogMessage'], + params: SearchStrategyParams, + state: LatencyCorrelationsSearchServiceState, expectations: number[], ranges: estypes.AggregationsAggregationRange[], fractions: number[], histogramRangeSteps: number[], totalDocCount: number, - fieldValuePairs: FieldValuePairs + fieldValuePairs: FieldValuePair[] ) { for (const item of getPrioritizedFieldValuePairs(fieldValuePairs)) { if (params === undefined || item === undefined || state.getIsCancelled()) { @@ -49,8 +50,8 @@ export async function* fetchTransactionDurationHistograms( ranges, fractions, totalDocCount, - item.field, - item.value + item.fieldName, + item.fieldValue ); if (state.getIsCancelled()) { @@ -68,8 +69,8 @@ export async function* fetchTransactionDurationHistograms( esClient, params, histogramRangeSteps, - item.field, - item.value + item.fieldName, + item.fieldValue ); yield { ...item, @@ -85,7 +86,7 @@ export async function* fetchTransactionDurationHistograms( // just add the error to the internal log and check if we'd want to set the // cross-cluster search compatibility warning to true. addLogMessage( - `Failed to fetch correlation/kstest for '${item.field}/${item.value}'`, + `Failed to fetch correlation/kstest for '${item.fieldName}/${item.fieldValue}'`, JSON.stringify(e) ); if (params?.index.includes(':')) { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_percentiles.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.test.ts similarity index 97% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_percentiles.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.test.ts index cab2e283935d63..1a5d518b7e47ac 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_percentiles.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.test.ts @@ -8,7 +8,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { fetchTransactionDurationPercentiles, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_percentiles.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.ts similarity index 80% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_percentiles.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.ts index bd230687314e66..8d9e2ed88ba370 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_percentiles.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.ts @@ -9,30 +9,22 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { TRANSACTION_DURATION } from '../../../../../common/elasticsearch_fieldnames'; -import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; +import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; +import type { + FieldValuePair, + ResponseHit, + SearchStrategyParams, +} from '../../../../common/search_strategies/types'; import { getQueryWithParams } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; import { SIGNIFICANT_VALUE_DIGITS } from '../constants'; -export interface HistogramItem { - key: number; - doc_count: number; -} - -interface ResponseHitSource { - [s: string]: unknown; -} -interface ResponseHit { - _source: ResponseHitSource; -} - export const getTransactionDurationPercentilesRequest = ( - params: SearchServiceFetchParams, + params: SearchStrategyParams, percents?: number[], - fieldName?: string, - fieldValue?: string + fieldName?: FieldValuePair['fieldName'], + fieldValue?: FieldValuePair['fieldValue'] ): estypes.SearchRequest => { const query = getQueryWithParams({ params, fieldName, fieldValue }); @@ -59,10 +51,10 @@ export const getTransactionDurationPercentilesRequest = ( export const fetchTransactionDurationPercentiles = async ( esClient: ElasticsearchClient, - params: SearchServiceFetchParams, + params: SearchStrategyParams, percents?: number[], - fieldName?: string, - fieldValue?: string + fieldName?: FieldValuePair['fieldName'], + fieldValue?: FieldValuePair['fieldValue'] ): Promise<{ totalDocs: number; percentiles: Record }> => { const resp = await esClient.search( getTransactionDurationPercentilesRequest( diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_ranges.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.test.ts similarity index 97% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_ranges.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.test.ts index 839d6a33cfe058..64b746b72534a3 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_ranges.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.test.ts @@ -8,7 +8,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { fetchTransactionDurationRanges, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_ranges.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.ts similarity index 78% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_ranges.ts rename to x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.ts index 6f662363d0c42a..e15962f2979ba0 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_ranges.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.ts @@ -9,29 +9,21 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { TRANSACTION_DURATION } from '../../../../../common/elasticsearch_fieldnames'; -import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; +import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; +import type { + FieldValuePair, + ResponseHit, + SearchStrategyParams, +} from '../../../../common/search_strategies/types'; import { getQueryWithParams } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; -export interface HistogramItem { - key: number; - doc_count: number; -} - -interface ResponseHitSource { - [s: string]: unknown; -} -interface ResponseHit { - _source: ResponseHitSource; -} - export const getTransactionDurationRangesRequest = ( - params: SearchServiceFetchParams, + params: SearchStrategyParams, rangesSteps: number[], - fieldName?: string, - fieldValue?: string + fieldName?: FieldValuePair['fieldName'], + fieldValue?: FieldValuePair['fieldValue'] ): estypes.SearchRequest => { const query = getQueryWithParams({ params, fieldName, fieldValue }); @@ -66,10 +58,10 @@ export const getTransactionDurationRangesRequest = ( export const fetchTransactionDurationRanges = async ( esClient: ElasticsearchClient, - params: SearchServiceFetchParams, + params: SearchStrategyParams, rangesSteps: number[], - fieldName?: string, - fieldValue?: string + fieldName?: FieldValuePair['fieldName'], + fieldValue?: FieldValuePair['fieldValue'] ): Promise> => { const resp = await esClient.search( getTransactionDurationRangesRequest( diff --git a/x-pack/plugins/apm/server/lib/search_strategies/register_search_strategies.ts b/x-pack/plugins/apm/server/lib/search_strategies/register_search_strategies.ts new file mode 100644 index 00000000000000..713c5e390ca8ba --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/register_search_strategies.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PluginSetup as DataPluginSetup } from 'src/plugins/data/server'; + +import { APM_SEARCH_STRATEGIES } from '../../../common/search_strategies/constants'; + +import type { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; + +import { failedTransactionsCorrelationsSearchServiceProvider } from './failed_transactions_correlations'; +import { latencyCorrelationsSearchServiceProvider } from './latency_correlations'; +import { searchStrategyProvider } from './search_strategy_provider'; + +export const registerSearchStrategies = ( + registerSearchStrategy: DataPluginSetup['search']['registerSearchStrategy'], + getApmIndices: () => Promise, + includeFrozen: boolean +) => { + registerSearchStrategy( + APM_SEARCH_STRATEGIES.APM_LATENCY_CORRELATIONS, + searchStrategyProvider( + latencyCorrelationsSearchServiceProvider, + getApmIndices, + includeFrozen + ) + ); + + registerSearchStrategy( + APM_SEARCH_STRATEGIES.APM_FAILED_TRANSACTIONS_CORRELATIONS, + searchStrategyProvider( + failedTransactionsCorrelationsSearchServiceProvider, + getApmIndices, + includeFrozen + ) + ); +}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/search_service_log.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/search_service_log.test.ts new file mode 100644 index 00000000000000..5b887f15a584e3 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/search_service_log.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + searchServiceLogProvider, + currentTimeAsString, +} from './search_service_log'; + +describe('search service', () => { + describe('currentTimeAsString', () => { + it('returns the current time as a string', () => { + const mockDate = new Date(1392202800000); + // @ts-ignore ignore the mockImplementation callback error + const spy = jest.spyOn(global, 'Date').mockReturnValue(mockDate); + + const timeString = currentTimeAsString(); + + expect(timeString).toEqual('2014-02-12T11:00:00.000Z'); + + spy.mockRestore(); + }); + }); + + describe('searchServiceLogProvider', () => { + it('adds and retrieves messages from the log', async () => { + const { addLogMessage, getLogMessages } = searchServiceLogProvider(); + + const mockDate = new Date(1392202800000); + // @ts-ignore ignore the mockImplementation callback error + const spy = jest.spyOn(global, 'Date').mockReturnValue(mockDate); + + addLogMessage('the first message'); + addLogMessage('the second message'); + + expect(getLogMessages()).toEqual([ + '2014-02-12T11:00:00.000Z: the first message', + '2014-02-12T11:00:00.000Z: the second message', + ]); + + spy.mockRestore(); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_log.ts b/x-pack/plugins/apm/server/lib/search_strategies/search_service_log.ts similarity index 78% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_log.ts rename to x-pack/plugins/apm/server/lib/search_strategies/search_service_log.ts index e69d2f55b6c565..73a59021b01ed8 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_log.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/search_service_log.ts @@ -5,15 +5,15 @@ * 2.0. */ -import { currentTimeAsString } from './utils'; - interface LogMessage { timestamp: string; message: string; error?: string; } -export const asyncSearchServiceLogProvider = () => { +export const currentTimeAsString = () => new Date().toISOString(); + +export const searchServiceLogProvider = () => { const log: LogMessage[] = []; function addLogMessage(message: string, error?: string) { @@ -31,6 +31,4 @@ export const asyncSearchServiceLogProvider = () => { return { addLogMessage, getLogMessages }; }; -export type AsyncSearchServiceLog = ReturnType< - typeof asyncSearchServiceLogProvider ->; +export type SearchServiceLog = ReturnType; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts similarity index 84% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts index b5ab4a072be6c9..b45b95666326f7 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts @@ -8,14 +8,16 @@ import type { estypes } from '@elastic/elasticsearch'; import { SearchStrategyDependencies } from 'src/plugins/data/server'; -import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; -import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; +import { IKibanaSearchRequest } from '../../../../../../src/plugins/data/common'; -import { - apmCorrelationsSearchStrategyProvider, - PartialSearchRequest, -} from './search_strategy'; +import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; +import type { LatencyCorrelationsParams } from '../../../common/search_strategies/latency_correlations/types'; + +import type { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; + +import { latencyCorrelationsSearchServiceProvider } from './latency_correlations'; +import { searchStrategyProvider } from './search_strategy_provider'; // helper to trigger promises in the async search service const flushPromises = () => new Promise(setImmediate); @@ -106,7 +108,8 @@ const getApmIndicesMock = async () => describe('APM Correlations search strategy', () => { describe('strategy interface', () => { it('returns a custom search strategy with a `search` and `cancel` function', async () => { - const searchStrategy = await apmCorrelationsSearchStrategyProvider( + const searchStrategy = await searchStrategyProvider( + latencyCorrelationsSearchServiceProvider, getApmIndicesMock, false ); @@ -120,7 +123,9 @@ describe('APM Correlations search strategy', () => { let mockClientSearch: jest.Mock; let mockGetApmIndicesMock: jest.Mock; let mockDeps: SearchStrategyDependencies; - let params: Required['params']; + let params: Required< + IKibanaSearchRequest + >['params']; beforeEach(() => { mockClientFieldCaps = jest.fn(clientFieldCapsMock); @@ -139,13 +144,16 @@ describe('APM Correlations search strategy', () => { end: '2021', environment: ENVIRONMENT_ALL.value, kuery: '', + percentileThreshold: 95, + analyzeCorrelations: true, }; }); describe('async functionality', () => { describe('when no params are provided', () => { it('throws an error', async () => { - const searchStrategy = await apmCorrelationsSearchStrategyProvider( + const searchStrategy = await searchStrategyProvider( + latencyCorrelationsSearchServiceProvider, mockGetApmIndicesMock, false ); @@ -160,7 +168,8 @@ describe('APM Correlations search strategy', () => { describe('when no ID is provided', () => { it('performs a client search with params', async () => { - const searchStrategy = await apmCorrelationsSearchStrategyProvider( + const searchStrategy = await searchStrategyProvider( + latencyCorrelationsSearchServiceProvider, mockGetApmIndicesMock, false ); @@ -178,6 +187,7 @@ describe('APM Correlations search strategy', () => { percentiles: { field: 'transaction.duration.us', hdr: { number_of_significant_value_digits: 3 }, + percents: [95], }, }, }, @@ -206,7 +216,8 @@ describe('APM Correlations search strategy', () => { describe('when an ID with params is provided', () => { it('retrieves the current request', async () => { - const searchStrategy = await apmCorrelationsSearchStrategyProvider( + const searchStrategy = await searchStrategyProvider( + latencyCorrelationsSearchServiceProvider, mockGetApmIndicesMock, false ); @@ -232,7 +243,8 @@ describe('APM Correlations search strategy', () => { mockClientSearch .mockReset() .mockRejectedValueOnce(new Error('client error')); - const searchStrategy = await apmCorrelationsSearchStrategyProvider( + const searchStrategy = await searchStrategyProvider( + latencyCorrelationsSearchServiceProvider, mockGetApmIndicesMock, false ); @@ -250,7 +262,8 @@ describe('APM Correlations search strategy', () => { it('triggers the subscription only once', async () => { expect.assertions(2); - const searchStrategy = await apmCorrelationsSearchStrategyProvider( + const searchStrategy = await searchStrategyProvider( + latencyCorrelationsSearchServiceProvider, mockGetApmIndicesMock, false ); @@ -267,7 +280,8 @@ describe('APM Correlations search strategy', () => { describe('response', () => { it('sends an updated response on consecutive search calls', async () => { - const searchStrategy = await apmCorrelationsSearchStrategyProvider( + const searchStrategy = await searchStrategyProvider( + latencyCorrelationsSearchServiceProvider, mockGetApmIndicesMock, false ); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.ts b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.ts new file mode 100644 index 00000000000000..c0376852b25051 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.ts @@ -0,0 +1,151 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import uuid from 'uuid'; +import { of } from 'rxjs'; + +import type { ElasticsearchClient } from 'src/core/server'; + +import type { ISearchStrategy } from '../../../../../../src/plugins/data/server'; +import { + IKibanaSearchRequest, + IKibanaSearchResponse, +} from '../../../../../../src/plugins/data/common'; + +import type { SearchStrategyClientParams } from '../../../common/search_strategies/types'; +import type { RawResponseBase } from '../../../common/search_strategies/types'; +import type { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; + +import type { + LatencyCorrelationsSearchServiceProvider, + LatencyCorrelationsSearchStrategy, +} from './latency_correlations'; +import type { + FailedTransactionsCorrelationsSearchServiceProvider, + FailedTransactionsCorrelationsSearchStrategy, +} from './failed_transactions_correlations'; + +interface SearchServiceState { + cancel: () => void; + error: Error; + meta: { + loaded: number; + total: number; + isRunning: boolean; + isPartial: boolean; + }; + rawResponse: TRawResponse; +} + +type GetSearchServiceState< + TRawResponse extends RawResponseBase +> = () => SearchServiceState; + +export type SearchServiceProvider< + TSearchStrategyClientParams extends SearchStrategyClientParams, + TRawResponse extends RawResponseBase +> = ( + esClient: ElasticsearchClient, + getApmIndices: () => Promise, + searchServiceParams: TSearchStrategyClientParams, + includeFrozen: boolean +) => GetSearchServiceState; + +// Failed Transactions Correlations function overload +export function searchStrategyProvider( + searchServiceProvider: FailedTransactionsCorrelationsSearchServiceProvider, + getApmIndices: () => Promise, + includeFrozen: boolean +): FailedTransactionsCorrelationsSearchStrategy; + +// Latency Correlations function overload +export function searchStrategyProvider( + searchServiceProvider: LatencyCorrelationsSearchServiceProvider, + getApmIndices: () => Promise, + includeFrozen: boolean +): LatencyCorrelationsSearchStrategy; + +export function searchStrategyProvider< + TSearchStrategyClientParams extends SearchStrategyClientParams, + TRawResponse extends RawResponseBase +>( + searchServiceProvider: SearchServiceProvider< + TSearchStrategyClientParams, + TRawResponse + >, + getApmIndices: () => Promise, + includeFrozen: boolean +): ISearchStrategy< + IKibanaSearchRequest, + IKibanaSearchResponse +> { + const searchServiceMap = new Map< + string, + GetSearchServiceState + >(); + + return { + search: (request, options, deps) => { + if (request.params === undefined) { + throw new Error('Invalid request parameters.'); + } + + // The function to fetch the current state of the search service. + // This will be either an existing service for a follow up fetch or a new one for new requests. + let getSearchServiceState: GetSearchServiceState; + + // If the request includes an ID, we require that the search service already exists + // otherwise we throw an error. The client should never poll a service that's been cancelled or finished. + // This also avoids instantiating search services when the service gets called with random IDs. + if (typeof request.id === 'string') { + const existingGetSearchServiceState = searchServiceMap.get(request.id); + + if (typeof existingGetSearchServiceState === 'undefined') { + throw new Error( + `SearchService with ID '${request.id}' does not exist.` + ); + } + + getSearchServiceState = existingGetSearchServiceState; + } else { + getSearchServiceState = searchServiceProvider( + deps.esClient.asCurrentUser, + getApmIndices, + request.params as TSearchStrategyClientParams, + includeFrozen + ); + } + + // Reuse the request's id or create a new one. + const id = request.id ?? uuid(); + + const { error, meta, rawResponse } = getSearchServiceState(); + + if (error instanceof Error) { + searchServiceMap.delete(id); + throw error; + } else if (meta.isRunning) { + searchServiceMap.set(id, getSearchServiceState); + } else { + searchServiceMap.delete(id); + } + + return of({ + id, + ...meta, + rawResponse, + }); + }, + cancel: async (id, options, deps) => { + const getSearchServiceState = searchServiceMap.get(id); + if (getSearchServiceState !== undefined) { + getSearchServiceState().cancel(); + searchServiceMap.delete(id); + } + }, + }; +} diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/compute_expectations_and_ranges.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/utils/compute_expectations_and_ranges.test.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/compute_expectations_and_ranges.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/utils/compute_expectations_and_ranges.test.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/compute_expectations_and_ranges.ts b/x-pack/plugins/apm/server/lib/search_strategies/utils/compute_expectations_and_ranges.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/compute_expectations_and_ranges.ts rename to x-pack/plugins/apm/server/lib/search_strategies/utils/compute_expectations_and_ranges.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/has_prefix_to_include.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/utils/has_prefix_to_include.test.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/has_prefix_to_include.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/utils/has_prefix_to_include.test.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/has_prefix_to_include.ts b/x-pack/plugins/apm/server/lib/search_strategies/utils/has_prefix_to_include.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/has_prefix_to_include.ts rename to x-pack/plugins/apm/server/lib/search_strategies/utils/has_prefix_to_include.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/index.ts b/x-pack/plugins/apm/server/lib/search_strategies/utils/index.ts similarity index 86% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/index.ts rename to x-pack/plugins/apm/server/lib/search_strategies/utils/index.ts index 000fd57c718b70..727bc6cd787a0d 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/index.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/utils/index.ts @@ -6,5 +6,4 @@ */ export { computeExpectationsAndRanges } from './compute_expectations_and_ranges'; -export { currentTimeAsString } from './current_time_as_string'; export { hasPrefixToInclude } from './has_prefix_to_include'; diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index 14c8bd9087b89b..1c6d1cdef37ca7 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -28,7 +28,7 @@ import { registerFleetPolicyCallbacks } from './lib/fleet/register_fleet_policy_ import { createApmTelemetry } from './lib/apm_telemetry'; import { createApmEventClient } from './lib/helpers/create_es_client/create_apm_event_client'; import { getInternalSavedObjectsClient } from './lib/helpers/get_internal_saved_objects_client'; -import { apmCorrelationsSearchStrategyProvider } from './lib/search_strategies/correlations'; +import { registerSearchStrategies } from './lib/search_strategies'; import { createApmAgentConfigurationIndex } from './lib/settings/agent_configuration/create_agent_config_index'; import { getApmIndices } from './lib/settings/apm_indices/get_apm_indices'; import { createApmCustomLinkIndex } from './lib/settings/custom_link/create_custom_link_index'; @@ -51,10 +51,6 @@ import { TRANSACTION_TYPE, } from '../common/elasticsearch_fieldnames'; import { tutorialProvider } from './tutorial'; -import { - apmFailedTransactionsCorrelationsSearchStrategyProvider, - FAILED_TRANSACTIONS_CORRELATION_SEARCH_STRATEGY, -} from './lib/search_strategies/failed_transactions_correlations'; export class APMPlugin implements @@ -217,22 +213,10 @@ export class APMPlugin .asScopedToClient(savedObjectsClient) .get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); - // Register APM latency correlations search strategy - plugins.data.search.registerSearchStrategy( - 'apmCorrelationsSearchStrategy', - apmCorrelationsSearchStrategyProvider( - boundGetApmIndices, - includeFrozen - ) - ); - - // Register APM failed transactions correlations search strategy - plugins.data.search.registerSearchStrategy( - FAILED_TRANSACTIONS_CORRELATION_SEARCH_STRATEGY, - apmFailedTransactionsCorrelationsSearchStrategyProvider( - boundGetApmIndices, - includeFrozen - ) + registerSearchStrategies( + plugins.data.search.registerSearchStrategy, + boundGetApmIndices, + includeFrozen ); })(); }); diff --git a/x-pack/plugins/drilldowns/url_drilldown/kibana.json b/x-pack/plugins/drilldowns/url_drilldown/kibana.json index a4552d201f2633..83ea4b7be54bf5 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/kibana.json +++ b/x-pack/plugins/drilldowns/url_drilldown/kibana.json @@ -7,6 +7,7 @@ "name": "App Services", "githubTeam": "kibana-app-services" }, + "description": "Adds drilldown implementations to Kibana", "requiredPlugins": ["embeddable", "uiActions", "uiActionsEnhanced"], "requiredBundles": ["kibanaUtils", "kibanaReact"] } diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts index 07c6addda27674..a6896367bd6134 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts @@ -262,7 +262,7 @@ describe('UrlDrilldown', () => { indexPatterns: [{ id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' }], } ); - const data: any = { + const data = { data: [ createPoint({ field: 'field0', value: 'value0' }), createPoint({ field: 'field1', value: 'value1' }), diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/context_variables.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/context_variables.ts index 65c665a182e18a..491501b9dd4a77 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/context_variables.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/context_variables.ts @@ -54,7 +54,7 @@ export interface ContextValues { panel: PanelValues; } -function hasSavedObjectId(obj: Record): obj is { savedObjectId: string } { +function hasSavedObjectId(obj: Record): obj is { savedObjectId: string } { return 'savedObjectId' in obj && typeof obj.savedObjectId === 'string'; } @@ -64,12 +64,13 @@ function hasSavedObjectId(obj: Record): obj is { savedObjectId: str */ function getIndexPatternIds(output: EmbeddableOutput): string[] { function hasIndexPatterns( - _output: Record + _output: unknown ): _output is { indexPatterns: Array<{ id?: string }> } { return ( - 'indexPatterns' in _output && - Array.isArray(_output.indexPatterns) && - _output.indexPatterns.length > 0 + typeof _output === 'object' && + !!_output && + Array.isArray((_output as { indexPatterns: unknown[] }).indexPatterns) && + (_output as { indexPatterns: Array<{ id?: string }> }).indexPatterns.length > 0 ); } return hasIndexPatterns(output) diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/event_variables.test.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/event_variables.test.ts index 3d0c55a08d1bff..2a56a5fa0e102a 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/event_variables.test.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/event_variables.test.ts @@ -53,7 +53,10 @@ describe('VALUE_CLICK_TRIGGER', () => { describe('handles undefined, null or missing values', () => { test('undefined or missing values are removed from the result scope', () => { - const point = createPoint({ field: undefined } as any); + const point = createPoint(({ field: undefined } as unknown) as { + field: string; + value: string | null | number | boolean; + }); const eventScope = getEventScopeValues({ data: { data: [point] }, }) as ValueClickTriggerEventScope; diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/util.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/util.ts index ef9045b9ba1083..660bcae9fe146e 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/util.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/util.ts @@ -15,7 +15,7 @@ export const toPrimitiveOrUndefined = (v: unknown): Primitive | undefined => { return String(v); }; -export const deleteUndefinedKeys = >(obj: T): T => { +export const deleteUndefinedKeys = >(obj: T): T => { Object.keys(obj).forEach((key) => { if (obj[key] === undefined) { delete obj[key]; diff --git a/x-pack/plugins/embeddable_enhanced/kibana.json b/x-pack/plugins/embeddable_enhanced/kibana.json index 09416ce18aecb1..36cd0440b64dd9 100644 --- a/x-pack/plugins/embeddable_enhanced/kibana.json +++ b/x-pack/plugins/embeddable_enhanced/kibana.json @@ -7,5 +7,6 @@ "name": "App Services", "githubTeam": "kibana-app-services" }, + "description": "Extends embeddable plugin with more functionality", "requiredPlugins": ["embeddable", "kibanaReact", "uiActions", "uiActionsEnhanced"] } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx index 90518de77f11f0..5bc0fffdf19632 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx @@ -58,6 +58,19 @@ export const DocumentCreationButtons: React.FC = ({ disabled = false }) = + + } + to={crawlerLink} + isDisabled={disabled} + /> + = ({ disabled = false }) = isDisabled={disabled} /> - - } - to={crawlerLink} - isDisabled={disabled} - /> - ); diff --git a/x-pack/plugins/fleet/dev_docs/data_model.md b/x-pack/plugins/fleet/dev_docs/data_model.md new file mode 100644 index 00000000000000..ec9fa031d09d36 --- /dev/null +++ b/x-pack/plugins/fleet/dev_docs/data_model.md @@ -0,0 +1,212 @@ +# Fleet Data Model + +The Fleet plugin has 3 sources of data that it reads and writes to, these large categories are: +- **Package Registry**: read-only data source for retrieving packages published by Elastic +- **`.fleet-*` Indices**: read & write data source for interacting with Elastic Agent policies, actions, and enrollment tokens +- **Saved Objects**: read & write data source for storing installed packages, configured policies, outputs, and other settings + +## Package Registry + +The package registry hosts all of the packages available for installation by Fleet. The Fleet plugin in Kibana interacts +with the registry exclusively through read-only JSON APIs for listing, searching, and download packages. Read more about +the available APIs in the [package-registry repository](https://github.com/elastic/package-registry). + +By default, the Fleet plugin will use Elastic's nightly 'snapshot' registry on the `master` branch, the 'staging' +registry on Kibana nightly snapshot builds, and the 'prod' registry for release builds. The registry that will be used +can be configured by setting the `xpack.fleet.registryUrl` in the `kibana.yml` file. + +The code that integrates with this registry API is contained in the +[`x-pack/plugins/fleet/server/services/epm/registry`](../server/services/epm/registry) directory. + +## `.fleet-*` Indices + +For any data that needs to be accessible by Fleet Service instances to push updates to, we write and read data +directly to a handful of `.fleet-` Elasticsearch indices. Fleet Server instances are configured with an API key that +has access only to these indices. + +In prior alpha versions of Fleet, this data was also stored in Saved Objects because Elastic Agent instances were +communicating directly with Kibana for policy updates. Once Fleet Server was introduced, that data was migrated to these +Elasticsearch indices to be readable by Fleet Server. + +### `.fleet-agents` index + +Each document in this index tracks an individual Elastic Agent's enrollment in the Fleet, which policy it is current +assigned to, its check in status, which packages are currently installed, and other metadata about the Agent. + +All of the code that interacts with this index is currently located in +[`x-pack/plugins/fleet/server/services/agents/crud.ts`](../server/services/agents/crud.ts) and the schema of these +documents is maintained by the `FleetServerAgent` TypeScript interface. + +Prior to Fleet Server, this data was stored in the `fleet-agents` Saved Object type which is now obsolete. + +### `.fleet-actions` index + +Each document in this index represents an action that was initiated by a user and needs to be processed by Fleet Server +and sent to any agents that it applies to. Actions can apply to one or more agents. There are different types of actions +that can be created such as policy changes, unenrollments, upgrades, etc. See the `AgentActionType` type for a complete +list. + +The total schema for actions is represented by the `FleetServerAgentAction` type. + +### `.fleet-actions-results` + +### `.fleet-servers` + +### `.fleet-artifacts` + +### `.fleet-entrollment-api-keys` + +### `.fleet-policies` + +### `.fleet-policies-leader` + +## Saved Object types + +The Fleet plugin leverages several Saved Object types to track metadata on install packages, agent policies, and more. +This document is intended to outline what each type is for, the primary places it's accessed from in the codebase, and +any caveats regarding the history of that saved object type. + +At this point in time, all types are currently: +- `hidden: false` +- `namespaceType: agnostic` +- `management.importableAndExportable: false` + +### `ingest_manager_settings` + +- Constant in code: `GLOBAL_SETTINGS_SAVED_OBJECT_TYPE` +- Introduced in ? +- Migrations: 7.10.0, 7.13.0 +- [Code Link](../server/saved_objects/index.ts#57) + +Tracks the Fleet server host addresses and whether or not the cluster has been shown the "add data" and +"fleet migration" notices in the UI. + +Can be accessed via the APIs exposed in the [server's settings service](../server/services/settings.ts). + + +### `ingest-agent-policies` + +- Constant in code: `AGENT_POLICY_SAVED_OBJECT_TYPE` +- Introduced in ? +- [Code Link](../server/saved_objects/index.ts#136) +- Migrations: 7.10.0, 7.12.0 +- References to other objects: + - `package_policies` - array of IDs that point to the specific integration instances for this agent policy (`ingest-package-policies`) + +The overall policy for a group of agents. Each policy consists of specific integration configurations for a group of +enrolled agents. + +### `ingest-package-policies` + +- Constant in code: `PACKAGE_POLICY_SAVED_OBJECT_TYPE` +- Introduced in ? +- [Code Link](../server/saved_objects/index.ts#212) +- Migrations: 7.10.0, 7.11.0, 7.12.0, 7.13.0, 7.14.0, 7.15.0 +- References to other objects: + - `policy_id` - ID that points to an agent policy (`ingest-agent-policies`) + - `output_id` - ID that points to an output (`ingest-outputs`) + +Contains the configuration for a specific instance of a package integration as configured for an agent policy. + +### `ingest-outputs` + +- Constant in code: `OUTPUT_SAVED_OBJECT_TYPE` +- Introduced in ? +- [Code Link](../server/saved_objects/index.ts#190) +- Migrations: 7.13.0 + +Contains configuration for ingest outputs that can be shared across multiple `ingest-package-policies`. Currently the UI +only exposes a single Elasticsearch output that will be used for all package policies, but in the future this may be +used for other types of outputs like separate monitoring clusters, Logstash, etc. + +### `epm-packages` + +- Constant in code: `PACKAGES_SAVED_OBJECT_TYPE` +- Introduced in ? +- [Code Link](../server/saved_objects/index.ts#279) +- Migrations: 7.14.0, 7.14.1 +- References to other objects: + - `installed_es` - array of assets installed into Elasticsearch + - `installed_es.id` - ID in Elasticsearch of an asset (eg. `logs-system.application-1.1.2`) + - `installed_es.type` - type of Elasticsearch asset (eg. `ingest_pipeline`) + - `installed_kibana` - array of assets that were installed into Kibana + - `installed_kibana.id` - Saved Object ID (eg. `system-01c54730-fee6-11e9-8405-516218e3d268`) + - `installed_kibana.type` - Saved Object type name (eg. `dashboard`) + - One caveat with this array is that the IDs are currently space-specific so if a package's assets were installed in + one space, they may not be visible in other spaces. We also do not keep track of which space these assets were + installed into. + - `package_assets` - array of original file contents of the package as it was installed + - `package_assets.id` - Saved Object ID for a `epm-package-assets` type + - `package_assets.type` - Saved Object type for the asset. As of now, only `epm-packages-assets` are supported. + +Contains metadata on an installed integration package including references to all assets installed in Kibana and +Elasticsearch. This allows for easy cleanup when a package is removed or upgraded. + +### `epm-packages-assets` + +- Constant in code: `ASSETS_SAVED_OBJECT_TYPE` +- Introduced in ? +- [Code Link](../server/saved_objects/index.ts#328) +- Migrations: +- References to other objects: + +Contains the raw file contents of a package, where each document represents one file from the original package. Storing +these as Saved Objects allows Fleet to install package contents when the package registry is down or unavailable. Also +allows for installing packages that were uploaded manually and are not from a package registry. The `asset_path` field +represents the relative file path of the file from the package contents +(eg. `system-1.1.2/data_stream/application/agent/stream/httpjson.yml.hbs`). + +### `fleet-preconfiguration-deletion-record` + +- Constant in code: `PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE` +- Introduced in ? +- [Code Link](../server/saved_objects/index.ts#328) +- Migrations: +- References to other objects: + - `id` - references the policy ID from the preconfiguration API + +Used as "tombstone record" to indicate that a package that was installed by default through preconfiguration was +explicitly deleted by user. Used to avoid recreating a preconfiguration policy that a user explicitly does not want. + +### `fleet-agents` + +**DEPRECATED in favor of `.fleet-agents` index.** + +- Constant in code: `AGENT_SAVED_OBJECT_TYPE` +- Introduced in ? +- [Code Link](../server/saved_objects/index.ts#76) +- Migrations: 7.10.0, 7.12.0 +- References to other objects: + - `policy_id` - ID that points to the policy (`ingest-agent-policies`) this agent is assigned to. + - `access_api_key_id` + - `default_api_key_id` + +Tracks an individual Elastic Agent's enrollment in the Fleet, which policy it is current assigned to, its check in +status, which packages are currently installed, and other metadata about the Agent. + +### `fleet-agent-actions` + +**DEPRECATED in favor of `.fleet-agent-actions` index.** + +- Constant in code: `AGENT_ACTION_SAVED_OBJECT_TYPE` +- Introduced in ? +- [Code Link](../server/saved_objects/index.ts#113) +- Migrations: 7.10.0 +- References to other objects: + - `agent_id` - ID that points to the agent for this action (`fleet-agents`) + - `policy_id`- ID that points to the policy for this action (`ingest-agent-policies`) + + +### `fleet-enrollment-api-keys` + +**DEPRECATED in favor of `.fleet-enrollment-api-keys` index.** + +- Constant in code: `ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE` +- Introduced in ? +- [Code Link](../server/saved_objects/index.ts#166) +- Migrations: 7.10.0 +- References to other objects: + - `api_key_id` + - `policy_id` - ID that points to an agent policy (`ingest-agent-policies`) + +Contains an enrollment key that can be used to enroll a new agent in a specific agent policy. \ No newline at end of file diff --git a/x-pack/plugins/fleet/server/services/artifacts/mocks.ts b/x-pack/plugins/fleet/server/services/artifacts/mocks.ts index 8bc1768da23a2d..2799e1807123d4 100644 --- a/x-pack/plugins/fleet/server/services/artifacts/mocks.ts +++ b/x-pack/plugins/fleet/server/services/artifacts/mocks.ts @@ -12,7 +12,12 @@ import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks'; import type { SearchHit, ESSearchResponse } from '../../../../../../src/core/types/elasticsearch'; -import type { Artifact, ArtifactElasticsearchProperties, ArtifactsClientInterface } from './types'; +import type { + Artifact, + ArtifactElasticsearchProperties, + ArtifactsClientInterface, + NewArtifact, +} from './types'; import { newArtifactToElasticsearchProperties } from './mappings'; export const createArtifactsClientMock = (): jest.Mocked => { @@ -77,10 +82,12 @@ export const generateEsRequestErrorApiResponseMock = ( ); }; -export const generateArtifactEsGetSingleHitMock = (): SearchHit => { +export const generateArtifactEsGetSingleHitMock = ( + artifact?: NewArtifact +): SearchHit => { const { id, created, ...newArtifact } = generateArtifactMock(); const _source = { - ...newArtifactToElasticsearchProperties(newArtifact), + ...newArtifactToElasticsearchProperties(artifact ?? newArtifact), created, }; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap index eff35a30ba2d6e..0425573c5afaaf 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap @@ -8,25 +8,7 @@ exports[`EPM template tests loading base.yml: base.yml 1`] = ` ], "template": { "settings": { - "index": { - "lifecycle": { - "name": "logs" - }, - "codec": "best_compression", - "mapping": { - "total_fields": { - "limit": "10000" - } - }, - "refresh_interval": "5s", - "number_of_shards": "1", - "number_of_routing_shards": "30", - "query": { - "default_field": [ - "long.nested.foo" - ] - } - } + "index": {} }, "mappings": { "dynamic_templates": [ @@ -123,30 +105,7 @@ exports[`EPM template tests loading coredns.logs.yml: coredns.logs.yml 1`] = ` ], "template": { "settings": { - "index": { - "lifecycle": { - "name": "logs" - }, - "codec": "best_compression", - "mapping": { - "total_fields": { - "limit": "10000" - } - }, - "refresh_interval": "5s", - "number_of_shards": "1", - "number_of_routing_shards": "30", - "query": { - "default_field": [ - "coredns.id", - "coredns.query.class", - "coredns.query.name", - "coredns.query.type", - "coredns.response.code", - "coredns.response.flags" - ] - } - } + "index": {} }, "mappings": { "dynamic_templates": [ @@ -239,58 +198,7 @@ exports[`EPM template tests loading system.yml: system.yml 1`] = ` ], "template": { "settings": { - "index": { - "lifecycle": { - "name": "metrics" - }, - "codec": "best_compression", - "mapping": { - "total_fields": { - "limit": "10000" - } - }, - "refresh_interval": "5s", - "number_of_shards": "1", - "number_of_routing_shards": "30", - "query": { - "default_field": [ - "system.diskio.name", - "system.diskio.serial_number", - "system.filesystem.device_name", - "system.filesystem.type", - "system.filesystem.mount_point", - "system.network.name", - "system.process.state", - "system.process.cmdline", - "system.process.cgroup.id", - "system.process.cgroup.path", - "system.process.cgroup.cpu.id", - "system.process.cgroup.cpu.path", - "system.process.cgroup.cpuacct.id", - "system.process.cgroup.cpuacct.path", - "system.process.cgroup.memory.id", - "system.process.cgroup.memory.path", - "system.process.cgroup.blkio.id", - "system.process.cgroup.blkio.path", - "system.raid.name", - "system.raid.status", - "system.raid.level", - "system.raid.sync_action", - "system.socket.remote.host", - "system.socket.remote.etld_plus_one", - "system.socket.remote.host_error", - "system.socket.process.cmdline", - "system.users.id", - "system.users.seat", - "system.users.path", - "system.users.type", - "system.users.service", - "system.users.state", - "system.users.scope", - "system.users.remote_host" - ] - } - } + "index": {} }, "mappings": { "dynamic_templates": [ diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/default_settings.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/default_settings.test.ts new file mode 100644 index 00000000000000..5e7a3b35c544a1 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/default_settings.test.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggerMock } from '@kbn/logging/mocks'; +import type { Logger } from 'src/core/server'; + +import { appContextService } from '../../../app_context'; + +import { buildDefaultSettings } from './default_settings'; + +jest.mock('../../../app_context'); + +const mockedAppContextService = appContextService as jest.Mocked; +let mockedLogger: jest.Mocked; +describe('buildDefaultSettings', () => { + beforeEach(() => { + mockedLogger = loggerMock.create(); + mockedAppContextService.getLogger.mockReturnValue(mockedLogger); + }); + + it('should generate default settings', () => { + const settings = buildDefaultSettings({ + templateName: 'test_template', + packageName: 'test_package', + type: 'logs', + fields: [ + { + name: 'field1Keyword', + type: 'keyword', + }, + { + name: 'field2Boolean', + type: 'boolean', + }, + ], + }); + + expect(settings).toMatchInlineSnapshot(` + Object { + "index": Object { + "codec": "best_compression", + "lifecycle": Object { + "name": "logs", + }, + "mapping": Object { + "total_fields": Object { + "limit": "10000", + }, + }, + "number_of_routing_shards": "30", + "number_of_shards": "1", + "query": Object { + "default_field": Array [ + "field1Keyword", + ], + }, + "refresh_interval": "5s", + }, + } + `); + }); + + it('should log a warning if there is too many default fields', () => { + const fields = []; + for (let i = 0; i < 20000; i++) { + fields.push({ name: `field${i}`, type: 'keyword' }); + } + buildDefaultSettings({ + type: 'logs', + templateName: 'test_template', + packageName: 'test_package', + fields, + }); + + expect(mockedLogger.warn).toBeCalledWith( + 'large amount of default fields detected for index template test_template in package test_package, applying the first 1024 fields' + ); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/default_settings.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/default_settings.ts new file mode 100644 index 00000000000000..2dced977229e13 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/default_settings.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { appContextService } from '../../../app_context'; +import type { Field, Fields } from '../../fields/field'; + +const QUERY_DEFAULT_FIELD_TYPES = ['keyword', 'text']; +const QUERY_DEFAULT_FIELD_LIMIT = 1024; + +const flattenFieldsToNameAndType = ( + fields: Fields, + path: string = '' +): Array> => { + let newFields: Array> = []; + fields.forEach((field) => { + const fieldName = path ? `${path}.${field.name}` : field.name; + newFields.push({ + name: fieldName, + type: field.type, + }); + if (field.fields && field.fields.length) { + newFields = newFields.concat(flattenFieldsToNameAndType(field.fields, fieldName)); + } + }); + return newFields; +}; + +export function buildDefaultSettings({ + templateName, + packageName, + fields, + ilmPolicy, + type, +}: { + type: string; + templateName: string; + packageName: string; + ilmPolicy?: string | undefined; + fields: Field[]; +}) { + const logger = appContextService.getLogger(); + // Find all field names to set `index.query.default_field` to, which will be + // the first 1024 keyword or text fields + const defaultFields = flattenFieldsToNameAndType(fields).filter( + (field) => field.type && QUERY_DEFAULT_FIELD_TYPES.includes(field.type) + ); + if (defaultFields.length > QUERY_DEFAULT_FIELD_LIMIT) { + logger.warn( + `large amount of default fields detected for index template ${templateName} in package ${packageName}, applying the first ${QUERY_DEFAULT_FIELD_LIMIT} fields` + ); + } + const defaultFieldNames = (defaultFields.length > QUERY_DEFAULT_FIELD_LIMIT + ? defaultFields.slice(0, QUERY_DEFAULT_FIELD_LIMIT) + : defaultFields + ).map((field) => field.name); + + return { + index: { + // ILM Policy must be added here, for now point to the default global ILM policy name + lifecycle: { + name: ilmPolicy ? ilmPolicy : type, + }, + // What should be our default for the compression? + codec: 'best_compression', + mapping: { + total_fields: { + limit: '10000', + }, + }, + // This is the default from Beats? So far seems to be a good value + refresh_interval: '5s', + // Default in the stack now, still good to have it in + number_of_shards: '1', + // We are setting 30 because it can be devided by several numbers. Useful when shrinking. + number_of_routing_shards: '30', + + // All the default fields which should be queried have to be added here. + // So far we add all keyword and text fields here if there are any, otherwise + // this setting is skipped. + ...(defaultFieldNames.length + ? { + query: { + default_field: defaultFieldNames, + }, + } + : {}), + }, + }; +} diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts index e8dac60ddba1a9..9dae4158388902 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { merge } from 'lodash'; import Boom from '@hapi/boom'; import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; @@ -14,6 +15,7 @@ import type { IndexTemplateEntry, RegistryElasticsearch, InstallablePackage, + IndexTemplate, } from '../../../../types'; import { loadFieldsFromYaml, processFields } from '../../fields/field'; import type { Field } from '../../fields/field'; @@ -32,6 +34,7 @@ import { getTemplate, getTemplatePriority, } from './template'; +import { buildDefaultSettings } from './default_settings'; export const installTemplates = async ( installablePackage: InstallablePackage, @@ -210,8 +213,9 @@ function buildComponentTemplates(params: { templateName: string; registryElasticsearch: RegistryElasticsearch | undefined; packageName: string; + defaultSettings: IndexTemplate['template']['settings']; }) { - const { templateName, registryElasticsearch, packageName } = params; + const { templateName, registryElasticsearch, packageName, defaultSettings } = params; const mappingsTemplateName = `${templateName}${mappingsSuffix}`; const settingsTemplateName = `${templateName}${settingsSuffix}`; const userSettingsTemplateName = `${templateName}${userSettingsSuffix}`; @@ -228,14 +232,12 @@ function buildComponentTemplates(params: { }; } - if (registryElasticsearch && registryElasticsearch['index_template.settings']) { - templatesMap[settingsTemplateName] = { - template: { - settings: registryElasticsearch['index_template.settings'], - }, - _meta, - }; - } + templatesMap[settingsTemplateName] = { + template: { + settings: merge(defaultSettings, registryElasticsearch?.['index_template.settings'] ?? {}), + }, + _meta, + }; // return empty/stub template templatesMap[userSettingsTemplateName] = { @@ -253,9 +255,15 @@ async function installDataStreamComponentTemplates(params: { registryElasticsearch: RegistryElasticsearch | undefined; esClient: ElasticsearchClient; packageName: string; + defaultSettings: IndexTemplate['template']['settings']; }) { - const { templateName, registryElasticsearch, esClient, packageName } = params; - const templates = buildComponentTemplates({ templateName, registryElasticsearch, packageName }); + const { templateName, registryElasticsearch, esClient, packageName, defaultSettings } = params; + const templates = buildComponentTemplates({ + templateName, + registryElasticsearch, + packageName, + defaultSettings, + }); const templateNames = Object.keys(templates); const templateEntries = Object.entries(templates); @@ -362,11 +370,20 @@ export async function installTemplate({ await esClient.indices.putIndexTemplate(updateIndexTemplateParams, { ignore: [404] }); } + const defaultSettings = buildDefaultSettings({ + templateName, + packageName, + fields, + type: dataStream.type, + ilmPolicy: dataStream.ilm_policy, + }); + const composedOfTemplates = await installDataStreamComponentTemplates({ templateName, registryElasticsearch: dataStream.elasticsearch, esClient, packageName, + defaultSettings, }); const template = getTemplate({ @@ -378,7 +395,6 @@ export async function installTemplate({ packageName, composedOfTemplates, templatePriority, - ilmPolicy: dataStream.ilm_policy, hidden: dataStream.hidden, }); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts index c999a135e2116b..44d633d5f6e534 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts @@ -40,9 +40,6 @@ const DEFAULT_IGNORE_ABOVE = 1024; const DEFAULT_TEMPLATE_PRIORITY = 200; const DATASET_IS_PREFIX_TEMPLATE_PRIORITY = 150; -const QUERY_DEFAULT_FIELD_TYPES = ['keyword', 'text']; -const QUERY_DEFAULT_FIELD_LIMIT = 1024; - const META_PROP_KEYS = ['metric_type', 'unit']; /** @@ -59,7 +56,6 @@ export function getTemplate({ packageName, composedOfTemplates, templatePriority, - ilmPolicy, hidden, }: { type: string; @@ -70,7 +66,6 @@ export function getTemplate({ packageName: string; composedOfTemplates: string[]; templatePriority: number; - ilmPolicy?: string | undefined; hidden?: boolean; }): IndexTemplate { const template = getBaseTemplate( @@ -81,7 +76,6 @@ export function getTemplate({ packageName, composedOfTemplates, templatePriority, - ilmPolicy, hidden ); if (pipelineName) { @@ -370,11 +364,8 @@ function getBaseTemplate( packageName: string, composedOfTemplates: string[], templatePriority: number, - ilmPolicy?: string | undefined, hidden?: boolean ): IndexTemplate { - const logger = appContextService.getLogger(); - // Meta information to identify Ingest Manager's managed templates and indices const _meta = { package: { @@ -384,57 +375,13 @@ function getBaseTemplate( managed: true, }; - // Find all field names to set `index.query.default_field` to, which will be - // the first 1024 keyword or text fields - const defaultFields = flattenFieldsToNameAndType(fields).filter( - (field) => field.type && QUERY_DEFAULT_FIELD_TYPES.includes(field.type) - ); - if (defaultFields.length > QUERY_DEFAULT_FIELD_LIMIT) { - logger.warn( - `large amount of default fields detected for index template ${templateIndexPattern} in package ${packageName}, applying the first ${QUERY_DEFAULT_FIELD_LIMIT} fields` - ); - } - const defaultFieldNames = (defaultFields.length > QUERY_DEFAULT_FIELD_LIMIT - ? defaultFields.slice(0, QUERY_DEFAULT_FIELD_LIMIT) - : defaultFields - ).map((field) => field.name); - return { priority: templatePriority, // To be completed with the correct index patterns index_patterns: [templateIndexPattern], template: { settings: { - index: { - // ILM Policy must be added here, for now point to the default global ILM policy name - lifecycle: { - name: ilmPolicy ? ilmPolicy : type, - }, - // What should be our default for the compression? - codec: 'best_compression', - // W - mapping: { - total_fields: { - limit: '10000', - }, - }, - // This is the default from Beats? So far seems to be a good value - refresh_interval: '5s', - // Default in the stack now, still good to have it in - number_of_shards: '1', - // We are setting 30 because it can be devided by several numbers. Useful when shrinking. - number_of_routing_shards: '30', - // All the default fields which should be queried have to be added here. - // So far we add all keyword and text fields here if there are any, otherwise - // this setting is skipped. - ...(defaultFieldNames.length - ? { - query: { - default_field: defaultFieldNames, - }, - } - : {}), - }, + index: {}, }, mappings: { // All the dynamic field mappings diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx index 4fa96ea6828d42..0ad6378a22960a 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx @@ -197,14 +197,11 @@ const CriterionPreviewChart: React.FC = ({ const hasData = series.length > 0; const { yMin, yMax, xMin, xMax } = getDomain(filteredSeries, isStacked); const chartDomain = { - max: - showThreshold && threshold && threshold.value - ? Math.max(yMax, threshold.value) * 1.1 - : yMax * 1.1, // Add 10% headroom. - min: showThreshold && threshold && threshold.value ? Math.min(yMin, threshold.value) : yMin, + max: showThreshold && threshold ? Math.max(yMax, threshold.value) * 1.1 : yMax * 1.1, // Add 10% headroom. + min: showThreshold && threshold ? Math.min(yMin, threshold.value) : yMin, }; - if (showThreshold && threshold && threshold.value && chartDomain.min === threshold.value) { + if (showThreshold && threshold && chartDomain.min === threshold.value) { chartDomain.min = chartDomain.min * 0.9; // Allow some padding so the threshold annotation has better visibility } @@ -246,7 +243,7 @@ const CriterionPreviewChart: React.FC = ({ }} color={!isGrouped ? colorTransformer(Color.color0) : undefined} /> - {showThreshold && threshold && threshold.value ? ( + {showThreshold && threshold ? ( = ({ }} /> ) : null} - {showThreshold && threshold && threshold.value && isBelow ? ( + {showThreshold && threshold && isBelow ? ( = ({ ]} /> ) : null} - {showThreshold && threshold && threshold.value && isAbove ? ( + {showThreshold && threshold && isAbove ? ( = ({ comparator, value, updateThreshold, setThresholdPopoverOpenState(!isThresholdPopoverOpen)} /> diff --git a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts index d1690ddfff43d1..8e996934ef69e1 100644 --- a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts @@ -12,6 +12,7 @@ import { SortDirection } from 'src/plugins/data/common/search'; import { RENDER_AS, SCALING_TYPES } from '../constants'; import { MapExtent, MapQuery } from './map_descriptor'; import { Filter, TimeRange } from '../../../../../src/plugins/data/common'; +import { ESTermSourceDescriptor } from './source_descriptor_types'; export type Timeslice = { from: number; @@ -50,9 +51,7 @@ type ESGeoLineSourceSyncMeta = { sortField: string; }; -type ESTermSourceSyncMeta = { - size: number; -}; +export type ESTermSourceSyncMeta = Pick; export type VectorSourceSyncMeta = | ESSearchSourceSyncMeta diff --git a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js index 1f4a1ab7c9afaa..362b2b341714f7 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js +++ b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js @@ -109,3 +109,20 @@ describe('extractPropertiesMap', () => { expect(properties[minPropName]).toBe(0); }); }); + +describe('getSyncMeta', () => { + it('should contain meta requiring source re-fetch when changed', () => { + const source = new ESTermSource({ + id: '1234', + indexPatternTitle: indexPatternTitle, + term: termFieldName, + indexPatternId: 'foobar', + size: 10, + }); + expect(source.getSyncMeta()).toEqual({ + indexPatternId: 'foobar', + size: 10, + term: 'myTermField', + }); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts index caae4385aeec69..93342d1167aebd 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts @@ -25,8 +25,8 @@ import { } from '../../../../common/elasticsearch_util'; import { ESTermSourceDescriptor, + ESTermSourceSyncMeta, VectorJoinSourceRequestMeta, - VectorSourceSyncMeta, } from '../../../../common/descriptor_types'; import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters'; import { PropertiesMap } from '../../../../common/elasticsearch_util'; @@ -171,12 +171,12 @@ export class ESTermSource extends AbstractESAggSource implements ITermJoinSource return this.getMetricFields().map((esAggMetricField) => esAggMetricField.getName()); } - getSyncMeta(): VectorSourceSyncMeta | null { - return this._descriptor.size !== undefined - ? { - size: this._descriptor.size, - } - : null; + getSyncMeta(): ESTermSourceSyncMeta | null { + return { + indexPatternId: this._descriptor.indexPatternId, + size: this._descriptor.size, + term: this._descriptor.term, + }; } getRightFields(): IField[] { diff --git a/x-pack/plugins/maps/server/sample_data/web_logs_saved_objects.js b/x-pack/plugins/maps/server/sample_data/web_logs_saved_objects.js index 86c6c14306faf4..f3efe4c6e74ddb 100644 --- a/x-pack/plugins/maps/server/sample_data/web_logs_saved_objects.js +++ b/x-pack/plugins/maps/server/sample_data/web_logs_saved_objects.js @@ -24,7 +24,7 @@ const layerList = [ }, { id: 'edh66', - label: 'Total Requests by Country', + label: 'Total Requests by Destination', minZoom: 0, maxZoom: 24, alpha: 0.5, @@ -41,7 +41,7 @@ const layerList = [ type: 'DYNAMIC', options: { field: { - name: '__kbnjoin__count_groupby_kibana_sample_data_logs.geo.src', + name: '__kbnjoin__count_groupby_kibana_sample_data_logs.geo.dest', origin: 'join', }, color: 'Greys', @@ -75,7 +75,7 @@ const layerList = [ type: 'ES_TERM_SOURCE', id: '673ff994-fc75-4c67-909b-69fcb0e1060e', indexPatternTitle: 'kibana_sample_data_logs', - term: 'geo.src', + term: 'geo.dest', indexPatternRefName: 'layer_1_join_0_index_pattern', metrics: [ { diff --git a/x-pack/plugins/monitoring/public/application/global_state_context.tsx b/x-pack/plugins/monitoring/public/application/global_state_context.tsx index 57bb638651d050..a244840e004fde 100644 --- a/x-pack/plugins/monitoring/public/application/global_state_context.tsx +++ b/x-pack/plugins/monitoring/public/application/global_state_context.tsx @@ -7,6 +7,8 @@ import React, { createContext } from 'react'; import { GlobalState } from '../url_state'; import { MonitoringStartPluginDependencies } from '../types'; +import { TimeRange, RefreshInterval } from '../../../../../src/plugins/data/public'; +import { Legacy } from '../legacy_shims'; interface GlobalStateProviderProps { query: MonitoringStartPluginDependencies['data']['query']; @@ -14,10 +16,13 @@ interface GlobalStateProviderProps { } export interface State { + [key: string]: unknown; cluster_uuid?: string; ccs?: any; inSetupMode?: boolean; save?: () => void; + time?: TimeRange; + refreshInterval?: RefreshInterval; } export const GlobalStateContext = createContext({} as State); @@ -45,13 +50,13 @@ export const GlobalStateProvider: React.FC = ({ }, }; - const localState: { [key: string]: unknown } = {}; + const localState: State = {}; const state = new GlobalState( query, toasts, fakeAngularRootScope, fakeAngularLocation, - localState + localState as { [key: string]: unknown } ); const initialState: any = state.getState(); @@ -62,11 +67,19 @@ export const GlobalStateProvider: React.FC = ({ localState[key] = initialState[key]; } + localState.refreshInterval = { value: 10000, pause: false }; + localState.save = () => { const newState = { ...localState }; delete newState.save; state.setState(newState); }; + const { value, pause } = Legacy.shims.timefilter.getRefreshInterval(); + if (!value && pause) { + Legacy.shims.timefilter.setRefreshInterval(localState.refreshInterval); + localState.save?.(); + } + return {children}; }; diff --git a/x-pack/plugins/monitoring/public/application/index.tsx b/x-pack/plugins/monitoring/public/application/index.tsx index e15ad995ca1611..19a367977ffc86 100644 --- a/x-pack/plugins/monitoring/public/application/index.tsx +++ b/x-pack/plugins/monitoring/public/application/index.tsx @@ -11,6 +11,7 @@ import ReactDOM from 'react-dom'; import { Route, Switch, Redirect, Router } from 'react-router-dom'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { LoadingPage } from './pages/loading_page'; +import { LicensePage } from './pages/license_page'; import { ClusterOverview } from './pages/cluster/overview_page'; import { MonitoringStartPluginDependencies } from '../types'; import { GlobalStateProvider } from './global_state_context'; @@ -53,7 +54,7 @@ const MonitoringApp: React.FC<{ @@ -91,7 +92,3 @@ const NoData: React.FC<{}> = () => { const Home: React.FC<{}> = () => { return
Home page (Cluster listing)
; }; - -const License: React.FC<{}> = () => { - return
License page
; -}; diff --git a/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx b/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx index f329323bafda87..a7b498ddb88c18 100644 --- a/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx @@ -5,18 +5,18 @@ * 2.0. */ -import React, { useContext } from 'react'; +import React, { useContext, useState, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { CODE_PATH_ALL } from '../../../../common/constants'; import { PageTemplate } from '../page_template'; -import { useClusters } from '../../hooks/use_clusters'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { GlobalStateContext } from '../../global_state_context'; import { TabMenuItem } from '../page_template'; -import { PageLoading } from '../../../components'; import { Overview } from '../../../components/cluster/overview'; import { ExternalConfigContext } from '../../external_config_context'; import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; +import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../../common/constants'; const CODE_PATHS = [CODE_PATH_ALL]; interface SetupModeProps { @@ -26,10 +26,14 @@ interface SetupModeProps { } export const ClusterOverview: React.FC<{}> = () => { - // TODO: check how many requests with useClusters const state = useContext(GlobalStateContext); const externalConfig = useContext(ExternalConfigContext); - const { clusters, loaded } = useClusters(state.cluster_uuid, state.ccs, CODE_PATHS); + const { services } = useKibana<{ data: any }>(); + const clusterUuid = state.cluster_uuid; + const ccs = state.ccs; + const [clusters, setClusters] = useState([] as any); + const [loaded, setLoaded] = useState(false); + let tabs: TabMenuItem[] = []; const title = i18n.translate('xpack.monitoring.cluster.overviewTitle', { @@ -53,27 +57,62 @@ export const ClusterOverview: React.FC<{}> = () => { ]; } + const getPageData = useCallback(async () => { + const bounds = services.data?.query.timefilter.timefilter.getBounds(); + let url = '../api/monitoring/v1/clusters'; + if (clusterUuid) { + url += `/${clusterUuid}`; + } + + try { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + codePaths: CODE_PATHS, + }), + }); + + setClusters(formatClusters(response)); + } catch (err) { + // TODO: handle errors + } finally { + setLoaded(true); + } + }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http]); + return ( - - {loaded ? ( - ( - - {flyoutComponent} - - {/* */} - {bottomBarComponent} - - )} - /> - ) : ( - - )} + + ( + + {flyoutComponent} + + {/* */} + {bottomBarComponent} + + )} + /> ); }; + +function formatClusters(clusters: any) { + return clusters.map(formatCluster); +} + +function formatCluster(cluster: any) { + if (cluster.cluster_uuid === STANDALONE_CLUSTER_CLUSTER_UUID) { + cluster.cluster_name = 'Standalone Cluster'; + } + return cluster; +} diff --git a/x-pack/plugins/monitoring/public/application/pages/license_page.tsx b/x-pack/plugins/monitoring/public/application/pages/license_page.tsx new file mode 100644 index 00000000000000..3a46840e7d30c4 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/license_page.tsx @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useContext, useState, useCallback, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import moment from 'moment-timezone'; +import { PageTemplate } from './page_template'; +import { License } from '../../components'; +import { GlobalStateContext } from '../global_state_context'; +import { CODE_PATH_LICENSE, STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../common/constants'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { MonitoringTimeContainer } from './use_monitoring_time'; + +const CODE_PATHS = [CODE_PATH_LICENSE]; + +export const LicensePage: React.FC<{}> = () => { + const title = i18n.translate('xpack.monitoring.license.licenseRouteTitle', { + defaultMessage: 'License', + }); + + const { setIsDisabled } = useContext(MonitoringTimeContainer.Context); + useEffect(() => { + setIsDisabled(true); + return () => { + setIsDisabled(false); + }; + }, [setIsDisabled]); + + const state = useContext(GlobalStateContext); + const clusterUuid = state.cluster_uuid; + const ccs = state.ccs; + const [clusters, setClusters] = useState([] as any); + + const { services } = useKibana<{ data: any }>(); + const timezone = services.uiSettings?.get('dateFormat:tz'); + const uploadLicensePath = services.application?.getUrlForApp('management', { + path: 'stack/license_management/upload_license', + }); + + const getPageData = useCallback(async () => { + const bounds = services.data?.query.timefilter.timefilter.getBounds(); + let url = '../api/monitoring/v1/clusters'; + if (clusterUuid) { + url += `/${clusterUuid}`; + } + + try { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + codePaths: CODE_PATHS, + }), + }); + + setClusters(formatClusters(response)); + } catch (err) { + // TODO handle error + } + }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http]); + + return ( + + {licenseComponent(clusters, timezone, uploadLicensePath)} + + ); +}; + +function licenseComponent( + clusters: any, + timezone: string, + uploadLicensePath: string | undefined +): any { + if (clusters.length) { + const cluster = clusters[0]; + const isPrimaryCluster = cluster.isPrimary; + const license = cluster.license; + let expiryDate = license?.expiry_date_in_millis; + + if (expiryDate !== undefined) { + expiryDate = formatDateTimeLocal(expiryDate, timezone); + } + + const isExpired = Date.now() > expiryDate; + + return ( + + ); + } else { + return null; + } +} + +// From x-pack/plugins/monitoring/common/formatting.ts with corrected typing +// TODO open github issue to correct other usages +export function formatDateTimeLocal(date: number | Date, timezone: string | null) { + return moment.tz(date, timezone || moment.tz.guess()).format('LL LTS'); +} + +function formatClusters(clusters: any) { + return clusters.map(formatCluster); +} + +function formatCluster(cluster: any) { + if (cluster.cluster_uuid === STANDALONE_CLUSTER_CLUSTER_UUID) { + cluster.cluster_name = 'Standalone Cluster'; + } + return cluster; +} diff --git a/x-pack/plugins/monitoring/public/application/pages/page_template.tsx b/x-pack/plugins/monitoring/public/application/pages/page_template.tsx index 29aafa09814fbe..3461f8650ca6c1 100644 --- a/x-pack/plugins/monitoring/public/application/pages/page_template.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/page_template.tsx @@ -6,9 +6,11 @@ */ import { EuiTab, EuiTabs } from '@elastic/eui'; -import React from 'react'; +import React, { useContext, useState, useEffect } from 'react'; import { useTitle } from '../hooks/use_title'; import { MonitoringToolbar } from '../../components/shared/toolbar'; +import { MonitoringTimeContainer } from './use_monitoring_time'; +import { PageLoading } from '../../components'; export interface TabMenuItem { id: string; @@ -22,14 +24,40 @@ interface PageTemplateProps { title: string; pageTitle?: string; tabs?: TabMenuItem[]; + getPageData?: () => Promise; } -export const PageTemplate: React.FC = ({ title, pageTitle, tabs, children }) => { +export const PageTemplate: React.FC = ({ + title, + pageTitle, + tabs, + getPageData, + children, +}) => { useTitle('', title); + const { currentTimerange } = useContext(MonitoringTimeContainer.Context); + const [loaded, setLoaded] = useState(false); + + useEffect(() => { + getPageData?.() + .catch((err) => { + // TODO: handle errors + }) + .finally(() => { + setLoaded(true); + }); + }, [getPageData, currentTimerange]); + + const onRefresh = () => { + getPageData?.().catch((err) => { + // TODO: handle errors + }); + }; + return (
- + {tabs && ( {tabs.map((item, idx) => { @@ -47,7 +75,7 @@ export const PageTemplate: React.FC = ({ title, pageTitle, ta })} )} -
{children}
+
{!getPageData ? children : loaded ? children : }
); }; diff --git a/x-pack/plugins/monitoring/public/application/pages/use_monitoring_time.tsx b/x-pack/plugins/monitoring/public/application/pages/use_monitoring_time.tsx index f54d40ed29a065..8a343a5c61cd6d 100644 --- a/x-pack/plugins/monitoring/public/application/pages/use_monitoring_time.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/use_monitoring_time.tsx @@ -6,6 +6,7 @@ */ import { useCallback, useState } from 'react'; import createContainer from 'constate'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; interface TimeOptions { from: string; @@ -19,15 +20,21 @@ export const DEFAULT_TIMERANGE: TimeOptions = { interval: '>=10s', }; +const DEFAULT_REFRESH_INTERVAL_VALUE = 10000; +const DEFAULT_REFRESH_INTERVAL_PAUSE = false; + export const useMonitoringTime = () => { + const { services } = useKibana<{ data: any }>(); const defaultTimeRange = { - from: 'now-1h', - to: 'now', - interval: DEFAULT_TIMERANGE.interval, + ...DEFAULT_TIMERANGE, + ...services.data?.query.timefilter.timefilter.getTime(), }; - const [refreshInterval, setRefreshInterval] = useState(5000); - const [isPaused, setIsPaused] = useState(false); + + const { value, pause } = services.data?.query.timefilter.timefilter.getRefreshInterval(); + const [refreshInterval, setRefreshInterval] = useState(value || DEFAULT_REFRESH_INTERVAL_VALUE); + const [isPaused, setIsPaused] = useState(pause || DEFAULT_REFRESH_INTERVAL_PAUSE); const [currentTimerange, setTimeRange] = useState(defaultTimeRange); + const [isDisabled, setIsDisabled] = useState(false); const handleTimeChange = useCallback( (start: string, end: string) => { @@ -44,6 +51,8 @@ export const useMonitoringTime = () => { refreshInterval, setIsPaused, isPaused, + setIsDisabled, + isDisabled, }; }; diff --git a/x-pack/plugins/monitoring/public/components/index.d.ts b/x-pack/plugins/monitoring/public/components/index.d.ts index d027298c81c4c0..fc1a81cc4dba27 100644 --- a/x-pack/plugins/monitoring/public/components/index.d.ts +++ b/x-pack/plugins/monitoring/public/components/index.d.ts @@ -6,3 +6,4 @@ */ export const PageLoading: FunctionComponent; +export const License: FunctionComponent; diff --git a/x-pack/plugins/monitoring/public/components/shared/toolbar.tsx b/x-pack/plugins/monitoring/public/components/shared/toolbar.tsx index e5962b7f808769..9a77b07b96a6e6 100644 --- a/x-pack/plugins/monitoring/public/components/shared/toolbar.tsx +++ b/x-pack/plugins/monitoring/public/components/shared/toolbar.tsx @@ -14,12 +14,15 @@ import { } from '@elastic/eui'; import React, { useContext, useCallback } from 'react'; import { MonitoringTimeContainer } from '../../application/pages/use_monitoring_time'; +import { GlobalStateContext } from '../../application/global_state_context'; +import { Legacy } from '../../legacy_shims'; interface MonitoringToolbarProps { pageTitle?: string; + onRefresh?: () => void; } -export const MonitoringToolbar: React.FC = ({ pageTitle }) => { +export const MonitoringToolbar: React.FC = ({ pageTitle, onRefresh }) => { const { currentTimerange, handleTimeChange, @@ -27,7 +30,9 @@ export const MonitoringToolbar: React.FC = ({ pageTitle refreshInterval, setIsPaused, isPaused, + isDisabled, } = useContext(MonitoringTimeContainer.Context); + const state = useContext(GlobalStateContext); const onTimeChange = useCallback( (selectedTime: { start: string; end: string; isInvalid: boolean }) => { @@ -35,16 +40,28 @@ export const MonitoringToolbar: React.FC = ({ pageTitle return; } handleTimeChange(selectedTime.start, selectedTime.end); + state.time = { + from: selectedTime.start, + to: selectedTime.end, + }; + Legacy.shims.timefilter.setTime(state.time); + state.save?.(); }, - [handleTimeChange] + [handleTimeChange, state] ); const onRefreshChange = useCallback( ({ refreshInterval: ri, isPaused: isP }: OnRefreshChangeProps) => { setRefreshInterval(ri); setIsPaused(isP); + state.refreshInterval = { + pause: isP, + value: ri, + }; + Legacy.shims.timefilter.setRefreshInterval(state.refreshInterval); + state.save?.(); }, - [setRefreshInterval, setIsPaused] + [setRefreshInterval, setIsPaused, state] ); return ( @@ -69,10 +86,11 @@ export const MonitoringToolbar: React.FC = ({ pageTitle
{}} + onRefresh={onRefresh} isPaused={isPaused} refreshInterval={refreshInterval} onRefreshChange={onRefreshChange} diff --git a/x-pack/plugins/reporting/common/types.ts b/x-pack/plugins/reporting/common/types.ts index c324cf363faa13..4051c006a50348 100644 --- a/x-pack/plugins/reporting/common/types.ts +++ b/x-pack/plugins/reporting/common/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { SerializableRecord } from '@kbn/utility-types'; +import type { Ensure, SerializableRecord } from '@kbn/utility-types'; export interface PageSizeParams { pageMarginTop: number; @@ -21,15 +21,21 @@ export interface PdfImageSize { height?: number; } -export interface Size { - width: number; - height: number; -} +export type Size = Ensure< + { + width: number; + height: number; + }, + SerializableRecord +>; -export interface LayoutParams { - id: string; - dimensions?: Size; -} +export type LayoutParams = Ensure< + { + id: string; + dimensions?: Size; + }, + SerializableRecord +>; export interface ReportDocumentHead { _id: string; @@ -50,13 +56,16 @@ export interface TaskRunResult { warnings?: string[]; } -export interface BaseParams { - layout?: LayoutParams; - objectType: string; - title: string; - browserTimezone: string; // to format dates in the user's time zone - version: string; // to handle any state migrations -} +export type BaseParams = Ensure< + { + layout?: LayoutParams; + objectType: string; + title: string; + browserTimezone: string; // to format dates in the user's time zone + version: string; // to handle any state migrations + }, + SerializableRecord +>; // base params decorated with encrypted headers that come into runJob functions export interface BasePayload extends BaseParams { diff --git a/x-pack/plugins/reporting/public/lib/license_check.test.ts b/x-pack/plugins/reporting/public/lib/license_check.test.ts index d1f0b56cdfa628..8f46ca5616f190 100644 --- a/x-pack/plugins/reporting/public/lib/license_check.test.ts +++ b/x-pack/plugins/reporting/public/lib/license_check.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { LicenseCheck } from '../shared_imports'; import { checkLicense } from './license_check'; describe('License check', () => { @@ -42,7 +43,7 @@ describe('License check', () => { }); it('shows and enables links if state is not known', () => { - expect(checkLicense({ state: 'PONYFOO' } as any)).toEqual({ + expect(checkLicense(({ state: 'PONYFOO' } as unknown) as LicenseCheck)).toEqual({ enableLinks: true, showLinks: true, message: '', diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts index 5c8327df171bda..7a391368f65b3c 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts @@ -4,11 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { stringify } from 'query-string'; -import rison, { RisonObject } from 'rison-node'; +import rison from 'rison-node'; +import type { HttpFetchQuery } from 'src/core/public'; import { HttpSetup, IUiSettingsClient } from 'src/core/public'; import { API_BASE_GENERATE, @@ -45,7 +45,7 @@ interface IReportingAPI { // Helpers getReportURL(jobId: string): string; getReportingJobPath(exportType: string, jobParams: BaseParams & T): string; // Return a URL to queue a job, with the job params encoded in the query string of the URL. Used for copying POST URL - createReportingJob(exportType: string, jobParams: any): Promise; // Sends a request to queue a job, with the job params in the POST body + createReportingJob(exportType: string, jobParams: BaseParams & T): Promise; // Sends a request to queue a job, with the job params in the POST body getServerBasePath(): string; // Provides the raw server basePath to allow it to be stripped out from relativeUrls in job params // CRUD @@ -93,7 +93,7 @@ export class ReportingAPIClient implements IReportingAPI { } public async list(page = 0, jobIds: string[] = []) { - const query = { page } as any; + const query: HttpFetchQuery = { page }; if (jobIds.length > 0) { // Only getting the first 10, to prevent URL overflows query.ids = jobIds.slice(0, 10).join(','); @@ -143,14 +143,14 @@ export class ReportingAPIClient implements IReportingAPI { } public getReportingJobPath(exportType: string, jobParams: BaseParams) { - const risonObject: RisonObject = jobParams as Record; - const params = stringify({ jobParams: rison.encode(risonObject) }); + const params = stringify({ + jobParams: rison.encode(jobParams), + }); return `${this.http.basePath.prepend(API_BASE_GENERATE)}/${exportType}?${params}`; } public async createReportingJob(exportType: string, jobParams: BaseParams) { - const risonObject: RisonObject = jobParams as Record; - const jobParamsRison = rison.encode(risonObject); + const jobParamsRison = rison.encode(jobParams); const resp: { job: ReportApiJSON } = await this.http.post( `${API_BASE_GENERATE}/${exportType}`, { diff --git a/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap b/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap index e9fd76eb62c797..26cee9e5ebb48f 100644 --- a/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap +++ b/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap @@ -693,9 +693,31 @@ exports[`ReportListing Report job listing with some items 1`] = ` { return { @@ -206,11 +207,11 @@ const mockJobs: ReportApiJSON[] = [ }), ]; -const reportingAPIClient = { - list: () => Promise.resolve(mockJobs.map((j) => new Job(j))), - total: () => Promise.resolve(18), +const reportingAPIClient = ({ + list: jest.fn(() => Promise.resolve(mockJobs.map((j) => new Job(j)))), + total: jest.fn(() => Promise.resolve(18)), migrateReportingIndicesIlmPolicy: jest.fn(), -} as any; +} as unknown) as DeeplyMockedKeys; const validCheck = { check: () => ({ @@ -220,8 +221,8 @@ const validCheck = { }; const license$ = { - subscribe: (handler: any) => { - return handler(validCheck); + subscribe: (handler: unknown) => { + return (handler as Function)(validCheck); }, } as Observable; @@ -239,7 +240,7 @@ const mockPollConfig = { describe('ReportListing', () => { let httpService: ReturnType; let applicationService: ReturnType; - let ilmLocator: undefined | LocatorPublic; + let ilmLocator: undefined | LocatorPublic; let urlService: SharePluginSetup['url']; let testBed: UnwrapPromise>; let toasts: NotificationsSetup['toasts']; @@ -303,7 +304,7 @@ describe('ReportListing', () => { }; ilmLocator = ({ getUrl: jest.fn(), - } as unknown) as LocatorPublic; + } as unknown) as LocatorPublic; urlService = ({ locators: { @@ -325,11 +326,11 @@ describe('ReportListing', () => { it('subscribes to license changes, and unsubscribes on dismount', async () => { const unsubscribeMock = jest.fn(); - const subMock = { + const subMock = ({ subscribe: jest.fn().mockReturnValue({ unsubscribe: unsubscribeMock, }), - } as any; + } as unknown) as Observable; await runSetup({ license$: subMock }); @@ -344,7 +345,7 @@ describe('ReportListing', () => { httpService = httpServiceMock.createSetupContract(); ilmLocator = ({ getUrl: jest.fn(), - } as unknown) as LocatorPublic; + } as unknown) as LocatorPublic; urlService = ({ locators: { diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts index 45bd20df85660a..654d46cdfbcb15 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts @@ -8,9 +8,12 @@ import * as Rx from 'rxjs'; import { first } from 'rxjs/operators'; import { CoreStart } from 'src/core/public'; +import type { SearchSource } from 'src/plugins/data/common'; +import type { SavedSearch } from 'src/plugins/discover/public'; import { coreMock } from '../../../../../src/core/public/mocks'; -import { LicensingPluginSetup } from '../../../licensing/public'; +import type { ILicense, LicensingPluginSetup } from '../../../licensing/public'; import { ReportingAPIClient } from '../lib/reporting_api_client'; +import type { ActionContext } from './get_csv_panel_action'; import { ReportingCsvPanelAction } from './get_csv_panel_action'; type LicenseResults = 'valid' | 'invalid' | 'unavailable' | 'expired'; @@ -19,9 +22,9 @@ const core = coreMock.createSetup(); let apiClient: ReportingAPIClient; describe('GetCsvReportPanelAction', () => { - let context: any; - let mockLicense$: any; - let mockSearchSource: any; + let context: ActionContext; + let mockLicense$: (state?: LicenseResults) => Rx.Observable; + let mockSearchSource: SearchSource; let mockStartServicesPayload: [CoreStart, object, unknown]; let mockStartServices$: Rx.Subject; @@ -54,15 +57,15 @@ describe('GetCsvReportPanelAction', () => { null, ]; - mockSearchSource = { + mockSearchSource = ({ createCopy: () => mockSearchSource, removeField: jest.fn(), setField: jest.fn(), getField: jest.fn(), getSerializedFields: jest.fn().mockImplementation(() => ({})), - }; + } as unknown) as SearchSource; - context = { + context = ({ embeddable: { type: 'search', getSavedSearch: () => { @@ -78,7 +81,7 @@ describe('GetCsvReportPanelAction', () => { }, }), }, - } as any; + } as unknown) as ActionContext; }); it('translates empty embeddable context into job params', async () => { @@ -105,18 +108,18 @@ describe('GetCsvReportPanelAction', () => { }); it('translates embeddable context into job params', async () => { - mockSearchSource = { + mockSearchSource = ({ createCopy: () => mockSearchSource, removeField: jest.fn(), setField: jest.fn(), getField: jest.fn(), getSerializedFields: jest.fn().mockImplementation(() => ({ testData: 'testDataValue' })), - }; + } as unknown) as SearchSource; context.embeddable.getSavedSearch = () => { - return { + return ({ searchSource: mockSearchSource, columns: ['column_a', 'column_b'], - }; + } as unknown) as SavedSearch; }; const panel = new ReportingCsvPanelAction({ diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index 8b6e258c06535d..eb14e321608690 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -29,7 +29,7 @@ function isSavedSearchEmbeddable( return embeddable.type === SEARCH_EMBEDDABLE_TYPE; } -interface ActionContext { +export interface ActionContext { embeddable: ISearchEmbeddable; } diff --git a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx index 64f1ecddcbb41a..59afa91aaa9c36 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx @@ -18,7 +18,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component, ReactElement } from 'react'; -import { ToastsSetup, IUiSettingsClient } from 'src/core/public'; +import { IUiSettingsClient, ToastsSetup } from 'src/core/public'; import url from 'url'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { @@ -41,7 +41,7 @@ export interface ReportingPanelProps { layoutId?: string; objectId?: string; getJobParams: () => Omit; - options?: ReactElement | null; + options?: ReactElement | null; isDirty?: boolean; onClose?: () => void; } @@ -277,7 +277,7 @@ class ReportingPanelContentUi extends Component { this.props.onClose(); } }) - .catch((error: any) => { + .catch((error) => { this.props.toasts.addError(error, { title: intl.formatMessage({ id: 'xpack.reporting.panelContent.notification.reportingErrorTitle', diff --git a/x-pack/plugins/reporting/public/shared_imports.ts b/x-pack/plugins/reporting/public/shared_imports.ts index a18ceaf151c7d1..e719d720a7895f 100644 --- a/x-pack/plugins/reporting/public/shared_imports.ts +++ b/x-pack/plugins/reporting/public/shared_imports.ts @@ -5,18 +5,14 @@ * 2.0. */ -export type { - SharePluginSetup, - SharePluginStart, - LocatorPublic, -} from '../../../../src/plugins/share/public'; +export type { SharePluginSetup, SharePluginStart, LocatorPublic } from 'src/plugins/share/public'; export { useRequest, UseRequestResponse } from '../../../../src/plugins/es_ui_shared/public'; export { KibanaContextProvider } from '../../../../src/plugins/kibana_react/public'; import { useKibana as _useKibana } from '../../../../src/plugins/kibana_react/public'; -import { KibanaContext } from './types'; +import type { KibanaContext } from './types'; export const useKibana = () => _useKibana(); export type { SerializableRecord } from '@kbn/utility-types'; @@ -24,3 +20,5 @@ export type { SerializableRecord } from '@kbn/utility-types'; export type { UiActionsSetup, UiActionsStart } from 'src/plugins/ui_actions/public'; export type { ManagementAppMountParams } from 'src/plugins/management/public'; + +export type { LicenseCheck } from '../../licensing/public'; diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_agent_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_agent_generator.ts index 79888d9a971873..61f71e2ee253bb 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_agent_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/fleet_agent_generator.ts @@ -82,6 +82,10 @@ export class FleetAgentGenerator extends BaseDataGenerator { action_seq_no: -1, active: true, enrolled_at: now, + agent: { + id: this.randomUUID(), + version: this.randomVersion(), + }, local_metadata: { elastic: { agent: { diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_agent.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_agent.ts index 52998b090e3b66..901af259a5d2b2 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_agent.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_agent.ts @@ -48,6 +48,10 @@ export const indexFleetAgentForHost = async ( ): Promise => { const agentDoc = fleetAgentGenerator.generateEsHit({ _source: { + agent: { + id: endpointHost.agent.id, + version: endpointHost.agent.version, + }, local_metadata: { elastic: { agent: { diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/use_host_isolation_action.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/use_host_isolation_action.tsx index 24bc670a13ec48..4282a584ea9f33 100644 --- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/use_host_isolation_action.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/use_host_isolation_action.tsx @@ -80,12 +80,13 @@ export const useHostIsolationAction = ({ isIsolationAllowed && isEndpointAlert && isolationSupported && - isHostIsolationPanelOpen === false + isHostIsolationPanelOpen === false && + loadingHostIsolationStatus === false ? [ {isolateHostTitle} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index 71542e69314891..063dc849027a7f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -12,6 +12,7 @@ import { EuiFlexGroup, EuiFlexItem, + EuiLoadingSpinner, EuiSpacer, EuiWindowEvent, EuiHorizontalRule, @@ -103,6 +104,9 @@ const DetectionEnginePageComponent: React.FC = ({ const updatedAt = useShallowEqualSelector( (state) => (getTimeline(state, TimelineId.detectionsPage) ?? timelineDefaults).updated ); + const isAlertsLoading = useShallowEqualSelector( + (state) => (getTimeline(state, TimelineId.detectionsPage) ?? timelineDefaults).isLoading + ); const getGlobalFiltersQuerySelector = useMemo( () => inputsSelectors.globalFiltersQuerySelector(), [] @@ -143,6 +147,8 @@ const DetectionEnginePageComponent: React.FC = ({ } = useKibana().services; const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); + const showUpdating = useMemo(() => isAlertsLoading || loading, [isAlertsLoading, loading]); + const updateDateRangeCallback = useCallback( ({ x }) => { if (!x) { @@ -268,6 +274,17 @@ const DetectionEnginePageComponent: React.FC = ({ [docLinks] ); + if (loading) { + return ( + + + + + + + ); + } + if (isUserAuthenticated != null && !isUserAuthenticated && !loading) { return ( @@ -330,10 +347,11 @@ const DetectionEnginePageComponent: React.FC = ({ /> - {timelinesUi.getLastUpdated({ - updatedAt: updatedAt || 0, - showUpdating: loading, - })} + {updatedAt && + timelinesUi.getLastUpdated({ + updatedAt: updatedAt || Date.now(), + showUpdating, + })} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 4d4ac102ea645c..6276d934fed413 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -180,6 +180,9 @@ const RuleDetailsPageComponent: React.FC = ({ const updatedAt = useShallowEqualSelector( (state) => (getTimeline(state, TimelineId.detectionsPage) ?? timelineDefaults).updated ); + const isAlertsLoading = useShallowEqualSelector( + (state) => (getTimeline(state, TimelineId.detectionsPage) ?? timelineDefaults).isLoading + ); const getGlobalFiltersQuerySelector = useMemo( () => inputsSelectors.globalFiltersQuerySelector(), [] @@ -285,6 +288,8 @@ const RuleDetailsPageComponent: React.FC = ({ } }, [hasIndexRead]); + const showUpdating = useMemo(() => isAlertsLoading || loading, [isAlertsLoading, loading]); + const title = useMemo( () => ( <> @@ -772,10 +777,11 @@ const RuleDetailsPageComponent: React.FC = ({ /> - {timelinesUi.getLastUpdated({ - updatedAt: updatedAt || 0, - showUpdating: loading, - })} + {updatedAt && + timelinesUi.getLastUpdated({ + updatedAt: updatedAt || Date.now(), + showUpdating, + })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/reason_column_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/reason_column_renderer.test.tsx index addb991af58d72..ee8a2752796072 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/reason_column_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/reason_column_renderer.test.tsx @@ -144,5 +144,22 @@ describe('reasonColumnRenderer', () => { expect(wrapper.queryByTestId('test-row-render')).toBeInTheDocument(); }); + + it('the popover always contains a class that hides it when an overlay (e.g. the inspect modal) is displayed', () => { + const renderedColumn = reasonColumnRenderer.renderColumn({ + ...defaultProps, + ecsData: validEcs, + rowRenderers, + browserFields, + }); + + const wrapper = render({renderedColumn}); + + fireEvent.click(wrapper.getByTestId('reason-cell-button')); + + expect(wrapper.getByRole('dialog')).toHaveClass( + 'euiPanel euiPanel--paddingMedium euiPanel--borderRadiusMedium euiPanel--plain euiPanel--noShadow euiPopover__panel euiPopover__panel--right withHoverActions__popover' + ); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/reason_column_renderer.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/reason_column_renderer.tsx index 00f5fd5717aeb9..52483b4853cbc3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/reason_column_renderer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/reason_column_renderer.tsx @@ -122,6 +122,7 @@ const ReasonCell: React.FC<{ isOpen={isOpen} anchorPosition="rightCenter" closePopover={handleClosePopOver} + panelClassName="withHoverActions__popover" button={button} > diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.test.ts index 2071d4b8c27b71..d6599f26866707 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.test.ts @@ -15,14 +15,20 @@ import { SavedObjectsFindResponse, SavedObjectsFindResult, } from 'kibana/server'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { migrateArtifactsToFleet } from './migrate_artifacts_to_fleet'; import { createEndpointArtifactClientMock } from '../../services/artifacts/mocks'; -import { getInternalArtifactMock } from '../../schemas/artifacts/saved_objects.mock'; +import { InternalArtifactCompleteSchema } from '../../schemas'; +import { generateArtifactEsGetSingleHitMock } from '../../../../../fleet/server/services/artifacts/mocks'; +import { NewArtifact } from '../../../../../fleet/server/services'; +import { CreateRequest } from '@elastic/elasticsearch/api/types'; describe('When migrating artifacts to fleet', () => { let soClient: jest.Mocked; let logger: jest.Mocked; let artifactClient: ReturnType; + /** An artifact that was created prior to 7.14 */ + let soArtifactEntry: InternalArtifactCompleteSchema; const createSoFindResult = ( soHits: SavedObjectsFindResult[] = [], @@ -41,6 +47,41 @@ describe('When migrating artifacts to fleet', () => { soClient = savedObjectsClientMock.create() as jest.Mocked; logger = loggingSystemMock.create().get() as jest.Mocked; artifactClient = createEndpointArtifactClientMock(); + // pre-v7.14 artifact, which is compressed + soArtifactEntry = { + identifier: 'endpoint-exceptionlist-macos-v1', + compressionAlgorithm: 'zlib', + encryptionAlgorithm: 'none', + decodedSha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + encodedSha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + decodedSize: 14, + encodedSize: 22, + body: 'eJyrVkrNKynKTC1WsoqOrQUAJxkFKQ==', + }; + + // Mock the esClient create response to include the artifact properties that were provide + // to it by fleet artifact client + artifactClient._esClient.create.mockImplementation((props: CreateRequest) => { + return elasticsearchServiceMock.createSuccessTransportRequestPromise({ + ...generateArtifactEsGetSingleHitMock({ + ...((props?.body ?? {}) as NewArtifact), + }), + _index: '.fleet-artifacts-7', + _id: `endpoint:endpoint-exceptionlist-macos-v1-${ + // @ts-ignore + props?.body?.decodedSha256 ?? 'UNKNOWN?' + }`, + _version: 1, + result: 'created', + _shards: { + total: 1, + successful: 1, + failed: 0, + }, + _seq_no: 0, + _primary_term: 1, + }); + }); soClient.find.mockResolvedValue(createSoFindResult([], 0)).mockResolvedValueOnce( createSoFindResult([ @@ -49,7 +90,7 @@ describe('When migrating artifacts to fleet', () => { type: '', id: 'abc123', references: [], - attributes: await getInternalArtifactMock('windows', 'v1'), + attributes: soArtifactEntry, }, ]) ); @@ -70,6 +111,17 @@ describe('When migrating artifacts to fleet', () => { expect(soClient.delete).toHaveBeenCalled(); }); + it('should create artifact in fleet with attributes that match the SO version', async () => { + await migrateArtifactsToFleet(soClient, artifactClient, logger); + + await expect(artifactClient.createArtifact.mock.results[0].value).resolves.toEqual( + expect.objectContaining({ + ...soArtifactEntry, + compressionAlgorithm: 'zlib', + }) + ); + }); + it('should ignore 404 responses for SO delete (multi-node kibana setup)', async () => { const notFoundError: Error & { output?: { statusCode: number } } = new Error('not found'); notFoundError.output = { statusCode: 404 }; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.ts index 4518e23bb7fea5..07edfce24affdc 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/migrate_artifacts_to_fleet.ts @@ -5,9 +5,11 @@ * 2.0. */ +import { inflate as _inflate } from 'zlib'; +import { promisify } from 'util'; import { SavedObjectsClient, Logger } from 'kibana/server'; import { EndpointArtifactClientInterface } from '../../services'; -import { InternalArtifactCompleteSchema } from '../../schemas'; +import { InternalArtifactCompleteSchema, InternalArtifactSchema } from '../../schemas'; import { ArtifactConstants } from './common'; class ArtifactMigrationError extends Error { @@ -16,6 +18,12 @@ class ArtifactMigrationError extends Error { } } +const inflateAsync = promisify(_inflate); + +function isCompressed(artifact: InternalArtifactSchema) { + return artifact.compressionAlgorithm === 'zlib'; +} + /** * With v7.13, artifact storage was moved from a security_solution saved object to a fleet index * in order to support Fleet Server. @@ -57,6 +65,15 @@ export const migrateArtifactsToFleet = async ( } for (const artifact of artifactList) { + if (isCompressed(artifact.attributes)) { + artifact.attributes = { + ...artifact.attributes, + body: (await inflateAsync(Buffer.from(artifact.attributes.body, 'base64'))).toString( + 'base64' + ), + }; + } + // Create new artifact in fleet index await endpointArtifactClient.createArtifact(artifact.attributes); // Delete old artifact from SO and if there are errors here, then ignore 404's diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/mocks.ts index 18be9f299c15ce..e2a4f9a3f53563 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/mocks.ts @@ -8,7 +8,14 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { savedObjectsClientMock } from 'src/core/server/mocks'; import { ManifestClient } from './manifest_client'; -import { EndpointArtifactClientInterface } from './artifact_client'; +import { EndpointArtifactClient, EndpointArtifactClientInterface } from './artifact_client'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ElasticsearchClientMock } from '../../../../../../../src/core/server/elasticsearch/client/mocks'; +import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks'; +// Because mocks are for testing only, should be ok to import the FleetArtifactsClient directly +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { FleetArtifactsClient } from '../../../../../fleet/server/services'; +import { createArtifactsClientMock } from '../../../../../fleet/server/mocks'; export const getManifestClientMock = ( savedObjectsClient?: SavedObjectsClientContract @@ -19,10 +26,29 @@ export const getManifestClientMock = ( return new ManifestClient(savedObjectsClientMock.create(), 'v1'); }; -export const createEndpointArtifactClientMock = (): jest.Mocked => { +/** + * Returns back a mocked EndpointArtifactClient along with the internal FleetArtifactsClient and the Es Clients that are being used + * @param esClient + */ +export const createEndpointArtifactClientMock = ( + esClient: ElasticsearchClientMock = elasticsearchServiceMock.createScopedClusterClient() + .asInternalUser +): jest.Mocked & { + _esClient: ElasticsearchClientMock; +} => { + const fleetArtifactClientMocked = createArtifactsClientMock(); + const endpointArtifactClientMocked = new EndpointArtifactClient(fleetArtifactClientMocked); + + // Return the interface mocked with jest.fn() that fowards calls to the real instance return { - createArtifact: jest.fn(), - getArtifact: jest.fn(), - deleteArtifact: jest.fn(), + createArtifact: jest.fn(async (...args) => { + const fleetArtifactClient = new FleetArtifactsClient(esClient, 'endpoint'); + const endpointArtifactClient = new EndpointArtifactClient(fleetArtifactClient); + const response = await endpointArtifactClient.createArtifact(...args); + return response; + }), + getArtifact: jest.fn((...args) => endpointArtifactClientMocked.getArtifact(...args)), + deleteArtifact: jest.fn((...args) => endpointArtifactClientMocked.deleteArtifact(...args)), + _esClient: esClient, }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_privacy_controls_tcc_database_modification.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_privacy_controls_tcc_database_modification.json index f437b2606c35df..e92620eaca93ec 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_privacy_controls_tcc_database_modification.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_privacy_controls_tcc_database_modification.json @@ -14,7 +14,7 @@ "query": "process where event.type in (\"start\", \"process_started\") and process.name : \"sqlite*\" and \n process.args : \"/*/Application Support/com.apple.TCC/TCC.db\"\n", "references": [ "https://applehelpwriter.com/2016/08/29/discovering-how-dropbox-hacks-your-mac/", - "https://github.com/bp88/JSS-Scripts/blob/master/TCC.db Modifier.sh", + "https://github.com/bp88/JSS-Scripts/blob/master/TCC.db%20Modifier.sh", "https://medium.com/@mattshockl/cve-2020-9934-bypassing-the-os-x-transparency-consent-and-control-tcc-framework-for-4e14806f1de8" ], "risk_score": 47, @@ -53,5 +53,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security.json index 9c59f69b121137..63bf6fea698aea 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security.json @@ -20,7 +20,7 @@ "license": "Elastic License v2", "max_signals": 10000, "name": "Endpoint Security", - "query": "event.kind:alert and event.module:(endpoint and not endgame) and not event.code: behavior\n", + "query": "event.kind:alert and event.module:(endpoint and not endgame)\n", "risk_score": 47, "risk_score_mapping": [ { @@ -64,5 +64,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 4 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_behavior_protection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_behavior_protection.json deleted file mode 100644 index f0a523fff96d4c..00000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_behavior_protection.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "author": [ - "Elastic" - ], - "description": "Generates a detection alert each time an Elastic Endpoint Security alert is received for Behavior Protection alerts. Enabling this rule allows you to immediately begin investigating your Endpoint alerts for Behavior Protection.", - "enabled": true, - "exceptions_list": [ - { - "id": "endpoint_list", - "list_id": "endpoint_list", - "namespace_type": "agnostic", - "type": "endpoint" - } - ], - "from": "now-10m", - "index": [ - "logs-endpoint.alerts-*" - ], - "language": "kuery", - "license": "Elastic License v2", - "max_signals": 10000, - "name": "Endpoint Security Behavior Protection", - "query": "event.kind:alert and event.module:(endpoint and not endgame) and event.code: behavior\n", - "risk_score": 47, - "risk_score_mapping": [ - { - "field": "event.risk_score", - "operator": "equals", - "value": "" - } - ], - "rule_id": "d516af98-19f3-45bb-b590-dd623535b746", - "rule_name_override": "rule.name", - "severity": "medium", - "severity_mapping": [ - { - "field": "event.severity", - "operator": "equals", - "severity": "low", - "value": "21" - }, - { - "field": "event.severity", - "operator": "equals", - "severity": "medium", - "value": "47" - }, - { - "field": "event.severity", - "operator": "equals", - "severity": "high", - "value": "73" - }, - { - "field": "event.severity", - "operator": "equals", - "severity": "critical", - "value": "99" - } - ], - "tags": [ - "Elastic", - "Endpoint Security" - ], - "timestamp_override": "event.ingested", - "type": "query", - "version": 1 -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts index 1aa54dedef5ef4..093d5c806c2828 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts @@ -580,8 +580,7 @@ import rule567 from './defense_evasion_parent_process_pid_spoofing.json'; import rule568 from './defense_evasion_defender_exclusion_via_powershell.json'; import rule569 from './defense_evasion_whitespace_padding_in_command_line.json'; import rule570 from './persistence_webshell_detection.json'; -import rule571 from './elastic_endpoint_security_behavior_protection.json'; -import rule572 from './persistence_via_bits_job_notify_command.json'; +import rule571 from './persistence_via_bits_job_notify_command.json'; export const rawRules = [ rule1, @@ -1155,5 +1154,4 @@ export const rawRules = [ rule569, rule570, rule571, - rule572, ]; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_azure_active_directory_high_risk_signin.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_azure_active_directory_high_risk_signin.json index 008f6ac7b874c1..5abbbb1b1c6ed3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_azure_active_directory_high_risk_signin.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_azure_active_directory_high_risk_signin.json @@ -13,7 +13,7 @@ "license": "Elastic License v2", "name": "Azure Active Directory High Risk Sign-in", "note": "## Config\n\nThe Azure Fleet integration, Filebeat module, or similarly structured data is required to be compatible with this rule.", - "query": "event.dataset:azure.signinlogs and\n azure.signinlogs.properties.risk_level_during_signin:high and\n event.outcome:(success or Success)\n", + "query": "event.dataset:azure.signinlogs and\n (azure.signinlogs.properties.risk_level_during_signin:high or azure.signinlogs.properties.risk_level_aggregated:high) and\n event.outcome:(success or Success)\n", "references": [ "https://docs.microsoft.com/en-us/azure/active-directory/conditional-access/howto-conditional-access-policy-risk", "https://docs.microsoft.com/en-us/azure/active-directory/identity-protection/overview-identity-protection", @@ -49,5 +49,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_kernel_module_arguments.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_kernel_module_arguments.json index 358443e675c6eb..c04a68171f6f82 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_kernel_module_arguments.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_kernel_module_arguments.json @@ -12,9 +12,6 @@ "license": "Elastic License v2", "machine_learning_job_id": "linux_rare_kernel_module_arguments", "name": "Anomalous Kernel Module Activity", - "references": [ - "references" - ], "risk_score": 21, "rule_id": "37b0816d-af40-40b4-885f-bb162b3c88a9", "severity": "low", @@ -50,5 +47,5 @@ } ], "type": "machine_learning", - "version": 3 + "version": 4 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_docker_shortcuts_plist_modification.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_docker_shortcuts_plist_modification.json index 941fe5cbf5484b..e513b5ace737f5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_docker_shortcuts_plist_modification.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_docker_shortcuts_plist_modification.json @@ -13,7 +13,7 @@ "name": "Persistence via Docker Shortcut Modification", "query": "event.category : file and event.action : modification and \n file.path : /Users/*/Library/Preferences/com.apple.dock.plist and \n not process.name : (xpcproxy or cfprefsd or plutil or jamf or PlistBuddy or InstallerRemotePluginService)\n", "references": [ - "https://github.com/specterops/presentations/raw/master/Leo Pitt/Hey_Im_Still_in_Here_Modern_macOS_Persistence_SO-CON2020.pdf" + "https://github.com/specterops/presentations/raw/master/Leo%20Pitt/Hey_Im_Still_in_Here_Modern_macOS_Persistence_SO-CON2020.pdf" ], "risk_score": 47, "rule_id": "c81cefcb-82b9-4408-a533-3c3df549e62d", @@ -44,5 +44,5 @@ ], "timestamp_override": "event.ingested", "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_finder_sync_plugin_pluginkit.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_finder_sync_plugin_pluginkit.json index f2b6364301fe29..cdef729404ceb7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_finder_sync_plugin_pluginkit.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_finder_sync_plugin_pluginkit.json @@ -16,7 +16,7 @@ "name": "Finder Sync Plugin Registered and Enabled", "query": "sequence by host.id, user.id with maxspan = 5s\n [process where event.type in (\"start\", \"process_started\") and process.name : \"pluginkit\" and process.args : \"-a\"]\n [process where event.type in (\"start\", \"process_started\") and process.name : \"pluginkit\" and\n process.args : \"-e\" and process.args : \"use\" and process.args : \"-i\" and\n not process.args :\n (\n \"com.google.GoogleDrive.FinderSyncAPIExtension\",\n \"com.google.drivefs.findersync\",\n \"com.boxcryptor.osx.Rednif\",\n \"com.adobe.accmac.ACCFinderSync\",\n \"com.microsoft.OneDrive.FinderSync\",\n \"com.insynchq.Insync.Insync-Finder-Integration\",\n \"com.box.desktop.findersyncext\"\n )\n ]\n", "references": [ - "https://github.com/specterops/presentations/raw/master/Leo Pitt/Hey_Im_Still_in_Here_Modern_macOS_Persistence_SO-CON2020.pdf" + "https://github.com/specterops/presentations/raw/master/Leo%20Pitt/Hey_Im_Still_in_Here_Modern_macOS_Persistence_SO-CON2020.pdf" ], "risk_score": 47, "rule_id": "37f638ea-909d-4f94-9248-edd21e4a9906", @@ -46,5 +46,5 @@ } ], "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json index 26d62c2da95b07..9cdf474efb4504 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json @@ -14,7 +14,7 @@ "name": "Unusual Parent-Child Relationship", "query": "process where event.type in (\"start\", \"process_started\") and\nprocess.parent.name != null and\n (\n /* suspicious parent processes */\n (process.name:\"autochk.exe\" and not process.parent.name:\"smss.exe\") or\n (process.name:(\"fontdrvhost.exe\", \"dwm.exe\") and not process.parent.name:(\"wininit.exe\", \"winlogon.exe\")) or\n (process.name:(\"consent.exe\", \"RuntimeBroker.exe\", \"TiWorker.exe\") and not process.parent.name:\"svchost.exe\") or\n (process.name:\"SearchIndexer.exe\" and not process.parent.name:\"services.exe\") or\n (process.name:\"SearchProtocolHost.exe\" and not process.parent.name:(\"SearchIndexer.exe\", \"dllhost.exe\")) or\n (process.name:\"dllhost.exe\" and not process.parent.name:(\"services.exe\", \"svchost.exe\")) or\n (process.name:\"smss.exe\" and not process.parent.name:(\"System\", \"smss.exe\")) or\n (process.name:\"csrss.exe\" and not process.parent.name:(\"smss.exe\", \"svchost.exe\")) or\n (process.name:\"wininit.exe\" and not process.parent.name:\"smss.exe\") or\n (process.name:\"winlogon.exe\" and not process.parent.name:\"smss.exe\") or\n (process.name:(\"lsass.exe\", \"LsaIso.exe\") and not process.parent.name:\"wininit.exe\") or\n (process.name:\"LogonUI.exe\" and not process.parent.name:(\"wininit.exe\", \"winlogon.exe\")) or\n (process.name:\"services.exe\" and not process.parent.name:\"wininit.exe\") or\n (process.name:\"svchost.exe\" and not process.parent.name:(\"MsMpEng.exe\", \"services.exe\")) or\n (process.name:\"spoolsv.exe\" and not process.parent.name:\"services.exe\") or\n (process.name:\"taskhost.exe\" and not process.parent.name:(\"services.exe\", \"svchost.exe\")) or\n (process.name:\"taskhostw.exe\" and not process.parent.name:(\"services.exe\", \"svchost.exe\")) or\n (process.name:\"userinit.exe\" and not process.parent.name:(\"dwm.exe\", \"winlogon.exe\")) or\n (process.name:(\"wmiprvse.exe\", \"wsmprovhost.exe\", \"winrshost.exe\") and not process.parent.name:\"svchost.exe\") or\n /* suspicious child processes */\n (process.parent.name:(\"SearchProtocolHost.exe\", \"taskhost.exe\", \"csrss.exe\") and not process.name:(\"werfault.exe\", \"wermgr.exe\", \"WerFaultSecure.exe\")) or\n (process.parent.name:\"autochk.exe\" and not process.name:(\"chkdsk.exe\", \"doskey.exe\", \"WerFault.exe\")) or\n (process.parent.name:\"smss.exe\" and not process.name:(\"autochk.exe\", \"smss.exe\", \"csrss.exe\", \"wininit.exe\", \"winlogon.exe\", \"setupcl.exe\", \"WerFault.exe\")) or\n (process.parent.name:\"wermgr.exe\" and not process.name:(\"WerFaultSecure.exe\", \"wermgr.exe\", \"WerFault.exe\")) or\n (process.parent.name:\"conhost.exe\" and not process.name:(\"mscorsvw.exe\", \"wermgr.exe\", \"WerFault.exe\", \"WerFaultSecure.exe\"))\n )\n", "references": [ - "https://github.com/sbousseaden/Slides/blob/master/Hunting MindMaps/PNG/Windows Processes TH.map.png", + "https://github.com/sbousseaden/Slides/blob/master/Hunting MindMaps/PNG/Windows Processes%20TH.map.png", "https://www.andreafortuna.org/2017/06/15/standard-windows-processes-a-brief-reference/" ], "risk_score": 47, @@ -53,5 +53,5 @@ ], "timestamp_override": "event.ingested", "type": "eql", - "version": 8 + "version": 9 } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 0a8d10d01469ee..3058d95c0237d0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3279,9 +3279,6 @@ "savedObjectsManagement.importSummary.overwrittenCountHeader": "{overwrittenCount}件上書きされました", "savedObjectsManagement.importSummary.overwrittenOutcomeLabel": "上書き", "savedObjectsManagement.importSummary.warnings.defaultButtonLabel": "Go", - "savedObjectsManagement.indexPattern.confirmOverwriteButton": "上書き", - "savedObjectsManagement.indexPattern.confirmOverwriteLabel": "「{title}」に上書きしてよろしいですか?", - "savedObjectsManagement.indexPattern.confirmOverwriteTitle": "{type}を上書きしますか?", "savedObjectsManagement.managementSectionLabel": "保存されたオブジェクト", "savedObjectsManagement.objects.savedObjectsDescription": "保存された検索、ビジュアライゼーション、ダッシュボードのインポート、エクスポート、管理を行います。", "savedObjectsManagement.objects.savedObjectsTitle": "保存されたオブジェクト", @@ -3378,7 +3375,6 @@ "savedObjectsManagement.objectsTable.unableFindSavedObjectNotificationMessage": "保存されたオブジェクトが見つかりません", "savedObjectsManagement.objectsTable.unableFindSavedObjectsNotificationMessage": "保存されたオブジェクトが見つかりません", "savedObjectsManagement.objectView.unableFindSavedObjectNotificationMessage": "保存されたオブジェクトが見つかりません", - "savedObjectsManagement.parsingFieldErrorMessage": "{fieldName}をインデックスパターン{indexName}用にパース中にエラーが発生しました:{errorMessage}", "savedObjectsManagement.view.cancelButtonAriaLabel": "キャンセル", "savedObjectsManagement.view.cancelButtonLabel": "キャンセル", "savedObjectsManagement.view.deleteItemButtonLabel": "{title}を削除", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d29307eddd422f..7655afb144b8f4 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3294,9 +3294,6 @@ "savedObjectsManagement.importSummary.overwrittenCountHeader": "{overwrittenCount} 个已覆盖", "savedObjectsManagement.importSummary.overwrittenOutcomeLabel": "已覆盖", "savedObjectsManagement.importSummary.warnings.defaultButtonLabel": "执行", - "savedObjectsManagement.indexPattern.confirmOverwriteButton": "覆盖", - "savedObjectsManagement.indexPattern.confirmOverwriteLabel": "确定要覆盖“{title}”?", - "savedObjectsManagement.indexPattern.confirmOverwriteTitle": "覆盖“{type}”?", "savedObjectsManagement.managementSectionLabel": "已保存对象", "savedObjectsManagement.objects.savedObjectsDescription": "导入、导出和管理您的已保存搜索、可视化和仪表板。", "savedObjectsManagement.objects.savedObjectsTitle": "已保存对象", @@ -3397,7 +3394,6 @@ "savedObjectsManagement.objectsTable.unableFindSavedObjectNotificationMessage": "找不到已保存对象", "savedObjectsManagement.objectsTable.unableFindSavedObjectsNotificationMessage": "找不到已保存对象", "savedObjectsManagement.objectView.unableFindSavedObjectNotificationMessage": "找不到已保存对象", - "savedObjectsManagement.parsingFieldErrorMessage": "为索引模式“{indexName}”解析“{fieldName}”时发生错误:{errorMessage}", "savedObjectsManagement.view.cancelButtonAriaLabel": "取消", "savedObjectsManagement.view.cancelButtonLabel": "取消", "savedObjectsManagement.view.deleteItemButtonLabel": "删除“{title}”", diff --git a/x-pack/plugins/ui_actions_enhanced/kibana.json b/x-pack/plugins/ui_actions_enhanced/kibana.json index 6fcac67c5a66b3..da34dfd46e5778 100644 --- a/x-pack/plugins/ui_actions_enhanced/kibana.json +++ b/x-pack/plugins/ui_actions_enhanced/kibana.json @@ -4,6 +4,7 @@ "name": "Kibana App Services", "githubTeam": "kibana-app-services" }, + "description": "Extends UI Actions plugin with more functionality", "version": "kibana", "configPath": ["xpack", "ui_actions_enhanced"], "server": true, diff --git a/x-pack/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts b/x-pack/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts index 7ea59e77c4e2fa..41846f1f71ad65 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts @@ -13,7 +13,7 @@ import { TimeRangeEmbeddable, TimeRangeContainer } from './test_helpers'; test('canInheritTimeRange returns false if embeddable is inside container without a time range', () => { const embeddable = new TimeRangeEmbeddable( { id: '1234', timeRange: { from: 'noxw-15m', to: 'now' } }, - new HelloWorldContainer({ id: '123', panels: {} }, (() => null) as any) + new HelloWorldContainer({ id: '123', panels: {} }, {}) ); expect(canInheritTimeRange(embeddable)).toBe(false); @@ -22,7 +22,7 @@ test('canInheritTimeRange returns false if embeddable is inside container withou test('canInheritTimeRange returns false if embeddable is without a time range', () => { const embeddable = new HelloWorldEmbeddable( { id: '1234' }, - new HelloWorldContainer({ id: '123', panels: {} }, (() => null) as any) + new HelloWorldContainer({ id: '123', panels: {} }, {}) ); // @ts-ignore expect(canInheritTimeRange(embeddable)).toBe(false); @@ -33,7 +33,7 @@ test('canInheritTimeRange returns true if embeddable is inside a container with { id: '1234', timeRange: { from: 'noxw-15m', to: 'now' } }, new TimeRangeContainer( { id: '123', panels: {}, timeRange: { from: 'noxw-15m', to: 'now' } }, - (() => null) as any + () => undefined ) ); expect(canInheritTimeRange(embeddable)).toBe(true); diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.stories.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.stories.tsx index 9e7344acb6d99a..8eb5c45ac3a65f 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.stories.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.stories.tsx @@ -7,26 +7,29 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; +import { SerializableRecord } from '@kbn/utility-types'; import { Demo, dashboardFactory, urlFactory } from './test_data'; +import { ActionFactory, BaseActionFactoryContext } from '../../dynamic_actions'; + +const dashboard = (dashboardFactory as unknown) as ActionFactory< + SerializableRecord, + object, + BaseActionFactoryContext +>; + +const url = (urlFactory as unknown) as ActionFactory< + SerializableRecord, + object, + BaseActionFactoryContext +>; storiesOf('components/ActionWizard', module) - .add('default', () => ) + .add('default', () => ) .add('Only one factory is available', () => ( // to make sure layout doesn't break - + )) .add('Long list of action factories', () => ( // to make sure layout doesn't break - + )); diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx index a89091f6802871..5f98ebacea9801 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx @@ -15,11 +15,20 @@ import { urlFactory, urlDrilldownActionFactory, } from './test_data'; -import { ActionFactory } from '../../dynamic_actions'; +import { ActionFactory, BaseActionFactoryContext } from '../../dynamic_actions'; import { licensingMock } from '../../../../licensing/public/mocks'; +import { SerializableRecord } from '@kbn/utility-types'; test('Pick and configure action', () => { - const screen = render(); + const screen = render( + + > + } + /> + ); // check that all factories are displayed to pick expect(screen.getAllByTestId(new RegExp(TEST_SUBJ_ACTION_FACTORY_ITEM))).toHaveLength(2); @@ -44,7 +53,17 @@ test('Pick and configure action', () => { }); test('If only one actions factory is available then actionFactory selection is emitted without user input', () => { - const screen = render(); + const screen = render( + , + ]} + /> + ); // check that no factories are displayed to pick from expect(screen.queryByTestId(new RegExp(TEST_SUBJ_ACTION_FACTORY_ITEM))).not.toBeInTheDocument(); @@ -72,7 +91,15 @@ test('If not enough license, button is disabled', () => { getFeatureUsageStart: () => licensingMock.createStart().featureUsage, } ); - const screen = render(); + const screen = render( + + > + } + /> + ); // check that all factories are displayed to pick expect(screen.getAllByTestId(new RegExp(TEST_SUBJ_ACTION_FACTORY_ITEM))).toHaveLength(2); @@ -91,7 +118,15 @@ test('if action is beta, beta badge is shown', () => { getFeatureUsageStart: () => licensingMock.createStart().featureUsage, } ); - const screen = render(); + const screen = render( + + > + } + /> + ); // Uses the single letter beta badge expect(screen.getByText(/^B/i)).toBeVisible(); }); diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts index 64c13aeb9ce842..532a230a772737 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts @@ -45,6 +45,6 @@ export const txtBetaActionFactoryLabel = i18n.translate( export const txtBetaActionFactoryTooltip = i18n.translate( 'xpack.uiActionsEnhanced.components.actionWizard.betaActionTooltip', { - defaultMessage: `This action is in beta and is subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features. Please help us by reporting any bugs or providing other feedback.`, + defaultMessage: `This action is in beta and is subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features. Please help us by reporting bugs or providing other feedback.`, } ); diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx index 9a9d1a0f798576..cdbd8ea096495f 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx @@ -12,7 +12,10 @@ import { ActionWizard } from './action_wizard'; import { ActionFactory, ActionFactoryDefinition, BaseActionConfig } from '../../dynamic_actions'; import { CollectConfigProps } from '../../../../../../src/plugins/kibana_utils/public'; import { licensingMock } from '../../../../licensing/public/mocks'; -import { Trigger } from '../../../../../../src/plugins/ui_actions/public'; +import { + Trigger, + UiActionsActionDefinition as ActionDefinition, +} from '../../../../../../src/plugins/ui_actions/public'; import { APPLY_FILTER_TRIGGER } from '../../../../../../src/plugins/data/public'; import { SELECT_RANGE_TRIGGER, @@ -81,7 +84,7 @@ function DashboardDrilldownCollectConfig(props: CollectConfigProps = { id: 'Dashboard', getDisplayName: () => 'Go to Dashboard', @@ -108,7 +111,7 @@ export const dashboardDrilldownActionFactory: ActionFactoryDefinition< execute: async () => alert('Navigate to dashboard!'), enhancements: {}, }), - supportedTriggers(): any[] { + supportedTriggers(): string[] { return [APPLY_FILTER_TRIGGER]; }, }; @@ -169,8 +172,8 @@ export const urlDrilldownActionFactory: ActionFactoryDefinition { return Promise.resolve(true); }, - create: () => null as any, - supportedTriggers(): any[] { + create: () => ({} as ActionDefinition), + supportedTriggers(): string[] { return [VALUE_CLICK_TRIGGER, SELECT_RANGE_TRIGGER]; }, }; @@ -180,9 +183,10 @@ export const urlFactory = new ActionFactory(urlDrilldownActionFactory, { getFeatureUsageStart: () => licensingMock.createStart().featureUsage, }); -export const mockActionFactories: ActionFactory[] = ([dashboardFactory, urlFactory] as Array< - ActionFactory ->) as ActionFactory[]; +export const mockActionFactories: ActionFactory[] = ([ + dashboardFactory, + urlFactory, +] as unknown) as ActionFactory[]; export const mockSupportedTriggers: string[] = [ VALUE_CLICK_TRIGGER, @@ -194,13 +198,13 @@ export const mockGetTriggerInfo = (triggerId: string): Trigger => { [VALUE_CLICK_TRIGGER]: 'Single click', [SELECT_RANGE_TRIGGER]: 'Range selection', [APPLY_FILTER_TRIGGER]: 'Apply filter', - } as Record; + } as Record; const descriptionMap = { [VALUE_CLICK_TRIGGER]: 'A single point clicked on a visualization', [SELECT_RANGE_TRIGGER]: 'Select a group of values', [APPLY_FILTER_TRIGGER]: 'Apply filter description...', - } as Record; + } as Record; return { id: triggerId, @@ -209,7 +213,11 @@ export const mockGetTriggerInfo = (triggerId: string): Trigger => { }; }; -export function Demo({ actionFactories }: { actionFactories: Array> }) { +export function Demo({ + actionFactories, +}: { + actionFactories: Array>; +}) { const [state, setState] = useState<{ currentActionFactory?: ActionFactory; config?: BaseActionConfig; diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/presentable_picker/i18n.ts b/x-pack/plugins/ui_actions_enhanced/public/components/presentable_picker/i18n.ts index 36ca55901950fb..2d139095e81d78 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/presentable_picker/i18n.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/components/presentable_picker/i18n.ts @@ -17,7 +17,7 @@ export const txtBetaActionFactoryLabel = i18n.translate( export const txtBetaActionFactoryTooltip = i18n.translate( 'xpack.uiActionsEnhanced.components.actionWizard.betaActionTooltip', { - defaultMessage: `This action is in beta and is subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features. Please help us by reporting any bugs or providing other feedback.`, + defaultMessage: `This action is in beta and is subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features. Please help us by reporting bugs or providing other feedback.`, } ); diff --git a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts index a6a26def2a0eed..889b6377b16b54 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts @@ -48,7 +48,7 @@ test('Custom time range action prevents embeddable from using container time', a }, id: '123', }, - (() => {}) as any + () => undefined ); await container.untilEmbeddableLoaded('1'); @@ -112,7 +112,7 @@ test('Removing custom time range action resets embeddable back to container time }, id: '123', }, - (() => {}) as any + () => undefined ); await container.untilEmbeddableLoaded('1'); @@ -187,7 +187,7 @@ test('Cancelling custom time range action leaves state alone', async () => { }, id: '123', }, - (() => {}) as any + () => undefined ); await container.untilEmbeddableLoaded('1'); @@ -239,7 +239,7 @@ test(`badge is compatible with embeddable that inherits from parent`, async () = }, id: '123', }, - (() => {}) as any + () => undefined ); await container.untilEmbeddableLoaded('1'); @@ -272,7 +272,7 @@ test(`badge is compatible with embeddable that inherits from parent`, async () = // }, // id: '123', // }, -// (() => null) as any +// () => undefined // ); // await container.untilEmbeddableLoaded('1'); @@ -305,7 +305,7 @@ test('Attempting to execute on incompatible embeddable throws an error', async ( }, id: '123', }, - (() => null) as any + {} ); await container.untilEmbeddableLoaded('1'); diff --git a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx index 5b1ba3bcc206b5..ee2587f61fbc8a 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx @@ -7,7 +7,12 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; -import { IEmbeddable, Embeddable, EmbeddableInput } from 'src/plugins/embeddable/public'; +import { + IEmbeddable, + Embeddable, + EmbeddableInput, + EmbeddableOutput, +} from 'src/plugins/embeddable/public'; import { Action, IncompatibleActionError } from '../../../../src/plugins/ui_actions/public'; import { TimeRange } from '../../../../src/plugins/data/public'; import { CustomizeTimeRangeModal } from './customize_time_range_modal'; @@ -26,7 +31,8 @@ function hasTimeRange( } const VISUALIZE_EMBEDDABLE_TYPE = 'visualization'; -type VisualizeEmbeddable = any; + +type VisualizeEmbeddable = IEmbeddable<{ id: string }, EmbeddableOutput & { visTypeName: string }>; function isVisualizeEmbeddable( embeddable: IEmbeddable | VisualizeEmbeddable diff --git a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_badge.test.ts b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_badge.test.ts index b05931eab4be60..b4134ddd17395b 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_badge.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_badge.test.ts @@ -35,7 +35,7 @@ test('Removing custom time range from badge resets embeddable back to container }, id: '123', }, - (() => null) as any + () => undefined ); await container.untilEmbeddableLoaded('1'); @@ -87,7 +87,7 @@ test(`badge is not compatible with embeddable that inherits from parent`, async }, id: '123', }, - (() => null) as any + () => undefined ); await container.untilEmbeddableLoaded('1'); @@ -120,7 +120,7 @@ test(`badge is compatible with embeddable that has custom time range`, async () }, id: '123', }, - (() => null) as any + () => undefined ); await container.untilEmbeddableLoaded('1'); @@ -152,7 +152,7 @@ test('Attempting to execute on incompatible embeddable throws an error', async ( }, id: '123', }, - (() => null) as any + () => undefined ); await container.untilEmbeddableLoaded('1'); diff --git a/x-pack/plugins/ui_actions_enhanced/public/customize_time_range_modal.tsx b/x-pack/plugins/ui_actions_enhanced/public/customize_time_range_modal.tsx index 719a96d513c821..523d046d88e357 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/customize_time_range_modal.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/customize_time_range_modal.tsx @@ -74,7 +74,7 @@ export class CustomizeTimeRangeModal extends Component>; const parentPanels = parent!.getInput().panels; - // Remove any explicit input to this child from the parent. + // Remove explicit input to this child from the parent. parent!.updateInput({ panels: { ...parentPanels, diff --git a/x-pack/plugins/ui_actions_enhanced/public/does_inherit_time_range.ts b/x-pack/plugins/ui_actions_enhanced/public/does_inherit_time_range.ts index 5abf4c7a41ba83..4b921b0b222eac 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/does_inherit_time_range.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/does_inherit_time_range.ts @@ -15,9 +15,6 @@ export function doesInheritTimeRange(embeddable: Embeddable) { const parent = embeddable.parent as IContainer<{}, ContainerInput>; - // Note: this logic might not work in a container nested world... the explicit input - // may be on the root... or any of the interim parents. - // if it's a dashboard emptys screen, there will be no embeddable if (!parent.getInput().panels[embeddable.id]) { return false; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts index ec2906b2741768..62b13ded85bfad 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts @@ -91,8 +91,7 @@ export interface DrilldownDefinition< CollectConfig: ActionFactoryDefinition['CollectConfig']; /** - * A validator function for the config object. Should always return a boolean - * given any input. + * A validator function for the config object. Should always return a boolean. */ isConfigValid: ActionFactoryDefinition['isConfigValid']; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/action_factory/action_factory.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/action_factory/action_factory.tsx index 4391254d0a8aab..7ee9fe51e59b90 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/action_factory/action_factory.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/action_factory/action_factory.tsx @@ -42,7 +42,7 @@ const txtBetaActionFactoryLabel = i18n.translate( const txtBetaActionFactoryTooltip = i18n.translate( 'xpack.uiActionsEnhanced.components.DrilldownForm.betaActionTooltip', { - defaultMessage: `This action is in beta and is subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features. Please help us by reporting any bugs or providing other feedback.`, + defaultMessage: `This action is in beta and is subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features. Please help us by reporting bugs or providing other feedback.`, } ); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/flyout_frame.test.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/flyout_frame.test.tsx index a22ea792f50a87..11ee764ea87b6d 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/flyout_frame.test.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/components/flyout_frame/flyout_frame.test.tsx @@ -25,7 +25,7 @@ describe('', () => { expect(title?.textContent).toBe('foobar'); }); - test('title can be any react node', () => { + test('title can be a react node', () => { const div = document.createElement('div'); render( ', () => { expect(footer).toBe(null); }); - test('can render anything in footer', () => { + test('can render a React node in footer', () => { const div = document.createElement('div'); render( { + type Mutable = { + -readonly [Property in keyof Type]: Type[Property]; + }; const factory1 = new ActionFactory( { id: 'FACTORY1', @@ -87,9 +91,10 @@ const createDrilldownManagerState = () => { }; const uiActions = uiActionsEnhancedPluginMock.createPlugin(); const uiActionsStart = uiActions.doStart(); - (uiActionsStart as any).attachAction = () => {}; - (uiActionsStart as any).detachAction = () => {}; - (uiActionsStart as any).hasActionFactory = (actionFactoryId: string): boolean => { + const uiActionsStartMutable = uiActionsStart as Mutable; + uiActionsStartMutable.attachAction = () => {}; + uiActionsStartMutable.detachAction = () => {}; + uiActionsStartMutable.hasActionFactory = (actionFactoryId: string): boolean => { switch (actionFactoryId) { case 'FACTORY1': case 'FACTORY2': @@ -98,7 +103,7 @@ const createDrilldownManagerState = () => { } return false; }; - (uiActionsStart as any).getActionFactory = (actionFactoryId: string): ActionFactory => { + uiActionsStartMutable.getActionFactory = (actionFactoryId: string): ActionFactory => { switch (actionFactoryId) { case 'FACTORY1': return factory1; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/types.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/types.ts index 1ff6053126e1cf..78944a17127285 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/types.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/types.ts @@ -88,7 +88,7 @@ export interface PublicDrilldownManagerProps { */ export interface DrilldownTemplate { /** - * Any string that uniquely identifies this item in a list of `DrilldownTemplate[]`. + * A string that uniquely identifies this item in a list of `DrilldownTemplate[]`. */ id: string; diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts index b90ab198b9c9d0..9e3582404bf091 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts @@ -10,19 +10,19 @@ import { ActionFactoryDefinition } from './action_factory_definition'; import { licensingMock } from '../../../licensing/public/mocks'; import { PublicLicense } from '../../../licensing/public'; -const def: ActionFactoryDefinition = { +const def: ActionFactoryDefinition = ({ id: 'ACTION_FACTORY_1', - CollectConfig: {} as any, + CollectConfig: {}, createConfig: () => ({}), - isConfigValid: (() => true) as any, - create: ({ name }) => ({ + isConfigValid: () => true, + create: ({ name }: { name: string }) => ({ id: '', execute: async () => {}, getDisplayName: () => name, enhancements: {}, }), supportedTriggers: () => [], -}; +} as unknown) as ActionFactoryDefinition; const featureUsage = licensingMock.createStart().featureUsage; diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts index 93c1b33268bf4d..7b5cb3d8e121aa 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts @@ -122,7 +122,10 @@ export class ActionFactory< }); } - public telemetry(state: SerializedEvent, telemetryData: Record) { + public telemetry( + state: SerializedEvent, + telemetryData: Record + ) { return this.def.telemetry ? this.def.telemetry(state, telemetryData) : telemetryData; } diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_enhancement.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_enhancement.ts index 71ae3cfcc19e38..6a2cd0eed348ae 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_enhancement.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_enhancement.ts @@ -16,7 +16,10 @@ export const dynamicActionEnhancement = ( ): EnhancementRegistryDefinition => { return { id: 'dynamicActions', - telemetry: (state: SerializableRecord, telemetryData: Record) => { + telemetry: ( + state: SerializableRecord, + telemetryData: Record + ) => { return uiActionsEnhanced.telemetry(state as DynamicActionsState, telemetryData); }, extract: (state: SerializableRecord) => { diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts index 7e6ac78f93327f..533a0617d1affa 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts @@ -16,12 +16,12 @@ import { SerializedAction, SerializedEvent } from './types'; import { licensingMock } from '../../../licensing/public/mocks'; import { dynamicActionGrouping } from './dynamic_action_grouping'; -const actionFactoryDefinition1: ActionFactoryDefinition = { +const actionFactoryDefinition1: ActionFactoryDefinition = ({ id: 'ACTION_FACTORY_1', - CollectConfig: {} as any, + CollectConfig: {}, createConfig: () => ({}), - isConfigValid: (() => true) as any, - create: ({ name }) => ({ + isConfigValid: () => true, + create: ({ name }: { name: string }) => ({ id: '', execute: async () => {}, getDisplayName: () => name, @@ -29,14 +29,14 @@ const actionFactoryDefinition1: ActionFactoryDefinition = { supportedTriggers() { return ['VALUE_CLICK_TRIGGER']; }, -}; +} as unknown) as ActionFactoryDefinition; -const actionFactoryDefinition2: ActionFactoryDefinition = { +const actionFactoryDefinition2: ActionFactoryDefinition = ({ id: 'ACTION_FACTORY_2', - CollectConfig: {} as any, + CollectConfig: {}, createConfig: () => ({}), - isConfigValid: (() => true) as any, - create: ({ name }) => ({ + isConfigValid: () => true, + create: ({ name }: { name: string }) => ({ id: '', execute: async () => {}, getDisplayName: () => name, @@ -44,7 +44,7 @@ const actionFactoryDefinition2: ActionFactoryDefinition = { supportedTriggers() { return ['VALUE_CLICK_TRIGGER']; }, -}; +} as unknown) as ActionFactoryDefinition; const event1: SerializedEvent = { eventId: 'EVENT_ID_1', @@ -709,10 +709,7 @@ describe('DynamicActionManager', () => { await manager.start(); - const basicActions = await uiActions.getTriggerCompatibleActions( - 'VALUE_CLICK_TRIGGER', - {} as any - ); + const basicActions = await uiActions.getTriggerCompatibleActions('VALUE_CLICK_TRIGGER', {}); expect(basicActions).toHaveLength(1); getLicenseInfo.mockImplementation(() => @@ -721,7 +718,7 @@ describe('DynamicActionManager', () => { const basicAndGoldActions = await uiActions.getTriggerCompatibleActions( 'VALUE_CLICK_TRIGGER', - {} as any + {} ); expect(basicAndGoldActions).toHaveLength(2); diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts index fbc3d7229df6f8..2deb0ab6edeb25 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts @@ -102,13 +102,13 @@ export class DynamicActionManager { const supportedTriggers = factory.supportedTriggers(); for (const trigger of triggers) { - if (!supportedTriggers.includes(trigger as any)) + if (!supportedTriggers.includes(trigger)) throw new Error( `Can't attach [action=${actionId}] to [trigger=${trigger}]. Supported triggers for this action: ${supportedTriggers.join( ',' )}` ); - uiActions.attachAction(trigger as any, actionId); + uiActions.attachAction(trigger as string, actionId); } } @@ -117,7 +117,7 @@ export class DynamicActionManager { const actionId = this.generateActionId(eventId); if (!uiActions.hasAction(actionId)) return; - for (const trigger of triggers) uiActions.detachAction(trigger as any, actionId); + for (const trigger of triggers) uiActions.detachAction(trigger, actionId); uiActions.unregisterAction(actionId); } diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager_state.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager_state.ts index e41e013df51cc4..47b601084ac23a 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager_state.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager_state.ts @@ -8,7 +8,7 @@ import { SerializedEvent } from './types'; /** - * This interface represents the state of @type {DynamicActionManager} at any + * This interface represents the state of @type {DynamicActionManager} at every * point in time. */ export interface State { diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts index 0c5709b691ba2c..907d689e76ab07 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts @@ -20,26 +20,26 @@ const deps: UiActionsServiceEnhancementsParams = { describe('UiActionsService', () => { describe('action factories', () => { - const factoryDefinition1: ActionFactoryDefinition = { + const factoryDefinition1: ActionFactoryDefinition = ({ id: 'test-factory-1', - CollectConfig: {} as any, + CollectConfig: {}, createConfig: () => ({}), isConfigValid: () => true, - create: () => ({} as any), + create: () => ({}), supportedTriggers() { return ['VALUE_CLICK_TRIGGER']; }, - }; - const factoryDefinition2: ActionFactoryDefinition = { + } as unknown) as ActionFactoryDefinition; + const factoryDefinition2: ActionFactoryDefinition = ({ id: 'test-factory-2', - CollectConfig: {} as any, + CollectConfig: {}, createConfig: () => ({}), isConfigValid: () => true, - create: () => ({} as any), + create: () => ({}), supportedTriggers() { return ['VALUE_CLICK_TRIGGER']; }, - }; + } as unknown) as ActionFactoryDefinition; test('.getActionFactories() returns empty array if no action factories registered', () => { const service = new UiActionsServiceEnhancements(deps); diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts index 132c8a16783e06..2b696db83e0c0b 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { SerializableRecord } from '@kbn/utility-types'; import { ActionFactoryRegistry } from '../types'; import { ActionFactory, @@ -60,8 +61,15 @@ export class UiActionsServiceEnhancements this.deps ); - this.actionFactories.set(actionFactory.id, actionFactory as ActionFactory); - this.registerFeatureUsage(definition); + this.actionFactories.set( + actionFactory.id, + (actionFactory as unknown) as ActionFactory< + SerializableRecord, + ExecutionContext, + BaseActionFactoryContext + > + ); + this.registerFeatureUsage((definition as unknown) as ActionFactoryDefinition); }; public readonly getActionFactory = (actionFactoryId: string): ActionFactory => { @@ -143,12 +151,15 @@ export class UiActionsServiceEnhancements this.registerActionFactory(actionFactory); }; - private registerFeatureUsage = (definition: ActionFactoryDefinition): void => { + private registerFeatureUsage = (definition: ActionFactoryDefinition): void => { if (!definition.minimalLicense || !definition.licenseFeatureName) return; this.deps.featureUsageSetup.register(definition.licenseFeatureName, definition.minimalLicense); }; - public readonly telemetry = (state: DynamicActionsState, telemetry: Record = {}) => { + public readonly telemetry = ( + state: DynamicActionsState, + telemetry: Record = {} + ) => { let telemetryData = telemetry; state.events.forEach((event: SerializedEvent) => { if (this.actionFactories.has(event.action.factoryId)) { diff --git a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts index 07cafef084a61a..45062f36bdfbf5 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts @@ -17,9 +17,12 @@ export const dynamicActionEnhancement = ( ): EnhancementRegistryDefinition => { return { id: 'dynamicActions', - telemetry: (serializableState: SerializableRecord, stats: Record) => { + telemetry: ( + serializableState: SerializableRecord, + stats: Record + ) => { const state = serializableState as DynamicActionsState; - stats = dynamicActionsCollector(state, stats); + stats = dynamicActionsCollector(state, stats as Record); stats = dynamicActionFactoriesCollector(getActionFactory, state, stats); return stats; diff --git a/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.test.ts b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.test.ts index 0d4090f50442c5..c74c56723eb8ed 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.test.ts @@ -16,7 +16,7 @@ type GetActionFactory = (id: string) => undefined | ActionFactory; const factories: Record = { FACTORY_ID_1: ({ id: 'FACTORY_ID_1', - telemetry: jest.fn((state: DynamicActionsState, stats: Record) => { + telemetry: jest.fn((state: DynamicActionsState, stats: Record) => { stats.myStat_1 = 1; stats.myStat_2 = 123; return stats; @@ -24,11 +24,11 @@ const factories: Record = { } as unknown) as ActionFactory, FACTORY_ID_2: ({ id: 'FACTORY_ID_2', - telemetry: jest.fn((state: DynamicActionsState, stats: Record) => stats), + telemetry: jest.fn((state: DynamicActionsState, stats: Record) => stats), } as unknown) as ActionFactory, FACTORY_ID_3: ({ id: 'FACTORY_ID_3', - telemetry: jest.fn((state: DynamicActionsState, stats: Record) => { + telemetry: jest.fn((state: DynamicActionsState, stats: Record) => { stats.myStat_1 = 2; stats.stringStat = 'abc'; return stats; diff --git a/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.ts b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.ts index 874cb8cfb5a699..68a314daa121af 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_action_factories_collector.ts @@ -11,8 +11,8 @@ import { ActionFactory } from '../types'; export const dynamicActionFactoriesCollector = ( getActionFactory: (id: string) => undefined | ActionFactory, state: DynamicActionsState, - stats: Record -): Record => { + stats: Record +): Record => { for (const event of state.events) { const factory = getActionFactory(event.action.factoryId); diff --git a/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.ts b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.ts index c89d93f5f5e283..0f0f737e9d21f2 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/telemetry/dynamic_actions_collector.ts @@ -10,9 +10,9 @@ import { getMetricKey } from './get_metric_key'; export const dynamicActionsCollector = ( state: DynamicActionsState, - currentStats: Record -): Record => { - const stats: Record = { ...currentStats }; + currentStats: Record +): Record => { + const stats: Record = { ...currentStats }; const countMetricKey = getMetricKey('count'); stats[countMetricKey] = state.events.length + (stats[countMetricKey] || 0); diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/enabled_alerts.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/enabled_alerts.tsx index 33e1303b416432..ca38dff4606adb 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/enabled_alerts.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/enabled_alerts.tsx @@ -61,7 +61,7 @@ export const EnabledAlerts = ({ monitorAlerts, loading }: Props) => { )} diff --git a/x-pack/test/api_integration/apis/maps/migrations.js b/x-pack/test/api_integration/apis/maps/migrations.js index 89e9c0975f9f62..37dc9c32958fff 100644 --- a/x-pack/test/api_integration/apis/maps/migrations.js +++ b/x-pack/test/api_integration/apis/maps/migrations.js @@ -21,7 +21,7 @@ export default function ({ getService }) { attributes: { title: '[Logs] Total Requests and Bytes', layerListJSON: - '[{"id":"edh66","label":"Total Requests by Country","minZoom":0,"maxZoom":24,"alpha":0.5,"sourceDescriptor":{"type":"EMS_FILE","id":"world_countries"},"visible":true,"style":{"type":"VECTOR","properties":{"fillColor":{"type":"DYNAMIC","options":{"field":{"label":"count of kibana_sample_data_logs:geo.src","name":"__kbnjoin__count_groupby_kibana_sample_data_logs.geo.src","origin":"join"},"color":"Greys"}},"lineColor":{"type":"STATIC","options":{"color":"#FFFFFF"}},"lineWidth":{"type":"STATIC","options":{"size":1}},"iconSize":{"type":"STATIC","options":{"size":10}}}},"type":"VECTOR","joins":[{"leftField":"iso2","right":{"id":"673ff994-fc75-4c67-909b-69fcb0e1060e","indexPatternId":"90943e30-9a47-11e8-b64d-95841ca0b247","indexPatternTitle":"kibana_sample_data_logs","term":"geo.src"}}]},{"id":"gaxya","label":"Actual Requests","minZoom":9,"maxZoom":24,"alpha":1,"sourceDescriptor":{"id":"b7486535-171b-4d3b-bb2e-33c1a0a2854c","type":"ES_SEARCH","indexPatternId":"90943e30-9a47-11e8-b64d-95841ca0b247","geoField":"geo.coordinates","limit":2048,"filterByMapBounds":true,"tooltipProperties":["clientip","timestamp","host","request","response","machine.os","agent","bytes"]},"visible":true,"style":{"type":"VECTOR","properties":{"fillColor":{"type":"STATIC","options":{"color":"#2200ff"}},"lineColor":{"type":"STATIC","options":{"color":"#FFFFFF"}},"lineWidth":{"type":"STATIC","options":{"size":2}},"iconSize":{"type":"DYNAMIC","options":{"field":{"label":"bytes","name":"bytes","origin":"source"},"minSize":1,"maxSize":23}}}},"type":"VECTOR"},{"id":"tfi3f","label":"Total Requests and Bytes","minZoom":0,"maxZoom":9,"alpha":1,"sourceDescriptor":{"type":"ES_GEO_GRID","resolution":"COARSE","id":"8aaa65b5-a4e9-448b-9560-c98cb1c5ac5b","indexPatternId":"90943e30-9a47-11e8-b64d-95841ca0b247","geoField":"geo.coordinates","requestType":"point","metrics":[{"type":"count"},{"type":"sum","field":"bytes"}]},"visible":true,"style":{"type":"VECTOR","properties":{"fillColor":{"type":"DYNAMIC","options":{"field":{"label":"Count","name":"doc_count","origin":"source"},"color":"Blues"}},"lineColor":{"type":"STATIC","options":{"color":"#cccccc"}},"lineWidth":{"type":"STATIC","options":{"size":1}},"iconSize":{"type":"DYNAMIC","options":{"field":{"label":"sum of bytes","name":"sum_of_bytes","origin":"source"},"minSize":1,"maxSize":25}}}},"type":"VECTOR"}]', + '[{"id":"edh66","label":"Total Requests by Destination","minZoom":0,"maxZoom":24,"alpha":0.5,"sourceDescriptor":{"type":"EMS_FILE","id":"world_countries"},"visible":true,"style":{"type":"VECTOR","properties":{"fillColor":{"type":"DYNAMIC","options":{"field":{"label":"count of kibana_sample_data_logs:geo.src","name":"__kbnjoin__count_groupby_kibana_sample_data_logs.geo.src","origin":"join"},"color":"Greys"}},"lineColor":{"type":"STATIC","options":{"color":"#FFFFFF"}},"lineWidth":{"type":"STATIC","options":{"size":1}},"iconSize":{"type":"STATIC","options":{"size":10}}}},"type":"VECTOR","joins":[{"leftField":"iso2","right":{"id":"673ff994-fc75-4c67-909b-69fcb0e1060e","indexPatternId":"90943e30-9a47-11e8-b64d-95841ca0b247","indexPatternTitle":"kibana_sample_data_logs","term":"geo.src"}}]},{"id":"gaxya","label":"Actual Requests","minZoom":9,"maxZoom":24,"alpha":1,"sourceDescriptor":{"id":"b7486535-171b-4d3b-bb2e-33c1a0a2854c","type":"ES_SEARCH","indexPatternId":"90943e30-9a47-11e8-b64d-95841ca0b247","geoField":"geo.coordinates","limit":2048,"filterByMapBounds":true,"tooltipProperties":["clientip","timestamp","host","request","response","machine.os","agent","bytes"]},"visible":true,"style":{"type":"VECTOR","properties":{"fillColor":{"type":"STATIC","options":{"color":"#2200ff"}},"lineColor":{"type":"STATIC","options":{"color":"#FFFFFF"}},"lineWidth":{"type":"STATIC","options":{"size":2}},"iconSize":{"type":"DYNAMIC","options":{"field":{"label":"bytes","name":"bytes","origin":"source"},"minSize":1,"maxSize":23}}}},"type":"VECTOR"},{"id":"tfi3f","label":"Total Requests and Bytes","minZoom":0,"maxZoom":9,"alpha":1,"sourceDescriptor":{"type":"ES_GEO_GRID","resolution":"COARSE","id":"8aaa65b5-a4e9-448b-9560-c98cb1c5ac5b","indexPatternId":"90943e30-9a47-11e8-b64d-95841ca0b247","geoField":"geo.coordinates","requestType":"point","metrics":[{"type":"count"},{"type":"sum","field":"bytes"}]},"visible":true,"style":{"type":"VECTOR","properties":{"fillColor":{"type":"DYNAMIC","options":{"field":{"label":"Count","name":"doc_count","origin":"source"},"color":"Blues"}},"lineColor":{"type":"STATIC","options":{"color":"#cccccc"}},"lineWidth":{"type":"STATIC","options":{"size":1}},"iconSize":{"type":"DYNAMIC","options":{"field":{"label":"sum of bytes","name":"sum_of_bytes","origin":"source"},"minSize":1,"maxSize":25}}}},"type":"VECTOR"}]', }, migrationVersion: {}, }) diff --git a/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts b/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts index 0205940d77724b..4b484502d5826a 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts @@ -6,9 +6,14 @@ */ import expect from '@kbn/expect'; + +import { IKibanaSearchRequest } from '../../../../../src/plugins/data/common'; + +import type { FailedTransactionsCorrelationsParams } from '../../../../plugins/apm/common/search_strategies/failed_transactions_correlations/types'; +import { APM_SEARCH_STRATEGIES } from '../../../../plugins/apm/common/search_strategies/constants'; + import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; -import { PartialSearchRequest } from '../../../../plugins/apm/server/lib/search_strategies/correlations/search_strategy'; import { parseBfetchResponse } from '../../common/utils/parse_b_fetch'; export default function ApiTest({ getService }: FtrProviderContext) { @@ -16,7 +21,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('legacySupertestAsApmReadUser'); const getRequestBody = () => { - const partialSearchRequest: PartialSearchRequest = { + const request: IKibanaSearchRequest = { params: { environment: 'ENVIRONMENT_ALL', start: '2020', @@ -28,8 +33,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { return { batch: [ { - request: partialSearchRequest, - options: { strategy: 'apmFailedTransactionsCorrelationsSearchStrategy' }, + request, + options: { strategy: APM_SEARCH_STRATEGIES.APM_FAILED_TRANSACTIONS_CORRELATIONS }, }, ], }; @@ -117,9 +122,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(typeof finalRawResponse?.took).to.be('number'); - expect(finalRawResponse?.values.length).to.eql( + expect(finalRawResponse?.failedTransactionsCorrelations.length).to.eql( 0, - `Expected 0 identified correlations, got ${finalRawResponse?.values.length}.` + `Expected 0 identified correlations, got ${finalRawResponse?.failedTransactionsCorrelations.length}.` ); }); }); @@ -209,9 +214,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(finalRawResponse?.percentileThresholdValue).to.be(undefined); expect(finalRawResponse?.overallHistogram).to.be(undefined); - expect(finalRawResponse?.values.length).to.eql( + expect(finalRawResponse?.failedTransactionsCorrelations.length).to.eql( 43, - `Expected 43 identified correlations, got ${finalRawResponse?.values.length}.` + `Expected 43 identified correlations, got ${finalRawResponse?.failedTransactionsCorrelations.length}.` ); expect(finalRawResponse?.log.map((d: string) => d.split(': ')[1])).to.eql([ @@ -220,7 +225,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { 'Identified 43 significant correlations relating to failed transactions.', ]); - const sortedCorrelations = finalRawResponse?.values.sort(); + const sortedCorrelations = finalRawResponse?.failedTransactionsCorrelations.sort(); const correlation = sortedCorrelations[0]; expect(typeof correlation).to.be('object'); diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency.ts b/x-pack/test/apm_api_integration/tests/correlations/latency.ts index 21cd855f4ed851..a51c0c8b9d1517 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/latency.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/latency.ts @@ -6,9 +6,14 @@ */ import expect from '@kbn/expect'; + +import { IKibanaSearchRequest } from '../../../../../src/plugins/data/common'; + +import type { LatencyCorrelationsParams } from '../../../../plugins/apm/common/search_strategies/latency_correlations/types'; +import { APM_SEARCH_STRATEGIES } from '../../../../plugins/apm/common/search_strategies/constants'; + import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; -import { PartialSearchRequest } from '../../../../plugins/apm/server/lib/search_strategies/correlations/search_strategy'; import { parseBfetchResponse } from '../../common/utils/parse_b_fetch'; export default function ApiTest({ getService }: FtrProviderContext) { @@ -16,21 +21,22 @@ export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('legacySupertestAsApmReadUser'); const getRequestBody = () => { - const partialSearchRequest: PartialSearchRequest = { + const request: IKibanaSearchRequest = { params: { environment: 'ENVIRONMENT_ALL', start: '2020', end: '2021', - percentileThreshold: 95, kuery: '', + percentileThreshold: 95, + analyzeCorrelations: true, }, }; return { batch: [ { - request: partialSearchRequest, - options: { strategy: 'apmCorrelationsSearchStrategy' }, + request, + options: { strategy: APM_SEARCH_STRATEGIES.APM_LATENCY_CORRELATIONS }, }, ], }; @@ -122,7 +128,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(typeof finalRawResponse?.took).to.be('number'); expect(finalRawResponse?.percentileThresholdValue).to.be(undefined); expect(finalRawResponse?.overallHistogram).to.be(undefined); - expect(finalRawResponse?.values.length).to.be(0); + expect(finalRawResponse?.latencyCorrelations.length).to.be(0); expect(finalRawResponse?.log.map((d: string) => d.split(': ')[1])).to.eql([ 'Fetched 95th percentile value of undefined based on 0 documents.', 'Abort service since percentileThresholdValue could not be determined.', @@ -176,7 +182,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const { rawResponse } = result; expect(typeof rawResponse?.took).to.be('number'); - expect(rawResponse?.values).to.eql([]); + expect(rawResponse?.latencyCorrelations).to.eql([]); // follow up request body including search strategy ID const reqBody = getRequestBody(); @@ -230,9 +236,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(finalRawResponse?.percentileThresholdValue).to.be(1309695.875); expect(finalRawResponse?.overallHistogram.length).to.be(101); - expect(finalRawResponse?.values.length).to.eql( + expect(finalRawResponse?.latencyCorrelations.length).to.eql( 13, - `Expected 13 identified correlations, got ${finalRawResponse?.values.length}.` + `Expected 13 identified correlations, got ${finalRawResponse?.latencyCorrelations.length}.` ); expect(finalRawResponse?.log.map((d: string) => d.split(': ')[1])).to.eql([ 'Fetched 95th percentile value of 1309695.875 based on 1244 documents.', @@ -245,10 +251,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { 'Identified 13 significant correlations out of 379 field/value pairs.', ]); - const correlation = finalRawResponse?.values[0]; + const correlation = finalRawResponse?.latencyCorrelations[0]; expect(typeof correlation).to.be('object'); - expect(correlation?.field).to.be('transaction.result'); - expect(correlation?.value).to.be('success'); + expect(correlation?.fieldName).to.be('transaction.result'); + expect(correlation?.fieldValue).to.be('success'); expect(correlation?.correlation).to.be(0.6275246559191225); expect(correlation?.ksTest).to.be(4.806503252860024e-13); expect(correlation?.histogram.length).to.be(101); diff --git a/x-pack/test/fleet_api_integration/apis/epm/__snapshots__/install_by_upload.snap b/x-pack/test/fleet_api_integration/apis/epm/__snapshots__/install_by_upload.snap index 13c2dd24f91037..8f9428d8a12db8 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/__snapshots__/install_by_upload.snap +++ b/x-pack/test/fleet_api_integration/apis/epm/__snapshots__/install_by_upload.snap @@ -341,6 +341,10 @@ Object { "id": "logs-apache.access", "type": "index_template", }, + Object { + "id": "logs-apache.access@settings", + "type": "component_template", + }, Object { "id": "logs-apache.access@custom", "type": "component_template", @@ -349,6 +353,10 @@ Object { "id": "metrics-apache.status", "type": "index_template", }, + Object { + "id": "metrics-apache.status@settings", + "type": "component_template", + }, Object { "id": "metrics-apache.status@custom", "type": "component_template", @@ -357,6 +365,10 @@ Object { "id": "logs-apache.error", "type": "index_template", }, + Object { + "id": "logs-apache.error@settings", + "type": "component_template", + }, Object { "id": "logs-apache.error@custom", "type": "component_template", diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts b/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts index 06130775ec3cbc..be1007e632ef41 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts @@ -70,7 +70,7 @@ export default function (providerContext: FtrProviderContext) { .type('application/gzip') .send(buf) .expect(200); - expect(res.body.response.length).to.be(26); + expect(res.body.response.length).to.be(29); }); it('should install a zip archive correctly and package info should return correctly after validation', async function () { @@ -81,7 +81,7 @@ export default function (providerContext: FtrProviderContext) { .type('application/zip') .send(buf) .expect(200); - expect(res.body.response.length).to.be(26); + expect(res.body.response.length).to.be(29); const packageInfoRes = await supertest .get(`/api/fleet/epm/packages/${testPkgKey}`) diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_overrides.ts b/x-pack/test/fleet_api_integration/apis/epm/install_overrides.ts index 770502db49daeb..d1c3eae785f474 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_overrides.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_overrides.ts @@ -115,10 +115,18 @@ export default function (providerContext: FtrProviderContext) { template: { settings: { index: { + codec: 'best_compression', lifecycle: { name: 'overridden by user', }, + mapping: { + total_fields: { + limit: '10000', + }, + }, + number_of_routing_shards: 30, number_of_shards: '3', + refresh_interval: '5s', }, }, mappings: { diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts index 85573560177eea..02ecc9570afefa 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts @@ -538,6 +538,10 @@ const expectAssetsInstalled = ({ id: 'logs-all_assets.test_logs@custom', type: 'component_template', }, + { + id: 'metrics-all_assets.test_metrics@settings', + type: 'component_template', + }, { id: 'metrics-all_assets.test_metrics@custom', type: 'component_template', diff --git a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts index 6b4d104423144d..8c59533ce98dc4 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts @@ -214,7 +214,21 @@ export default function (providerContext: FtrProviderContext) { }); expect(resSettings.statusCode).equal(200); expect(resSettings.body.component_templates[0].component_template.template.settings).eql({ - index: { lifecycle: { name: 'reference2' } }, + index: { + lifecycle: { name: 'reference2' }, + codec: 'best_compression', + mapping: { + total_fields: { + limit: '10000', + }, + }, + number_of_routing_shards: '30', + number_of_shards: '1', + query: { + default_field: ['logs_test_name', 'new_field_name'], + }, + refresh_interval: '5s', + }, }); const resUserSettings = await es.transport.request({ method: 'GET', @@ -359,6 +373,10 @@ export default function (providerContext: FtrProviderContext) { id: 'logs-all_assets.test_logs2', type: 'index_template', }, + { + id: 'logs-all_assets.test_logs2@settings', + type: 'component_template', + }, { id: 'logs-all_assets.test_logs2@custom', type: 'component_template', @@ -367,6 +385,10 @@ export default function (providerContext: FtrProviderContext) { id: 'metrics-all_assets.test_metrics', type: 'index_template', }, + { + id: 'metrics-all_assets.test_metrics@settings', + type: 'component_template', + }, { id: 'metrics-all_assets.test_metrics@custom', type: 'component_template', diff --git a/x-pack/test/functional/apps/dashboard/dashboard_tagging.ts b/x-pack/test/functional/apps/dashboard/dashboard_tagging.ts new file mode 100644 index 00000000000000..707b3877a70b51 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/dashboard_tagging.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const listingTable = getService('listingTable'); + const testSubjects = getService('testSubjects'); + const esArchiver = getService('esArchiver'); + const find = getService('find'); + const PageObjects = getPageObjects([ + 'common', + 'tagManagement', + 'header', + 'dashboard', + 'visualize', + 'lens', + ]); + + const dashboardTag = 'extremely-cool-dashboard'; + const dashboardTitle = 'Coolest Blank Dashboard'; + + describe('dashboard tagging', () => { + const verifyTagFromListingPage = async () => { + await PageObjects.dashboard.gotoDashboardLandingPage(); + await listingTable.waitUntilTableIsLoaded(); + + // open the filter dropdown + const filterButton = await find.byCssSelector('.euiFilterGroup .euiFilterButton'); + await filterButton.click(); + await testSubjects.click( + `tag-searchbar-option-${PageObjects.tagManagement.testSubjFriendly(dashboardTag)}` + ); + // click elsewhere to close the filter dropdown + const searchFilter = await find.byCssSelector('.euiPageBody .euiFieldSearch'); + await searchFilter.click(); + // wait until the table refreshes + await listingTable.waitUntilTableIsLoaded(); + const itemNames = await listingTable.getAllItemsNames(); + expect(itemNames).to.contain(dashboardTitle); + }; + + const createTagFromDashboard = async () => { + await testSubjects.click('dashboardSaveMenuItem'); + await testSubjects.click('savedObjectTagSelector'); + await testSubjects.click(`tagSelectorOption-action__create`); + + expect(await PageObjects.tagManagement.tagModal.isOpened()).to.be(true); + + await PageObjects.tagManagement.tagModal.fillForm( + { + name: dashboardTag, + color: '#fc03db', + description: '', + }, + { + submit: true, + } + ); + expect(await PageObjects.tagManagement.tagModal.isOpened()).to.be(false); + }; + + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/lens/basic'); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.clickNewDashboard(); + }); + + it('adds a new tag to a new Dashboard', async () => { + await createTagFromDashboard(); + PageObjects.dashboard.saveDashboard(dashboardTitle, {}, false); + await verifyTagFromListingPage(); + }); + + it('retains its saved object tags after quicksave', async () => { + await PageObjects.dashboard.gotoDashboardEditMode(dashboardTitle); + await PageObjects.dashboard.useMargins(false); // turn margins off to cause quicksave to be enabled + await PageObjects.dashboard.clickQuickSave(); + await verifyTagFromListingPage(); + }); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/index.ts b/x-pack/test/functional/apps/dashboard/index.ts index 5979ae378c22bb..73c9b83de917ff 100644 --- a/x-pack/test/functional/apps/dashboard/index.ts +++ b/x-pack/test/functional/apps/dashboard/index.ts @@ -17,6 +17,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./drilldowns')); loadTestFile(require.resolve('./sync_colors')); loadTestFile(require.resolve('./_async_dashboard')); + loadTestFile(require.resolve('./dashboard_tagging')); loadTestFile(require.resolve('./dashboard_lens_by_value')); loadTestFile(require.resolve('./dashboard_maps_by_value')); diff --git a/x-pack/test/functional/apps/maps/sample_data.js b/x-pack/test/functional/apps/maps/sample_data.js index fa12e5158ac772..a6e4500d30593b 100644 --- a/x-pack/test/functional/apps/maps/sample_data.js +++ b/x-pack/test/functional/apps/maps/sample_data.js @@ -161,7 +161,7 @@ export default function ({ getPageObjects, getService, updateBaselines }) { before(async () => { await PageObjects.maps.loadSavedMap('[Logs] Total Requests and Bytes'); await PageObjects.maps.toggleLayerVisibility('Road map'); - await PageObjects.maps.toggleLayerVisibility('Total Requests by Country'); + await PageObjects.maps.toggleLayerVisibility('Total Requests by Destination'); await PageObjects.timePicker.setCommonlyUsedTime('sample_data range'); await PageObjects.maps.enterFullScreen(); await PageObjects.maps.closeLegend(); diff --git a/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts b/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts index 5a98011386567d..da296e5a4f60aa 100644 --- a/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts +++ b/x-pack/test/upgrade/apps/maps/maps_smoke_tests.ts @@ -168,7 +168,7 @@ export default function ({ await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.maps.waitForLayersToLoad(); await PageObjects.maps.toggleLayerVisibility('Road map'); - await PageObjects.maps.toggleLayerVisibility('Total Requests by Country'); + await PageObjects.maps.toggleLayerVisibility('Total Requests by Destination'); await PageObjects.timePicker.setCommonlyUsedTime('sample_data range'); await PageObjects.maps.enterFullScreen(); await PageObjects.maps.closeLegend();