diff --git a/dev_docs/key_concepts/performance.mdx b/dev_docs/key_concepts/performance.mdx new file mode 100644 index 0000000000000..f7b71b0258914 --- /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 0000000000000..6ce0671eb5230 --- /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 7285b4a00a0ec..ffc77dd653c0f 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.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index 253b0671cdd52..c556d58421737 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -12,6 +12,9 @@ readonly links: { readonly apm: { readonly kibanaSettings: string; readonly supportedServiceMaps: string; + readonly customLinks: string; + readonly droppedTransactionSpans: string; + readonly upgrading: string; readonly metaData: string; }; readonly canvas: { 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 7e409f23790f0..7d862f50bba18 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly 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 548a574293403..64ab6fca0714e 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/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 77bbeabb7f73b..282abe1e1741a 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -34,7 +34,7 @@ pageLoadAssetSize: indexPatternManagement: 28222 indexPatternEditor: 25000 infra: 184320 - fleet: 465774 + fleet: 250000 ingestPipelines: 58003 inputControlVis: 172675 inspector: 148711 @@ -112,7 +112,7 @@ pageLoadAssetSize: expressionImage: 19288 expressionMetric: 22238 expressionShape: 34008 - interactiveSetup: 70000 + interactiveSetup: 80000 expressionTagcloud: 27505 expressions: 239290 securitySolution: 231753 diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index 347b81abf6d51..a92179ca9283c 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 8df8d76a13c46..b3815c02674e1 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 5740e1739280a..8c8b264b094cc 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 813f385fc94d2..98987678d64cd 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/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 73ba816ff9b4b..dcae9052f930e 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -34,6 +34,9 @@ export class DocLinksService { apm: { kibanaSettings: `${KIBANA_DOCS}apm-settings-in-kibana.html`, supportedServiceMaps: `${KIBANA_DOCS}service-maps.html#service-maps-supported`, + customLinks: `${KIBANA_DOCS}custom-links.html`, + droppedTransactionSpans: `${APM_DOCS}get-started/${DOC_LINK_VERSION}/transaction-spans.html#dropped-spans`, + upgrading: `${APM_DOCS}server/${DOC_LINK_VERSION}/upgrading.html`, metaData: `${APM_DOCS}get-started/${DOC_LINK_VERSION}/metadata.html`, }, canvas: { @@ -460,6 +463,9 @@ export interface DocLinksStart { readonly apm: { readonly kibanaSettings: string; readonly supportedServiceMaps: string; + readonly customLinks: string; + readonly droppedTransactionSpans: string; + readonly upgrading: string; readonly metaData: string; }; readonly canvas: { diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index d9c64f29eb684..554e6704657f0 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; @@ -476,6 +477,9 @@ export interface DocLinksStart { readonly apm: { readonly kibanaSettings: string; readonly supportedServiceMaps: string; + readonly customLinks: string; + readonly droppedTransactionSpans: string; + readonly upgrading: string; readonly metaData: string; }; readonly canvas: { 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 e96aeb6a93b65..8e79e6342c0d5 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 ff5bf3d01c641..8294a61caae6e 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 889edfb66a20f..3ab870c276d29 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 38e94112f63e7..0000000000000 --- 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 0c0b04b49b7b8..0000000000000 --- 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 06b402c580151..26ed25e801475 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/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index adf0be3b5aa54..c9b6fa3d9dda5 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -73,6 +73,8 @@ kibana_vars=( enterpriseSearch.host externalUrl.policy i18n.locale + interactiveSetup.enabled + interactiveSetup.connectionCheck.interval interpreter.enableInVisualize kibana.autocompleteTerminateAfter kibana.autocompleteTimeout diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile b/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile index eb4708b6ac555..60dabbffc6312 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile @@ -89,6 +89,7 @@ COPY --from=builder --chown=1000:0 /usr/share/kibana /usr/share/kibana WORKDIR /usr/share/kibana RUN ln -s /usr/share/kibana /opt/kibana +{{! Please notify @elastic/kibana-security if you want to remove or change this environment variable. }} ENV ELASTIC_CONTAINER true ENV PATH=/usr/share/kibana/bin:$PATH diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/Dockerfile b/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/Dockerfile index c1335f6c7a396..79b089c097344 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/Dockerfile +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/ironbank/Dockerfile @@ -51,6 +51,7 @@ COPY --from=prep_files --chown=1000:0 /usr/share/kibana /usr/share/kibana WORKDIR /usr/share/kibana RUN ln -s /usr/share/kibana /opt/kibana +{{! Please notify @elastic/kibana-security if you want to remove or change this environment variable. }} ENV ELASTIC_CONTAINER true ENV PATH=/usr/share/kibana/bin:$PATH diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts index e22d8ecdd4fd8..340a035adea4c 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts @@ -10,6 +10,8 @@ import dedent from 'dedent'; import { TemplateContext } from '../template_context'; +// IMPORTANT: Please notify @elastic/kibana-security if you're changing any of the Docker specific +// configuration defaults. We rely on these defaults in the interactive setup mode. function generator({ imageFlavor }: TemplateContext) { return dedent(` # diff --git a/src/plugins/apm_oss/server/index.ts b/src/plugins/apm_oss/server/index.ts index 7b16c42f4c9b9..bf6baf1876074 100644 --- a/src/plugins/apm_oss/server/index.ts +++ b/src/plugins/apm_oss/server/index.ts @@ -10,7 +10,10 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { ConfigDeprecationProvider, PluginInitializerContext } from '../../../core/server'; import { APMOSSPlugin } from './plugin'; -const deprecations: ConfigDeprecationProvider = ({ unused }) => [unused('fleetMode')]; +const deprecations: ConfigDeprecationProvider = ({ unused }) => [ + unused('fleetMode'), + unused('indexPattern'), +]; export const config = { schema: schema.object({ 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 7bf390b0bee5a..1b24062ccd9b5 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/components/doc_table/doc_table_infinite.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_infinite.tsx index 778c6c1abe274..5f0825d9cbd15 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_infinite.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_infinite.tsx @@ -13,6 +13,7 @@ import { debounce } from 'lodash'; import { EuiButtonEmpty } from '@elastic/eui'; import { DocTableProps, DocTableRenderProps, DocTableWrapper } from './doc_table_wrapper'; import { SkipBottomButton } from '../skip_bottom_button'; +import { shouldLoadNextDocPatch } from './lib/should_load_next_doc_patch'; const FOOTER_PADDING = { padding: 0 }; @@ -35,12 +36,7 @@ const DocTableInfiniteContent = (props: DocTableRenderProps) => { const scheduleCheck = debounce(() => { const isMobileView = document.getElementsByClassName('dscSidebar__mobile').length > 0; const usedScrollDiv = isMobileView ? scrollMobileElem : scrollDiv; - - const scrollusedHeight = usedScrollDiv.scrollHeight; - const scrollTop = Math.abs(usedScrollDiv.scrollTop); - const clientHeight = usedScrollDiv.clientHeight; - - if (scrollTop + clientHeight === scrollusedHeight) { + if (shouldLoadNextDocPatch(usedScrollDiv)) { setLimit((prevLimit) => prevLimit + 50); } }, 50); diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/should_load_next_doc_patch.test.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/should_load_next_doc_patch.test.ts new file mode 100644 index 0000000000000..ea6dd4f9b3e31 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/should_load_next_doc_patch.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { shouldLoadNextDocPatch } from './should_load_next_doc_patch'; + +describe('shouldLoadNextDocPatch', () => { + test('next patch should not be loaded', () => { + const scrollingDomEl = { + scrollHeight: 500, + scrollTop: 100, + clientHeight: 100, + } as HTMLElement; + + expect(shouldLoadNextDocPatch(scrollingDomEl)).toBeFalsy(); + }); + + test('next patch should be loaded', () => { + const scrollingDomEl = { + scrollHeight: 500, + scrollTop: 350, + clientHeight: 100, + } as HTMLElement; + + expect(shouldLoadNextDocPatch(scrollingDomEl)).toBeTruthy(); + }); + + test("next patch should be loaded even there's a decimal scroll height", () => { + const scrollingDomEl = { + scrollHeight: 500, + scrollTop: 350.34234234, + clientHeight: 100, + } as HTMLElement; + + expect(shouldLoadNextDocPatch(scrollingDomEl)).toBeTruthy(); + }); +}); diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/should_load_next_doc_patch.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/should_load_next_doc_patch.ts new file mode 100644 index 0000000000000..5834b6808fac1 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/should_load_next_doc_patch.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// use a buffer to start rendering more documents before the user completely scrolles down +const verticalScrollBuffer = 100; + +/** + * Helper function to determine if the next patch of 50 documents should be loaded + */ +export function shouldLoadNextDocPatch(domEl: HTMLElement) { + // the height of the scrolling div, including content not visible on the screen due to overflow. + const scrollHeight = domEl.scrollHeight; + // the number of pixels that the div is is scrolled vertically + const scrollTop = domEl.scrollTop; + // the inner height of the scrolling div, excluding content that's visible on the screen + const clientHeight = domEl.clientHeight; + + const consumedHeight = scrollTop + clientHeight; + const remainingHeight = scrollHeight - consumedHeight; + return remainingHeight < verticalScrollBuffer; +} 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 17f1802a47327..554aca6ddb8f1 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 fc835d4d3dd16..4061d9a61f0a3 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 25e95061ed2ac..1f4b6ff7b7f37 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 59888b00fc576..919058bcbbc32 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 46e6ef8b4ea75..82b16d4f00369 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 0b0ecf3a2a409..241b5cecb71c9 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 b8b8adba307db..f7e8c824030bd 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/log_legacy_import.ts b/src/plugins/interactive_setup/common/constants.ts similarity index 68% rename from src/plugins/saved_objects_management/public/lib/log_legacy_import.ts rename to src/plugins/interactive_setup/common/constants.ts index 70120f887afef..00a3efc316cd9 100644 --- a/src/plugins/saved_objects_management/public/lib/log_legacy_import.ts +++ b/src/plugins/interactive_setup/common/constants.ts @@ -6,8 +6,4 @@ * 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'); -} +export const VERIFICATION_CODE_LENGTH = 6; diff --git a/src/plugins/interactive_setup/common/index.ts b/src/plugins/interactive_setup/common/index.ts index ab8c00cfa5a8e..3833873eb2a18 100644 --- a/src/plugins/interactive_setup/common/index.ts +++ b/src/plugins/interactive_setup/common/index.ts @@ -8,3 +8,4 @@ export type { InteractiveSetupViewState, EnrollmentToken, Certificate, PingResult } from './types'; export { ElasticsearchConnectionStatus } from './elasticsearch_connection_status'; +export { VERIFICATION_CODE_LENGTH } from './constants'; diff --git a/src/plugins/interactive_setup/public/app.tsx b/src/plugins/interactive_setup/public/app.tsx index 0c206cb4fa215..da1318d84cf03 100644 --- a/src/plugins/interactive_setup/public/app.tsx +++ b/src/plugins/interactive_setup/public/app.tsx @@ -20,7 +20,11 @@ import { ClusterConfigurationForm } from './cluster_configuration_form'; import { EnrollmentTokenForm } from './enrollment_token_form'; import { ProgressIndicator } from './progress_indicator'; -export const App: FunctionComponent = () => { +export interface AppProps { + onSuccess?(): void; +} + +export const App: FunctionComponent = ({ onSuccess }) => { const [page, setPage] = useState<'token' | 'manual' | 'success'>('token'); const [cluster, setCluster] = useState< Omit @@ -71,9 +75,7 @@ export const App: FunctionComponent = () => { /> )} - {page === 'success' && ( - window.location.replace(window.location.href)} /> - )} + {page === 'success' && } diff --git a/src/plugins/interactive_setup/public/cluster_address_form.tsx b/src/plugins/interactive_setup/public/cluster_address_form.tsx index ba7b1d46182a1..6f97680066373 100644 --- a/src/plugins/interactive_setup/public/cluster_address_form.tsx +++ b/src/plugins/interactive_setup/public/cluster_address_form.tsx @@ -51,7 +51,7 @@ export const ClusterAddressForm: FunctionComponent = ({ const [form, eventHandlers] = useForm({ defaultValues, validate: async (values) => { - const errors: ValidationErrors = {}; + const errors: ValidationErrors = {}; if (!values.host) { errors.host = i18n.translate('interactiveSetup.clusterAddressForm.hostRequiredError', { diff --git a/src/plugins/interactive_setup/public/cluster_configuration_form.tsx b/src/plugins/interactive_setup/public/cluster_configuration_form.tsx index cd3541fe0318f..dfb5148ddb288 100644 --- a/src/plugins/interactive_setup/public/cluster_configuration_form.tsx +++ b/src/plugins/interactive_setup/public/cluster_configuration_form.tsx @@ -26,6 +26,7 @@ import { } from '@elastic/eui'; import type { FunctionComponent } from 'react'; import React from 'react'; +import useUpdateEffect from 'react-use/lib/useUpdateEffect'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -37,6 +38,8 @@ import type { ValidationErrors } from './use_form'; import { useForm } from './use_form'; import { useHtmlId } from './use_html_id'; import { useHttp } from './use_http'; +import { useVerification } from './use_verification'; +import { useVisibility } from './use_visibility'; export interface ClusterConfigurationFormValues { username: string; @@ -66,10 +69,10 @@ export const ClusterConfigurationForm: FunctionComponent { const http = useHttp(); - + const { status, getCode } = useVerification(); const [form, eventHandlers] = useForm({ defaultValues, - validate: async (values) => { + validate: (values) => { const errors: ValidationErrors = {}; if (authRequired) { @@ -93,7 +96,7 @@ export const ClusterConfigurationForm: FunctionComponent(); const trustCaCertId = useHtmlId('clusterConfigurationForm', 'trustCaCert'); + useUpdateEffect(() => { + if (status === 'verified' && isVisible) { + form.submit(); + } + }, [status]); + return ( - {form.submitError && ( + {status !== 'unverified' && !form.isSubmitting && !form.isValidating && form.submitError && ( <> )} - - {authRequired ? ( <> )} - {certificateChain && certificateChain.length > 0 && ( <> { const intermediateCa = certificateChain[Math.min(1, certificateChain.length - 1)]; - form.setValue('caCert', form.values.caCert ? '' : intermediateCa.raw); form.setTouched('caCert'); + form.setValue('caCert', form.values.caCert ? '' : intermediateCa.raw); }} > @@ -252,7 +259,6 @@ export const ClusterConfigurationForm: FunctionComponent )} - @@ -264,6 +270,7 @@ export const ClusterConfigurationForm: FunctionComponent = onSuccess, }) => { const http = useHttp(); + const { status, getCode } = useVerification(); const [form, eventHandlers] = useForm({ defaultValues, validate: (values) => { @@ -77,17 +80,25 @@ export const EnrollmentTokenForm: FunctionComponent = hosts: decoded.adr, apiKey: decoded.key, caFingerprint: decoded.fgr, + code: getCode(), }), }); onSuccess?.(); }, }); + const [isVisible, buttonRef] = useVisibility(); + + useUpdateEffect(() => { + if (status === 'verified' && isVisible) { + form.submit(); + } + }, [status]); const enrollmentToken = decodeEnrollmentToken(form.values.token); return ( - {form.submitError && ( + {status !== 'unverified' && !form.isSubmitting && !form.isValidating && form.submitError && ( <> = = ( defaultMessage="Connect to" /> - - - - {token.adr[0]} - - - - - - + + {token.adr[0]} + diff --git a/src/plugins/interactive_setup/public/plugin.tsx b/src/plugins/interactive_setup/public/plugin.tsx index 00fd38d3e78a4..9d58479081234 100644 --- a/src/plugins/interactive_setup/public/plugin.tsx +++ b/src/plugins/interactive_setup/public/plugin.tsx @@ -15,6 +15,7 @@ import type { CoreSetup, CoreStart, HttpSetup, Plugin } from 'src/core/public'; import { App } from './app'; import { HttpProvider } from './use_http'; +import { VerificationProvider } from './use_verification'; export class InteractiveSetupPlugin implements Plugin { public setup(core: CoreSetup) { @@ -24,9 +25,16 @@ export class InteractiveSetupPlugin implements Plugin { appRoute: '/', chromeless: true, mount: (params) => { + const url = new URL(window.location.href); + const defaultCode = url.searchParams.get('code') || undefined; + const onSuccess = () => { + url.searchParams.delete('code'); + window.location.replace(url.href); + }; + ReactDOM.render( - - + + , params.element ); @@ -40,10 +48,13 @@ export class InteractiveSetupPlugin implements Plugin { export interface ProvidersProps { http: HttpSetup; + defaultCode?: string; } -export const Providers: FunctionComponent = ({ http, children }) => ( +export const Providers: FunctionComponent = ({ defaultCode, http, children }) => ( - {children} + + {children} + ); diff --git a/src/plugins/interactive_setup/public/single_chars_field.tsx b/src/plugins/interactive_setup/public/single_chars_field.tsx new file mode 100644 index 0000000000000..8d5cd2854c0aa --- /dev/null +++ b/src/plugins/interactive_setup/public/single_chars_field.tsx @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { EuiFieldText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import type { FunctionComponent, ReactNode } from 'react'; +import React, { useEffect, useRef } from 'react'; +import useList from 'react-use/lib/useList'; +import useUpdateEffect from 'react-use/lib/useUpdateEffect'; + +import { euiThemeVars } from '@kbn/ui-shared-deps/theme'; + +export interface SingleCharsFieldProps { + defaultValue: string; + length: number; + separator?: number; + pattern?: RegExp; + onChange(value: string): void; + isInvalid?: boolean; + autoFocus?: boolean; +} + +export const SingleCharsField: FunctionComponent = ({ + defaultValue, + length, + separator, + pattern = /^[0-9]$/, + onChange, + isInvalid, + autoFocus = false, +}) => { + // Strip any invalid characters from input or clipboard and restrict length. + const sanitize = (str: string) => { + return str + .split('') + .filter((char) => char.match(pattern)) + .join('') + .substr(0, length); + }; + + const inputRefs = useRef>([]); + const [chars, { set, updateAt }] = useList(sanitize(defaultValue).split('')); + + const focusField = (i: number) => { + const input = inputRefs.current[i]; + if (input) { + input.focus(); + } + }; + + // Trigger `onChange` callback when characters change + useUpdateEffect(() => { + onChange(chars.join('')); + }, [chars]); + + // Focus first field on initial render + useEffect(() => { + if (autoFocus) { + focusField(0); + } + }, [autoFocus]); + + const children: ReactNode[] = []; + for (let i = 0; i < length; i++) { + if (separator && i !== 0 && i % separator === 0) { + children.push( + + ); + } + + children.push( + + { + inputRefs.current[i] = el; + }} + value={chars[i] ?? ''} + onChange={(event) => { + // Ensure event doesn't bubble up since we manage our own `onChange` event + event.stopPropagation(); + }} + onInput={(event) => { + // Ignore input if invalid character was entered (unless empty) + if (event.currentTarget.value !== '' && sanitize(event.currentTarget.value) === '') { + return event.preventDefault(); + } + updateAt(i, event.currentTarget.value); + // Do not focus the next field if value is empty (e.g. when hitting backspace) + if (event.currentTarget.value) { + focusField(i + 1); + } + }} + onKeyDown={(event) => { + if (event.key === 'Backspace') { + // Clear previous field if current field is already empty + if (event.currentTarget.value === '') { + updateAt(i - 1, event.currentTarget.value); + focusField(i - 1); + } + } else if (event.key === 'ArrowLeft') { + focusField(i - 1); + } else if (event.key === 'ArrowRight') { + focusField(i + 1); + } + }} + onPaste={(event) => { + const text = sanitize(event.clipboardData.getData('text')); + set(text.split('')); + focusField(Math.min(text.length, length - 1)); + event.preventDefault(); + }} + onFocus={(event) => { + const input = event.currentTarget; + setTimeout(() => input.select(), 0); + }} + maxLength={1} + isInvalid={isInvalid} + style={{ textAlign: 'center' }} + /> + + ); + } + + return ( + + {children} + + ); +}; diff --git a/src/plugins/interactive_setup/public/use_form.ts b/src/plugins/interactive_setup/public/use_form.ts index 8ed1d89ea087e..abd00edee6750 100644 --- a/src/plugins/interactive_setup/public/use_form.ts +++ b/src/plugins/interactive_setup/public/use_form.ts @@ -9,7 +9,7 @@ import { set } from '@elastic/safer-lodash-set'; import { cloneDeep, cloneDeepWith, get } from 'lodash'; import type { ChangeEventHandler, FocusEventHandler, ReactEventHandler } from 'react'; -import { useRef } from 'react'; +import { useState } from 'react'; import useAsyncFn from 'react-use/lib/useAsyncFn'; export type FormReturnTuple = [FormState, FormProps]; @@ -81,12 +81,11 @@ export type ValidationErrors = DeepMap; export type TouchedFields = DeepMap; export interface FormState { - setValue(name: string, value: any, revalidate?: boolean): Promise; + setValue(name: string, value: any): Promise; setError(name: string, message: string): void; - setTouched(name: string, touched?: boolean, revalidate?: boolean): Promise; - reset(values: Values): void; + setTouched(name: string): Promise; + reset(values?: Values): void; submit(): Promise; - validate(): Promise>; values: Values; errors: ValidationErrors; touched: TouchedFields; @@ -123,63 +122,75 @@ export function useFormState({ validate, defaultValues, }: FormOptions): FormState { - const valuesRef = useRef(defaultValues); - const errorsRef = useRef>({}); - const touchedRef = useRef>({}); - const submitCountRef = useRef(0); - - const [validationState, validateForm] = useAsyncFn(async (formValues: Values) => { + const [values, setValues] = useState(defaultValues); + const [errors, setErrors] = useState>({}); + const [touched, setTouched] = useState>({}); + const [submitCount, setSubmitCount] = useState(0); + + async function validateFormFn(formValues: Values): Promise; + async function validateFormFn(formValues: undefined): Promise; + async function validateFormFn(formValues: Values | undefined) { + // Allows resetting `useAsyncFn` state + if (!formValues) { + return Promise.resolve(undefined); + } const nextErrors = await validate(formValues); - errorsRef.current = nextErrors; + setErrors(nextErrors); if (Object.keys(nextErrors).length === 0) { - submitCountRef.current = 0; + setSubmitCount(0); } return nextErrors; - }, []); + } - const [submitState, submitForm] = useAsyncFn(async (formValues: Values) => { + async function submitFormFn(formValues: Values): Promise; + async function submitFormFn(formValues: undefined): Promise; + async function submitFormFn(formValues: Values | undefined) { + // Allows resetting `useAsyncFn` state + if (!formValues) { + return Promise.resolve(undefined); + } const nextErrors = await validateForm(formValues); - touchedRef.current = mapDeep(formValues, true); - submitCountRef.current += 1; + setTouched(mapDeep(formValues, true)); + setSubmitCount(submitCount + 1); if (Object.keys(nextErrors).length === 0) { return onSubmit(formValues); } - }, []); + } + + const [validationState, validateForm] = useAsyncFn(validateFormFn, [validate]); + const [submitState, submitForm] = useAsyncFn(submitFormFn, [validateForm, onSubmit]); return { - setValue: async (name, value, revalidate = true) => { - const nextValues = setDeep(valuesRef.current, name, value); - valuesRef.current = nextValues; - if (revalidate) { - await validateForm(nextValues); - } + setValue: async (name, value) => { + const nextValues = setDeep(values, name, value); + setValues(nextValues); + await validateForm(nextValues); }, - setTouched: async (name, touched = true, revalidate = true) => { - touchedRef.current = setDeep(touchedRef.current, name, touched); - if (revalidate) { - await validateForm(valuesRef.current); - } + setTouched: async (name, value = true) => { + setTouched(setDeep(touched, name, value)); + await validateForm(values); }, setError: (name, message) => { - errorsRef.current = setDeep(errorsRef.current, name, message); - touchedRef.current = setDeep(touchedRef.current, name, true); + setErrors(setDeep(errors, name, message)); + setTouched(setDeep(touched, name, true)); }, - reset: (nextValues) => { - valuesRef.current = nextValues; - errorsRef.current = {}; - touchedRef.current = {}; - submitCountRef.current = 0; + reset: (nextValues = defaultValues) => { + setValues(nextValues); + setErrors({}); + setTouched({}); + setSubmitCount(0); + validateForm(undefined); // Resets `validationState` + submitForm(undefined); // Resets `submitState` }, - submit: () => submitForm(valuesRef.current), - validate: () => validateForm(valuesRef.current), - values: valuesRef.current, - errors: errorsRef.current, - touched: touchedRef.current, + submit: () => submitForm(values), + values, + errors, + touched, isValidating: validationState.loading, isSubmitting: submitState.loading, submitError: submitState.error, - isInvalid: Object.keys(errorsRef.current).length > 0, - isSubmitted: submitCountRef.current > 0, + isInvalid: Object.keys(errors).length > 0, + isSubmitted: submitCount > 0, }; } diff --git a/src/plugins/interactive_setup/public/use_verification.tsx b/src/plugins/interactive_setup/public/use_verification.tsx new file mode 100644 index 0000000000000..62483ba9cb62e --- /dev/null +++ b/src/plugins/interactive_setup/public/use_verification.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EuiModal, EuiModalHeader } from '@elastic/eui'; +import constate from 'constate'; +import type { FunctionComponent } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; + +import { useHttp } from './use_http'; +import { VerificationCodeForm } from './verification_code_form'; + +export interface VerificationProps { + defaultCode?: string; +} + +const [OuterVerificationProvider, useVerification] = constate( + ({ defaultCode }: VerificationProps) => { + const codeRef = useRef(defaultCode); + const [status, setStatus] = useState<'unknown' | 'unverified' | 'verified'>('unknown'); + + return { + status, + setStatus, + getCode() { + return codeRef.current; + }, + setCode(code: string | undefined) { + codeRef.current = code; + }, + }; + } +); + +const InnerVerificationProvider: FunctionComponent = ({ children }) => { + const http = useHttp(); + const { status, setStatus, setCode } = useVerification(); + + useEffect(() => { + return http.intercept({ + responseError: (error) => { + if (error.response?.status === 403) { + setStatus('unverified'); + setCode(undefined); + } + }, + }); + }, [http]); // eslint-disable-line react-hooks/exhaustive-deps + + return ( + <> + {status === 'unverified' && ( + setStatus('unknown')}> + + { + setStatus('verified'); + setCode(values.code); + }} + /> + + + )} + {children} + + ); +}; + +export const VerificationProvider: FunctionComponent = ({ + defaultCode, + children, +}) => { + return ( + + {children} + + ); +}; + +export { useVerification }; diff --git a/src/plugins/interactive_setup/public/use_visibility.ts b/src/plugins/interactive_setup/public/use_visibility.ts new file mode 100644 index 0000000000000..f21b5669a36aa --- /dev/null +++ b/src/plugins/interactive_setup/public/use_visibility.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { RefObject } from 'react'; +import { useRef } from 'react'; + +export type VisibilityReturnTuple = [boolean, RefObject]; + +export function useVisibility(): VisibilityReturnTuple { + const elementRef = useRef(null); + + // When an element is hidden using `display: none` or `hidden` attribute it has no offset parent. + return [!!elementRef.current?.offsetParent, elementRef]; +} diff --git a/src/plugins/interactive_setup/public/verification_code_form.tsx b/src/plugins/interactive_setup/public/verification_code_form.tsx new file mode 100644 index 0000000000000..8f4a9ea8c5d01 --- /dev/null +++ b/src/plugins/interactive_setup/public/verification_code_form.tsx @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { + EuiButton, + EuiCallOut, + EuiEmptyPrompt, + EuiForm, + EuiFormRow, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import type { FunctionComponent } from 'react'; +import React from 'react'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import type { IHttpFetchError } from 'kibana/public'; + +import { VERIFICATION_CODE_LENGTH } from '../common'; +import { SingleCharsField } from './single_chars_field'; +import type { ValidationErrors } from './use_form'; +import { useForm } from './use_form'; +import { useHttp } from './use_http'; + +export interface VerificationCodeFormValues { + code: string; +} + +export interface VerificationCodeFormProps { + defaultValues?: VerificationCodeFormValues; + onSuccess?(values: VerificationCodeFormValues): void; +} + +export const VerificationCodeForm: FunctionComponent = ({ + defaultValues = { + code: '', + }, + onSuccess, +}) => { + const http = useHttp(); + const [form, eventHandlers] = useForm({ + defaultValues, + validate: async (values) => { + const errors: ValidationErrors = {}; + + if (!values.code) { + errors.code = i18n.translate('interactiveSetup.verificationCodeForm.codeRequiredError', { + defaultMessage: 'Enter a verification code.', + }); + } else if (values.code.length !== VERIFICATION_CODE_LENGTH) { + errors.code = i18n.translate('interactiveSetup.verificationCodeForm.codeMinLengthError', { + defaultMessage: 'Enter all six digits.', + }); + } + + return errors; + }, + onSubmit: async (values) => { + try { + await http.post('/internal/interactive_setup/verify', { + body: JSON.stringify({ + code: values.code, + }), + }); + } catch (error) { + if (error.response?.status === 403) { + form.setError('code', error.body?.message); + return; + } else { + throw error; + } + } + onSuccess?.(values); + }, + }); + + return ( + + + + + } + body={ + <> + {form.submitError && ( + <> + + {(form.submitError as IHttpFetchError).body?.message} + + + + )} + +

+ +

+
+ + + + form.setValue('code', value)} + isInvalid={form.touched.code && !!form.errors.code} + autoFocus + /> + + + } + actions={ + + + + } + /> +
+ ); +}; diff --git a/src/plugins/interactive_setup/server/kibana_config_writer.test.ts b/src/plugins/interactive_setup/server/kibana_config_writer.test.ts index 7dc119b87f20a..4b68451930a3d 100644 --- a/src/plugins/interactive_setup/server/kibana_config_writer.test.ts +++ b/src/plugins/interactive_setup/server/kibana_config_writer.test.ts @@ -16,7 +16,7 @@ import { KibanaConfigWriter } from './kibana_config_writer'; describe('KibanaConfigWriter', () => { let mockFsAccess: jest.Mock; let mockWriteFile: jest.Mock; - let mockAppendFile: jest.Mock; + let mockReadFile: jest.Mock; let kibanaConfigWriter: KibanaConfigWriter; beforeEach(() => { jest.spyOn(Date, 'now').mockReturnValue(1234); @@ -24,7 +24,9 @@ describe('KibanaConfigWriter', () => { const fsMocks = jest.requireMock('fs/promises'); mockFsAccess = fsMocks.access; mockWriteFile = fsMocks.writeFile; - mockAppendFile = fsMocks.appendFile; + mockReadFile = fsMocks.readFile; + + mockReadFile.mockResolvedValue(''); kibanaConfigWriter = new KibanaConfigWriter( '/some/path/kibana.yml', @@ -69,39 +71,42 @@ describe('KibanaConfigWriter', () => { }); describe('#writeConfig()', () => { - it('throws if cannot write CA file', async () => { - mockWriteFile.mockRejectedValue(new Error('Oh no!')); - - await expect( - kibanaConfigWriter.writeConfig({ - caCert: 'ca-content', - host: '', - serviceAccountToken: { name: '', value: '' }, - }) - ).rejects.toMatchInlineSnapshot(`[Error: Oh no!]`); - - expect(mockWriteFile).toHaveBeenCalledTimes(1); - expect(mockWriteFile).toHaveBeenCalledWith('/some/path/ca_1234.crt', 'ca-content'); - expect(mockAppendFile).not.toHaveBeenCalled(); - }); - - it('throws if cannot append config to yaml file', async () => { - mockAppendFile.mockRejectedValue(new Error('Oh no!')); - - await expect( - kibanaConfigWriter.writeConfig({ - caCert: 'ca-content', - host: 'some-host', - serviceAccountToken: { name: 'some-token', value: 'some-value' }, - }) - ).rejects.toMatchInlineSnapshot(`[Error: Oh no!]`); - - expect(mockWriteFile).toHaveBeenCalledTimes(1); - expect(mockWriteFile).toHaveBeenCalledWith('/some/path/ca_1234.crt', 'ca-content'); - expect(mockAppendFile).toHaveBeenCalledTimes(1); - expect(mockAppendFile).toHaveBeenCalledWith( - '/some/path/kibana.yml', - ` + describe('without existing config', () => { + beforeEach(() => { + mockReadFile.mockResolvedValue(''); + }); + + it('throws if cannot write CA file', async () => { + mockWriteFile.mockRejectedValue(new Error('Oh no!')); + + await expect( + kibanaConfigWriter.writeConfig({ + caCert: 'ca-content', + host: '', + serviceAccountToken: { name: '', value: '' }, + }) + ).rejects.toMatchInlineSnapshot(`[Error: Oh no!]`); + + expect(mockWriteFile).toHaveBeenCalledTimes(1); + expect(mockWriteFile).toHaveBeenCalledWith('/some/path/ca_1234.crt', 'ca-content'); + }); + + it('throws if cannot write config to yaml file', async () => { + mockWriteFile.mockResolvedValueOnce(undefined).mockRejectedValueOnce(new Error('Oh no!')); + + await expect( + kibanaConfigWriter.writeConfig({ + caCert: 'ca-content', + host: 'some-host', + serviceAccountToken: { name: 'some-token', value: 'some-value' }, + }) + ).rejects.toMatchInlineSnapshot(`[Error: Oh no!]`); + + expect(mockWriteFile).toHaveBeenCalledTimes(2); + expect(mockWriteFile).toHaveBeenCalledWith('/some/path/ca_1234.crt', 'ca-content'); + expect(mockWriteFile).toHaveBeenCalledWith( + '/some/path/kibana.yml', + ` # This section was automatically generated during setup. elasticsearch.hosts: [some-host] @@ -109,24 +114,55 @@ elasticsearch.serviceAccountToken: some-value elasticsearch.ssl.certificateAuthorities: [/some/path/ca_1234.crt] ` - ); - }); - - it('can successfully write CA certificate and elasticsearch config with service token', async () => { - await expect( - kibanaConfigWriter.writeConfig({ - caCert: 'ca-content', - host: 'some-host', - serviceAccountToken: { name: 'some-token', value: 'some-value' }, - }) - ).resolves.toBeUndefined(); - - expect(mockWriteFile).toHaveBeenCalledTimes(1); - expect(mockWriteFile).toHaveBeenCalledWith('/some/path/ca_1234.crt', 'ca-content'); - expect(mockAppendFile).toHaveBeenCalledTimes(1); - expect(mockAppendFile).toHaveBeenCalledWith( - '/some/path/kibana.yml', - ` + ); + }); + + it('throws if cannot read existing config', async () => { + mockReadFile.mockRejectedValue(new Error('Oh no!')); + + await expect( + kibanaConfigWriter.writeConfig({ + caCert: 'ca-content', + host: 'some-host', + serviceAccountToken: { name: 'some-token', value: 'some-value' }, + }) + ).rejects.toMatchInlineSnapshot(`[Error: Oh no!]`); + + expect(mockWriteFile).not.toHaveBeenCalled(); + }); + + it('throws if cannot parse existing config', async () => { + mockReadFile.mockResolvedValue('foo: bar\nfoo: baz'); + + await expect( + kibanaConfigWriter.writeConfig({ + caCert: 'ca-content', + host: 'some-host', + serviceAccountToken: { name: 'some-token', value: 'some-value' }, + }) + ).rejects.toMatchInlineSnapshot(` + [YAMLException: duplicated mapping key at line 2, column 1: + foo: baz + ^] + `); + + expect(mockWriteFile).not.toHaveBeenCalled(); + }); + + it('can successfully write CA certificate and elasticsearch config with service token', async () => { + await expect( + kibanaConfigWriter.writeConfig({ + caCert: 'ca-content', + host: 'some-host', + serviceAccountToken: { name: 'some-token', value: 'some-value' }, + }) + ).resolves.toBeUndefined(); + + expect(mockWriteFile).toHaveBeenCalledTimes(2); + expect(mockWriteFile).toHaveBeenCalledWith('/some/path/ca_1234.crt', 'ca-content'); + expect(mockWriteFile).toHaveBeenCalledWith( + '/some/path/kibana.yml', + ` # This section was automatically generated during setup. elasticsearch.hosts: [some-host] @@ -134,25 +170,24 @@ elasticsearch.serviceAccountToken: some-value elasticsearch.ssl.certificateAuthorities: [/some/path/ca_1234.crt] ` - ); - }); - - it('can successfully write CA certificate and elasticsearch config with credentials', async () => { - await expect( - kibanaConfigWriter.writeConfig({ - caCert: 'ca-content', - host: 'some-host', - username: 'username', - password: 'password', - }) - ).resolves.toBeUndefined(); - - expect(mockWriteFile).toHaveBeenCalledTimes(1); - expect(mockWriteFile).toHaveBeenCalledWith('/some/path/ca_1234.crt', 'ca-content'); - expect(mockAppendFile).toHaveBeenCalledTimes(1); - expect(mockAppendFile).toHaveBeenCalledWith( - '/some/path/kibana.yml', - ` + ); + }); + + it('can successfully write CA certificate and elasticsearch config with credentials', async () => { + await expect( + kibanaConfigWriter.writeConfig({ + caCert: 'ca-content', + host: 'some-host', + username: 'username', + password: 'password', + }) + ).resolves.toBeUndefined(); + + expect(mockWriteFile).toHaveBeenCalledTimes(2); + expect(mockWriteFile).toHaveBeenCalledWith('/some/path/ca_1234.crt', 'ca-content'); + expect(mockWriteFile).toHaveBeenCalledWith( + '/some/path/kibana.yml', + ` # This section was automatically generated during setup. elasticsearch.hosts: [some-host] @@ -161,23 +196,22 @@ elasticsearch.username: username elasticsearch.ssl.certificateAuthorities: [/some/path/ca_1234.crt] ` - ); - }); - - it('can successfully write elasticsearch config without CA certificate', async () => { - await expect( - kibanaConfigWriter.writeConfig({ - host: 'some-host', - username: 'username', - password: 'password', - }) - ).resolves.toBeUndefined(); - - expect(mockWriteFile).not.toHaveBeenCalled(); - expect(mockAppendFile).toHaveBeenCalledTimes(1); - expect(mockAppendFile).toHaveBeenCalledWith( - '/some/path/kibana.yml', - ` + ); + }); + + it('can successfully write elasticsearch config without CA certificate', async () => { + await expect( + kibanaConfigWriter.writeConfig({ + host: 'some-host', + username: 'username', + password: 'password', + }) + ).resolves.toBeUndefined(); + + expect(mockWriteFile).toHaveBeenCalledTimes(1); + expect(mockWriteFile).toHaveBeenCalledWith( + '/some/path/kibana.yml', + ` # This section was automatically generated during setup. elasticsearch.hosts: [some-host] @@ -185,7 +219,106 @@ elasticsearch.password: password elasticsearch.username: username ` - ); + ); + }); + }); + + describe('with existing config (no conflicts)', () => { + beforeEach(() => { + mockReadFile.mockResolvedValue( + '# Default Kibana configuration for docker target\nserver.host: "0.0.0.0"\nserver.shutdownTimeout: "5s"' + ); + }); + + it('can successfully write CA certificate and elasticsearch config', async () => { + await expect( + kibanaConfigWriter.writeConfig({ + caCert: 'ca-content', + host: 'some-host', + serviceAccountToken: { name: 'some-token', value: 'some-value' }, + }) + ).resolves.toBeUndefined(); + + expect(mockReadFile).toHaveBeenCalledTimes(1); + expect(mockReadFile).toHaveBeenCalledWith('/some/path/kibana.yml', 'utf-8'); + + expect(mockWriteFile).toHaveBeenCalledTimes(2); + expect(mockWriteFile.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/some/path/ca_1234.crt", + "ca-content", + ], + Array [ + "/some/path/kibana.yml", + "# Default Kibana configuration for docker target + server.host: \\"0.0.0.0\\" + server.shutdownTimeout: \\"5s\\" + + # This section was automatically generated during setup. + elasticsearch.hosts: [some-host] + elasticsearch.serviceAccountToken: some-value + elasticsearch.ssl.certificateAuthorities: [/some/path/ca_1234.crt] + + ", + ], + ] + `); + }); + }); + + describe('with existing config (with conflicts)', () => { + beforeEach(() => { + jest.spyOn(Date.prototype, 'toISOString').mockReturnValue('some date'); + mockReadFile.mockResolvedValue( + '# Default Kibana configuration for docker target\nserver.host: "0.0.0.0"\nserver.shutdownTimeout: "5s"\nelasticsearch.hosts: [ "http://elasticsearch:9200" ]\n\nmonitoring.ui.container.elasticsearch.enabled: true' + ); + }); + + it('can successfully write CA certificate and elasticsearch config', async () => { + await expect( + kibanaConfigWriter.writeConfig({ + caCert: 'ca-content', + host: 'some-host', + serviceAccountToken: { name: 'some-token', value: 'some-value' }, + }) + ).resolves.toBeUndefined(); + + expect(mockReadFile).toHaveBeenCalledTimes(1); + expect(mockReadFile).toHaveBeenCalledWith('/some/path/kibana.yml', 'utf-8'); + + expect(mockWriteFile).toHaveBeenCalledTimes(2); + expect(mockWriteFile.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/some/path/ca_1234.crt", + "ca-content", + ], + Array [ + "/some/path/kibana.yml", + "### >>>>>>> BACKUP START: Kibana interactive setup (some date) + + # Default Kibana configuration for docker target + #server.host: \\"0.0.0.0\\" + #server.shutdownTimeout: \\"5s\\" + #elasticsearch.hosts: [ \\"http://elasticsearch:9200\\" ] + + #monitoring.ui.container.elasticsearch.enabled: true + ### >>>>>>> BACKUP END: Kibana interactive setup (some date) + + # This section was automatically generated during setup. + server.host: 0.0.0.0 + server.shutdownTimeout: 5s + elasticsearch.hosts: [some-host] + monitoring.ui.container.elasticsearch.enabled: true + elasticsearch.serviceAccountToken: some-value + elasticsearch.ssl.certificateAuthorities: [/some/path/ca_1234.crt] + + ", + ], + ] + `); + }); }); }); }); diff --git a/src/plugins/interactive_setup/server/kibana_config_writer.ts b/src/plugins/interactive_setup/server/kibana_config_writer.ts index a59aa7640caa6..ff67e887fab49 100644 --- a/src/plugins/interactive_setup/server/kibana_config_writer.ts +++ b/src/plugins/interactive_setup/server/kibana_config_writer.ts @@ -11,6 +11,7 @@ import fs from 'fs/promises'; import yaml from 'js-yaml'; import path from 'path'; +import { getFlattenedObject } from '@kbn/std'; import type { Logger } from 'src/core/server'; import { getDetailedErrorMessage } from './errors'; @@ -61,6 +62,45 @@ export class KibanaConfigWriter { */ public async writeConfig(params: WriteConfigParameters) { const caPath = path.join(path.dirname(this.configPath), `ca_${Date.now()}.crt`); + const config: Record = { 'elasticsearch.hosts': [params.host] }; + if ('serviceAccountToken' in params) { + config['elasticsearch.serviceAccountToken'] = params.serviceAccountToken.value; + } else if ('username' in params) { + config['elasticsearch.password'] = params.password; + config['elasticsearch.username'] = params.username; + } + if (params.caCert) { + config['elasticsearch.ssl.certificateAuthorities'] = [caPath]; + } + + // Load and parse existing configuration file to check if it already has values for the config + // entries we want to write. + const existingConfig = await this.loadAndParseKibanaConfig(); + const conflictingKeys = Object.keys(config).filter( + (configKey) => configKey in existingConfig.parsed + ); + + // If existing config has conflicting entries, back it up first. + let configToWrite; + if (conflictingKeys.length > 0) { + this.logger.warn( + `Kibana configuration file has the following conflicting keys that will be overridden: [${conflictingKeys.join( + ', ' + )}].` + ); + + const existingCommentedConfig = KibanaConfigWriter.commentOutKibanaConfig(existingConfig.raw); + configToWrite = `${existingCommentedConfig}\n\n# This section was automatically generated during setup.\n${yaml.safeDump( + { ...existingConfig.parsed, ...config }, + { flowLevel: 1 } + )}\n`; + } else { + configToWrite = `${ + existingConfig.raw + }\n\n# This section was automatically generated during setup.\n${yaml.safeDump(config, { + flowLevel: 1, + })}\n`; + } if (params.caCert) { this.logger.debug(`Writing CA certificate to ${caPath}.`); @@ -75,25 +115,9 @@ export class KibanaConfigWriter { } } - const config: Record = { 'elasticsearch.hosts': [params.host] }; - if ('serviceAccountToken' in params) { - config['elasticsearch.serviceAccountToken'] = params.serviceAccountToken.value; - } else if ('username' in params) { - config['elasticsearch.password'] = params.password; - config['elasticsearch.username'] = params.username; - } - if (params.caCert) { - config['elasticsearch.ssl.certificateAuthorities'] = [caPath]; - } - this.logger.debug(`Writing Elasticsearch configuration to ${this.configPath}.`); try { - await fs.appendFile( - this.configPath, - `\n\n# This section was automatically generated during setup.\n${yaml.safeDump(config, { - flowLevel: 1, - })}\n` - ); + await fs.writeFile(this.configPath, configToWrite); this.logger.debug(`Successfully wrote Elasticsearch configuration to ${this.configPath}.`); } catch (err) { this.logger.error( @@ -101,7 +125,55 @@ export class KibanaConfigWriter { this.configPath }: ${getDetailedErrorMessage(err)}.` ); + + throw err; + } + } + + /** + * Loads and parses existing Kibana configuration file. + */ + private async loadAndParseKibanaConfig() { + let rawConfig: string; + try { + rawConfig = await fs.readFile(this.configPath, 'utf-8'); + } catch (err) { + this.logger.error(`Failed to read configuration file: ${getDetailedErrorMessage(err)}.`); throw err; } + + let parsedConfig: Record; + try { + parsedConfig = getFlattenedObject(yaml.safeLoad(rawConfig) ?? {}); + } catch (err) { + this.logger.error(`Failed to parse configuration file: ${getDetailedErrorMessage(err)}.`); + throw err; + } + + return { raw: rawConfig, parsed: parsedConfig }; + } + + /** + * Comments out all non-commented entries in the Kibana configuration file. + * @param rawConfig Content of the Kibana configuration file. + */ + private static commentOutKibanaConfig(rawConfig: string) { + const backupTimestamp = new Date().toISOString(); + const commentedRawConfigLines = [ + `### >>>>>>> BACKUP START: Kibana interactive setup (${backupTimestamp})\n`, + ]; + for (const rawConfigLine of rawConfig.split('\n')) { + const trimmedLine = rawConfigLine.trim(); + commentedRawConfigLines.push( + trimmedLine.length === 0 || trimmedLine.startsWith('#') + ? rawConfigLine + : `#${rawConfigLine}` + ); + } + + return [ + ...commentedRawConfigLines, + `### >>>>>>> BACKUP END: Kibana interactive setup (${backupTimestamp})`, + ].join('\n'); } } diff --git a/src/plugins/interactive_setup/server/plugin.ts b/src/plugins/interactive_setup/server/plugin.ts index 91a151e17b697..8c57b6e8514c0 100644 --- a/src/plugins/interactive_setup/server/plugin.ts +++ b/src/plugins/interactive_setup/server/plugin.ts @@ -17,10 +17,19 @@ import type { ConfigSchema, ConfigType } from './config'; import { ElasticsearchService } from './elasticsearch_service'; import { KibanaConfigWriter } from './kibana_config_writer'; import { defineRoutes } from './routes'; +import { VerificationCode } from './verification_code'; + +// List of the Elasticsearch hosts Kibana uses by default. +const DEFAULT_ELASTICSEARCH_HOSTS = [ + 'http://localhost:9200', + // It's a default host we use in the official Kibana Docker image (see `kibana_yml.template.ts`). + ...(process.env.ELASTIC_CONTAINER ? ['http://elasticsearch:9200'] : []), +]; export class InteractiveSetupPlugin implements PrebootPlugin { readonly #logger: Logger; readonly #elasticsearch: ElasticsearchService; + readonly #verificationCode: VerificationCode; #elasticsearchConnectionStatusSubscription?: Subscription; @@ -38,6 +47,9 @@ export class InteractiveSetupPlugin implements PrebootPlugin { this.#elasticsearch = new ElasticsearchService( this.initializerContext.logger.get('elasticsearch') ); + this.#verificationCode = new VerificationCode( + this.initializerContext.logger.get('verification') + ); } public setup(core: CorePreboot) { @@ -53,7 +65,7 @@ export class InteractiveSetupPlugin implements PrebootPlugin { const shouldActiveSetupMode = !core.elasticsearch.config.credentialsSpecified && core.elasticsearch.config.hosts.length === 1 && - core.elasticsearch.config.hosts[0] === 'http://localhost:9200'; + DEFAULT_ELASTICSEARCH_HOSTS.includes(core.elasticsearch.config.hosts[0]); if (!shouldActiveSetupMode) { this.#logger.debug( 'Interactive setup mode will not be activated since Elasticsearch connection is already configured.' @@ -92,13 +104,18 @@ export class InteractiveSetupPlugin implements PrebootPlugin { this.#logger.debug( 'Starting interactive setup mode since Kibana cannot to connect to Elasticsearch at http://localhost:9200.' ); - const serverInfo = core.http.getServerInfo(); - const url = `${serverInfo.protocol}://${serverInfo.hostname}:${serverInfo.port}`; - this.#logger.info(` + const { code } = this.#verificationCode; + const pathname = core.http.basePath.prepend('/'); + const { protocol, hostname, port } = core.http.getServerInfo(); + const url = `${protocol}://${hostname}:${port}${pathname}?code=${code}`; + + // eslint-disable-next-line no-console + console.log(` ${chalk.whiteBright.bold(`${chalk.cyanBright('i')} Kibana has not been configured.`)} Go to ${chalk.cyanBright.underline(url)} to get started. + `); } } @@ -118,6 +135,7 @@ Go to ${chalk.cyanBright.underline(url)} to get started. preboot: { ...core.preboot, completeSetup }, kibanaConfigWriter: new KibanaConfigWriter(configPath, this.#logger.get('kibana-config')), elasticsearch, + verificationCode: this.#verificationCode, getConfig: this.#getConfig.bind(this), }); }); diff --git a/src/plugins/interactive_setup/server/routes/configure.test.ts b/src/plugins/interactive_setup/server/routes/configure.test.ts index d6b7404fce516..ac4507331db4b 100644 --- a/src/plugins/interactive_setup/server/routes/configure.test.ts +++ b/src/plugins/interactive_setup/server/routes/configure.test.ts @@ -57,7 +57,11 @@ describe('Configure routes', () => { expect(() => bodySchema.validate({ host: 'localhost:9200' }) ).toThrowErrorMatchingInlineSnapshot(`"[host]: expected URI with scheme [http|https]."`); - expect(() => bodySchema.validate({ host: 'http://localhost:9200' })).not.toThrowError(); + expect(bodySchema.validate({ host: 'http://localhost:9200' })).toMatchInlineSnapshot(` + Object { + "host": "http://localhost:9200", + } + `); expect(() => bodySchema.validate({ host: 'http://localhost:9200', username: 'elastic' }) ).toThrowErrorMatchingInlineSnapshot( @@ -71,21 +75,57 @@ describe('Configure routes', () => { expect(() => bodySchema.validate({ host: 'http://localhost:9200', password: 'password' }) ).toThrowErrorMatchingInlineSnapshot(`"[password]: a value wasn't expected to be present"`); - expect(() => + expect( bodySchema.validate({ host: 'http://localhost:9200', username: 'kibana_system', password: '', }) - ).not.toThrowError(); + ).toMatchInlineSnapshot(` + Object { + "host": "http://localhost:9200", + "password": "", + "username": "kibana_system", + } + `); expect(() => bodySchema.validate({ host: 'https://localhost:9200' }) ).toThrowErrorMatchingInlineSnapshot( `"[caCert]: expected value of type [string] but got [undefined]"` ); - expect(() => - bodySchema.validate({ host: 'https://localhost:9200', caCert: 'der' }) - ).not.toThrowError(); + expect(bodySchema.validate({ host: 'https://localhost:9200', caCert: 'der' })) + .toMatchInlineSnapshot(` + Object { + "caCert": "der", + "host": "https://localhost:9200", + } + `); + expect(bodySchema.validate({ host: 'https://localhost:9200', caCert: 'der', code: '123456' })) + .toMatchInlineSnapshot(` + Object { + "caCert": "der", + "code": "123456", + "host": "https://localhost:9200", + } + `); + }); + + it('fails if verification code is invalid.', async () => { + mockRouteParams.verificationCode.verify.mockReturnValue(false); + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { host: 'host1' }, + }); + + await expect(routeHandler(mockContext, mockRequest, kibanaResponseFactory)).resolves.toEqual( + expect.objectContaining({ + status: 403, + }) + ); + + expect(mockRouteParams.elasticsearch.authenticate).not.toHaveBeenCalled(); + expect(mockRouteParams.kibanaConfigWriter.writeConfig).not.toHaveBeenCalled(); + expect(mockRouteParams.preboot.completeSetup).not.toHaveBeenCalled(); }); it('fails if setup is not on hold.', async () => { diff --git a/src/plugins/interactive_setup/server/routes/configure.ts b/src/plugins/interactive_setup/server/routes/configure.ts index a34af0296ea04..75499d048cf93 100644 --- a/src/plugins/interactive_setup/server/routes/configure.ts +++ b/src/plugins/interactive_setup/server/routes/configure.ts @@ -21,15 +21,13 @@ export function defineConfigureRoute({ logger, kibanaConfigWriter, elasticsearch, + verificationCode, preboot, }: RouteDefinitionParams) { router.post( { path: '/internal/interactive_setup/configure', validate: { - query: schema.object({ - code: schema.maybe(schema.string()), - }), body: schema.object({ host: schema.uri({ scheme: ['http', 'https'] }), username: schema.maybe( @@ -56,11 +54,16 @@ export function defineConfigureRoute({ schema.string(), schema.never() ), + code: schema.maybe(schema.string()), }), }, options: { authRequired: false }, }, async (context, request, response) => { + if (!verificationCode.verify(request.body.code)) { + return response.forbidden(); + } + if (!preboot.isSetupOnHold()) { logger.error(`Invalid request to [path=${request.url.pathname}] outside of preboot stage`); return response.badRequest({ body: 'Cannot process request outside of preboot stage.' }); diff --git a/src/plugins/interactive_setup/server/routes/enroll.test.ts b/src/plugins/interactive_setup/server/routes/enroll.test.ts index e42248704134a..859c3fb70ce83 100644 --- a/src/plugins/interactive_setup/server/routes/enroll.test.ts +++ b/src/plugins/interactive_setup/server/routes/enroll.test.ts @@ -95,18 +95,55 @@ describe('Enroll routes', () => { ); expect( - bodySchema.validate( - bodySchema.validate({ - apiKey: 'some-key', - hosts: ['https://localhost:9200'], - caFingerprint: 'a'.repeat(64), - }) - ) - ).toEqual({ - apiKey: 'some-key', - hosts: ['https://localhost:9200'], - caFingerprint: 'a'.repeat(64), + bodySchema.validate({ + apiKey: 'some-key', + hosts: ['https://localhost:9200'], + caFingerprint: 'a'.repeat(64), + }) + ).toMatchInlineSnapshot(` + Object { + "apiKey": "some-key", + "caFingerprint": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "hosts": Array [ + "https://localhost:9200", + ], + } + `); + expect( + bodySchema.validate({ + apiKey: 'some-key', + hosts: ['https://localhost:9200'], + caFingerprint: 'a'.repeat(64), + code: '123456', + }) + ).toMatchInlineSnapshot(` + Object { + "apiKey": "some-key", + "caFingerprint": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "code": "123456", + "hosts": Array [ + "https://localhost:9200", + ], + } + `); + }); + + it('fails if verification code is invalid.', async () => { + mockRouteParams.verificationCode.verify.mockReturnValue(false); + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { apiKey: 'some-key', hosts: ['host1', 'host2'], caFingerprint: 'deadbeef' }, }); + + await expect(routeHandler(mockContext, mockRequest, kibanaResponseFactory)).resolves.toEqual( + expect.objectContaining({ + status: 403, + }) + ); + + expect(mockRouteParams.elasticsearch.enroll).not.toHaveBeenCalled(); + expect(mockRouteParams.kibanaConfigWriter.writeConfig).not.toHaveBeenCalled(); + expect(mockRouteParams.preboot.completeSetup).not.toHaveBeenCalled(); }); it('fails if setup is not on hold.', async () => { diff --git a/src/plugins/interactive_setup/server/routes/enroll.ts b/src/plugins/interactive_setup/server/routes/enroll.ts index 41291246802e6..769d763a7d45d 100644 --- a/src/plugins/interactive_setup/server/routes/enroll.ts +++ b/src/plugins/interactive_setup/server/routes/enroll.ts @@ -23,6 +23,7 @@ export function defineEnrollRoutes({ logger, kibanaConfigWriter, elasticsearch, + verificationCode, preboot, }: RouteDefinitionParams) { router.post( @@ -35,11 +36,16 @@ export function defineEnrollRoutes({ }), apiKey: schema.string({ minLength: 1 }), caFingerprint: schema.string({ maxLength: 64, minLength: 64 }), + code: schema.maybe(schema.string()), }), }, options: { authRequired: false }, }, async (context, request, response) => { + if (!verificationCode.verify(request.body.code)) { + return response.forbidden(); + } + if (!preboot.isSetupOnHold()) { logger.error(`Invalid request to [path=${request.url.pathname}] outside of preboot stage`); return response.badRequest({ body: 'Cannot process request outside of preboot stage.' }); diff --git a/src/plugins/interactive_setup/server/routes/index.mock.ts b/src/plugins/interactive_setup/server/routes/index.mock.ts index 249d1277269e7..15ec86031b6f2 100644 --- a/src/plugins/interactive_setup/server/routes/index.mock.ts +++ b/src/plugins/interactive_setup/server/routes/index.mock.ts @@ -11,6 +11,7 @@ import { coreMock, httpServiceMock, loggingSystemMock } from 'src/core/server/mo import { ConfigSchema } from '../config'; import { elasticsearchServiceMock } from '../elasticsearch_service.mock'; import { kibanaConfigWriterMock } from '../kibana_config_writer.mock'; +import { verificationCodeMock } from '../verification_code.mock'; export const routeDefinitionParamsMock = { create: (config: Record = {}) => ({ @@ -21,6 +22,7 @@ export const routeDefinitionParamsMock = { preboot: { ...coreMock.createPreboot().preboot, completeSetup: jest.fn() }, getConfig: jest.fn().mockReturnValue(ConfigSchema.validate(config)), elasticsearch: elasticsearchServiceMock.createSetup(), + verificationCode: verificationCodeMock.create(), kibanaConfigWriter: kibanaConfigWriterMock.create(), }), }; diff --git a/src/plugins/interactive_setup/server/routes/index.ts b/src/plugins/interactive_setup/server/routes/index.ts index 75c383176e7e9..fb9e06c4c2a18 100644 --- a/src/plugins/interactive_setup/server/routes/index.ts +++ b/src/plugins/interactive_setup/server/routes/index.ts @@ -6,15 +6,17 @@ * Side Public License, v 1. */ -import type { PublicMethodsOf } from '@kbn/utility-types'; +import type { PublicContract, PublicMethodsOf } from '@kbn/utility-types'; import type { IBasePath, IRouter, Logger, PrebootServicePreboot } from 'src/core/server'; import type { ConfigType } from '../config'; import type { ElasticsearchServiceSetup } from '../elasticsearch_service'; import type { KibanaConfigWriter } from '../kibana_config_writer'; +import type { VerificationCode } from '../verification_code'; import { defineConfigureRoute } from './configure'; import { defineEnrollRoutes } from './enroll'; import { definePingRoute } from './ping'; +import { defineVerifyRoute } from './verify'; /** * Describes parameters used to define HTTP routes. @@ -28,11 +30,13 @@ export interface RouteDefinitionParams { }; readonly kibanaConfigWriter: PublicMethodsOf; readonly elasticsearch: ElasticsearchServiceSetup; + readonly verificationCode: PublicContract; readonly getConfig: () => ConfigType; } export function defineRoutes(params: RouteDefinitionParams) { - defineEnrollRoutes(params); defineConfigureRoute(params); + defineEnrollRoutes(params); definePingRoute(params); + defineVerifyRoute(params); } diff --git a/src/plugins/interactive_setup/server/routes/verify.test.ts b/src/plugins/interactive_setup/server/routes/verify.test.ts new file mode 100644 index 0000000000000..ff8a7753320c2 --- /dev/null +++ b/src/plugins/interactive_setup/server/routes/verify.test.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { ObjectType } from '@kbn/config-schema'; +import type { IRouter, RequestHandler, RequestHandlerContext, RouteConfig } from 'src/core/server'; +import { kibanaResponseFactory } from 'src/core/server'; +import { httpServerMock } from 'src/core/server/mocks'; + +import { routeDefinitionParamsMock } from './index.mock'; +import { defineVerifyRoute } from './verify'; + +describe('Configure routes', () => { + let router: jest.Mocked; + let mockRouteParams: ReturnType; + let mockContext: RequestHandlerContext; + beforeEach(() => { + mockRouteParams = routeDefinitionParamsMock.create(); + router = mockRouteParams.router; + + mockContext = ({} as unknown) as RequestHandlerContext; + + defineVerifyRoute(mockRouteParams); + }); + + describe('#verify', () => { + let routeHandler: RequestHandler; + let routeConfig: RouteConfig; + + beforeEach(() => { + const [verifyRouteConfig, verifyRouteHandler] = router.post.mock.calls.find( + ([{ path }]) => path === '/internal/interactive_setup/verify' + )!; + + routeConfig = verifyRouteConfig; + routeHandler = verifyRouteHandler; + }); + + it('correctly defines route.', () => { + expect(routeConfig.options).toEqual({ authRequired: false }); + + const bodySchema = (routeConfig.validate as any).body as ObjectType; + expect(() => bodySchema.validate({})).toThrowErrorMatchingInlineSnapshot( + `"[code]: expected value of type [string] but got [undefined]"` + ); + expect(bodySchema.validate({ code: '123456' })).toMatchInlineSnapshot(` + Object { + "code": "123456", + } + `); + }); + + it('fails if verification code is invalid.', async () => { + mockRouteParams.verificationCode.verify.mockReturnValue(false); + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { code: '123456' }, + }); + + await expect(routeHandler(mockContext, mockRequest, kibanaResponseFactory)).resolves.toEqual( + expect.objectContaining({ + status: 403, + }) + ); + }); + + it('succeeds if verification code is valid.', async () => { + mockRouteParams.verificationCode.verify.mockReturnValue(true); + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { code: '123456' }, + }); + + await expect(routeHandler(mockContext, mockRequest, kibanaResponseFactory)).resolves.toEqual( + expect.objectContaining({ + status: 204, + }) + ); + }); + }); +}); diff --git a/src/plugins/interactive_setup/server/routes/verify.ts b/src/plugins/interactive_setup/server/routes/verify.ts new file mode 100644 index 0000000000000..ebdbb58ed9530 --- /dev/null +++ b/src/plugins/interactive_setup/server/routes/verify.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { schema } from '@kbn/config-schema'; + +import type { RouteDefinitionParams } from '.'; + +export function defineVerifyRoute({ router, verificationCode }: RouteDefinitionParams) { + router.post( + { + path: '/internal/interactive_setup/verify', + validate: { + body: schema.object({ + code: schema.string(), + }), + }, + options: { authRequired: false }, + }, + async (context, request, response) => { + if (!verificationCode.verify(request.body.code)) { + return response.forbidden({ + body: { + message: verificationCode.remainingAttempts + ? 'Invalid verification code.' + : 'Maximum number of attempts exceeded. Restart Kibana to generate a new code and retry.', + attributes: { + remainingAttempts: verificationCode.remainingAttempts, + }, + }, + }); + } + + return response.noContent(); + } + ); +} diff --git a/src/plugins/saved_objects_management/public/lib/import_legacy_file.ts b/src/plugins/interactive_setup/server/verification_code.mock.ts similarity index 50% rename from src/plugins/saved_objects_management/public/lib/import_legacy_file.ts rename to src/plugins/interactive_setup/server/verification_code.mock.ts index 3e605154f5339..d4e9fc2028590 100644 --- a/src/plugins/saved_objects_management/public/lib/import_legacy_file.ts +++ b/src/plugins/interactive_setup/server/verification_code.mock.ts @@ -6,17 +6,14 @@ * 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); - }); -} +import type { PublicContract } from '@kbn/utility-types'; + +import type { VerificationCode } from './verification_code'; + +export const verificationCodeMock = { + create: (): jest.Mocked> => ({ + code: '123456', + remainingAttempts: 5, + verify: jest.fn().mockReturnValue(true), + }), +}; diff --git a/src/plugins/interactive_setup/server/verification_code.test.ts b/src/plugins/interactive_setup/server/verification_code.test.ts new file mode 100644 index 0000000000000..7387f285a2f62 --- /dev/null +++ b/src/plugins/interactive_setup/server/verification_code.test.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { loggingSystemMock } from 'src/core/server/mocks'; + +import { VERIFICATION_CODE_LENGTH } from '../common'; +import { VerificationCode } from './verification_code'; + +const loggerMock = loggingSystemMock.createLogger(); + +describe('VerificationCode', () => { + it('should generate a 6 digit code', () => { + for (let i = 0; i < 10; i++) { + const { code } = new VerificationCode(loggerMock); + expect(code).toHaveLength(VERIFICATION_CODE_LENGTH); + expect(code).toEqual(expect.stringMatching(/^[0-9]+$/)); + } + }); + + it('should verify code correctly', () => { + const verificationCode = new VerificationCode(loggerMock); + + expect(verificationCode.verify(undefined)).toBe(false); + expect(verificationCode.verify('')).toBe(false); + expect(verificationCode.verify('invalid')).toBe(false); + expect(verificationCode.verify(verificationCode.code)).toBe(true); + }); + + it('should track number of failed attempts', () => { + const verificationCode = new VerificationCode(loggerMock); + + verificationCode.verify('invalid'); + verificationCode.verify('invalid'); + verificationCode.verify('invalid'); + expect(verificationCode['failedAttempts']).toBe(3); // eslint-disable-line dot-notation + }); + + it('should reset number of failed attempts if valid code is entered', () => { + const verificationCode = new VerificationCode(loggerMock); + + verificationCode.verify('invalid'); + verificationCode.verify('invalid'); + verificationCode.verify('invalid'); + expect(verificationCode.verify(verificationCode.code)).toBe(true); + expect(verificationCode['failedAttempts']).toBe(0); // eslint-disable-line dot-notation + }); + + it('should permanently fail once maximum number of failed attempts has been reached', () => { + const verificationCode = new VerificationCode(loggerMock); + + // eslint-disable-next-line dot-notation + for (let i = 0; i < verificationCode['maxFailedAttempts']; i++) { + verificationCode.verify('invalid'); + } + expect(verificationCode.verify(verificationCode.code)).toBe(false); + }); + + it('should ignore empty calls in number of failed attempts', () => { + const verificationCode = new VerificationCode(loggerMock); + + verificationCode.verify(undefined); + verificationCode.verify(undefined); + verificationCode.verify(undefined); + expect(verificationCode['failedAttempts']).toBe(0); // eslint-disable-line dot-notation + }); +}); diff --git a/src/plugins/interactive_setup/server/verification_code.ts b/src/plugins/interactive_setup/server/verification_code.ts new file mode 100644 index 0000000000000..849ece5f4e0b0 --- /dev/null +++ b/src/plugins/interactive_setup/server/verification_code.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 chalk from 'chalk'; +import crypto from 'crypto'; + +import type { Logger } from 'src/core/server'; + +import { VERIFICATION_CODE_LENGTH } from '../common'; + +export class VerificationCode { + public readonly code = VerificationCode.generate(VERIFICATION_CODE_LENGTH); + private failedAttempts = 0; + private readonly maxFailedAttempts = 5; + + constructor(private readonly logger: Logger) {} + + public get remainingAttempts() { + return this.maxFailedAttempts - this.failedAttempts; + } + + public verify(code: string | undefined) { + if (this.failedAttempts >= this.maxFailedAttempts) { + this.logger.error( + 'Maximum number of attempts exceeded. Restart Kibana to generate a new code and retry.' + ); + return false; + } + + const highlightedCode = chalk.black.bgCyanBright( + ` ${this.code.substr(0, 3)} ${this.code.substr(3)} ` + ); + + if (code === undefined) { + // eslint-disable-next-line no-console + console.log(` + +Your verification code is: ${highlightedCode} + +`); + return false; + } + + if (code !== this.code) { + this.failedAttempts++; + this.logger.error( + `Invalid verification code '${code}' provided. ${this.remainingAttempts} attempts left.` + ); + // eslint-disable-next-line no-console + console.log(` + +Your verification code is: ${highlightedCode} + +`); + return false; + } + + this.logger.debug(`Code '${code}' verified successfully`); + + this.failedAttempts = 0; + return true; + } + + /** + * Returns a cryptographically secure and random 6-digit code. + * + * Implementation notes: `secureRandomNumber` returns a random number like `0.05505769583xxxx`. To + * turn that into a 6 digit code we multiply it by `10^6` and result is `055057`. + */ + private static generate(length: number) { + return Math.floor(secureRandomNumber() * Math.pow(10, length)) + .toString() + .padStart(length, '0'); + } +} + +/** + * Cryptographically secure equivalent of `Math.random()`. + */ +function secureRandomNumber() { + return crypto.randomBytes(4).readUInt32LE() / 0x100000000; +} 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 2554c78514de3..0000000000000 --- 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/index.ts b/src/plugins/saved_objects_management/public/lib/index.ts index df1485bedfc69..aefa5b614f618 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/process_import_response.ts b/src/plugins/saved_objects_management/public/lib/process_import_response.ts index d7da441ce9230..67c66bce1b331 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 b1c052e56ef22..0000000000000 --- 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 95bd41745553d..0000000000000 --- 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 015c7068d72b6..1affbd4d96463 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 7b716e1b813c9..78f12d0753b26 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 28190e6bd872f..f055817f69105 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 aca229b9a70ed..26de8c5f8b25a 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 5ea433f91d1a6..d4067cc21c2be 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 5580b723a095a..2616b299da28d 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 d112f6310a1fe..ceeadc05ee91e 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 913db9aee9c0f..a3ae8f55135b5 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/context/_size.ts b/test/functional/apps/context/_size.ts index b11af7cd5c72f..52b16d2b9abe5 100644 --- a/test/functional/apps/context/_size.ts +++ b/test/functional/apps/context/_size.ts @@ -15,6 +15,7 @@ const TEST_STEP_SIZE = 2; export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); + const security = getService('security'); const retry = getService('retry'); const docTable = getService('docTable'); const browser = getService('browser'); @@ -23,6 +24,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('context size', function contextSize() { before(async function () { + await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); await kibanaServer.uiSettings.update({ 'context:defaultSize': `${TEST_DEFAULT_CONTEXT_SIZE}`, 'context:step': `${TEST_STEP_SIZE}`, diff --git a/test/functional/apps/discover/_data_grid_doc_navigation.ts b/test/functional/apps/discover/_data_grid_doc_navigation.ts index cf5532aa6d762..73077dcc9749a 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 8d156cb305586..19f61851ef961 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 a77bc4c77568a..fa42b0ac49617 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 d8701261126c4..a74f4367e657b 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 ae6841b85c98d..98eeed7bcf53e 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 a57ce0596abac..016cead53f0c4 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 f477b25086431..844ebc5a90f60 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 210c8f61b2391..3d2ba53e7ba98 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 0000000000000..b1bd321e1c914 --- /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 886c5fd6161d8..0000000000000 --- 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 a80918f0e399e..09e3e22a1d352 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 0000000000000..dca07e52107e7 --- /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 2b0d2b5642e0c..0000000000000 --- 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 0000000000000..29f419da350ef --- /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 0000000000000..d7c6eab1f07c1 --- /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 d977f44445148..ea6741f572bad 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/alerting/alerting_flyout/index.tsx b/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx index 7cf3100046d57..fa56c44d8d374 100644 --- a/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/alerting_flyout/index.tsx @@ -60,7 +60,7 @@ export function AlertingFlyout(props: Props) { metadata: { environment, serviceName, - transactionType, + ...(alertType === AlertType.ErrorCount ? {} : { transactionType }), start, end, } as AlertMetadata, diff --git a/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/Documentation.tsx b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/Documentation.tsx index 58bcd756582a4..95f7cc871bcb3 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/Documentation.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/create_edit_custom_link_flyout/Documentation.tsx @@ -6,19 +6,14 @@ */ import React from 'react'; -import { ElasticDocsLink } from '../../../../../shared/Links/ElasticDocsLink'; +import { EuiLink } from '@elastic/eui'; +import { useApmPluginContext } from '../../../../../../context/apm_plugin/use_apm_plugin_context'; interface Props { label: string; } + export function Documentation({ label }: Props) { - return ( - - {label} - - ); + const { docLinks } = useApmPluginContext().core; + return {label}; } 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 f7e62b76a61c0..c700533ca4f45 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 4fb7bf5d6fcfb..fc1d9a3324b24 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 b0da5b6d60d74..c1fb1beb1918e 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 ad8a56a3ac6f9..1a769adb621df 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 edb7c8c16e267..e4c08b42b2420 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 5a806aba5371e..cbfaee88ff6f4 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 0000000000000..c323b69594013 --- /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 0000000000000..3a90eb4b89123 --- /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/service_inventory/service_list/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx index 8732084e6331e..a3820622f8c9d 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx @@ -99,14 +99,14 @@ export function getServiceColumns({ }), width: '40%', sortable: true, - render: (_, { serviceName, agentName }) => ( + render: (_, { serviceName, agentName, transactionType }) => ( } diff --git a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx index c0578514ff9ad..29bc639ee9832 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx @@ -10,6 +10,7 @@ import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, + EuiLink, EuiPanel, EuiSpacer, EuiStat, @@ -24,6 +25,7 @@ import { SERVICE_NODE_NAME_MISSING, } from '../../../../common/service_nodes'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb'; import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; import { useApmParams } from '../../../hooks/use_apm_params'; @@ -33,7 +35,6 @@ import { useServiceMetricChartsFetcher } from '../../../hooks/use_service_metric import { useTimeRange } from '../../../hooks/use_time_range'; import { truncate, unit } from '../../../utils/style'; import { MetricsChart } from '../../shared/charts/metrics_chart'; -import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; const INITIAL_DATA = { host: '', @@ -99,6 +100,7 @@ export function ServiceNodeMetrics() { [kuery, serviceName, serviceNodeName, start, end] ); + const { docLinks } = useApmPluginContext().core; const isLoading = status === FETCH_STATUS.LOADING; const isAggregatedData = serviceNodeName === SERVICE_NODE_NAME_MISSING; @@ -120,16 +122,12 @@ export function ServiceNodeMetrics() { defaultMessage="We could not identify which JVMs these metrics belong to. This is likely caused by running a version of APM Server that is older than 7.5. Upgrading to APM Server 7.5 or higher should resolve this issue. For more information on upgrading, see the {link}. As an alternative, you can use the Kibana Query bar to filter by hostname, container ID or other fields." values={{ link: ( - + {i18n.translate( 'xpack.apm.serviceNodeMetrics.unidentifiedServiceNodesWarningDocumentationLink', { defaultMessage: 'documentation of APM Server' } )} - +
), }} /> diff --git a/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx index 1158a671bfe0a..4bc33d440eeaf 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx @@ -19,7 +19,7 @@ import { } from '../../../../common/utils/formatters'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { useApmParams } from '../../../hooks/use_apm_params'; -import { useFetcher } from '../../../hooks/use_fetcher'; +import { useFetcher, FETCH_STATUS } from '../../../hooks/use_fetcher'; import { useTimeRange } from '../../../hooks/use_time_range'; import { truncate, unit } from '../../../utils/style'; import { ServiceNodeMetricOverviewLink } from '../../shared/Links/apm/ServiceNodeMetricOverviewLink'; @@ -42,7 +42,7 @@ function ServiceNodeOverview() { const { serviceName } = useApmServiceContext(); - const { data } = useFetcher( + const { data, status } = useFetcher( (callApmApi) => { if (!start || !end) { return undefined; @@ -164,6 +164,7 @@ function ServiceNodeOverview() { return ( { setTableOptions({ pageIndex: newTableOptions.page?.index ?? 0, diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx index ee971bf82f86e..1c3fe76b997ff 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx @@ -52,6 +52,7 @@ interface Props { }) => void; detailedStatsData?: ServiceInstanceDetailedStatistics; isLoading: boolean; + isNotInitiated: boolean; } export function ServiceOverviewInstancesTable({ mainStatsItems = [], @@ -62,6 +63,7 @@ export function ServiceOverviewInstancesTable({ onChangeTableOptions, detailedStatsData: detailedStatsData, isLoading, + isNotInitiated, }: Props) { const { agentName } = useApmServiceContext(); @@ -151,13 +153,13 @@ export function ServiceOverviewInstancesTable({ ['items'][0]; @@ -127,14 +126,14 @@ const noItemsMessage = ( ); export function TraceList({ items = [], isLoading }: Props) { - const noItems = isLoading ? : noItemsMessage; return ( ); 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 5a9977b373c33..9a38e0fcf6289 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 2da61bc0fc555..acd8c5f4d57d3 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/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx index 06acaeeb5dd3b..ab59b60333e38 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx @@ -7,6 +7,8 @@ import { EuiSpacer, EuiTitle } from '@elastic/eui'; import React from 'react'; +import { useHistory } from 'react-router-dom'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb'; import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; import { useApmParams } from '../../../hooks/use_apm_params'; @@ -15,18 +17,29 @@ import { useTimeRange } from '../../../hooks/use_time_range'; import { useFallbackToTransactionsFetcher } from '../../../hooks/use_fallback_to_transactions_fetcher'; import { AggregatedTransactionsBadge } from '../../shared/aggregated_transactions_badge'; import { TransactionCharts } from '../../shared/charts/transaction_charts'; - +import { replace } from '../../shared/Links/url_helpers'; import { TransactionDetailsTabs } from './transaction_details_tabs'; export function TransactionDetails() { const { path, query } = useApmParams( '/services/:serviceName/transactions/view' ); - const { transactionName, rangeFrom, rangeTo } = query; - + const { + transactionName, + rangeFrom, + rangeTo, + transactionType: transactionTypeFromUrl, + } = query; const { start, end } = useTimeRange({ rangeFrom, rangeTo }); - const apmRouter = useApmRouter(); + const { transactionType } = useApmServiceContext(); + + const history = useHistory(); + + // redirect to first transaction type + if (!transactionTypeFromUrl && transactionType) { + replace(history, { query: { transactionType } }); + } useBreadcrumb({ title: transactionName, 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 ea02cfea5a941..ad629b7a2d132 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/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/transaction_flyout/DroppedSpansWarning.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/transaction_flyout/DroppedSpansWarning.tsx index 6fb1cdc45805e..2c6dbe99b6061 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/transaction_flyout/DroppedSpansWarning.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/transaction_flyout/DroppedSpansWarning.tsx @@ -5,17 +5,18 @@ * 2.0. */ -import { EuiCallOut, EuiHorizontalRule } from '@elastic/eui'; +import { EuiCallOut, EuiHorizontalRule, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; -import { ElasticDocsLink } from '../../../../../../shared/Links/ElasticDocsLink'; +import { useApmPluginContext } from '../../../../../../../context/apm_plugin/use_apm_plugin_context'; export function DroppedSpansWarning({ transactionDoc, }: { transactionDoc: Transaction; }) { + const { docLinks } = useApmPluginContext().core; const dropped = transactionDoc.transaction.span_count?.dropped; if (!dropped) { return null; @@ -32,18 +33,14 @@ export function DroppedSpansWarning({ values: { dropped }, } )}{' '} - + {i18n.translate( 'xpack.apm.transactionDetails.transFlyout.callout.learnMoreAboutDroppedSpansLinkText', { defaultMessage: 'Learn more about dropped spans.', } )} - +
diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx index be12522920740..571ba99d9bf08 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx @@ -5,48 +5,27 @@ * 2.0. */ -import { EuiPanel, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { Location } from 'history'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui'; import React from 'react'; -import { useLocation } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; -import type { ApmUrlParams } from '../../../context/url_params_context/types'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useApmParams } from '../../../hooks/use_apm_params'; import { useFallbackToTransactionsFetcher } from '../../../hooks/use_fallback_to_transactions_fetcher'; import { useTimeRange } from '../../../hooks/use_time_range'; import { AggregatedTransactionsBadge } from '../../shared/aggregated_transactions_badge'; import { TransactionCharts } from '../../shared/charts/transaction_charts'; -import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; +import { replace } from '../../shared/Links/url_helpers'; import { TransactionsTable } from '../../shared/transactions_table'; -import { useRedirect } from './useRedirect'; - -function getRedirectLocation({ - location, - transactionType, - urlParams, -}: { - location: Location; - transactionType?: string; - urlParams: ApmUrlParams; -}): Location | undefined { - const transactionTypeFromUrlParams = urlParams.transactionType; - - if (!transactionTypeFromUrlParams && transactionType) { - return { - ...location, - search: fromQuery({ - ...toQuery(location.search), - transactionType, - }), - }; - } -} - export function TransactionOverview() { const { - query: { environment, kuery, rangeFrom, rangeTo }, + query: { + environment, + kuery, + rangeFrom, + rangeTo, + transactionType: transactionTypeFromUrl, + }, } = useApmParams('/services/:serviceName/transactions'); const { start, end } = useTimeRange({ rangeFrom, rangeTo }); @@ -54,12 +33,14 @@ export function TransactionOverview() { const { fallbackToTransactions } = useFallbackToTransactionsFetcher({ kuery, }); - const location = useLocation(); - const { urlParams } = useUrlParams(); const { transactionType, serviceName } = useApmServiceContext(); + const history = useHistory(); + // redirect to first transaction type - useRedirect(getRedirectLocation({ location, transactionType, urlParams })); + if (!transactionTypeFromUrl && transactionType) { + replace(history, { query: { transactionType } }); + } // TODO: improve urlParams typings. // `serviceName` or `transactionType` will never be undefined here, and this check should not be needed diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/useRedirect.ts b/x-pack/plugins/apm/public/components/app/transaction_overview/useRedirect.ts deleted file mode 100644 index fae80eec42f9b..0000000000000 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/useRedirect.ts +++ /dev/null @@ -1,20 +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 { Location } from 'history'; -import { useEffect } from 'react'; -import { useHistory } from 'react-router-dom'; - -export function useRedirect(redirectLocation?: Location) { - const history = useHistory(); - - useEffect(() => { - if (redirectLocation) { - history.replace(redirectLocation); - } - }, [history, redirectLocation]); -} 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 a58a2887b1576..ee5ae0d4dc840 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 columns: Array> = [ { field: 'name', name: nameColumnTitle, @@ -170,6 +158,18 @@ export function DependenciesTable(props: Props) { impactValue: item.currentStats.impact, })) ?? []; + const noItemsMessage = !compact ? ( + + ) : ( + i18n.translate('xpack.apm.dependenciesTable.notFoundLabel', { + defaultMessage: 'No dependencies found', + }) + ); + return ( @@ -186,22 +186,19 @@ export function DependenciesTable(props: Props) { - diff --git a/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx b/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx index 541ed63a080a2..f7009956bcf84 100644 --- a/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx @@ -127,16 +127,18 @@ function UnoptimizedManagedTable(props: Props) { }; }, [hidePerPageOptions, items, page, pageSize, pagination]); + const showNoItemsMessage = useMemo(() => { + return isLoading + ? i18n.translate('xpack.apm.managedTable.loadingDescription', { + defaultMessage: 'Loading…', + }) + : noItemsMessage; + }, [isLoading, noItemsMessage]); + return ( >} // EuiBasicTableColumn is stricter than ITableColumn sorting={sort} diff --git a/x-pack/plugins/apm/public/components/shared/overview_table_container/index.tsx b/x-pack/plugins/apm/public/components/shared/overview_table_container/index.tsx index 207fa8e1fea76..6caf6aca02733 100644 --- a/x-pack/plugins/apm/public/components/shared/overview_table_container/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/overview_table_container/index.tsx @@ -22,11 +22,11 @@ const tableHeight = 282; * * Only do this when we're at a non-mobile breakpoint. * - * Hide the empty message when we don't yet have any items and are still loading. + * Hide the empty message when we don't yet have any items and are still not initiated. */ const OverviewTableContainerDiv = euiStyled.div<{ fixedHeight?: boolean; - isEmptyAndLoading: boolean; + isEmptyAndNotInitiated: boolean; shouldUseMobileLayout: boolean; }>` ${({ fixedHeight, shouldUseMobileLayout }) => @@ -48,26 +48,26 @@ const OverviewTableContainerDiv = euiStyled.div<{ `} .euiTableRowCell { - visibility: ${({ isEmptyAndLoading }) => - isEmptyAndLoading ? 'hidden' : 'visible'}; + visibility: ${({ isEmptyAndNotInitiated }) => + isEmptyAndNotInitiated ? 'hidden' : 'visible'}; } `; export function OverviewTableContainer({ children, fixedHeight, - isEmptyAndLoading, + isEmptyAndNotInitiated, }: { children?: ReactNode; fixedHeight?: boolean; - isEmptyAndLoading: boolean; + isEmptyAndNotInitiated: boolean; }) { const { isMedium } = useBreakPoints(); return ( {children} diff --git a/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx b/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx index 593ce4c10609c..5d9f96500f101 100644 --- a/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx @@ -217,6 +217,7 @@ export function TransactionsTable({ }); const isLoading = status === FETCH_STATUS.LOADING; + const isNotInitiated = status === FETCH_STATUS.NOT_INITIATED; const pagination = { pageIndex, @@ -296,7 +297,9 @@ export function TransactionsTable({ { - 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 0000000000000..6f6c9bf151c00 --- /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 2ff1b83ef1782..0000000000000 --- 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 0b035c6af2354..0000000000000 --- 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/public/services/rest/index_pattern.ts b/x-pack/plugins/apm/public/services/rest/index_pattern.ts index bc321d700a7a6..0bb5839759f64 100644 --- a/x-pack/plugins/apm/public/services/rest/index_pattern.ts +++ b/x-pack/plugins/apm/public/services/rest/index_pattern.ts @@ -13,10 +13,3 @@ export const createStaticIndexPattern = async () => { signal: null, }); }; - -export const getApmIndexPatternTitle = async () => { - return await callApmApi({ - endpoint: 'GET /api/apm/index_pattern/title', - signal: null, - }); -}; diff --git a/x-pack/plugins/apm/server/index.test.ts b/x-pack/plugins/apm/server/index.test.ts index 56c9825db5a5c..be93557fea6fc 100644 --- a/x-pack/plugins/apm/server/index.test.ts +++ b/x-pack/plugins/apm/server/index.test.ts @@ -18,7 +18,6 @@ describe('mergeConfigs', () => { spanIndices: 'apm-*-span-*', errorIndices: 'apm-*-error-*', metricsIndices: 'apm-*-metric-*', - indexPattern: 'apm-*', } as APMOSSConfig; const apmConfig = { @@ -30,7 +29,6 @@ describe('mergeConfigs', () => { expect(mergeConfigs(apmOssConfig, apmConfig)).toEqual({ 'apm_oss.errorIndices': 'logs-apm*,apm-*-error-*', - 'apm_oss.indexPattern': 'traces-apm*,logs-apm*,metrics-apm*,apm-*', 'apm_oss.metricsIndices': 'metrics-apm*,apm-*-metric-*', 'apm_oss.spanIndices': 'traces-apm*,apm-*-span-*', 'apm_oss.transactionIndices': 'traces-apm*,apm-*-transaction-*', diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index 5b97173601950..db62cc7adae2b 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -72,7 +72,6 @@ export function mergeConfigs( 'apm_oss.metricsIndices': apmOssConfig.metricsIndices, 'apm_oss.sourcemapIndices': apmOssConfig.sourcemapIndices, 'apm_oss.onboardingIndices': apmOssConfig.onboardingIndices, - 'apm_oss.indexPattern': apmOssConfig.indexPattern, /* eslint-enable @typescript-eslint/naming-convention */ 'xpack.apm.serviceMapEnabled': apmConfig.serviceMapEnabled, 'xpack.apm.serviceMapFingerprintBucketSize': @@ -117,10 +116,6 @@ export function mergeConfigs( 'apm_oss.metricsIndices' ] = `metrics-apm*,${mergedConfig['apm_oss.metricsIndices']}`; - mergedConfig[ - 'apm_oss.indexPattern' - ] = `traces-apm*,logs-apm*,metrics-apm*,${mergedConfig['apm_oss.indexPattern']}`; - return mergedConfig; } 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 28f3041d65d70..1ea9ae6b65ac5 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/index_pattern/create_static_index_pattern.test.ts b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts index ef869a0ed6cfa..35f5721eee05c 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts @@ -5,28 +5,36 @@ * 2.0. */ +/* eslint-disable @typescript-eslint/naming-convention */ import { createStaticIndexPattern } from './create_static_index_pattern'; import { Setup } from '../helpers/setup_request'; import * as HistoricalAgentData from '../services/get_services/has_historical_agent_data'; import { InternalSavedObjectsClient } from '../helpers/get_internal_saved_objects_client'; import { APMConfig } from '../..'; -function getMockSavedObjectsClient() { +function getMockSavedObjectsClient(existingIndexPatternTitle: string) { return ({ get: jest.fn(() => ({ attributes: { - title: 'apm-*', + title: existingIndexPatternTitle, }, })), create: jest.fn(), } as unknown) as InternalSavedObjectsClient; } +const setup = ({ + indices: { + 'apm_oss.transactionIndices': 'apm-*-transaction-*', + 'apm_oss.spanIndices': 'apm-*-span-*', + 'apm_oss.errorIndices': 'apm-*-error-*', + 'apm_oss.metricsIndices': 'apm-*-metrics-*', + }, +} as unknown) as Setup; + describe('createStaticIndexPattern', () => { it(`should not create index pattern if 'xpack.apm.autocreateApmIndexPattern=false'`, async () => { - const setup = {} as Setup; - - const savedObjectsClient = getMockSavedObjectsClient(); + const savedObjectsClient = getMockSavedObjectsClient('apm-*'); await createStaticIndexPattern({ setup, config: { 'xpack.apm.autocreateApmIndexPattern': false } as APMConfig, @@ -37,14 +45,12 @@ describe('createStaticIndexPattern', () => { }); it(`should not create index pattern if no APM data is found`, async () => { - const setup = {} as Setup; - // does not have APM data jest .spyOn(HistoricalAgentData, 'hasHistoricalAgentData') .mockResolvedValue(false); - const savedObjectsClient = getMockSavedObjectsClient(); + const savedObjectsClient = getMockSavedObjectsClient('apm-*'); await createStaticIndexPattern({ setup, @@ -56,14 +62,12 @@ describe('createStaticIndexPattern', () => { }); it(`should create index pattern`, async () => { - const setup = {} as Setup; - // does have APM data jest .spyOn(HistoricalAgentData, 'hasHistoricalAgentData') .mockResolvedValue(true); - const savedObjectsClient = getMockSavedObjectsClient(); + const savedObjectsClient = getMockSavedObjectsClient('apm-*'); await createStaticIndexPattern({ setup, @@ -75,23 +79,20 @@ describe('createStaticIndexPattern', () => { expect(savedObjectsClient.create).toHaveBeenCalled(); }); - it(`should upgrade an index pattern if 'apm_oss.indexPattern' does not match title`, async () => { - const setup = {} as Setup; - + it(`should overwrite the index pattern if the new index pattern title does not match the old index pattern title`, async () => { // does have APM data jest .spyOn(HistoricalAgentData, 'hasHistoricalAgentData') .mockResolvedValue(true); - const savedObjectsClient = getMockSavedObjectsClient(); - const apmIndexPatternTitle = 'traces-apm*,logs-apm*,metrics-apm*,apm-*'; + const savedObjectsClient = getMockSavedObjectsClient('apm-*'); + const expectedIndexPatternTitle = + 'apm-*-transaction-*,apm-*-span-*,apm-*-error-*,apm-*-metrics-*'; await createStaticIndexPattern({ setup, config: { 'xpack.apm.autocreateApmIndexPattern': true, - // eslint-disable-next-line @typescript-eslint/naming-convention - 'apm_oss.indexPattern': apmIndexPatternTitle, } as APMConfig, savedObjectsClient, spaceId: 'default', @@ -101,29 +102,26 @@ describe('createStaticIndexPattern', () => { expect(savedObjectsClient.create).toHaveBeenCalled(); // @ts-ignore expect(savedObjectsClient.create.mock.calls[0][1].title).toBe( - apmIndexPatternTitle + expectedIndexPatternTitle ); // @ts-ignore expect(savedObjectsClient.create.mock.calls[0][2].overwrite).toBe(true); }); - it(`should not upgrade an index pattern if 'apm_oss.indexPattern' already match existing title`, async () => { - const setup = {} as Setup; - + it(`should not overwrite an index pattern if the new index pattern title matches the old index pattern title`, async () => { // does have APM data jest .spyOn(HistoricalAgentData, 'hasHistoricalAgentData') .mockResolvedValue(true); - const savedObjectsClient = getMockSavedObjectsClient(); - const apmIndexPatternTitle = 'apm-*'; + const savedObjectsClient = getMockSavedObjectsClient( + 'apm-*-transaction-*,apm-*-span-*,apm-*-error-*,apm-*-metrics-*' + ); await createStaticIndexPattern({ setup, config: { 'xpack.apm.autocreateApmIndexPattern': true, - // eslint-disable-next-line @typescript-eslint/naming-convention - 'apm_oss.indexPattern': apmIndexPatternTitle, } as APMConfig, savedObjectsClient, spaceId: 'default', diff --git a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts index 5dbee59b4ce86..414414c6bfe65 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts @@ -45,29 +45,12 @@ export async function createStaticIndexPattern({ return false; } - const apmIndexPatternTitle = getApmIndexPatternTitle(config); - - if (!overwrite) { - try { - const { - attributes: { title: existingApmIndexPatternTitle }, - }: { - attributes: ApmIndexPatternAttributes; - } = await savedObjectsClient.get( - 'index-pattern', - APM_STATIC_INDEX_PATTERN_ID - ); - // if the existing index pattern does not matches the new one, force an update - if (existingApmIndexPatternTitle !== apmIndexPatternTitle) { - overwrite = true; - } - } catch (e) { - // if the index pattern (saved object) is not found, then we can continue with creation - if (!SavedObjectsErrorHelpers.isNotFoundError(e)) { - throw e; - } - } - } + const apmIndexPatternTitle = getApmIndexPatternTitle(setup.indices); + const forceOverwrite = await getForceOverwrite({ + apmIndexPatternTitle, + overwrite, + savedObjectsClient, + }); try { await withApmSpan('create_index_pattern_saved_object', () => @@ -79,7 +62,7 @@ export async function createStaticIndexPattern({ }, { id: APM_STATIC_INDEX_PATTERN_ID, - overwrite, + overwrite: forceOverwrite ? true : overwrite, namespace: spaceId, } ) @@ -95,3 +78,33 @@ export async function createStaticIndexPattern({ } }); } + +// force an overwrite of the index pattern if the index pattern has been changed +async function getForceOverwrite({ + savedObjectsClient, + overwrite, + apmIndexPatternTitle, +}: { + savedObjectsClient: InternalSavedObjectsClient; + overwrite: boolean; + apmIndexPatternTitle: string; +}) { + if (!overwrite) { + try { + const existingIndexPattern = await savedObjectsClient.get( + 'index-pattern', + APM_STATIC_INDEX_PATTERN_ID + ); + + // if the existing index pattern does not matches the new one, force an update + return existingIndexPattern.attributes.title !== apmIndexPatternTitle; + } catch (e) { + // ignore exception if the index pattern (saved object) is not found + if (SavedObjectsErrorHelpers.isNotFoundError(e)) { + return false; + } + + throw e; + } + } +} diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.test.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.test.ts new file mode 100644 index 0000000000000..8103630157584 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; +import { getApmIndexPatternTitle } from './get_apm_index_pattern_title'; + +describe('getApmIndexPatternTitle', () => { + it('returns an index pattern title by combining existing indicies', () => { + const title = getApmIndexPatternTitle({ + 'apm_oss.transactionIndices': 'apm-*-transaction-*', + 'apm_oss.spanIndices': 'apm-*-span-*', + 'apm_oss.errorIndices': 'apm-*-error-*', + 'apm_oss.metricsIndices': 'apm-*-metrics-*', + } as ApmIndicesConfig); + expect(title).toBe( + 'apm-*-transaction-*,apm-*-span-*,apm-*-error-*,apm-*-metrics-*' + ); + }); + + it('removes duplicates', () => { + const title = getApmIndexPatternTitle({ + 'apm_oss.transactionIndices': 'apm-*', + 'apm_oss.spanIndices': 'apm-*', + 'apm_oss.errorIndices': 'apm-*', + 'apm_oss.metricsIndices': 'apm-*', + } as ApmIndicesConfig); + expect(title).toBe('apm-*'); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts index faec64c798c7d..e65f200130e9a 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts @@ -5,10 +5,14 @@ * 2.0. */ -import { APMRouteHandlerResources } from '../../routes/typings'; +import { uniq } from 'lodash'; +import { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; -export function getApmIndexPatternTitle( - config: APMRouteHandlerResources['config'] -) { - return config['apm_oss.indexPattern']; +export function getApmIndexPatternTitle(apmIndicesConfig: ApmIndicesConfig) { + return uniq([ + apmIndicesConfig['apm_oss.transactionIndices'], + apmIndicesConfig['apm_oss.spanIndices'], + apmIndicesConfig['apm_oss.errorIndices'], + apmIndicesConfig['apm_oss.metricsIndices'], + ]).join(','); } diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts index 8bbc22fbf289d..19bd801a1f0e1 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts @@ -11,6 +11,8 @@ import { } from '../../../../../../src/plugins/data/server'; import { APMRouteHandlerResources } from '../../routes/typings'; import { withApmSpan } from '../../utils/with_apm_span'; +import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; +import { getApmIndexPatternTitle } from './get_apm_index_pattern_title'; export interface IndexPatternTitleAndFields { title: string; @@ -18,14 +20,17 @@ export interface IndexPatternTitleAndFields { fields: FieldDescriptor[]; } -// TODO: this is currently cached globally. In the future we might want to cache this per user export const getDynamicIndexPattern = ({ config, context, logger, }: Pick) => { return withApmSpan('get_dynamic_index_pattern', async () => { - const indexPatternTitle = config['apm_oss.indexPattern']; + const apmIndicies = await getApmIndices({ + savedObjectsClient: context.core.savedObjects.client, + config, + }); + const indexPatternTitle = getApmIndexPatternTitle(apmIndicies); const indexPatternsFetcher = new IndexPatternsFetcher( context.core.elasticsearch.client.asCurrentUser 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 6b96b6b9d2131..5500e336c3542 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 bbeb8435e61bf..0000000000000 --- 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 7f67147a75580..0000000000000 --- 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 2034c29b01d94..0000000000000 --- 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 f454b8c8274f1..0000000000000 --- 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 89fcda926d547..12f7902b51488 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 fb0c6fea4879a..13cf752618537 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 f7e24ac6e1335..ec91165cb481b 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 415f19e892741..0000000000000 --- 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 711c5f736d774..b4668138eefab 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 5ba7b4d7c957a..073bb122896ff 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 e9986bd9f0cf5..b623f6c73f896 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 cfa1bf2a5ad71..ce9014004f4b0 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 d0aac8987e070..53f357ed1135f 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 dc11b4860a8b6..cb1500e70babc 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 ddfd87c83f9f3..6338422b022da 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 3be3438b2d18f..b8bce75320942 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 8bd9f3d4e582c..445f432f2d5ad 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 b95db6d2691f1..fd5f52207d4c5 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 e2cdbab830e0d..fb1639b5d5f4a 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 c33b131d9cbd7..e691b81e4adcf 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 5245af6cdadcd..d3d14260df65c 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 823abe936e223..6e2981032d67d 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 81fe6697d1fb1..bc8ab4be97c11 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 688af72e8f6d3..150348e2a7aa2 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 aeb67a4d6884b..390243295c4f0 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 a20720944f19b..1fff8cde5bbb3 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 33adff4af7a52..aa7d9f341a345 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 73df48a0d8170..fdf383453e17f 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 35e59054ad01f..25e5f62564b04 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 9b2a4807d4863..e6faeb16247fb 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 18fc18af1472e..1dac98d785f3c 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 bb76769fe94b5..7b0d00d0d9b57 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 cc50c8d4d860a..7a8752e45c6f2 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 52cfe6168232d..88d4f1a57adeb 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 116b5d1645601..31ab7392155bc 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 22876684bec7e..c80b2533d7e32 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 c4869aac187c6..a07abd356db6d 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 cab2e283935d6..1a5d518b7e47a 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 bd230687314e6..8d9e2ed88ba37 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 839d6a33cfe05..64b746b72534a 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 6f662363d0c42..e15962f2979ba 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 0000000000000..713c5e390ca8b --- /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 0000000000000..5b887f15a584e --- /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 e69d2f55b6c56..73a59021b01ed 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 b5ab4a072be6c..b45b95666326f 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 0000000000000..c0376852b2505 --- /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 000fd57c718b7..727bc6cd787a0 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 fadeae338cbdb..1c6d1cdef37ca 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 @@ -88,13 +84,12 @@ export class APMPlugin plugins.apmOss.config, this.initContext.config.get() ); - this.currentConfig = currentConfig; if ( plugins.taskManager && plugins.usageCollection && - this.currentConfig['xpack.apm.telemetryCollectionEnabled'] + currentConfig['xpack.apm.telemetryCollectionEnabled'] ) { createApmTelemetry({ core, @@ -156,21 +151,22 @@ export class APMPlugin }; }) as APMRouteHandlerResources['plugins']; - plugins.home?.tutorials.registerTutorial( - tutorialProvider({ - isEnabled: this.currentConfig['xpack.apm.ui.enabled'], - indexPatternTitle: this.currentConfig['apm_oss.indexPattern'], - cloud: plugins.cloud, - isFleetPluginEnabled: !isEmpty(resourcePlugins.fleet), - indices: { - errorIndices: this.currentConfig['apm_oss.errorIndices'], - metricsIndices: this.currentConfig['apm_oss.metricsIndices'], - onboardingIndices: this.currentConfig['apm_oss.onboardingIndices'], - sourcemapIndices: this.currentConfig['apm_oss.sourcemapIndices'], - transactionIndices: this.currentConfig['apm_oss.transactionIndices'], - }, - }) - ); + const boundGetApmIndices = async () => + getApmIndices({ + savedObjectsClient: await getInternalSavedObjectsClient(core), + config: await mergedConfig$.pipe(take(1)).toPromise(), + }); + + boundGetApmIndices().then((indices) => { + plugins.home?.tutorials.registerTutorial( + tutorialProvider({ + apmConfig: currentConfig, + apmIndices: indices, + cloud: plugins.cloud, + isFleetPluginEnabled: !isEmpty(resourcePlugins.fleet), + }) + ); + }); const telemetryUsageCounter = resourcePlugins.usageCollection?.setup.createUsageCounter( APM_SERVER_FEATURE_ID @@ -189,12 +185,6 @@ export class APMPlugin telemetryUsageCounter, }); - const boundGetApmIndices = async () => - getApmIndices({ - savedObjectsClient: await getInternalSavedObjectsClient(core), - config: await mergedConfig$.pipe(take(1)).toPromise(), - }); - if (plugins.alerting) { registerApmAlerts({ ruleDataClient, @@ -208,7 +198,7 @@ export class APMPlugin registerFleetPolicyCallbacks({ plugins: resourcePlugins, ruleDataClient, - config: this.currentConfig, + config: currentConfig, logger: this.logger, }); @@ -223,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/apm/server/routes/index_pattern.ts b/x-pack/plugins/apm/server/routes/index_pattern.ts index 190baf3bbc270..c957e828bf12a 100644 --- a/x-pack/plugins/apm/server/routes/index_pattern.ts +++ b/x-pack/plugins/apm/server/routes/index_pattern.ts @@ -8,7 +8,6 @@ import { createStaticIndexPattern } from '../lib/index_pattern/create_static_index_pattern'; import { createApmServerRouteRepository } from './create_apm_server_route_repository'; import { setupRequest } from '../lib/helpers/setup_request'; -import { getApmIndexPatternTitle } from '../lib/index_pattern/get_apm_index_pattern_title'; import { getDynamicIndexPattern } from '../lib/index_pattern/get_dynamic_index_pattern'; import { createApmServerRoute } from './create_apm_server_route'; @@ -56,17 +55,6 @@ const dynamicIndexPatternRoute = createApmServerRoute({ }, }); -const indexPatternTitleRoute = createApmServerRoute({ - endpoint: 'GET /api/apm/index_pattern/title', - options: { tags: ['access:apm'] }, - handler: async ({ config }) => { - return { - indexPatternTitle: getApmIndexPatternTitle(config), - }; - }, -}); - export const indexPatternRouteRepository = createApmServerRouteRepository() .add(staticIndexPatternRoute) - .add(dynamicIndexPatternRoute) - .add(indexPatternTitleRoute); + .add(dynamicIndexPatternRoute); diff --git a/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts b/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts index 38c8dbfcbe8ba..fb9fbae33ac82 100644 --- a/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts +++ b/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { APMConfig } from '../..'; import { INSTRUCTION_VARIANT, InstructionsSchema, @@ -33,18 +34,16 @@ import { } from '../../../common/tutorial/instructions/apm_server_instructions'; export function onPremInstructions({ - errorIndices, - transactionIndices, - metricsIndices, - sourcemapIndices, - onboardingIndices, + apmConfig, isFleetPluginEnabled, }: { - errorIndices: string; - transactionIndices: string; - metricsIndices: string; - sourcemapIndices: string; - onboardingIndices: string; + apmConfig: Pick< + APMConfig, + | 'apm_oss.errorIndices' + | 'apm_oss.transactionIndices' + | 'apm_oss.metricsIndices' + | 'apm_oss.onboardingIndices' + >; isFleetPluginEnabled: boolean; }): InstructionsSchema { const EDIT_CONFIG = createEditConfig(); @@ -145,7 +144,7 @@ export function onPremInstructions({ } ), esHitsCheck: { - index: onboardingIndices, + index: apmConfig['apm_oss.onboardingIndices'], query: { bool: { filter: [ @@ -238,22 +237,16 @@ export function onPremInstructions({ ), esHitsCheck: { index: [ - errorIndices, - transactionIndices, - metricsIndices, - sourcemapIndices, + apmConfig['apm_oss.errorIndices'], + apmConfig['apm_oss.transactionIndices'], + apmConfig['apm_oss.metricsIndices'], ], query: { bool: { filter: [ { terms: { - 'processor.event': [ - 'error', - 'transaction', - 'metric', - 'sourcemap', - ], + 'processor.event': ['error', 'transaction', 'metric'], }, }, { range: { 'observer.version_major': { gte: 7 } } }, diff --git a/x-pack/plugins/apm/server/tutorial/index.ts b/x-pack/plugins/apm/server/tutorial/index.ts index edf056a6d1be4..78dd1110e7c62 100644 --- a/x-pack/plugins/apm/server/tutorial/index.ts +++ b/x-pack/plugins/apm/server/tutorial/index.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { APMConfig } from '..'; import { ArtifactsSchema, TutorialsCategory, @@ -13,6 +14,8 @@ import { } from '../../../../../src/plugins/home/server'; import { CloudSetup } from '../../../cloud/server'; import { APM_STATIC_INDEX_PATTERN_ID } from '../../common/index_pattern_constants'; +import { getApmIndexPatternTitle } from '../lib/index_pattern/get_apm_index_pattern_title'; +import { ApmIndicesConfig } from '../lib/settings/apm_indices/get_apm_indices'; import { createElasticCloudInstructions } from './envs/elastic_cloud'; import { onPremInstructions } from './envs/on_prem'; import apmIndexPattern from './index_pattern.json'; @@ -24,24 +27,18 @@ const apmIntro = i18n.translate('xpack.apm.tutorial.introduction', { const moduleName = 'apm'; export const tutorialProvider = ({ - isEnabled, - indexPatternTitle, - indices, + apmConfig, + apmIndices, cloud, isFleetPluginEnabled, }: { - isEnabled: boolean; - indexPatternTitle: string; + apmConfig: APMConfig; + apmIndices: ApmIndicesConfig; cloud?: CloudSetup; - indices: { - errorIndices: string; - transactionIndices: string; - metricsIndices: string; - sourcemapIndices: string; - onboardingIndices: string; - }; isFleetPluginEnabled: boolean; }) => () => { + const indexPatternTitle = getApmIndexPatternTitle(apmIndices); + const savedObjects = [ { ...apmIndexPattern, @@ -68,7 +65,7 @@ export const tutorialProvider = ({ ], }; - if (isEnabled) { + if (apmConfig['xpack.apm.ui.enabled']) { // @ts-expect-error artifacts.application is readonly artifacts.application = { path: '/app/apm', @@ -106,7 +103,7 @@ It allows you to monitor the performance of thousands of applications in real ti euiIconType: 'apmApp', artifacts, customStatusCheckName: 'apm_fleet_server_status_check', - onPrem: onPremInstructions({ ...indices, isFleetPluginEnabled }), + onPrem: onPremInstructions({ apmConfig, isFleetPluginEnabled }), elasticCloud: createElasticCloudInstructions(cloud), previewImagePath: '/plugins/apm/assets/apm.png', savedObjects, diff --git a/x-pack/plugins/cases/public/components/create/description.tsx b/x-pack/plugins/cases/public/components/create/description.tsx index f589587356c4b..d831470ea1d54 100644 --- a/x-pack/plugins/cases/public/components/create/description.tsx +++ b/x-pack/plugins/cases/public/components/create/description.tsx @@ -7,7 +7,7 @@ import React, { memo, useEffect, useRef } from 'react'; import { MarkdownEditorForm } from '../markdown_editor'; -import { UseField, useFormContext } from '../../common/shared_imports'; +import { UseField, useFormContext, useFormData } from '../../common/shared_imports'; import { useLensDraftComment } from '../markdown_editor/plugins/lens/use_lens_draft_comment'; interface Props { @@ -24,12 +24,21 @@ const DescriptionComponent: React.FC = ({ isLoading }) => { clearDraftComment, } = useLensDraftComment(); const { setFieldValue } = useFormContext(); + const [{ title, tags }] = useFormData({ watch: ['title', 'tags'] }); const editorRef = useRef>(); useEffect(() => { if (draftComment?.commentId === fieldName && editorRef.current) { setFieldValue(fieldName, draftComment.comment); + if (draftComment.caseTitle) { + setFieldValue('title', draftComment.caseTitle); + } + + if (draftComment.caseTags && draftComment.caseTags.length > 0) { + setFieldValue('tags', draftComment.caseTags); + } + if (hasIncomingLensState) { openLensModal({ editorRef: editorRef.current }); } else { @@ -48,6 +57,8 @@ const DescriptionComponent: React.FC = ({ isLoading }) => { dataTestSubj: 'caseDescription', idAria: 'caseDescription', isDisabled: isLoading, + caseTitle: title, + caseTags: tags, }} /> ); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/context.tsx b/x-pack/plugins/cases/public/components/markdown_editor/context.tsx index d7f5b0612cb73..44b65d6277b3f 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/context.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/context.tsx @@ -10,4 +10,6 @@ import React from 'react'; export const CommentEditorContext = React.createContext<{ editorId: string; value: string; + caseTitle?: string; + caseTags?: string[]; } | null>(null); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx b/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx index b61fae25aa399..f2351a2b2d793 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx @@ -9,7 +9,6 @@ import React, { memo, forwardRef, useCallback, - useMemo, useRef, useState, useImperativeHandle, @@ -24,7 +23,6 @@ import { } from '@elastic/eui'; import { ContextShape } from '@elastic/eui/src/components/markdown_editor/markdown_context'; import { usePlugins } from './use_plugins'; -import { CommentEditorContext } from './context'; import { useLensButtonToggle } from './plugins/lens/use_lens_button_toggle'; interface MarkdownEditorProps { @@ -65,14 +63,6 @@ const MarkdownEditorComponent = forwardRef ({ - editorId, - value, - }), - [editorId, value] - ); - // @ts-expect-error useImperativeHandle(ref, () => { if (!editorRef.current) { @@ -88,22 +78,20 @@ const MarkdownEditorComponent = forwardRef - - + ); } ); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx index 2719f38f98fc2..94881e30e10f3 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx @@ -5,19 +5,22 @@ * 2.0. */ -import React, { forwardRef } from 'react'; +import React, { forwardRef, useMemo } from 'react'; import styled from 'styled-components'; import { EuiMarkdownEditorProps, EuiFormRow, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { FieldHook, getFieldValidityAndErrorMessage } from '../../common/shared_imports'; import { MarkdownEditor, MarkdownEditorRef } from './editor'; +import { CommentEditorContext } from './context'; type MarkdownEditorFormProps = EuiMarkdownEditorProps & { id: string; - field: FieldHook; + field: FieldHook; dataTestSubj: string; idAria: string; isDisabled?: boolean; bottomRightContent?: React.ReactNode; + caseTitle?: string; + caseTags?: string[]; }; const BottomContentWrapper = styled(EuiFlexGroup)` @@ -28,11 +31,21 @@ const BottomContentWrapper = styled(EuiFlexGroup)` export const MarkdownEditorForm = React.memo( forwardRef( - ({ id, field, dataTestSubj, idAria, bottomRightContent }, ref) => { + ({ id, field, dataTestSubj, idAria, bottomRightContent, caseTitle, caseTags }, ref) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + const commentEditorContextValue = useMemo( + () => ({ + editorId: id, + value: field.value, + caseTitle, + caseTags, + }), + [id, field.value, caseTitle, caseTags] + ); + return ( - <> + @@ -57,7 +70,7 @@ export const MarkdownEditorForm = React.memo( {bottomRightContent} )} - + ); } ) diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/plugin.tsx b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/plugin.tsx index 732a99968e883..f840b1ea5bb50 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/plugin.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/plugin.tsx @@ -135,6 +135,8 @@ const LensEditorComponent: LensEuiMarkdownEditorUiPlugin['editor'] = ({ commentId: commentEditorContext?.editorId, comment: commentEditorContext?.value, position: node?.position, + caseTitle: commentEditorContext?.caseTitle, + caseTags: commentEditorContext?.caseTags, }); lens?.navigateToPrefilledEditor(undefined, { @@ -145,10 +147,12 @@ const LensEditorComponent: LensEuiMarkdownEditorUiPlugin['editor'] = ({ storage, commentEditorContext?.editorId, commentEditorContext?.value, + commentEditorContext?.caseTitle, + commentEditorContext?.caseTags, node?.position, + lens, currentAppId, originatingPath, - lens, ]); const handleEditInLensClick = useCallback( @@ -157,6 +161,8 @@ const LensEditorComponent: LensEuiMarkdownEditorUiPlugin['editor'] = ({ commentId: commentEditorContext?.editorId, comment: commentEditorContext?.value, position: node?.position, + caseTitle: commentEditorContext?.caseTitle, + caseTags: commentEditorContext?.caseTags, }); lens?.navigateToPrefilledEditor( @@ -177,11 +183,13 @@ const LensEditorComponent: LensEuiMarkdownEditorUiPlugin['editor'] = ({ storage, commentEditorContext?.editorId, commentEditorContext?.value, + commentEditorContext?.caseTitle, + commentEditorContext?.caseTags, node?.position, + node?.attributes, + lens, currentAppId, originatingPath, - lens, - node?.attributes, ] ); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/use_lens_draft_comment.ts b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/use_lens_draft_comment.ts index 2a77037b300a3..a2dccc0e44d74 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/use_lens_draft_comment.ts +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/use_lens_draft_comment.ts @@ -16,6 +16,8 @@ interface DraftComment { commentId: string; comment: string; position: EuiMarkdownAstNodePosition; + caseTitle?: string; + caseTags?: string[]; } export const useLensDraftComment = () => { diff --git a/x-pack/plugins/drilldowns/url_drilldown/kibana.json b/x-pack/plugins/drilldowns/url_drilldown/kibana.json index a4552d201f263..83ea4b7be54bf 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 07c6addda2767..a6896367bd613 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 65c665a182e18..491501b9dd4a7 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 3d0c55a08d1bf..2a56a5fa0e102 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 ef9045b9ba108..660bcae9fe146 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 09416ce18aecb..36cd0440b64dd 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 90518de77f11f..5bc0fffdf1963 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/common/services/full_agent_policy_to_yaml.ts b/x-pack/plugins/fleet/common/services/full_agent_policy_to_yaml.ts index a2c1dcd83dd20..f4193d619e168 100644 --- a/x-pack/plugins/fleet/common/services/full_agent_policy_to_yaml.ts +++ b/x-pack/plugins/fleet/common/services/full_agent_policy_to_yaml.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { safeDump } from 'js-yaml'; +import type { safeDump } from 'js-yaml'; import type { FullAgentPolicy } from '../types'; @@ -25,8 +25,8 @@ const POLICY_KEYS_ORDER = [ 'input', ]; -export const fullAgentPolicyToYaml = (policy: FullAgentPolicy): string => { - return safeDump(policy, { +export const fullAgentPolicyToYaml = (policy: FullAgentPolicy, toYaml: typeof safeDump): string => { + return toYaml(policy, { skipInvalid: true, sortKeys: (keyA: string, keyB: string) => { const indexA = POLICY_KEYS_ORDER.indexOf(keyA); diff --git a/x-pack/plugins/fleet/common/services/validate_package_policy.test.ts b/x-pack/plugins/fleet/common/services/validate_package_policy.test.ts index 3e4f156da3379..30bd9f071feb8 100644 --- a/x-pack/plugins/fleet/common/services/validate_package_policy.test.ts +++ b/x-pack/plugins/fleet/common/services/validate_package_policy.test.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { safeLoad } from 'js-yaml'; + import { installationStatuses } from '../constants'; import type { PackageInfo, NewPackagePolicy, RegistryPolicyTemplate } from '../types'; @@ -371,13 +373,13 @@ describe('Fleet - validatePackagePolicy()', () => { }; it('returns no errors for valid package policy', () => { - expect(validatePackagePolicy(validPackagePolicy, mockPackage)).toEqual( + expect(validatePackagePolicy(validPackagePolicy, mockPackage, safeLoad)).toEqual( noErrorsValidationResults ); }); it('returns errors for invalid package policy', () => { - expect(validatePackagePolicy(invalidPackagePolicy, mockPackage)).toEqual({ + expect(validatePackagePolicy(invalidPackagePolicy, mockPackage, safeLoad)).toEqual({ name: ['Name is required'], description: null, namespace: null, @@ -423,7 +425,11 @@ describe('Fleet - validatePackagePolicy()', () => { enabled: false, })); expect( - validatePackagePolicy({ ...validPackagePolicy, inputs: disabledInputs }, mockPackage) + validatePackagePolicy( + { ...validPackagePolicy, inputs: disabledInputs }, + mockPackage, + safeLoad + ) ).toEqual(noErrorsValidationResults); }); @@ -439,7 +445,8 @@ describe('Fleet - validatePackagePolicy()', () => { expect( validatePackagePolicy( { ...invalidPackagePolicy, inputs: inputsWithDisabledStreams }, - mockPackage + mockPackage, + safeLoad ) ).toEqual({ name: ['Name is required'], @@ -485,10 +492,14 @@ describe('Fleet - validatePackagePolicy()', () => { it('returns no errors for packages with no package policies', () => { expect( - validatePackagePolicy(validPackagePolicy, { - ...mockPackage, - policy_templates: undefined, - }) + validatePackagePolicy( + validPackagePolicy, + { + ...mockPackage, + policy_templates: undefined, + }, + safeLoad + ) ).toEqual({ name: null, description: null, @@ -496,10 +507,14 @@ describe('Fleet - validatePackagePolicy()', () => { inputs: null, }); expect( - validatePackagePolicy(validPackagePolicy, { - ...mockPackage, - policy_templates: [], - }) + validatePackagePolicy( + validPackagePolicy, + { + ...mockPackage, + policy_templates: [], + }, + safeLoad + ) ).toEqual({ name: null, description: null, @@ -510,10 +525,14 @@ describe('Fleet - validatePackagePolicy()', () => { it('returns no errors for packages with no inputs', () => { expect( - validatePackagePolicy(validPackagePolicy, { - ...mockPackage, - policy_templates: [{} as RegistryPolicyTemplate], - }) + validatePackagePolicy( + validPackagePolicy, + { + ...mockPackage, + policy_templates: [{} as RegistryPolicyTemplate], + }, + safeLoad + ) ).toEqual({ name: null, description: null, @@ -521,10 +540,14 @@ describe('Fleet - validatePackagePolicy()', () => { inputs: null, }); expect( - validatePackagePolicy(validPackagePolicy, { - ...mockPackage, - policy_templates: [({ inputs: [] } as unknown) as RegistryPolicyTemplate], - }) + validatePackagePolicy( + validPackagePolicy, + { + ...mockPackage, + policy_templates: [({ inputs: [] } as unknown) as RegistryPolicyTemplate], + }, + safeLoad + ) ).toEqual({ name: null, description: null, @@ -539,7 +562,8 @@ describe('Fleet - validatePackagePolicy()', () => { expect( validatePackagePolicy( INVALID_AWS_POLICY as NewPackagePolicy, - (AWS_PACKAGE as unknown) as PackageInfo + (AWS_PACKAGE as unknown) as PackageInfo, + safeLoad ) ).toMatchSnapshot(); }); @@ -549,7 +573,8 @@ describe('Fleet - validatePackagePolicy()', () => { validationHasErrors( validatePackagePolicy( VALID_AWS_POLICY as NewPackagePolicy, - (AWS_PACKAGE as unknown) as PackageInfo + (AWS_PACKAGE as unknown) as PackageInfo, + safeLoad ) ) ).toBe(false); diff --git a/x-pack/plugins/fleet/common/services/validate_package_policy.ts b/x-pack/plugins/fleet/common/services/validate_package_policy.ts index 67df65b2f12bf..12bef2dfd7c27 100644 --- a/x-pack/plugins/fleet/common/services/validate_package_policy.ts +++ b/x-pack/plugins/fleet/common/services/validate_package_policy.ts @@ -7,7 +7,6 @@ import { getFlattenedObject } from '@kbn/std'; import { i18n } from '@kbn/i18n'; -import { safeLoad } from 'js-yaml'; import { keyBy } from 'lodash'; import type { @@ -47,7 +46,8 @@ export type PackagePolicyValidationResults = { */ export const validatePackagePolicy = ( packagePolicy: NewPackagePolicy, - packageInfo: PackageInfo + packageInfo: PackageInfo, + safeLoadYaml: (yaml: string) => any ): PackagePolicyValidationResults => { const hasIntegrations = doesPackageHaveIntegrations(packageInfo); const validationResults: PackagePolicyValidationResults = { @@ -75,7 +75,12 @@ export const validatePackagePolicy = ( const packageVars = Object.entries(packagePolicy.vars || {}); if (packageVars.length) { validationResults.vars = packageVars.reduce((results, [name, varEntry]) => { - results[name] = validatePackagePolicyConfig(varEntry, packageVarsByName[name], name); + results[name] = validatePackagePolicyConfig( + varEntry, + packageVarsByName[name], + name, + safeLoadYaml + ); return results; }, {} as ValidationEntry); } @@ -139,7 +144,8 @@ export const validatePackagePolicy = ( ? validatePackagePolicyConfig( configEntry, inputVarDefsByPolicyTemplateAndType[inputKey][name], - name + name, + safeLoadYaml ) : null; return results; @@ -162,7 +168,12 @@ export const validatePackagePolicy = ( (results, [name, configEntry]) => { results[name] = streamVarDefs && streamVarDefs[name] && input.enabled && stream.enabled - ? validatePackagePolicyConfig(configEntry, streamVarDefs[name], name) + ? validatePackagePolicyConfig( + configEntry, + streamVarDefs[name], + name, + safeLoadYaml + ) : null; return results; }, @@ -191,7 +202,8 @@ export const validatePackagePolicy = ( export const validatePackagePolicyConfig = ( configEntry: PackagePolicyConfigRecordEntry, varDef: RegistryVarsEntry, - varName: string + varName: string, + safeLoadYaml: (yaml: string) => any ): string[] | null => { const errors = []; const { value } = configEntry; @@ -223,7 +235,7 @@ export const validatePackagePolicyConfig = ( if (varDef.type === 'yaml') { try { - parsedValue = safeLoad(value); + parsedValue = safeLoadYaml(value); } catch (e) { errors.push( i18n.translate('xpack.fleet.packagePolicyValidation.invalidYamlFormatErrorMessage', { 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 0000000000000..ec9fa031d09d3 --- /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/public/applications/fleet/sections/agent_policy/components/agent_policy_yaml_flyout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_yaml_flyout.tsx index e434347c2c367..8cc07556eee7f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_yaml_flyout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_yaml_flyout.tsx @@ -8,6 +8,7 @@ import React, { memo } from 'react'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; +import { safeDump } from 'js-yaml'; import { EuiCodeBlock, EuiFlexGroup, @@ -54,7 +55,7 @@ export const AgentPolicyYamlFlyout = memo<{ policyId: string; onClose: () => voi ) : ( - {fullAgentPolicyToYaml(yamlData!.item)} + {fullAgentPolicyToYaml(yamlData!.item, safeDump)} ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx index 47ecfd6bd121c..1f7fa0ceb354b 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx @@ -23,6 +23,7 @@ import { } from '@elastic/eui'; import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; import type { ApplicationStart } from 'kibana/public'; +import { safeLoad } from 'js-yaml'; import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; import type { @@ -191,7 +192,8 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { if (packageInfo) { const newValidationResult = validatePackagePolicy( newPackagePolicy || packagePolicy, - packageInfo + packageInfo, + safeLoad ); setValidationResults(newValidationResult); // eslint-disable-next-line no-console diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/has_invalid_but_required_var.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/has_invalid_but_required_var.ts index bf75b05f41b8d..e41b98c05e41c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/has_invalid_but_required_var.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/has_invalid_but_required_var.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { safeLoad } from 'js-yaml'; + import type { PackagePolicyConfigRecord, RegistryVarsEntry } from '../../../../types'; import { validatePackagePolicyConfig } from './'; @@ -25,7 +27,8 @@ export const hasInvalidButRequiredVar = ( validatePackagePolicyConfig( packagePolicyVars[registryVar.name], registryVar, - registryVar.name + registryVar.name, + safeLoad )?.length) ) ) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index ea027f95eb9e8..a36bc988da89f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -9,6 +9,7 @@ import React, { useState, useEffect, useCallback, useMemo, memo } from 'react'; import { useRouteMatch } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { safeLoad } from 'js-yaml'; import { EuiButtonEmpty, EuiButton, @@ -201,7 +202,9 @@ export const EditPackagePolicyForm = memo<{ if (packageData?.response) { setPackageInfo(packageData.response); - setValidationResults(validatePackagePolicy(newPackagePolicy, packageData.response)); + setValidationResults( + validatePackagePolicy(newPackagePolicy, packageData.response, safeLoad) + ); setFormState('VALID'); } } @@ -239,7 +242,8 @@ export const EditPackagePolicyForm = memo<{ if (packageInfo) { const newValidationResult = validatePackagePolicy( newPackagePolicy || packagePolicy, - packageInfo + packageInfo, + safeLoad ); setValidationResults(newValidationResult); // eslint-disable-next-line no-console diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx index 59898b9190c00..d7b9ae2aef08a 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/standalone_instructions.tsx @@ -21,6 +21,7 @@ import { import type { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { safeDump } from 'js-yaml'; import { useStartServices, useLink, sendGetOneAgentPolicyFull } from '../../hooks'; import { fullAgentPolicyToYaml, agentPolicyRouteService } from '../../services'; @@ -71,7 +72,7 @@ export const StandaloneInstructions = React.memo(({ agentPolicy, agentPol fetchFullPolicy(); }, [selectedPolicyId, notifications.toasts]); - const yaml = useMemo(() => fullAgentPolicyToYaml(fullAgentPolicy), [fullAgentPolicy]); + const yaml = useMemo(() => fullAgentPolicyToYaml(fullAgentPolicy, safeDump), [fullAgentPolicy]); const steps = [ DownloadStep(), !agentPolicy diff --git a/x-pack/plugins/fleet/public/search_provider.test.ts b/x-pack/plugins/fleet/public/search_provider.test.ts index 8eee18710d477..ef6bda44d512b 100644 --- a/x-pack/plugins/fleet/public/search_provider.test.ts +++ b/x-pack/plugins/fleet/public/search_provider.test.ts @@ -10,7 +10,7 @@ import { NEVER } from 'rxjs'; import { coreMock } from 'src/core/public/mocks'; -import { createPackageSearchProvider } from './search_provider'; +import { createPackageSearchProvider, toSearchResult } from './search_provider'; import type { GetPackagesResponse } from './types'; jest.mock('./hooks/use_request/epm', () => { @@ -286,4 +286,42 @@ describe('Package search provider', () => { }); }); }); + + describe('toSearchResult', () => { + let startMock: ReturnType; + + beforeEach(() => { + startMock = coreMock.createStart(); + }); + + it('uses svg icon if available', () => { + const pkg = { + ...testResponse[0], + icons: [{ type: 'image/svg+xml', src: '/img_nginx.svg', path: '' }], + }; + const { icon } = toSearchResult(pkg, startMock.application, startMock.http.basePath); + expect(icon).toMatchInlineSnapshot(`"/api/fleet/epm/packages/test/test/img_nginx.svg"`); + }); + + it('prepends base path to svg URL', () => { + startMock = coreMock.createStart({ basePath: '/foo' }); + const pkg = { + ...testResponse[0], + icons: [{ type: 'image/svg+xml', src: '/img_nginx.svg', path: '' }], + }; + const { icon } = toSearchResult(pkg, startMock.application, startMock.http.basePath); + expect(icon).toMatchInlineSnapshot(`"/foo/api/fleet/epm/packages/test/test/img_nginx.svg"`); + }); + + // ICON_TYPES is empty in EUI: https://github.com/elastic/eui/issues/5138 + it.skip('uses eui icon type as fallback', () => { + const pkg = { + ...testResponse[0], + name: 'elasticsearch', + icons: [{ type: 'image/jpg', src: '/img_nginx.svg', path: '' }], + }; + const { icon } = toSearchResult(pkg, startMock.application, startMock.http.basePath); + expect(icon).toMatchInlineSnapshot(`"logoElasticsearch"`); + }); + }); }); diff --git a/x-pack/plugins/fleet/public/search_provider.ts b/x-pack/plugins/fleet/public/search_provider.ts index 5f53c0a8e44ba..403abf89715c8 100644 --- a/x-pack/plugins/fleet/public/search_provider.ts +++ b/x-pack/plugins/fleet/public/search_provider.ts @@ -4,21 +4,23 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { CoreSetup, CoreStart, ApplicationStart } from 'src/core/public'; +import type { CoreSetup, CoreStart, ApplicationStart, IBasePath } from 'src/core/public'; import type { Observable } from 'rxjs'; import { from, of, combineLatest } from 'rxjs'; import { map, shareReplay, takeUntil } from 'rxjs/operators'; +import { ICON_TYPES } from '@elastic/eui'; + import type { GlobalSearchResultProvider, GlobalSearchProviderResult, } from '../../global_search/public'; -import { INTEGRATIONS_PLUGIN_ID } from '../common'; +import { epmRouteService, INTEGRATIONS_PLUGIN_ID } from '../common'; import { sendGetPackages } from './hooks'; -import type { GetPackagesResponse } from './types'; +import type { GetPackagesResponse, PackageListItem } from './types'; import { pagePathGetters } from './constants'; const packageType = 'integration'; @@ -34,16 +36,31 @@ const createPackages$ = () => shareReplay(1) ); -const toSearchResult = ( - pkg: GetPackagesResponse['response'][number], - application: ApplicationStart -) => { +const getEuiIconType = (pkg: PackageListItem, basePath: IBasePath): string | undefined => { + const pkgIcon = pkg.icons?.find((icon) => icon.type === 'image/svg+xml'); + if (!pkgIcon) { + // If no valid SVG is available, attempt to fallback to built-in EUI icons + return ICON_TYPES.find((key) => key.toLowerCase() === `logo${pkg.name}`); + } + + return basePath.prepend( + epmRouteService.getFilePath(`/package/${pkg.name}/${pkg.version}${pkgIcon.src}`) + ); +}; + +/** Exported for testing only @internal */ +export const toSearchResult = ( + pkg: PackageListItem, + application: ApplicationStart, + basePath: IBasePath +): GlobalSearchProviderResult => { const pkgkey = `${pkg.name}-${pkg.version}`; return { id: pkgkey, type: packageType, title: pkg.title, score: 80, + icon: getEuiIconType(pkg, basePath), url: { // prettier-ignore path: `${application.getUrlForApp(INTEGRATIONS_PLUGIN_ID)}${pagePathGetters.integration_details_overview({ pkgkey })[1]}`, @@ -95,13 +112,13 @@ export const createPackageSearchProvider = (core: CoreSetup): GlobalSearchResult return packagesResponse .flatMap( includeAllPackages - ? (pkg) => toSearchResult(pkg, coreStart.application) + ? (pkg) => toSearchResult(pkg, coreStart.application, coreStart.http.basePath) : (pkg) => { if (!term || !pkg.title.toLowerCase().includes(term)) { return []; } - return toSearchResult(pkg, coreStart.application); + return toSearchResult(pkg, coreStart.application, coreStart.http.basePath); } ) .slice(0, maxResults); diff --git a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts index 57401a525b5d7..a7cf606e92c0b 100644 --- a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts @@ -8,6 +8,7 @@ import type { TypeOf } from '@kbn/config-schema'; import type { RequestHandler, ResponseHeaders } from 'src/core/server'; import bluebird from 'bluebird'; +import { safeDump } from 'js-yaml'; import { fullAgentPolicyToYaml } from '../../../common/services'; import { appContextService, agentPolicyService, packagePolicyService } from '../../services'; @@ -269,7 +270,7 @@ export const downloadFullAgentPolicy: RequestHandler< standalone: request.query.standalone === true, }); if (fullAgentPolicy) { - const body = fullAgentPolicyToYaml(fullAgentPolicy); + const body = fullAgentPolicyToYaml(fullAgentPolicy, safeDump); const headers: ResponseHeaders = { 'content-type': 'text/x-yaml', 'content-disposition': `attachment; filename="elastic-agent.yml"`, diff --git a/x-pack/plugins/fleet/server/services/artifacts/mocks.ts b/x-pack/plugins/fleet/server/services/artifacts/mocks.ts index 8bc1768da23a2..2799e1807123d 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 eff35a30ba2d6..0425573c5afaa 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 0000000000000..5e7a3b35c544a --- /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 0000000000000..2dced977229e1 --- /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 e8dac60ddba1a..9dae415838890 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 c999a135e2116..44d633d5f6e53 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/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 8ff3c20b7aa15..598dd16b2928e 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -16,6 +16,7 @@ import type { SavedObjectsClientContract, } from 'src/core/server'; import uuid from 'uuid'; +import { safeLoad } from 'js-yaml'; import type { AuthenticatedUser } from '../../../security/server'; import { @@ -988,7 +989,7 @@ export function overridePackageInputs( inputs, }; - const validationResults = validatePackagePolicy(resultingPackagePolicy, packageInfo); + const validationResults = validatePackagePolicy(resultingPackagePolicy, packageInfo, safeLoad); if (validationHasErrors(validationResults)) { const responseFormattedValidationErrors = Object.entries(getFlattenedObject(validationResults)) diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx index 5234b4e7b0ad5..c459b2c045681 100644 --- a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx @@ -135,8 +135,8 @@ const resultToOption = ( ): EuiSelectableTemplateSitewideOption => { const { id, title, url, icon, type, meta = {} } = result; const { tagIds = [], categoryLabel = '' } = meta as { tagIds: string[]; categoryLabel: string }; - // only displaying icons for applications - const useIcon = type === 'application'; + // only displaying icons for applications and integrations + const useIcon = type === 'application' || type === 'integration'; const option: EuiSelectableTemplateSitewideOption = { key: id, label: title, diff --git a/x-pack/plugins/graph/README.md b/x-pack/plugins/graph/README.md index 99becabf70002..a0d7eb25ff987 100644 --- a/x-pack/plugins/graph/README.md +++ b/x-pack/plugins/graph/README.md @@ -17,18 +17,18 @@ Graph shows only up in the side bar if your server is running on a platinum or t ### Client `public/` -Currently most of the state handling is done by a central angular controller. This controller will be broken up into a redux/saga setup step by step. +Currently state handled by react/redux/saga and the core mutable `GraphWorkspace` instance, which managing the nodes and edges state. It should be rewritten in typescript and integrated into redux store. -* `angular/` contains all code using javascript and angular. Rewriting this code in typescript and react is currently ongoing. When the migration is finished, this folder will go away +* `apps/` contains all graph app routes * `components/` contains react components for various parts of the interface. Components can hold local UI state (e.g. current form data), everything else should be passed in from the caller. Styles should reside in a component-specific stylesheet -* `services/` contains functions that encapsule other parts of Kibana. Stateful dependencies are passed in from the outside. Components should not rely on services directly but have callbacks passed in. Once the migration to redux/saga is complete, only sagas will use services. +* `services/` contains the core workspace logic and functions that encapsule other parts of Kibana. Stateful dependencies are passed in from the outside. Components should not rely on services directly but have callbacks passed in. Once the migration to redux/saga is complete, only sagas will use services * `helpers/` contains side effect free helper functions that can be imported and used from components and services * `state_management/` contains reducers, action creators, selectors and sagas. It also exports the central store creator * Each file covers one functional area (e.g. handling of fields, handling of url templates...) * Generally there is no file separation between reducers, action creators, selectors and sagas of the same functional area * Sagas may contain cross-references between multiple functional areas (e.g. the loading saga sets fields and meta data). Because of this it is possible that circular imports occur. In this case the sagas are moved to a separate file `.sagas.ts`. -* `types/` contains type definitions for unmigrated functions in `angular/` and business objects -* `app.js` is the central entrypoint of the app. It initializes router, state management and root components. This will become `app.tsx` when the migration is complete +* `types/` contains type definitions for unmigrated `GraphWorkspace` methods +* `router.tsx` is the central entrypoint of the app ### Server `server/` diff --git a/x-pack/plugins/graph/public/_main.scss b/x-pack/plugins/graph/public/_main.scss index 22a849b0b2a60..6b32de32c06d0 100644 --- a/x-pack/plugins/graph/public/_main.scss +++ b/x-pack/plugins/graph/public/_main.scss @@ -21,7 +21,6 @@ */ .gphNoUserSelect { - padding-right: $euiSizeXS; user-select: none; -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent; diff --git a/x-pack/plugins/graph/public/components/_sidebar.scss b/x-pack/plugins/graph/public/components/_sidebar.scss index 831032231fe8c..f835eeac10c0a 100644 --- a/x-pack/plugins/graph/public/components/_sidebar.scss +++ b/x-pack/plugins/graph/public/components/_sidebar.scss @@ -25,7 +25,7 @@ border-radius: $euiBorderRadius; margin-bottom: $euiSizeXS; - & > span { + .kuiIcon { padding-right: $euiSizeXS; } } diff --git a/x-pack/plugins/graph/public/components/workspace_layout/workspace_top_nav_menu.tsx b/x-pack/plugins/graph/public/components/workspace_layout/workspace_top_nav_menu.tsx index c5b10b9d92120..07e21287e5c16 100644 --- a/x-pack/plugins/graph/public/components/workspace_layout/workspace_top_nav_menu.tsx +++ b/x-pack/plugins/graph/public/components/workspace_layout/workspace_top_nav_menu.tsx @@ -39,8 +39,6 @@ export const WorkspaceTopNavMenu = (props: WorkspaceTopNavMenuProps) => { const store = useStore(); const location = useLocation(); const history = useHistory(); - - // register things for legacy angular UI const allSavingDisabled = props.graphSavePolicy === 'none'; // ===== Menubar configuration ========= diff --git a/x-pack/plugins/graph/public/services/workspace/graph_client_workspace.js b/x-pack/plugins/graph/public/services/workspace/graph_client_workspace.js index c849a25cb19bb..a1c85f8eaf80f 100644 --- a/x-pack/plugins/graph/public/services/workspace/graph_client_workspace.js +++ b/x-pack/plugins/graph/public/services/workspace/graph_client_workspace.js @@ -559,7 +559,7 @@ function GraphWorkspace(options) { } if (self.changeHandler) { // Hook to allow any client to respond to position changes - // e.g. angular adjusts and repaints node positions on screen. + // e.g. react adjusts and repaints node positions on screen. self.changeHandler(); } }); diff --git a/x-pack/plugins/graph/public/state_management/fields.ts b/x-pack/plugins/graph/public/state_management/fields.ts index 3a117fa6fe50a..34b1a74510e1a 100644 --- a/x-pack/plugins/graph/public/state_management/fields.ts +++ b/x-pack/plugins/graph/public/state_management/fields.ts @@ -65,7 +65,7 @@ export const hasFieldsSelector = createSelector( ); /** - * Saga making notifying angular when fields are selected to re-calculate the state of the save button. + * Saga making notifying react when fields are selected to re-calculate the state of the save button. * * Won't be necessary once the workspace is moved to redux */ diff --git a/x-pack/plugins/graph/public/state_management/legacy.test.ts b/x-pack/plugins/graph/public/state_management/legacy.test.ts index 5a05efdc478fc..fd5d31404f75b 100644 --- a/x-pack/plugins/graph/public/state_management/legacy.test.ts +++ b/x-pack/plugins/graph/public/state_management/legacy.test.ts @@ -80,7 +80,7 @@ describe('legacy sync sagas', () => { expect(env.mockedDeps.notifyReact).toHaveBeenCalled(); }); - it('notifies angular when fields are selected', () => { + it('notifies react when fields are selected', () => { env.store.dispatch(selectField('field1')); expect(env.mockedDeps.notifyReact).toHaveBeenCalled(); }); diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion.tsx index 969845ada1be0..47ba580c272bd 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion.tsx @@ -17,6 +17,7 @@ import { EuiFieldText, EuiButtonIcon, EuiFormRow, + EuiComboBox, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isNumber, isFinite } from 'lodash'; @@ -112,7 +113,7 @@ export const Criterion: React.FC = ({ const fieldOptions = useMemo(() => { return fields.map((field) => { - return { value: field.name, text: field.name }; + return { label: field.name }; }); }, [fields]); @@ -129,8 +130,14 @@ export const Criterion: React.FC = ({ }, [fieldInfo]); const handleFieldChange = useCallback( - (e) => { - const fieldName = e.target.value; + ([selectedOption]) => { + if (!selectedOption) { + updateCriterion(idx, { field: '' }); + return; + } + + const fieldName = selectedOption.label; + const nextFieldInfo = getFieldInfo(fields, fieldName); // If the field information we're dealing with has changed, reset the comparator and value. if ( @@ -146,10 +153,14 @@ export const Criterion: React.FC = ({ } else { updateCriterion(idx, { field: fieldName }); } + + setIsFieldPopoverOpen(false); }, [fieldInfo, fields, idx, updateCriterion] ); + const selectedField = criterion.field ? [{ label: criterion.field }] : []; + return ( @@ -181,13 +192,19 @@ export const Criterion: React.FC = ({ >
{criterionFieldTitle} - 0} error={errors.field}> - 0} + error={errors.field} + > +
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 4fa96ea6828d4..0ad6378a22960 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/lens/common/expressions/counter_rate/counter_rate.test.ts b/x-pack/plugins/lens/common/expressions/counter_rate/counter_rate.test.ts index 3e1a5ad8e3964..5e41e1e4f95f8 100644 --- a/x-pack/plugins/lens/common/expressions/counter_rate/counter_rate.test.ts +++ b/x-pack/plugins/lens/common/expressions/counter_rate/counter_rate.test.ts @@ -12,10 +12,10 @@ import { functionWrapper } from 'src/plugins/expressions/common/expression_funct describe('lens_counter_rate', () => { const fn = functionWrapper(counterRate); - const runFn = (input: Datatable, args: CounterRateArgs) => fn(input, args) as Datatable; + const runFn = (input: Datatable, args: CounterRateArgs) => fn(input, args) as Promise; - it('calculates counter rate', () => { - const result = runFn( + it('calculates counter rate', async () => { + const result = await runFn( { type: 'datatable', columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }], @@ -31,8 +31,8 @@ describe('lens_counter_rate', () => { expect(result.rows.map((row) => row.output)).toEqual([undefined, 0, 2, 3, 2]); }); - it('calculates counter rate with decreasing values in input', () => { - const result = runFn( + it('calculates counter rate with decreasing values in input', async () => { + const result = await runFn( { type: 'datatable', columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }], @@ -48,8 +48,8 @@ describe('lens_counter_rate', () => { expect(result.rows.map((row) => row.output)).toEqual([undefined, 6, 5, 4]); }); - it('skips null or undefined values until there is real data', () => { - const result = runFn( + it('skips null or undefined values until there is real data', async () => { + const result = await runFn( { type: 'datatable', columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }], @@ -85,8 +85,8 @@ describe('lens_counter_rate', () => { ]); }); - it('treats 0 as real data', () => { - const result = runFn( + it('treats 0 as real data', async () => { + const result = await runFn( { type: 'datatable', columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }], @@ -123,8 +123,8 @@ describe('lens_counter_rate', () => { ]); }); - it('calculates counter rate for multiple series', () => { - const result = runFn( + it('calculates counter rate for multiple series', async () => { + const result = await runFn( { type: 'datatable', columns: [ @@ -157,8 +157,8 @@ describe('lens_counter_rate', () => { ]); }); - it('treats missing split column as separate series', () => { - const result = runFn( + it('treats missing split column as separate series', async () => { + const result = await runFn( { type: 'datatable', columns: [ @@ -190,8 +190,8 @@ describe('lens_counter_rate', () => { ]); }); - it('treats null like undefined and empty string for split columns', () => { - const result = runFn( + it('treats null like undefined and empty string for split columns', async () => { + const result = await runFn( { type: 'datatable', columns: [ @@ -225,8 +225,8 @@ describe('lens_counter_rate', () => { ]); }); - it('calculates counter rate for multiple series by multiple split columns', () => { - const result = runFn( + it('calculates counter rate for multiple series by multiple split columns', async () => { + const result = await runFn( { type: 'datatable', columns: [ @@ -259,8 +259,8 @@ describe('lens_counter_rate', () => { ]); }); - it('splits separate series by the string representation of the cell values', () => { - const result = runFn( + it('splits separate series by the string representation of the cell values', async () => { + const result = await runFn( { type: 'datatable', columns: [ @@ -280,8 +280,8 @@ describe('lens_counter_rate', () => { expect(result.rows.map((row) => row.output)).toEqual([undefined, 2 - 1, undefined, 11 - 10]); }); - it('casts values to number before calculating counter rate', () => { - const result = runFn( + it('casts values to number before calculating counter rate', async () => { + const result = await runFn( { type: 'datatable', columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }], @@ -292,8 +292,8 @@ describe('lens_counter_rate', () => { expect(result.rows.map((row) => row.output)).toEqual([undefined, 7 - 5, 3, 2]); }); - it('casts values to number before calculating counter rate for NaN like values', () => { - const result = runFn( + it('casts values to number before calculating counter rate for NaN like values', async () => { + const result = await runFn( { type: 'datatable', columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }], @@ -304,8 +304,8 @@ describe('lens_counter_rate', () => { expect(result.rows.map((row) => row.output)).toEqual([undefined, 7 - 5, NaN, 2, 5 - 2]); }); - it('copies over meta information from the source column', () => { - const result = runFn( + it('copies over meta information from the source column', async () => { + const result = await runFn( { type: 'datatable', columns: [ @@ -346,8 +346,8 @@ describe('lens_counter_rate', () => { }); }); - it('sets output name on output column if specified', () => { - const result = runFn( + it('sets output name on output column if specified', async () => { + const result = await runFn( { type: 'datatable', columns: [ @@ -370,7 +370,7 @@ describe('lens_counter_rate', () => { }); }); - it('returns source table if input column does not exist', () => { + it('returns source table if input column does not exist', async () => { const input: Datatable = { type: 'datatable', columns: [ @@ -384,12 +384,16 @@ describe('lens_counter_rate', () => { ], rows: [{ val: 5 }], }; - expect(runFn(input, { inputColumnId: 'nonexisting', outputColumnId: 'output' })).toBe(input); + expect(await runFn(input, { inputColumnId: 'nonexisting', outputColumnId: 'output' })).toBe( + input + ); }); - it('throws an error if output column exists already', () => { - expect(() => - runFn( + it('throws an error if output column exists already', async () => { + let error: Error | undefined; + + try { + await runFn( { type: 'datatable', columns: [ @@ -404,7 +408,13 @@ describe('lens_counter_rate', () => { rows: [{ val: 5 }], }, { inputColumnId: 'val', outputColumnId: 'val' } - ) - ).toThrow(); + ); + } catch (e) { + error = e; + } + + expect(error).toMatchInlineSnapshot( + `[Error: Specified outputColumnId val already exists. Please pick another column id.]` + ); }); }); diff --git a/x-pack/plugins/lens/common/expressions/counter_rate/counter_rate_fn.ts b/x-pack/plugins/lens/common/expressions/counter_rate/counter_rate_fn.ts new file mode 100644 index 0000000000000..6412b508b9649 --- /dev/null +++ b/x-pack/plugins/lens/common/expressions/counter_rate/counter_rate_fn.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + buildResultColumns, + getBucketIdentifier, +} from '../../../../../../src/plugins/expressions/common'; +import type { CounterRateExpressionFunction } from './types'; + +export const counterRateFn: CounterRateExpressionFunction['fn'] = ( + input, + { by, inputColumnId, outputColumnId, outputColumnName } +) => { + const resultColumns = buildResultColumns(input, outputColumnId, inputColumnId, outputColumnName); + + if (!resultColumns) { + return input; + } + const previousValues: Partial> = {}; + + return { + ...input, + columns: resultColumns, + rows: input.rows.map((row) => { + const newRow = { ...row }; + + const bucketIdentifier = getBucketIdentifier(row, by); + const previousValue = previousValues[bucketIdentifier]; + const currentValue = newRow[inputColumnId]; + if (currentValue != null && previousValue != null) { + const currentValueAsNumber = Number(currentValue); + if (currentValueAsNumber >= previousValue) { + newRow[outputColumnId] = currentValueAsNumber - previousValue; + } else { + newRow[outputColumnId] = currentValueAsNumber; + } + } else { + newRow[outputColumnId] = undefined; + } + + if (currentValue != null) { + previousValues[bucketIdentifier] = Number(currentValue); + } else { + previousValues[bucketIdentifier] = undefined; + } + + return newRow; + }), + }; +}; diff --git a/x-pack/plugins/lens/common/expressions/counter_rate/index.ts b/x-pack/plugins/lens/common/expressions/counter_rate/index.ts index 41f5547dff969..f58b65814768d 100644 --- a/x-pack/plugins/lens/common/expressions/counter_rate/index.ts +++ b/x-pack/plugins/lens/common/expressions/counter_rate/index.ts @@ -6,14 +6,8 @@ */ import { i18n } from '@kbn/i18n'; -import { - getBucketIdentifier, - buildResultColumns, -} from '../../../../../../src/plugins/expressions/common'; -import type { - ExpressionFunctionDefinition, - Datatable, -} from '../../../../../../src/plugins/expressions/common'; + +import type { CounterRateExpressionFunction } from './types'; export interface CounterRateArgs { by?: string[]; @@ -22,13 +16,6 @@ export interface CounterRateArgs { outputColumnName?: string; } -export type ExpressionFunctionCounterRate = ExpressionFunctionDefinition< - 'lens_counter_rate', - Datatable, - CounterRateArgs, - Datatable ->; - /** * Calculates the counter rate of a specified column in the data table. * @@ -59,7 +46,7 @@ export type ExpressionFunctionCounterRate = ExpressionFunctionDefinition< * before comparison. If the values are objects, the return value of their `toString` method will be used for comparison. * Missing values (`null` and `undefined`) will be treated as empty strings. */ -export const counterRate: ExpressionFunctionCounterRate = { +export const counterRate: CounterRateExpressionFunction = { name: 'lens_counter_rate', type: 'datatable', @@ -101,46 +88,9 @@ export const counterRate: ExpressionFunctionCounterRate = { }, }, - fn(input, { by, inputColumnId, outputColumnId, outputColumnName }) { - const resultColumns = buildResultColumns( - input, - outputColumnId, - inputColumnId, - outputColumnName - ); - - if (!resultColumns) { - return input; - } - const previousValues: Partial> = {}; - return { - ...input, - columns: resultColumns, - rows: input.rows.map((row) => { - const newRow = { ...row }; - - const bucketIdentifier = getBucketIdentifier(row, by); - const previousValue = previousValues[bucketIdentifier]; - const currentValue = newRow[inputColumnId]; - if (currentValue != null && previousValue != null) { - const currentValueAsNumber = Number(currentValue); - if (currentValueAsNumber >= previousValue) { - newRow[outputColumnId] = currentValueAsNumber - previousValue; - } else { - newRow[outputColumnId] = currentValueAsNumber; - } - } else { - newRow[outputColumnId] = undefined; - } - - if (currentValue != null) { - previousValues[bucketIdentifier] = Number(currentValue); - } else { - previousValues[bucketIdentifier] = undefined; - } - - return newRow; - }), - }; + async fn(...args) { + /** Build optimization: prevent adding extra code into initial bundle **/ + const { counterRateFn } = await import('./counter_rate_fn'); + return counterRateFn(...args); }, }; diff --git a/x-pack/plugins/lens/common/expressions/counter_rate/types.ts b/x-pack/plugins/lens/common/expressions/counter_rate/types.ts new file mode 100644 index 0000000000000..f9e1cbbb43de3 --- /dev/null +++ b/x-pack/plugins/lens/common/expressions/counter_rate/types.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Datatable, ExpressionFunctionDefinition } from '../../../../../../src/plugins/expressions'; +import { CounterRateArgs } from './index'; + +export type CounterRateExpressionFunction = ExpressionFunctionDefinition< + 'lens_counter_rate', + Datatable, + CounterRateArgs, + Datatable | Promise +>; diff --git a/x-pack/plugins/lens/common/expressions/datatable/datatable.ts b/x-pack/plugins/lens/common/expressions/datatable/datatable.ts index 32f6c1c089543..d9f1f9c1196ff 100644 --- a/x-pack/plugins/lens/common/expressions/datatable/datatable.ts +++ b/x-pack/plugins/lens/common/expressions/datatable/datatable.ts @@ -6,35 +6,16 @@ */ import { i18n } from '@kbn/i18n'; -import { cloneDeep } from 'lodash'; -import type { - ExecutionContext, - DatatableColumnMeta, - ExpressionFunctionDefinition, -} from '../../../../../../src/plugins/expressions/common'; -import type { FormatFactory, LensMultiTable } from '../../types'; +import type { ExecutionContext } from '../../../../../../src/plugins/expressions/common'; +import type { FormatFactory } from '../../types'; import type { ColumnConfigArg } from './datatable_column'; -import { getSortingCriteria } from './sorting'; -import { computeSummaryRowForColumn } from './summary'; -import { transposeTable } from './transpose_helpers'; +import type { DatatableExpressionFunction } from './types'; export interface SortingState { columnId: string | undefined; direction: 'asc' | 'desc' | 'none'; } -export interface DatatableProps { - data: LensMultiTable; - untransposedData?: LensMultiTable; - args: DatatableArgs; -} - -export interface DatatableRender { - type: 'render'; - as: 'lens_datatable_renderer'; - value: DatatableProps; -} - export interface DatatableArgs { title: string; description?: string; @@ -43,18 +24,9 @@ export interface DatatableArgs { sortingDirection: SortingState['direction']; } -function isRange(meta: { params?: { id?: string } } | undefined) { - return meta?.params?.id === 'range'; -} - export const getDatatable = ( getFormatFactory: (context: ExecutionContext) => FormatFactory | Promise -): ExpressionFunctionDefinition< - 'lens_datatable', - LensMultiTable, - DatatableArgs, - Promise -> => ({ +): DatatableExpressionFunction => ({ name: 'lens_datatable', type: 'render', inputTypes: ['lens_multitable'], @@ -86,73 +58,9 @@ export const getDatatable = ( help: '', }, }, - async fn(data, args, context) { - let untransposedData: LensMultiTable | undefined; - // do the sorting at this level to propagate it also at CSV download - const [firstTable] = Object.values(data.tables); - const [layerId] = Object.keys(context.inspectorAdapters.tables || {}); - const formatters: Record> = {}; - const formatFactory = await getFormatFactory(context); - - firstTable.columns.forEach((column) => { - formatters[column.id] = formatFactory(column.meta?.params); - }); - - const hasTransposedColumns = args.columns.some((c) => c.isTransposed); - if (hasTransposedColumns) { - // store original shape of data separately - untransposedData = cloneDeep(data); - // transposes table and args inplace - transposeTable(args, firstTable, formatters); - } - - const { sortingColumnId: sortBy, sortingDirection: sortDirection } = args; - - const columnsReverseLookup = firstTable.columns.reduce< - Record - >((memo, { id, name, meta }, i) => { - memo[id] = { name, index: i, meta }; - return memo; - }, {}); - - const columnsWithSummary = args.columns.filter((c) => c.summaryRow); - for (const column of columnsWithSummary) { - column.summaryRowValue = computeSummaryRowForColumn( - column, - firstTable, - formatters, - formatFactory({ id: 'number' }) - ); - } - - if (sortBy && columnsReverseLookup[sortBy] && sortDirection !== 'none') { - // Sort on raw values for these types, while use the formatted value for the rest - const sortingCriteria = getSortingCriteria( - isRange(columnsReverseLookup[sortBy]?.meta) - ? 'range' - : columnsReverseLookup[sortBy]?.meta?.type, - sortBy, - formatters[sortBy], - sortDirection - ); - // replace the table here - context.inspectorAdapters.tables[layerId].rows = (firstTable.rows || []) - .slice() - .sort(sortingCriteria); - // replace also the local copy - firstTable.rows = context.inspectorAdapters.tables[layerId].rows; - } else { - args.sortingColumnId = undefined; - args.sortingDirection = 'none'; - } - return { - type: 'render', - as: 'lens_datatable_renderer', - value: { - data, - untransposedData, - args, - }, - }; + async fn(...args) { + /** Build optimization: prevent adding extra code into initial bundle **/ + const { datatableFn } = await import('./datatable_fn'); + return datatableFn(getFormatFactory)(...args); }, }); diff --git a/x-pack/plugins/lens/common/expressions/datatable/datatable_fn.ts b/x-pack/plugins/lens/common/expressions/datatable/datatable_fn.ts new file mode 100644 index 0000000000000..cb03ec7f9dda6 --- /dev/null +++ b/x-pack/plugins/lens/common/expressions/datatable/datatable_fn.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 { cloneDeep } from 'lodash'; +import { FormatFactory, LensMultiTable } from '../../types'; +import { transposeTable } from './transpose_helpers'; +import { computeSummaryRowForColumn } from './summary'; +import { getSortingCriteria } from './sorting'; +import type { + DatatableColumnMeta, + ExecutionContext, +} from '../../../../../../src/plugins/expressions'; +import type { DatatableExpressionFunction } from './types'; + +function isRange(meta: { params?: { id?: string } } | undefined) { + return meta?.params?.id === 'range'; +} + +export const datatableFn = ( + getFormatFactory: (context: ExecutionContext) => FormatFactory | Promise +): DatatableExpressionFunction['fn'] => async (data, args, context) => { + let untransposedData: LensMultiTable | undefined; + // do the sorting at this level to propagate it also at CSV download + const [firstTable] = Object.values(data.tables); + const [layerId] = Object.keys(context.inspectorAdapters.tables || {}); + const formatters: Record> = {}; + const formatFactory = await getFormatFactory(context); + + firstTable.columns.forEach((column) => { + formatters[column.id] = formatFactory(column.meta?.params); + }); + + const hasTransposedColumns = args.columns.some((c) => c.isTransposed); + if (hasTransposedColumns) { + // store original shape of data separately + untransposedData = cloneDeep(data); + // transposes table and args inplace + transposeTable(args, firstTable, formatters); + } + + const { sortingColumnId: sortBy, sortingDirection: sortDirection } = args; + + const columnsReverseLookup = firstTable.columns.reduce< + Record + >((memo, { id, name, meta }, i) => { + memo[id] = { name, index: i, meta }; + return memo; + }, {}); + + const columnsWithSummary = args.columns.filter((c) => c.summaryRow); + for (const column of columnsWithSummary) { + column.summaryRowValue = computeSummaryRowForColumn( + column, + firstTable, + formatters, + formatFactory({ id: 'number' }) + ); + } + + if (sortBy && columnsReverseLookup[sortBy] && sortDirection !== 'none') { + // Sort on raw values for these types, while use the formatted value for the rest + const sortingCriteria = getSortingCriteria( + isRange(columnsReverseLookup[sortBy]?.meta) + ? 'range' + : columnsReverseLookup[sortBy]?.meta?.type, + sortBy, + formatters[sortBy], + sortDirection + ); + // replace the table here + context.inspectorAdapters.tables[layerId].rows = (firstTable.rows || []) + .slice() + .sort(sortingCriteria); + // replace also the local copy + firstTable.rows = context.inspectorAdapters.tables[layerId].rows; + } else { + args.sortingColumnId = undefined; + args.sortingDirection = 'none'; + } + return { + type: 'render', + as: 'lens_datatable_renderer', + value: { + data, + untransposedData, + args, + }, + }; +}; diff --git a/x-pack/plugins/lens/common/expressions/datatable/index.ts b/x-pack/plugins/lens/common/expressions/datatable/index.ts index 2602aae252ca9..cf9fb1d0b4791 100644 --- a/x-pack/plugins/lens/common/expressions/datatable/index.ts +++ b/x-pack/plugins/lens/common/expressions/datatable/index.ts @@ -10,3 +10,5 @@ export * from './datatable'; export * from './summary'; export * from './transpose_helpers'; export * from './utils'; + +export type { DatatableProps } from './types'; diff --git a/x-pack/plugins/lens/common/expressions/datatable/types.ts b/x-pack/plugins/lens/common/expressions/datatable/types.ts new file mode 100644 index 0000000000000..340f3eb0576b0 --- /dev/null +++ b/x-pack/plugins/lens/common/expressions/datatable/types.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ExpressionFunctionDefinition } from '../../../../../../src/plugins/expressions'; +import type { LensMultiTable } from '../../types'; +import type { DatatableArgs } from './datatable'; + +export interface DatatableProps { + data: LensMultiTable; + untransposedData?: LensMultiTable; + args: DatatableArgs; +} + +export interface DatatableRender { + type: 'render'; + as: 'lens_datatable_renderer'; + value: DatatableProps; +} + +export type DatatableExpressionFunction = ExpressionFunctionDefinition< + 'lens_datatable', + LensMultiTable, + DatatableArgs, + Promise +>; diff --git a/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts b/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts index 4428225b349da..fbf3ff9c05b19 100644 --- a/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts +++ b/x-pack/plugins/lens/common/expressions/format_column/format_column.test.ts @@ -10,7 +10,9 @@ import { functionWrapper } from 'src/plugins/expressions/common/expression_funct import { FormatColumnArgs, formatColumn } from './index'; describe('format_column', () => { - const fn: (input: Datatable, args: FormatColumnArgs) => Datatable = functionWrapper(formatColumn); + const fn: (input: Datatable, args: FormatColumnArgs) => Promise = functionWrapper( + formatColumn + ); let datatable: Datatable; @@ -33,17 +35,17 @@ describe('format_column', () => { }; }); - it('overwrites format', () => { + it('overwrites format', async () => { datatable.columns[0].meta.params = { id: 'myformatter', params: {} }; - const result = fn(datatable, { columnId: 'test', format: 'otherformatter' }); + const result = await fn(datatable, { columnId: 'test', format: 'otherformatter' }); expect(result.columns[0].meta.params).toEqual({ id: 'otherformatter', }); }); - it('overwrites format with well known pattern', () => { + it('overwrites format with well known pattern', async () => { datatable.columns[0].meta.params = { id: 'myformatter', params: {} }; - const result = fn(datatable, { columnId: 'test', format: 'number' }); + const result = await fn(datatable, { columnId: 'test', format: 'number' }); expect(result.columns[0].meta.params).toEqual({ id: 'number', params: { @@ -52,9 +54,9 @@ describe('format_column', () => { }); }); - it('uses number of decimals if provided', () => { + it('uses number of decimals if provided', async () => { datatable.columns[0].meta.params = { id: 'myformatter', params: {} }; - const result = fn(datatable, { columnId: 'test', format: 'number', decimals: 5 }); + const result = await fn(datatable, { columnId: 'test', format: 'number', decimals: 5 }); expect(result.columns[0].meta.params).toEqual({ id: 'number', params: { @@ -63,9 +65,9 @@ describe('format_column', () => { }); }); - it('has special handling for 0 decimals', () => { + it('has special handling for 0 decimals', async () => { datatable.columns[0].meta.params = { id: 'myformatter', params: {} }; - const result = fn(datatable, { columnId: 'test', format: 'number', decimals: 0 }); + const result = await fn(datatable, { columnId: 'test', format: 'number', decimals: 0 }); expect(result.columns[0].meta.params).toEqual({ id: 'number', params: { @@ -75,8 +77,8 @@ describe('format_column', () => { }); describe('parent format', () => { - it('should ignore parent format if it is not specifying an id', () => { - const result = fn(datatable, { + it('should ignore parent format if it is not specifying an id', async () => { + const result = await fn(datatable, { columnId: 'test', format: '', parentFormat: JSON.stringify({ some: 'key' }), @@ -84,8 +86,8 @@ describe('format_column', () => { expect(result.columns[0].meta.params).toEqual(datatable.columns[0].meta.params); }); - it('set parent format with params', () => { - const result = fn(datatable, { + it('set parent format with params', async () => { + const result = await fn(datatable, { columnId: 'test', format: '', parentFormat: JSON.stringify({ id: 'wrapper', params: { wrapperParam: 123 } }), @@ -99,9 +101,9 @@ describe('format_column', () => { }); }); - it('retain inner formatter params', () => { + it('retain inner formatter params', async () => { datatable.columns[0].meta.params = { id: 'myformatter', params: { innerParam: 456 } }; - const result = fn(datatable, { + const result = await fn(datatable, { columnId: 'test', format: '', parentFormat: JSON.stringify({ id: 'wrapper', params: { wrapperParam: 123 } }), @@ -118,12 +120,12 @@ describe('format_column', () => { }); }); - it('overwrite existing wrapper param', () => { + it('overwrite existing wrapper param', async () => { datatable.columns[0].meta.params = { id: 'wrapper', params: { wrapperParam: 0, id: 'myformatter', params: { innerParam: 456 } }, }; - const result = fn(datatable, { + const result = await fn(datatable, { columnId: 'test', format: '', parentFormat: JSON.stringify({ id: 'wrapper', params: { wrapperParam: 123 } }), @@ -140,12 +142,12 @@ describe('format_column', () => { }); }); - it('overwrites format with well known pattern including decimals', () => { + it('overwrites format with well known pattern including decimals', async () => { datatable.columns[0].meta.params = { id: 'previousWrapper', params: { id: 'myformatter', params: { innerParam: 456 } }, }; - const result = fn(datatable, { + const result = await fn(datatable, { columnId: 'test', format: 'number', decimals: 5, @@ -164,10 +166,10 @@ describe('format_column', () => { }); }); - it('does not touch other column meta data', () => { + it('does not touch other column meta data', async () => { const extraColumn: DatatableColumn = { id: 'test2', name: 'test2', meta: { type: 'number' } }; datatable.columns.push(extraColumn); - const result = fn(datatable, { columnId: 'test', format: 'number' }); + const result = await fn(datatable, { columnId: 'test', format: 'number' }); expect(result.columns[1]).toEqual(extraColumn); }); }); diff --git a/x-pack/plugins/lens/common/expressions/format_column/format_column_fn.ts b/x-pack/plugins/lens/common/expressions/format_column/format_column_fn.ts new file mode 100644 index 0000000000000..37540ee0950af --- /dev/null +++ b/x-pack/plugins/lens/common/expressions/format_column/format_column_fn.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { supportedFormats } from './supported_formats'; +import type { DatatableColumn } from '../../../../../../src/plugins/expressions'; +import type { FormatColumnArgs } from './index'; +import type { FormatColumnExpressionFunction } from './types'; + +function isNestedFormat(params: DatatableColumn['meta']['params']) { + // if there is a nested params object with an id, it's a nested format + return !!params?.params?.id; +} + +function withParams(col: DatatableColumn, params: Record) { + return { ...col, meta: { ...col.meta, params } }; +} + +export const formatColumnFn: FormatColumnExpressionFunction['fn'] = ( + input, + { format, columnId, decimals, parentFormat }: FormatColumnArgs +) => ({ + ...input, + columns: input.columns.map((col) => { + if (col.id === columnId) { + if (!parentFormat) { + if (supportedFormats[format]) { + return withParams(col, { + id: format, + params: { pattern: supportedFormats[format].decimalsToPattern(decimals) }, + }); + } else if (format) { + return withParams(col, { id: format }); + } else { + return col; + } + } + + const parsedParentFormat = JSON.parse(parentFormat); + const parentFormatId = parsedParentFormat.id; + const parentFormatParams = parsedParentFormat.params ?? {}; + + if (!parentFormatId) { + return col; + } + + if (format && supportedFormats[format]) { + return withParams(col, { + id: parentFormatId, + params: { + id: format, + params: { + pattern: supportedFormats[format].decimalsToPattern(decimals), + }, + ...parentFormatParams, + }, + }); + } + if (parentFormatParams) { + // if original format is already a nested one, we are just replacing the wrapper params + // otherwise wrapping it inside parentFormatId/parentFormatParams + const isNested = isNestedFormat(col.meta.params); + const innerParams = isNested + ? col.meta.params?.params + : { id: col.meta.params?.id, params: col.meta.params?.params }; + + const formatId = isNested ? col.meta.params?.id : parentFormatId; + + return withParams(col, { + ...col.meta.params, + id: formatId, + params: { + ...innerParams, + ...parentFormatParams, + }, + }); + } + } + return col; + }), +}); diff --git a/x-pack/plugins/lens/common/expressions/format_column/index.ts b/x-pack/plugins/lens/common/expressions/format_column/index.ts index c874eac1ede1f..0fc99ff8f7089 100644 --- a/x-pack/plugins/lens/common/expressions/format_column/index.ts +++ b/x-pack/plugins/lens/common/expressions/format_column/index.ts @@ -5,11 +5,7 @@ * 2.0. */ -import type { - ExpressionFunctionDefinition, - Datatable, - DatatableColumn, -} from '../../../../../../src/plugins/expressions/common'; +import type { FormatColumnExpressionFunction } from './types'; export interface FormatColumnArgs { format: string; @@ -18,42 +14,7 @@ export interface FormatColumnArgs { parentFormat?: string; } -export const supportedFormats: Record< - string, - { decimalsToPattern: (decimals?: number) => string } -> = { - number: { - decimalsToPattern: (decimals = 2) => { - if (decimals === 0) { - return `0,0`; - } - return `0,0.${'0'.repeat(decimals)}`; - }, - }, - percent: { - decimalsToPattern: (decimals = 2) => { - if (decimals === 0) { - return `0,0%`; - } - return `0,0.${'0'.repeat(decimals)}%`; - }, - }, - bytes: { - decimalsToPattern: (decimals = 2) => { - if (decimals === 0) { - return `0,0b`; - } - return `0,0.${'0'.repeat(decimals)}b`; - }, - }, -}; - -export const formatColumn: ExpressionFunctionDefinition< - 'lens_format_column', - Datatable, - FormatColumnArgs, - Datatable -> = { +export const formatColumn: FormatColumnExpressionFunction = { name: 'lens_format_column', type: 'datatable', help: '', @@ -78,75 +39,9 @@ export const formatColumn: ExpressionFunctionDefinition< }, }, inputTypes: ['datatable'], - fn(input, { format, columnId, decimals, parentFormat }: FormatColumnArgs) { - return { - ...input, - columns: input.columns.map((col) => { - if (col.id === columnId) { - if (!parentFormat) { - if (supportedFormats[format]) { - return withParams(col, { - id: format, - params: { pattern: supportedFormats[format].decimalsToPattern(decimals) }, - }); - } else if (format) { - return withParams(col, { id: format }); - } else { - return col; - } - } - - const parsedParentFormat = JSON.parse(parentFormat); - const parentFormatId = parsedParentFormat.id; - const parentFormatParams = parsedParentFormat.params ?? {}; - - if (!parentFormatId) { - return col; - } - - if (format && supportedFormats[format]) { - return withParams(col, { - id: parentFormatId, - params: { - id: format, - params: { - pattern: supportedFormats[format].decimalsToPattern(decimals), - }, - ...parentFormatParams, - }, - }); - } - if (parentFormatParams) { - // if original format is already a nested one, we are just replacing the wrapper params - // otherwise wrapping it inside parentFormatId/parentFormatParams - const isNested = isNestedFormat(col.meta.params); - const innerParams = isNested - ? col.meta.params?.params - : { id: col.meta.params?.id, params: col.meta.params?.params }; - - const formatId = isNested ? col.meta.params?.id : parentFormatId; - - return withParams(col, { - ...col.meta.params, - id: formatId, - params: { - ...innerParams, - ...parentFormatParams, - }, - }); - } - } - return col; - }), - }; + async fn(...args) { + /** Build optimization: prevent adding extra code into initial bundle **/ + const { formatColumnFn } = await import('./format_column_fn'); + return formatColumnFn(...args); }, }; - -function isNestedFormat(params: DatatableColumn['meta']['params']) { - // if there is a nested params object with an id, it's a nested format - return !!params?.params?.id; -} - -function withParams(col: DatatableColumn, params: Record) { - return { ...col, meta: { ...col.meta, params } }; -} diff --git a/x-pack/plugins/lens/common/expressions/format_column/supported_formats.ts b/x-pack/plugins/lens/common/expressions/format_column/supported_formats.ts new file mode 100644 index 0000000000000..d00d2f7dfc22f --- /dev/null +++ b/x-pack/plugins/lens/common/expressions/format_column/supported_formats.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const supportedFormats: Record< + string, + { decimalsToPattern: (decimals?: number) => string } +> = { + number: { + decimalsToPattern: (decimals = 2) => { + if (decimals === 0) { + return `0,0`; + } + return `0,0.${'0'.repeat(decimals)}`; + }, + }, + percent: { + decimalsToPattern: (decimals = 2) => { + if (decimals === 0) { + return `0,0%`; + } + return `0,0.${'0'.repeat(decimals)}%`; + }, + }, + bytes: { + decimalsToPattern: (decimals = 2) => { + if (decimals === 0) { + return `0,0b`; + } + return `0,0.${'0'.repeat(decimals)}b`; + }, + }, +}; diff --git a/x-pack/plugins/lens/common/expressions/format_column/types.ts b/x-pack/plugins/lens/common/expressions/format_column/types.ts new file mode 100644 index 0000000000000..589422b253b93 --- /dev/null +++ b/x-pack/plugins/lens/common/expressions/format_column/types.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + Datatable, + ExpressionFunctionDefinition, +} from '../../../../../../src/plugins/expressions'; +import type { FormatColumnArgs } from './index'; + +export type FormatColumnExpressionFunction = ExpressionFunctionDefinition< + 'lens_format_column', + Datatable, + FormatColumnArgs, + Datatable | Promise +>; diff --git a/x-pack/plugins/lens/common/expressions/merge_tables/index.ts b/x-pack/plugins/lens/common/expressions/merge_tables/index.ts index e190da19886df..7ede2236e8b07 100644 --- a/x-pack/plugins/lens/common/expressions/merge_tables/index.ts +++ b/x-pack/plugins/lens/common/expressions/merge_tables/index.ts @@ -15,7 +15,7 @@ import { toAbsoluteDates } from '../../../../../../src/plugins/data/common'; import type { ExpressionValueSearchContext } from '../../../../../../src/plugins/data/common'; import type { LensMultiTable } from '../../types'; -import { Adapters } from '../../../../../../src/plugins/inspector/common'; +import type { Adapters } from '../../../../../../src/plugins/inspector/common'; interface MergeTables { layerIds: string[]; diff --git a/x-pack/plugins/lens/common/expressions/metric_chart/metric_chart.ts b/x-pack/plugins/lens/common/expressions/metric_chart/metric_chart.ts index 6c05502bb2b03..0a867e4155c22 100644 --- a/x-pack/plugins/lens/common/expressions/metric_chart/metric_chart.ts +++ b/x-pack/plugins/lens/common/expressions/metric_chart/metric_chart.ts @@ -9,17 +9,17 @@ import type { ExpressionFunctionDefinition } from '../../../../../../src/plugins import type { LensMultiTable } from '../../types'; import type { MetricConfig } from './types'; -export interface MetricChartProps { - data: LensMultiTable; - args: MetricConfig; -} - -export interface MetricRender { +interface MetricRender { type: 'render'; as: 'lens_metric_chart_renderer'; value: MetricChartProps; } +export interface MetricChartProps { + data: LensMultiTable; + args: MetricConfig; +} + export const metricChart: ExpressionFunctionDefinition< 'lens_metric_chart', LensMultiTable, diff --git a/x-pack/plugins/lens/common/expressions/pie_chart/index.ts b/x-pack/plugins/lens/common/expressions/pie_chart/index.ts index e82294f8aff25..1c1f6fdae4578 100644 --- a/x-pack/plugins/lens/common/expressions/pie_chart/index.ts +++ b/x-pack/plugins/lens/common/expressions/pie_chart/index.ts @@ -5,5 +5,12 @@ * 2.0. */ -export * from './types'; -export * from './pie_chart'; +export { pie } from './pie_chart'; + +export type { + SharedPieLayerState, + PieLayerState, + PieVisualizationState, + PieExpressionArgs, + PieExpressionProps, +} from './types'; diff --git a/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts b/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts index 7d228f04c25e7..ed0391a16af25 100644 --- a/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts +++ b/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts @@ -7,11 +7,12 @@ import { Position } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; + import type { ExpressionFunctionDefinition } from '../../../../../../src/plugins/expressions/common'; import type { LensMultiTable } from '../../types'; import type { PieExpressionProps, PieExpressionArgs } from './types'; -export interface PieRender { +interface PieRender { type: 'render'; as: 'lens_pie_renderer'; value: PieExpressionProps; diff --git a/x-pack/plugins/lens/common/expressions/rename_columns/index.ts b/x-pack/plugins/lens/common/expressions/rename_columns/index.ts index 4cb8ff75f486d..86ab16e06ec01 100644 --- a/x-pack/plugins/lens/common/expressions/rename_columns/index.ts +++ b/x-pack/plugins/lens/common/expressions/rename_columns/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export * from './rename_columns'; +export { renameColumns } from './rename_columns'; diff --git a/x-pack/plugins/lens/common/expressions/rename_columns/rename_columns.test.ts b/x-pack/plugins/lens/common/expressions/rename_columns/rename_columns.test.ts index f3db64c1d2257..3bfed88625c89 100644 --- a/x-pack/plugins/lens/common/expressions/rename_columns/rename_columns.test.ts +++ b/x-pack/plugins/lens/common/expressions/rename_columns/rename_columns.test.ts @@ -10,7 +10,7 @@ import { Datatable } from '../../../../../../src/plugins/expressions/common'; import { createMockExecutionContext } from '../../../../../../src/plugins/expressions/common/mocks'; describe('rename_columns', () => { - it('should rename columns of a given datatable', () => { + it('should rename columns of a given datatable', async () => { const input: Datatable = { type: 'datatable', columns: [ @@ -36,7 +36,7 @@ describe('rename_columns', () => { }, }; - const result = renameColumns.fn( + const result = await renameColumns.fn( input, { idMap: JSON.stringify(idMap) }, createMockExecutionContext() @@ -83,7 +83,7 @@ describe('rename_columns', () => { `); }); - it('should keep columns which are not mapped', () => { + it('should keep columns which are not mapped', async () => { const input: Datatable = { type: 'datatable', columns: [ @@ -102,7 +102,7 @@ describe('rename_columns', () => { b: { id: 'c', label: 'Catamaran' }, }; - const result = renameColumns.fn( + const result = await renameColumns.fn( input, { idMap: JSON.stringify(idMap) }, createMockExecutionContext() @@ -149,7 +149,7 @@ describe('rename_columns', () => { `); }); - it('should rename date histograms', () => { + it('should rename date histograms', async () => { const input: Datatable = { type: 'datatable', columns: [ @@ -168,7 +168,7 @@ describe('rename_columns', () => { b: { id: 'c', label: 'Apple', operationType: 'date_histogram', sourceField: 'banana' }, }; - const result = renameColumns.fn( + const result = await renameColumns.fn( input, { idMap: JSON.stringify(idMap) }, createMockExecutionContext() diff --git a/x-pack/plugins/lens/common/expressions/rename_columns/rename_columns.ts b/x-pack/plugins/lens/common/expressions/rename_columns/rename_columns.ts index 517bd683d80ae..d425d5c80d18d 100644 --- a/x-pack/plugins/lens/common/expressions/rename_columns/rename_columns.ts +++ b/x-pack/plugins/lens/common/expressions/rename_columns/rename_columns.ts @@ -6,27 +6,9 @@ */ import { i18n } from '@kbn/i18n'; -import { - ExpressionFunctionDefinition, - Datatable, - DatatableColumn, -} from '../../../../../../src/plugins/expressions/common'; +import type { RenameColumnsExpressionFunction } from './types'; -interface RemapArgs { - idMap: string; -} - -type OriginalColumn = { id: string; label: string } & ( - | { operationType: 'date_histogram'; sourceField: string } - | { operationType: string; sourceField: never } -); - -export const renameColumns: ExpressionFunctionDefinition< - 'lens_rename_columns', - Datatable, - RemapArgs, - Datatable -> = { +export const renameColumns: RenameColumnsExpressionFunction = { name: 'lens_rename_columns', type: 'datatable', help: i18n.translate('xpack.lens.functions.renameColumns.help', { @@ -42,53 +24,9 @@ export const renameColumns: ExpressionFunctionDefinition< }, }, inputTypes: ['datatable'], - fn(data, { idMap: encodedIdMap }) { - const idMap = JSON.parse(encodedIdMap) as Record; - - return { - type: 'datatable', - rows: data.rows.map((row) => { - const mappedRow: Record = {}; - Object.entries(idMap).forEach(([fromId, toId]) => { - mappedRow[toId.id] = row[fromId]; - }); - - Object.entries(row).forEach(([id, value]) => { - if (id in idMap) { - mappedRow[idMap[id].id] = value; - } else { - mappedRow[id] = value; - } - }); - - return mappedRow; - }), - columns: data.columns.map((column) => { - const mappedItem = idMap[column.id]; - - if (!mappedItem) { - return column; - } - - return { - ...column, - id: mappedItem.id, - name: getColumnName(mappedItem, column), - }; - }), - }; + async fn(...args) { + /** Build optimization: prevent adding extra code into initial bundle **/ + const { renameColumnFn } = await import('./rename_columns_fn'); + return renameColumnFn(...args); }, }; - -function getColumnName(originalColumn: OriginalColumn, newColumn: DatatableColumn) { - if (originalColumn?.operationType === 'date_histogram') { - const fieldName = originalColumn.sourceField; - - // HACK: This is a hack, and introduces some fragility into - // column naming. Eventually, this should be calculated and - // built more systematically. - return newColumn.name.replace(fieldName, originalColumn.label); - } - - return originalColumn.label; -} diff --git a/x-pack/plugins/lens/common/expressions/rename_columns/rename_columns_fn.ts b/x-pack/plugins/lens/common/expressions/rename_columns/rename_columns_fn.ts new file mode 100644 index 0000000000000..ee0c7ed1eebec --- /dev/null +++ b/x-pack/plugins/lens/common/expressions/rename_columns/rename_columns_fn.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DatatableColumn } from '../../../../../../src/plugins/expressions/common'; +import type { OriginalColumn, RenameColumnsExpressionFunction } from './types'; + +function getColumnName(originalColumn: OriginalColumn, newColumn: DatatableColumn) { + if (originalColumn?.operationType === 'date_histogram') { + const fieldName = originalColumn.sourceField; + + // HACK: This is a hack, and introduces some fragility into + // column naming. Eventually, this should be calculated and + // built more systematically. + return newColumn.name.replace(fieldName, originalColumn.label); + } + + return originalColumn.label; +} + +export const renameColumnFn: RenameColumnsExpressionFunction['fn'] = ( + data, + { idMap: encodedIdMap } +) => { + const idMap = JSON.parse(encodedIdMap) as Record; + + return { + type: 'datatable', + rows: data.rows.map((row) => { + const mappedRow: Record = {}; + Object.entries(idMap).forEach(([fromId, toId]) => { + mappedRow[toId.id] = row[fromId]; + }); + + Object.entries(row).forEach(([id, value]) => { + if (id in idMap) { + mappedRow[idMap[id].id] = value; + } else { + mappedRow[id] = value; + } + }); + + return mappedRow; + }), + columns: data.columns.map((column) => { + const mappedItem = idMap[column.id]; + + if (!mappedItem) { + return column; + } + + return { + ...column, + id: mappedItem.id, + name: getColumnName(mappedItem, column), + }; + }), + }; +}; diff --git a/x-pack/plugins/lens/common/expressions/rename_columns/types.ts b/x-pack/plugins/lens/common/expressions/rename_columns/types.ts new file mode 100644 index 0000000000000..685ccfb89e4ca --- /dev/null +++ b/x-pack/plugins/lens/common/expressions/rename_columns/types.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Datatable, ExpressionFunctionDefinition } from '../../../../../../src/plugins/expressions'; + +export type OriginalColumn = { id: string; label: string } & ( + | { operationType: 'date_histogram'; sourceField: string } + | { operationType: string; sourceField: never } +); + +export type RenameColumnsExpressionFunction = ExpressionFunctionDefinition< + 'lens_rename_columns', + Datatable, + { + idMap: string; + }, + Datatable | Promise +>; diff --git a/x-pack/plugins/lens/common/expressions/time_scale/index.ts b/x-pack/plugins/lens/common/expressions/time_scale/index.ts index 92fec01a9ecbc..b5bd7dbcbb074 100644 --- a/x-pack/plugins/lens/common/expressions/time_scale/index.ts +++ b/x-pack/plugins/lens/common/expressions/time_scale/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export * from './time_scale'; -export * from './types'; +export { getTimeScale } from './time_scale'; +export type { TimeScaleUnit, TimeScaleArgs } from './types'; diff --git a/x-pack/plugins/lens/common/expressions/time_scale/time_scale.test.ts b/x-pack/plugins/lens/common/expressions/time_scale/time_scale.test.ts index 9f299d9e3d74f..d51f2594b4267 100644 --- a/x-pack/plugins/lens/common/expressions/time_scale/time_scale.test.ts +++ b/x-pack/plugins/lens/common/expressions/time_scale/time_scale.test.ts @@ -6,8 +6,8 @@ */ import moment from 'moment'; -import { Datatable } from 'src/plugins/expressions/public'; -import { TimeRange } from 'src/plugins/data/public'; +import type { Datatable } from 'src/plugins/expressions/public'; +import type { TimeRange } from 'src/plugins/data/public'; import { functionWrapper } from 'src/plugins/expressions/common/expression_functions/specs/tests/utils'; // mock the specific inner variable: @@ -22,7 +22,8 @@ jest.mock('../../../../../../src/plugins/data/common/query/timefilter/get_time', }; }); -import { getTimeScale, TimeScaleArgs } from './time_scale'; +import { getTimeScale } from './time_scale'; +import type { TimeScaleArgs } from './types'; describe('time_scale', () => { let timeScaleWrapped: (input: Datatable, args: TimeScaleArgs) => Promise; diff --git a/x-pack/plugins/lens/common/expressions/time_scale/time_scale.ts b/x-pack/plugins/lens/common/expressions/time_scale/time_scale.ts index 711b770fb140f..21bef6de980ac 100644 --- a/x-pack/plugins/lens/common/expressions/time_scale/time_scale.ts +++ b/x-pack/plugins/lens/common/expressions/time_scale/time_scale.ts @@ -5,46 +5,12 @@ * 2.0. */ -import moment from 'moment-timezone'; -import { i18n } from '@kbn/i18n'; -import type { - ExpressionFunctionDefinition, - Datatable, -} from '../../../../../../src/plugins/expressions/common'; -import { - getDateHistogramMetaDataByDatatableColumn, - parseInterval, - calculateBounds, -} from '../../../../../../src/plugins/data/common'; -import { - buildResultColumns, - ExecutionContext, -} from '../../../../../../src/plugins/expressions/common'; -import type { TimeScaleUnit } from './types'; - -export interface TimeScaleArgs { - dateColumnId: string; - inputColumnId: string; - outputColumnId: string; - targetUnit: TimeScaleUnit; - outputColumnName?: string; -} - -const unitInMs: Record = { - s: 1000, - m: 1000 * 60, - h: 1000 * 60 * 60, - d: 1000 * 60 * 60 * 24, -}; +import type { ExecutionContext } from '../../../../../../src/plugins/expressions/common'; +import type { TimeScaleExpressionFunction } from './types'; export const getTimeScale = ( getTimezone: (context: ExecutionContext) => string | Promise -): ExpressionFunctionDefinition< - 'lens_time_scale', - Datatable, - TimeScaleArgs, - Promise -> => ({ +): TimeScaleExpressionFunction => ({ name: 'lens_time_scale', type: 'datatable', help: '', @@ -76,84 +42,9 @@ export const getTimeScale = ( }, }, inputTypes: ['datatable'], - async fn( - input, - { dateColumnId, inputColumnId, outputColumnId, outputColumnName, targetUnit }: TimeScaleArgs, - context - ) { - const dateColumnDefinition = input.columns.find((column) => column.id === dateColumnId); - - if (!dateColumnDefinition) { - throw new Error( - i18n.translate('xpack.lens.functions.timeScale.dateColumnMissingMessage', { - defaultMessage: 'Specified dateColumnId {columnId} does not exist.', - values: { - columnId: dateColumnId, - }, - }) - ); - } - - const resultColumns = buildResultColumns( - input, - outputColumnId, - inputColumnId, - outputColumnName, - { allowColumnOverwrite: true } - ); - - if (!resultColumns) { - return input; - } - - const targetUnitInMs = unitInMs[targetUnit]; - const timeInfo = getDateHistogramMetaDataByDatatableColumn(dateColumnDefinition, { - timeZone: await getTimezone(context), - }); - const intervalDuration = timeInfo?.interval && parseInterval(timeInfo.interval); - - if (!timeInfo || !intervalDuration) { - throw new Error( - i18n.translate('xpack.lens.functions.timeScale.timeInfoMissingMessage', { - defaultMessage: 'Could not fetch date histogram information', - }) - ); - } - // the datemath plugin always parses dates by using the current default moment time zone. - // to use the configured time zone, we are switching just for the bounds calculation. - const defaultTimezone = moment().zoneName(); - moment.tz.setDefault(timeInfo.timeZone); - - const timeBounds = timeInfo.timeRange && calculateBounds(timeInfo.timeRange); - - const result = { - ...input, - columns: resultColumns, - rows: input.rows.map((row) => { - const newRow = { ...row }; - - let startOfBucket = moment(row[dateColumnId]); - let endOfBucket = startOfBucket.clone().add(intervalDuration); - if (timeBounds && timeBounds.min) { - startOfBucket = moment.max(startOfBucket, timeBounds.min); - } - if (timeBounds && timeBounds.max) { - endOfBucket = moment.min(endOfBucket, timeBounds.max); - } - const bucketSize = endOfBucket.diff(startOfBucket); - const factor = bucketSize / targetUnitInMs; - - const currentValue = newRow[inputColumnId]; - if (currentValue != null) { - newRow[outputColumnId] = Number(currentValue) / factor; - } - - return newRow; - }), - }; - // reset default moment timezone - moment.tz.setDefault(defaultTimezone); - - return result; + async fn(...args) { + /** Build optimization: prevent adding extra code into initial bundle **/ + const { timeScaleFn } = await import('./time_scale_fn'); + return timeScaleFn(getTimezone)(...args); }, }); diff --git a/x-pack/plugins/lens/common/expressions/time_scale/time_scale_fn.ts b/x-pack/plugins/lens/common/expressions/time_scale/time_scale_fn.ts new file mode 100644 index 0000000000000..e6113afebca22 --- /dev/null +++ b/x-pack/plugins/lens/common/expressions/time_scale/time_scale_fn.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment-timezone'; +import { i18n } from '@kbn/i18n'; +import { + buildResultColumns, + ExecutionContext, +} from '../../../../../../src/plugins/expressions/common'; +import { + calculateBounds, + getDateHistogramMetaDataByDatatableColumn, + parseInterval, +} from '../../../../../../src/plugins/data/common'; +import type { TimeScaleExpressionFunction, TimeScaleUnit, TimeScaleArgs } from './types'; + +const unitInMs: Record = { + s: 1000, + m: 1000 * 60, + h: 1000 * 60 * 60, + d: 1000 * 60 * 60 * 24, +}; + +export const timeScaleFn = ( + getTimezone: (context: ExecutionContext) => string | Promise +): TimeScaleExpressionFunction['fn'] => async ( + input, + { dateColumnId, inputColumnId, outputColumnId, outputColumnName, targetUnit }: TimeScaleArgs, + context +) => { + const dateColumnDefinition = input.columns.find((column) => column.id === dateColumnId); + + if (!dateColumnDefinition) { + throw new Error( + i18n.translate('xpack.lens.functions.timeScale.dateColumnMissingMessage', { + defaultMessage: 'Specified dateColumnId {columnId} does not exist.', + values: { + columnId: dateColumnId, + }, + }) + ); + } + + const resultColumns = buildResultColumns(input, outputColumnId, inputColumnId, outputColumnName, { + allowColumnOverwrite: true, + }); + + if (!resultColumns) { + return input; + } + + const targetUnitInMs = unitInMs[targetUnit]; + const timeInfo = getDateHistogramMetaDataByDatatableColumn(dateColumnDefinition, { + timeZone: await getTimezone(context), + }); + const intervalDuration = timeInfo?.interval && parseInterval(timeInfo.interval); + + if (!timeInfo || !intervalDuration) { + throw new Error( + i18n.translate('xpack.lens.functions.timeScale.timeInfoMissingMessage', { + defaultMessage: 'Could not fetch date histogram information', + }) + ); + } + // the datemath plugin always parses dates by using the current default moment time zone. + // to use the configured time zone, we are switching just for the bounds calculation. + const defaultTimezone = moment().zoneName(); + moment.tz.setDefault(timeInfo.timeZone); + + const timeBounds = timeInfo.timeRange && calculateBounds(timeInfo.timeRange); + + const result = { + ...input, + columns: resultColumns, + rows: input.rows.map((row) => { + const newRow = { ...row }; + + let startOfBucket = moment(row[dateColumnId]); + let endOfBucket = startOfBucket.clone().add(intervalDuration); + if (timeBounds && timeBounds.min) { + startOfBucket = moment.max(startOfBucket, timeBounds.min); + } + if (timeBounds && timeBounds.max) { + endOfBucket = moment.min(endOfBucket, timeBounds.max); + } + const bucketSize = endOfBucket.diff(startOfBucket); + const factor = bucketSize / targetUnitInMs; + + const currentValue = newRow[inputColumnId]; + if (currentValue != null) { + newRow[outputColumnId] = Number(currentValue) / factor; + } + + return newRow; + }), + }; + // reset default moment timezone + moment.tz.setDefault(defaultTimezone); + + return result; +}; diff --git a/x-pack/plugins/lens/common/expressions/time_scale/types.ts b/x-pack/plugins/lens/common/expressions/time_scale/types.ts index 4ee00ce53e68b..58c5e5723a7c1 100644 --- a/x-pack/plugins/lens/common/expressions/time_scale/types.ts +++ b/x-pack/plugins/lens/common/expressions/time_scale/types.ts @@ -5,4 +5,24 @@ * 2.0. */ +import type { + Datatable, + ExpressionFunctionDefinition, +} from '../../../../../../src/plugins/expressions'; + export type TimeScaleUnit = 's' | 'm' | 'h' | 'd'; + +export interface TimeScaleArgs { + dateColumnId: string; + inputColumnId: string; + outputColumnId: string; + targetUnit: TimeScaleUnit; + outputColumnName?: string; +} + +export type TimeScaleExpressionFunction = ExpressionFunctionDefinition< + 'lens_time_scale', + Datatable, + TimeScaleArgs, + Promise +>; diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 5bd0fa9594bdf..de091fd305e85 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -45,7 +45,7 @@ import { getPreloadedState } from '../state_management/lens_slice'; export async function getLensServices( coreStart: CoreStart, startDependencies: LensPluginStartDependencies, - attributeService: () => Promise + attributeService: LensAttributeService ): Promise { const { data, @@ -71,7 +71,7 @@ export async function getLensServices( stateTransfer, usageCollection, savedObjectsTagging, - attributeService: await attributeService(), + attributeService, http: coreStart.http, chrome: coreStart.chrome, overlays: coreStart.overlays, @@ -97,8 +97,8 @@ export async function mountApp( params: AppMountParameters, mountProps: { createEditorFrame: EditorFrameStart['createInstance']; - attributeService: () => Promise; - getPresentationUtilContext: () => Promise; + attributeService: LensAttributeService; + getPresentationUtilContext: () => FC; } ) { const { createEditorFrame, attributeService, getPresentationUtilContext } = mountProps; @@ -252,7 +252,7 @@ export async function mountApp( params.element.classList.add('lnsAppWrapper'); - const PresentationUtilContext = await getPresentationUtilContext(); + const PresentationUtilContext = getPresentationUtilContext(); render( diff --git a/x-pack/plugins/lens/public/app_plugin/shared/saved_modal_lazy.tsx b/x-pack/plugins/lens/public/app_plugin/shared/saved_modal_lazy.tsx index f1a537fe65928..e1df6ca60dd6e 100644 --- a/x-pack/plugins/lens/public/app_plugin/shared/saved_modal_lazy.tsx +++ b/x-pack/plugins/lens/public/app_plugin/shared/saved_modal_lazy.tsx @@ -8,11 +8,11 @@ import React, { Suspense, useEffect, useState } from 'react'; import { EuiLoadingSpinner, EuiOverlayMask } from '@elastic/eui'; -import { CoreStart } from 'kibana/public'; +import type { CoreStart } from 'kibana/public'; import type { SaveModalContainerProps } from '../save_modal_container'; -import type { LensAttributeService } from '../../lens_attribute_service'; import type { LensPluginStartDependencies } from '../../plugin'; import type { LensAppServices } from '../types'; + const SaveModal = React.lazy(() => import('../save_modal_container')); function LoadingSpinnerWithOverlay() { @@ -33,16 +33,20 @@ const LensSavedModalLazy = (props: SaveModalContainerProps) => { export function getSaveModalComponent( coreStart: CoreStart, - startDependencies: LensPluginStartDependencies, - attributeService: () => Promise + startDependencies: LensPluginStartDependencies ) { return (props: Omit) => { const [lensServices, setLensServices] = useState(); useEffect(() => { async function loadLensService() { - const { getLensServices } = await import('../../async_services'); - const lensServicesT = await getLensServices(coreStart, startDependencies, attributeService); + const { getLensServices, getLensAttributeService } = await import('../../async_services'); + + const lensServicesT = await getLensServices( + coreStart, + startDependencies, + getLensAttributeService(coreStart, startDependencies) + ); setLensServices(lensServicesT); } diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx index ac1324385dbd1..6f00dc37fcd52 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx @@ -20,7 +20,7 @@ import { } from '@elastic/eui'; import type { LensFilterEvent, LensTableRowContextMenuEvent } from '../../types'; import type { FormatFactory } from '../../../common'; -import { LensGridDirection } from '../../../common/expressions'; +import type { LensGridDirection } from '../../../common/expressions'; import { VisualizationContainer } from '../../visualization_container'; import { EmptyPlaceholder, findMinMaxByColumnId } from '../../shared_components'; import { LensIconChartDatatable } from '../../assets/chart_datatable'; diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx index b2a25cba329df..74c33a2b02a5b 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx @@ -9,7 +9,7 @@ import type { DatatableProps } from '../../common/expressions'; import type { LensMultiTable } from '../../common'; import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks'; import type { FormatFactory } from '../../common'; -import { getDatatable } from './expression'; +import { getDatatable } from '../../common/expressions'; function sampleArgs() { const indexPatternId = 'indexPatternId'; diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx index 4e541bce9a8c2..03691d56ee56a 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx @@ -18,9 +18,7 @@ import { DatatableComponent } from './components/table_basic'; import type { ILensInterpreterRenderHandlers } from '../types'; import type { FormatFactory } from '../../common'; -import { DatatableProps } from '../../common/expressions'; - -export { datatableColumn, getDatatable } from '../../common/expressions'; +import type { DatatableProps } from '../../common/expressions'; export const getDatatableRenderer = (dependencies: { formatFactory: FormatFactory; diff --git a/x-pack/plugins/lens/public/datatable_visualization/index.ts b/x-pack/plugins/lens/public/datatable_visualization/index.ts index 3349f229a6048..51f1f54cc03ce 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/index.ts +++ b/x-pack/plugins/lens/public/datatable_visualization/index.ts @@ -23,23 +23,14 @@ export interface DatatableVisualizationPluginSetupPlugins { } export class DatatableVisualization { - constructor() {} - setup( core: CoreSetup, { expressions, formatFactory, editorFrame, charts }: DatatableVisualizationPluginSetupPlugins ) { editorFrame.registerVisualization(async () => { - const { - getDatatable, - datatableColumn, - getDatatableRenderer, - getDatatableVisualization, - } = await import('../async_services'); + const { getDatatableRenderer, getDatatableVisualization } = await import('../async_services'); const palettes = await charts.palettes.getPalettes(); - expressions.registerFunction(() => datatableColumn); - expressions.registerFunction(() => getDatatable(() => formatFactory)); expressions.registerRenderer(() => getDatatableRenderer({ formatFactory, @@ -50,6 +41,7 @@ export class DatatableVisualization { uiSettings: core.uiSettings, }) ); + return getDatatableVisualization({ paletteService: palettes }); }); } diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx index e1b1c637fa24b..d97cfd3cbca23 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { CoreSetup, CoreStart } from 'kibana/public'; +import { CoreStart } from 'kibana/public'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; import { ExpressionsSetup, ExpressionsStart } from '../../../../../src/plugins/expressions/public'; import { EmbeddableSetup, EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; @@ -22,7 +22,6 @@ import { EditorFrameStart, } from '../types'; import { Document } from '../persistence/saved_object_store'; -import { mergeTables } from '../../common/expressions'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; import { DashboardStart } from '../../../../../src/plugins/dashboard/public'; @@ -59,8 +58,6 @@ async function collectAsyncDefinitions( } export class EditorFrameService { - constructor() {} - private readonly datasources: Array Promise)> = []; private readonly visualizations: Array Promise)> = []; @@ -81,12 +78,7 @@ export class EditorFrameService { return await persistedStateToExpression(resolvedDatasources, resolvedVisualizations, doc); }; - public setup( - core: CoreSetup, - plugins: EditorFrameSetupPlugins - ): EditorFrameSetup { - plugins.expressions.registerFunction(() => mergeTables); - + public setup(): EditorFrameSetup { return { registerDatasource: (datasource) => { this.datasources.push(datasource as Datasource); diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx b/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx index e8095f6c741a4..7f65e50bf4429 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx @@ -6,8 +6,8 @@ */ import React, { FC, useEffect } from 'react'; -import { CoreStart } from 'kibana/public'; -import { UiActionsStart } from 'src/plugins/ui_actions/public'; +import type { CoreStart } from 'kibana/public'; +import type { UiActionsStart } from 'src/plugins/ui_actions/public'; import type { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { EuiLoadingChart } from '@elastic/eui'; import { diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts index 5620f053cebf2..954905c51a4b7 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts @@ -21,7 +21,7 @@ import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; import { Start as InspectorStart } from '../../../../../src/plugins/inspector/public'; import { Document } from '../persistence/saved_object_store'; import { LensAttributeService } from '../lens_attribute_service'; -import { DOC_TYPE } from '../../common'; +import { DOC_TYPE } from '../../common/constants'; import { ErrorMessage } from '../editor_frame_service/types'; import { extract, inject } from '../../common/embeddable_factory'; diff --git a/x-pack/plugins/lens/public/expressions.ts b/x-pack/plugins/lens/public/expressions.ts new file mode 100644 index 0000000000000..27f3179a2d0c8 --- /dev/null +++ b/x-pack/plugins/lens/public/expressions.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ExpressionsSetup } from 'src/plugins/expressions/public'; + +import { + axisExtentConfig, + yAxisConfig, + axisTitlesVisibilityConfig, +} from '../common/expressions/xy_chart/axis_config'; +import { gridlinesConfig } from '../common/expressions/xy_chart/grid_lines_config'; +import { labelsOrientationConfig } from '../common/expressions/xy_chart/labels_orientation_config'; +import { layerConfig } from '../common/expressions/xy_chart/layer_config'; +import { legendConfig } from '../common/expressions/xy_chart/legend_config'; +import { tickLabelsConfig } from '../common/expressions/xy_chart/tick_labels_config'; +import { xyChart } from '../common/expressions/xy_chart/xy_chart'; + +import { getDatatable } from '../common/expressions/datatable/datatable'; +import { datatableColumn } from '../common/expressions/datatable/datatable_column'; + +import { heatmap } from '../common/expressions/heatmap_chart/heatmap_chart'; +import { heatmapGridConfig } from '../common/expressions/heatmap_chart/heatmap_grid'; +import { heatmapLegendConfig } from '../common/expressions/heatmap_chart/heatmap_legend'; + +import { mergeTables } from '../common/expressions/merge_tables'; +import { renameColumns } from '../common/expressions/rename_columns/rename_columns'; +import { pie } from '../common/expressions/pie_chart/pie_chart'; +import { formatColumn } from '../common/expressions/format_column'; +import { counterRate } from '../common/expressions/counter_rate'; +import { getTimeScale } from '../common/expressions/time_scale/time_scale'; +import { metricChart } from '../common/expressions/metric_chart/metric_chart'; + +export const setupExpressions = ( + expressions: ExpressionsSetup, + formatFactory: Parameters[0], + getTimeZone: Parameters[0] +) => + [ + pie, + xyChart, + mergeTables, + counterRate, + metricChart, + yAxisConfig, + layerConfig, + formatColumn, + legendConfig, + renameColumns, + gridlinesConfig, + datatableColumn, + tickLabelsConfig, + axisTitlesVisibilityConfig, + heatmap, + heatmapLegendConfig, + heatmapGridConfig, + axisExtentConfig, + labelsOrientationConfig, + getDatatable(formatFactory), + getTimeScale(getTimeZone), + ].forEach((expressionFn) => expressions.registerFunction(expressionFn)); diff --git a/x-pack/plugins/lens/public/heatmap_visualization/expression.tsx b/x-pack/plugins/lens/public/heatmap_visualization/expression.tsx index 98ce4b399ae8d..84c8f987f524d 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/heatmap_visualization/expression.tsx @@ -17,8 +17,6 @@ import type { ChartsPluginSetup, PaletteRegistry } from '../../../../../src/plug import { HeatmapChartReportable } from './chart_component'; import type { HeatmapExpressionProps } from './types'; -export { heatmapGridConfig, heatmapLegendConfig, heatmap } from '../../common/expressions'; - export const getHeatmapRenderer = (dependencies: { formatFactory: FormatFactory; chartsThemeService: ChartsPluginSetup['theme']; diff --git a/x-pack/plugins/lens/public/heatmap_visualization/index.ts b/x-pack/plugins/lens/public/heatmap_visualization/index.ts index 5fb4524939f11..3ac3f769e4b5f 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/index.ts +++ b/x-pack/plugins/lens/public/heatmap_visualization/index.ts @@ -20,28 +20,15 @@ export interface HeatmapVisualizationPluginSetupPlugins { } export class HeatmapVisualization { - constructor() {} - setup( core: CoreSetup, { expressions, formatFactory, editorFrame, charts }: HeatmapVisualizationPluginSetupPlugins ) { editorFrame.registerVisualization(async () => { const timeZone = getTimeZone(core.uiSettings); - - const { - getHeatmapVisualization, - heatmap, - heatmapLegendConfig, - heatmapGridConfig, - getHeatmapRenderer, - } = await import('../async_services'); + const { getHeatmapVisualization, getHeatmapRenderer } = await import('../async_services'); const palettes = await charts.palettes.getPalettes(); - expressions.registerFunction(() => heatmap); - expressions.registerFunction(() => heatmapLegendConfig); - expressions.registerFunction(() => heatmapGridConfig); - expressions.registerRenderer( getHeatmapRenderer({ formatFactory, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_scaling.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_scaling.tsx index 7c611230683d3..8a670e7562573 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_scaling.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/time_scaling.tsx @@ -5,9 +5,16 @@ * 2.0. */ -import { EuiToolTip } from '@elastic/eui'; -import { EuiIcon } from '@elastic/eui'; -import { EuiFormRow, EuiSelect, EuiFlexItem, EuiFlexGroup, EuiButtonIcon } from '@elastic/eui'; +import { + EuiToolTip, + EuiIcon, + EuiFormRow, + EuiSelect, + EuiFlexItem, + EuiFlexGroup, + EuiButtonIcon, +} from '@elastic/eui'; + import { i18n } from '@kbn/i18n'; import React from 'react'; import { @@ -17,7 +24,7 @@ import { } from '../operations'; import type { TimeScaleUnit } from '../../../common/expressions'; import { unitSuffixesLong } from '../../../common/suffix_formatter'; -import { IndexPatternLayer } from '../types'; +import type { IndexPatternLayer } from '../types'; export function setTimeScaling( columnId: string, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts index 9ff80f51bea97..5f4afc9df6179 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts @@ -20,7 +20,6 @@ import type { FieldFormatsStart, FieldFormatsSetup, } from '../../../../../src/plugins/field_formats/public'; -import { getTimeZone } from '../utils'; export interface IndexPatternDatasourceSetupPlugins { expressions: ExpressionsSetup; @@ -38,8 +37,6 @@ export interface IndexPatternDatasourceStartPlugins { } export class IndexPatternDatasource { - constructor() {} - setup( core: CoreSetup, { @@ -50,15 +47,9 @@ export class IndexPatternDatasource { }: IndexPatternDatasourceSetupPlugins ) { editorFrame.registerDatasource(async () => { - const { - getIndexPatternDatasource, - renameColumns, - formatColumn, - counterRate, - getTimeScale, - getSuffixFormatter, - suffixFormatterId, - } = await import('../async_services'); + const { getIndexPatternDatasource, getSuffixFormatter, suffixFormatterId } = await import( + '../async_services' + ); if (!fieldFormatsSetup.has(suffixFormatterId)) { const startServices = createStartServicesGetter(core.getStartServices); @@ -69,11 +60,6 @@ export class IndexPatternDatasource { fieldFormatsSetup.register([suffixFormatter]); } - expressions.registerFunction(getTimeScale(() => getTimeZone(core.uiSettings))); - expressions.registerFunction(counterRate); - expressions.registerFunction(renameColumns); - expressions.registerFunction(formatColumn); - const [ coreStart, { indexPatternFieldEditor, uiActions, data, fieldFormats }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 8f66bcf7fe49c..6a45e3c987f3d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -70,19 +70,13 @@ export function columnToOperation(column: IndexPatternColumn, uniqueLabel?: stri }; } -export { - CounterRateArgs, - ExpressionFunctionCounterRate, - counterRate, -} from '../../common/expressions'; -export { FormatColumnArgs, supportedFormats, formatColumn } from '../../common/expressions'; +export type { FormatColumnArgs, TimeScaleArgs, CounterRateArgs } from '../../common/expressions'; + export { getSuffixFormatter, unitSuffixesLong, suffixFormatterId, } from '../../common/suffix_formatter'; -export { getTimeScale, TimeScaleArgs } from '../../common/expressions'; -export { renameColumns } from '../../common/expressions'; export function getIndexPatternDatasource({ core, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index cff036db4813b..b0793bf912bb2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -8,7 +8,7 @@ import { flatten, minBy, pick, mapValues, partition } from 'lodash'; import { i18n } from '@kbn/i18n'; import { generateId } from '../id_generator'; -import { DatasourceSuggestion, TableChangeType } from '../types'; +import type { DatasourceSuggestion, TableChangeType } from '../types'; import { columnToOperation } from './indexpattern'; import { insertNewColumn, @@ -23,7 +23,7 @@ import { getReferencedColumnIds, } from './operations'; import { hasField } from './utils'; -import { +import type { IndexPattern, IndexPatternPrivateState, IndexPatternLayer, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx index ceb02ab724ac5..29e7de18ca4ad 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx @@ -21,7 +21,7 @@ import { RangeEditor } from './range_editor'; import { OperationDefinition } from '../index'; import { FieldBasedIndexPatternColumn } from '../column_types'; import { updateColumnParam } from '../../layer_helpers'; -import { supportedFormats } from '../../../../../common/expressions'; +import { supportedFormats } from '../../../../../common/expressions/format_column/supported_formats'; import { MODES, AUTO_BARS, DEFAULT_INTERVAL, MIN_HISTOGRAM_BARS, SLICES } from './constants'; import { IndexPattern, IndexPatternField } from '../../../types'; import { getInvalidFieldMessage, isValidNumber } from '../helpers'; diff --git a/x-pack/plugins/lens/public/lens_attribute_service.ts b/x-pack/plugins/lens/public/lens_attribute_service.ts index 09c98b3dcba72..fb4ef4fee72ef 100644 --- a/x-pack/plugins/lens/public/lens_attribute_service.ts +++ b/x-pack/plugins/lens/public/lens_attribute_service.ts @@ -5,17 +5,17 @@ * 2.0. */ -import { CoreStart } from '../../../../src/core/public'; -import { LensPluginStartDependencies } from './plugin'; -import { AttributeService } from '../../../../src/plugins/embeddable/public'; -import { +import type { CoreStart } from '../../../../src/core/public'; +import type { LensPluginStartDependencies } from './plugin'; +import type { AttributeService } from '../../../../src/plugins/embeddable/public'; +import type { ResolvedLensSavedObjectAttributes, LensByValueInput, LensByReferenceInput, } from './embeddable/embeddable'; import { SavedObjectIndexStore } from './persistence'; import { checkForDuplicateTitle, OnSaveProps } from '../../../../src/plugins/saved_objects/public'; -import { DOC_TYPE } from '../common'; +import { DOC_TYPE } from '../common/constants'; export type LensAttributeService = AttributeService< ResolvedLensSavedObjectAttributes, diff --git a/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx b/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx index 36ae3904f073c..a3ac5b5837772 100644 --- a/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import { MetricChart, metricChart } from './expression'; -import { MetricConfig } from '../../common/expressions'; +import { MetricChart } from './expression'; +import { MetricConfig, metricChart } from '../../common/expressions'; import React from 'react'; import { shallow } from 'enzyme'; import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks'; diff --git a/x-pack/plugins/lens/public/metric_visualization/expression.tsx b/x-pack/plugins/lens/public/metric_visualization/expression.tsx index 41b487e790a08..8838e6df777c9 100644 --- a/x-pack/plugins/lens/public/metric_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/expression.tsx @@ -19,9 +19,7 @@ import { EmptyPlaceholder } from '../shared_components'; import { LensIconChartMetric } from '../assets/chart_metric'; import type { FormatFactory } from '../../common'; import type { MetricChartProps } from '../../common/expressions'; - -export { metricChart } from '../../common/expressions'; -export type { MetricState, MetricConfig } from '../../common/expressions'; +export type { MetricChartProps, MetricState, MetricConfig } from '../../common/expressions'; export const getMetricChartRenderer = ( formatFactory: FormatFactory diff --git a/x-pack/plugins/lens/public/metric_visualization/index.ts b/x-pack/plugins/lens/public/metric_visualization/index.ts index 29138979ab858..20c25b285bd5b 100644 --- a/x-pack/plugins/lens/public/metric_visualization/index.ts +++ b/x-pack/plugins/lens/public/metric_visualization/index.ts @@ -17,18 +17,12 @@ export interface MetricVisualizationPluginSetupPlugins { } export class MetricVisualization { - constructor() {} - setup( _core: CoreSetup | null, { expressions, formatFactory, editorFrame }: MetricVisualizationPluginSetupPlugins ) { editorFrame.registerVisualization(async () => { - const { metricVisualization, metricChart, getMetricChartRenderer } = await import( - '../async_services' - ); - - expressions.registerFunction(() => metricChart); + const { metricVisualization, getMetricChartRenderer } = await import('../async_services'); expressions.registerRenderer(() => getMetricChartRenderer(formatFactory)); return metricVisualization; diff --git a/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.ts b/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.ts index de79f5f0a4cbc..3d6b2683b4ad2 100644 --- a/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.ts +++ b/x-pack/plugins/lens/public/metric_visualization/metric_suggestions.ts @@ -6,7 +6,7 @@ */ import { SuggestionRequest, VisualizationSuggestion, TableSuggestion } from '../types'; -import { MetricState } from '../../common/expressions'; +import type { MetricState } from '../../common/expressions'; import { layerTypes } from '../../common'; import { LensIconChartMetric } from '../assets/chart_metric'; diff --git a/x-pack/plugins/lens/public/pie_visualization/expression.tsx b/x-pack/plugins/lens/public/pie_visualization/expression.tsx index c947d50d5b910..d26289450bd0f 100644 --- a/x-pack/plugins/lens/public/pie_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/expression.tsx @@ -19,8 +19,6 @@ import type { FormatFactory } from '../../common'; import type { PieExpressionProps } from '../../common/expressions'; import type { ChartsPluginSetup, PaletteRegistry } from '../../../../../src/plugins/charts/public'; -export { pie } from '../../common/expressions'; - export const getPieRenderer = (dependencies: { formatFactory: FormatFactory; chartsThemeService: ChartsPluginSetup['theme']; diff --git a/x-pack/plugins/lens/public/pie_visualization/index.ts b/x-pack/plugins/lens/public/pie_visualization/index.ts index 6d34de85f1801..b4670b3b9c9dd 100644 --- a/x-pack/plugins/lens/public/pie_visualization/index.ts +++ b/x-pack/plugins/lens/public/pie_visualization/index.ts @@ -24,18 +24,14 @@ export interface PieVisualizationPluginStartPlugins { } export class PieVisualization { - constructor() {} - setup( core: CoreSetup, { expressions, formatFactory, editorFrame, charts }: PieVisualizationPluginSetupPlugins ) { editorFrame.registerVisualization(async () => { - const { getPieVisualization, pie, getPieRenderer } = await import('../async_services'); + const { getPieVisualization, getPieRenderer } = await import('../async_services'); const palettes = await charts.palettes.getPalettes(); - expressions.registerFunction(() => pie); - expressions.registerRenderer( getPieRenderer({ formatFactory, diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 26278f446c558..5326927d2c6c5 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -5,28 +5,34 @@ * 2.0. */ -import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public'; +import type { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public'; import type { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import type { FieldFormatsSetup, FieldFormatsStart } from 'src/plugins/field_formats/public'; -import { UsageCollectionSetup, UsageCollectionStart } from 'src/plugins/usage_collection/public'; -import { SpacesPluginStart } from '../../spaces/public'; -import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; -import { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public'; -import { DashboardStart } from '../../../../src/plugins/dashboard/public'; -import { +import type { + UsageCollectionSetup, + UsageCollectionStart, +} from 'src/plugins/usage_collection/public'; +import type { + DataPublicPluginSetup, + DataPublicPluginStart, +} from '../../../../src/plugins/data/public'; +import type { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public'; +import type { DashboardStart } from '../../../../src/plugins/dashboard/public'; +import type { SpacesPluginStart } from '../../spaces/public'; +import type { ExpressionsServiceSetup, ExpressionsSetup, ExpressionsStart, } from '../../../../src/plugins/expressions/public'; -import { +import type { VisualizationsSetup, VisualizationsStart, } from '../../../../src/plugins/visualizations/public'; -import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public'; -import { UrlForwardingSetup } from '../../../../src/plugins/url_forwarding/public'; -import { GlobalSearchPluginSetup } from '../../global_search/public'; -import { ChartsPluginSetup, ChartsPluginStart } from '../../../../src/plugins/charts/public'; -import { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public'; +import type { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public'; +import type { UrlForwardingSetup } from '../../../../src/plugins/url_forwarding/public'; +import type { GlobalSearchPluginSetup } from '../../global_search/public'; +import type { ChartsPluginSetup, ChartsPluginStart } from '../../../../src/plugins/charts/public'; +import type { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public'; import { EmbeddableStateTransfer } from '../../../../src/plugins/embeddable/public'; import type { EditorFrameService as EditorFrameServiceType } from './editor_frame_service'; import { IndexPatternFieldEditorStart } from '../../../../src/plugins/index_pattern_field_editor/public'; @@ -51,29 +57,33 @@ import type { PieVisualizationPluginSetupPlugins, } from './pie_visualization'; import type { HeatmapVisualization as HeatmapVisualizationType } from './heatmap_visualization'; -import { AppNavLinkStatus } from '../../../../src/core/public'; import type { SavedObjectTaggingPluginStart } from '../../saved_objects_tagging/public'; +import { AppNavLinkStatus } from '../../../../src/core/public'; + import { UiActionsStart, ACTION_VISUALIZE_FIELD, VISUALIZE_FIELD_TRIGGER, } from '../../../../src/plugins/ui_actions/public'; -import { APP_ID, FormatFactory, getEditPath, NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; -import type { EditorFrameStart, VisualizationType } from './types'; +import { APP_ID, getEditPath, NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common/constants'; +import type { FormatFactory } from '../common/types'; +import type { VisualizationType } from './types'; import { getLensAliasConfig } from './vis_type_alias'; import { visualizeFieldAction } from './trigger_actions/visualize_field_actions'; -import { getSearchProvider } from './search_provider'; -import { LensAttributeService } from './lens_attribute_service'; -import { LensEmbeddableInput } from './embeddable'; +import type { LensEmbeddableInput } from './embeddable'; import { EmbeddableFactory, LensEmbeddableStartServices } from './embeddable/embeddable_factory'; import { EmbeddableComponentProps, getEmbeddableComponent, } from './embeddable/embeddable_component'; import { getSaveModalComponent } from './app_plugin/shared/saved_modal_lazy'; -import { SaveModalContainerProps } from './app_plugin/save_modal_container'; +import type { SaveModalContainerProps } from './app_plugin/save_modal_container'; + +import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public'; +import { setupExpressions } from './expressions'; +import { getSearchProvider } from './search_provider'; export interface LensPluginSetupDependencies { urlForwarding: UrlForwardingSetup; @@ -154,8 +164,6 @@ export interface LensPublicStart { export class LensPlugin { private datatableVisualization: DatatableVisualizationType | undefined; private editorFrameService: EditorFrameServiceType | undefined; - private createEditorFrame: EditorFrameStart['createInstance'] | null = null; - private attributeService: (() => Promise) | null = null; private indexpatternDatasource: IndexPatternDatasourceType | undefined; private xyVisualization: XyVisualizationType | undefined; private metricVisualization: MetricVisualizationType | undefined; @@ -178,37 +186,32 @@ export class LensPlugin { usageCollection, }: LensPluginSetupDependencies ) { - this.attributeService = async () => { - const { getLensAttributeService } = await import('./async_services'); - const [coreStart, startDependencies] = await core.getStartServices(); - return getLensAttributeService(coreStart, startDependencies); - }; + const startServices = createStartServicesGetter(core.getStartServices); const getStartServices = async (): Promise => { - const [coreStart, deps] = await core.getStartServices(); + const { getLensAttributeService } = await import('./async_services'); + const { core: coreStart, plugins } = startServices(); - this.initParts( + await this.initParts( core, data, - embeddable, charts, expressions, - usageCollection, fieldFormats, - deps.fieldFormats.deserialize + plugins.fieldFormats.deserialize ); return { - attributeService: await this.attributeService!(), + attributeService: getLensAttributeService(coreStart, plugins), capabilities: coreStart.application.capabilities, coreHttp: coreStart.http, - timefilter: deps.data.query.timefilter.timefilter, - expressionRenderer: deps.expressions.ReactExpressionRenderer, + timefilter: plugins.data.query.timefilter.timefilter, + expressionRenderer: plugins.expressions.ReactExpressionRenderer, documentToExpression: this.editorFrameService!.documentToExpression, - indexPatternService: deps.data.indexPatterns, - uiActions: deps.uiActions, + indexPatternService: plugins.data.indexPatterns, + uiActions: plugins.uiActions, usageCollection, - inspector: deps.inspector, + inspector: plugins.inspector, }; }; @@ -218,17 +221,22 @@ export class LensPlugin { visualizations.registerAlias(getLensAliasConfig()); - const getPresentationUtilContext = async () => { - const [, deps] = await core.getStartServices(); - const { ContextProvider } = deps.presentationUtil; - return ContextProvider; - }; + setupExpressions( + expressions, + () => startServices().plugins.fieldFormats.deserialize, + async () => { + const { getTimeZone } = await import('./utils'); + return getTimeZone(core.uiSettings); + } + ); + + const getPresentationUtilContext = () => + startServices().plugins.presentationUtil.ContextProvider; const ensureDefaultIndexPattern = async () => { - const [, deps] = await core.getStartServices(); // make sure a default index pattern exists // if not, the page will be redirected to management and visualize won't be rendered - await deps.data.indexPatterns.ensureDefaultIndexPattern(); + await startServices().plugins.data.indexPatterns.ensureDefaultIndexPattern(); }; core.application.register({ @@ -236,25 +244,27 @@ export class LensPlugin { title: NOT_INTERNATIONALIZED_PRODUCT_NAME, navLinkStatus: AppNavLinkStatus.hidden, mount: async (params: AppMountParameters) => { - const [, deps] = await core.getStartServices(); + const { core: coreStart, plugins: deps } = startServices(); await this.initParts( core, data, - embeddable, charts, expressions, - usageCollection, fieldFormats, deps.fieldFormats.deserialize ); - const { mountApp, stopReportManager } = await import('./async_services'); + const { mountApp, stopReportManager, getLensAttributeService } = await import( + './async_services' + ); + const frameStart = this.editorFrameService!.start(coreStart, deps); + this.stopReportManager = stopReportManager; await ensureDefaultIndexPattern(); return mountApp(core, params, { - createEditorFrame: this.createEditorFrame!, - attributeService: this.attributeService!, + createEditorFrame: frameStart.createInstance, + attributeService: getLensAttributeService(coreStart, deps), getPresentationUtilContext, }); }, @@ -280,10 +290,8 @@ export class LensPlugin { private async initParts( core: CoreSetup, data: DataPublicPluginSetup, - embeddable: EmbeddableSetup | undefined, charts: ChartsPluginSetup, expressions: ExpressionsServiceSetup, - usageCollection: UsageCollectionSetup | undefined, fieldFormats: FieldFormatsSetup, formatFactory: FormatFactory ) { @@ -303,13 +311,9 @@ export class LensPlugin { this.metricVisualization = new MetricVisualization(); this.pieVisualization = new PieVisualization(); this.heatmapVisualization = new HeatmapVisualization(); - const editorFrameSetupInterface = this.editorFrameService.setup(core, { - data, - embeddable, - charts, - expressions, - usageCollection, - }); + + const editorFrameSetupInterface = this.editorFrameService.setup(); + const dependencies: IndexPatternDatasourceSetupPlugins & XyVisualizationPluginSetupPlugins & DatatableVisualizationPluginSetupPlugins & @@ -328,9 +332,6 @@ export class LensPlugin { this.metricVisualization.setup(core, dependencies); this.pieVisualization.setup(core, dependencies); this.heatmapVisualization.setup(core, dependencies); - const [coreStart, startDependencies] = await core.getStartServices(); - const frameStart = this.editorFrameService.start(coreStart, startDependencies); - this.createEditorFrame = frameStart.createInstance; } start(core: CoreStart, startDependencies: LensPluginStartDependencies): LensPublicStart { @@ -345,7 +346,7 @@ export class LensPlugin { return { EmbeddableComponent: getEmbeddableComponent(core, startDependencies), - SaveModalComponent: getSaveModalComponent(core, startDependencies, this.attributeService!), + SaveModalComponent: getSaveModalComponent(core, startDependencies), navigateToPrefilledEditor: ( input, { openInNewTab = false, originatingApp = '', originatingPath } = {} diff --git a/x-pack/plugins/lens/public/search_provider.ts b/x-pack/plugins/lens/public/search_provider.ts index 4bc18f2653a0b..a0d41da2d9740 100644 --- a/x-pack/plugins/lens/public/search_provider.ts +++ b/x-pack/plugins/lens/public/search_provider.ts @@ -5,12 +5,13 @@ * 2.0. */ -import { ApplicationStart } from 'kibana/public'; +import type { ApplicationStart } from 'kibana/public'; import { from, of } from 'rxjs'; import { i18n } from '@kbn/i18n'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; -import { GlobalSearchResultProvider } from '../../global_search/public'; -import { getFullPath } from '../common'; +import { getFullPath } from '../common/constants'; + +import type { GlobalSearchResultProvider } from '../../global_search/public'; /** * Global search provider adding a Lens entry. diff --git a/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.test.tsx b/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.test.tsx index cda891871168e..33f6ac379cd80 100644 --- a/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.test.tsx +++ b/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.test.tsx @@ -14,6 +14,7 @@ import { ReactWrapper } from 'enzyme'; import type { CustomPaletteParams } from '../../../common'; import { applyPaletteParams } from './utils'; import { CustomizablePalette } from './palette_configuration'; +import { CUSTOM_PALETTE } from './constants'; import { act } from 'react-dom/test-utils'; // mocking random id generator function @@ -129,6 +130,21 @@ describe('palette panel', () => { }); }); + it('should restore the reverse initial state on transitioning', () => { + const instance = mountWithIntl(); + + changePaletteIn(instance, 'negative'); + + expect(props.setPalette).toHaveBeenCalledWith({ + type: 'palette', + name: 'negative', + params: expect.objectContaining({ + name: 'negative', + reverse: false, + }), + }); + }); + it('should rewrite the min/max range values on palette change', () => { const instance = mountWithIntl(); @@ -175,6 +191,20 @@ describe('palette panel', () => { }) ); }); + + it('should transition a predefined palette to a custom one on reverse click', () => { + const instance = mountWithIntl(); + + toggleReverse(instance, true); + + expect(props.setPalette).toHaveBeenCalledWith( + expect.objectContaining({ + params: expect.objectContaining({ + name: CUSTOM_PALETTE, + }), + }) + ); + }); }); describe('percentage / number modes', () => { diff --git a/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx b/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx index 1d1e212b87c0c..019e83fb0aa59 100644 --- a/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx +++ b/x-pack/plugins/lens/public/shared_components/coloring/palette_configuration.tsx @@ -106,6 +106,7 @@ export function CustomizablePalette({ ...activePalette.params, name: newPalette.name, colorStops: undefined, + reverse: false, // restore the reverse flag }; const newColorStops = getColorStops(palettes, [], activePalette, dataBounds); @@ -317,28 +318,20 @@ export function CustomizablePalette({ className="lnsPalettePanel__reverseButton" data-test-subj="lnsPalettePanel_dynamicColoring_reverse" onClick={() => { - const params: CustomPaletteParams = { reverse: !activePalette.params?.reverse }; - if (isCurrentPaletteCustom) { - params.colorStops = reversePalette(colorStopsToShow); - params.stops = getPaletteStops( - palettes, - { - ...(activePalette?.params || {}), - colorStops: params.colorStops, - }, - { dataBounds } - ); - } else { - params.stops = reversePalette( - activePalette?.params?.stops || - getPaletteStops( - palettes, - { ...activePalette.params, ...params }, - { prevPalette: activePalette.name, dataBounds } - ) - ); - } - setPalette(mergePaletteParams(activePalette, params)); + // when reversing a palette, the palette is automatically transitioned to a custom palette + const newParams = getSwitchToCustomParams( + palettes, + activePalette, + { + colorStops: reversePalette(colorStopsToShow), + steps: activePalette.params?.steps || DEFAULT_COLOR_STEPS, + reverse: !activePalette.params?.reverse, // Store the reverse state + rangeMin: colorStopsToShow[0]?.stop, + rangeMax: colorStopsToShow[colorStopsToShow.length - 1]?.stop, + }, + dataBounds + ); + setPalette(newParams); }} > diff --git a/x-pack/plugins/lens/public/shared_components/coloring/palette_picker.tsx b/x-pack/plugins/lens/public/shared_components/coloring/palette_picker.tsx index 2a415cd178925..b21b732820eaa 100644 --- a/x-pack/plugins/lens/public/shared_components/coloring/palette_picker.tsx +++ b/x-pack/plugins/lens/public/shared_components/coloring/palette_picker.tsx @@ -83,7 +83,7 @@ export function PalettePicker({ value: id, title, type: FIXED_PROGRESSION, - palette: activePalette?.params?.reverse ? colors.reverse() : colors, + palette: colors, 'data-test-subj': `${id}-palette`, }; }); diff --git a/x-pack/plugins/lens/public/trigger_actions/visualize_field_actions.ts b/x-pack/plugins/lens/public/trigger_actions/visualize_field_actions.ts index 9e7507b01bc59..4e105ed9db499 100644 --- a/x-pack/plugins/lens/public/trigger_actions/visualize_field_actions.ts +++ b/x-pack/plugins/lens/public/trigger_actions/visualize_field_actions.ts @@ -11,7 +11,7 @@ import { ACTION_VISUALIZE_LENS_FIELD, VisualizeFieldContext, } from '../../../../../src/plugins/ui_actions/public'; -import { ApplicationStart } from '../../../../../src/core/public'; +import type { ApplicationStart } from '../../../../../src/core/public'; export const visualizeFieldAction = (application: ApplicationStart) => createAction({ diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index b7dd3ed3733cf..993be9a06a2d9 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -4,16 +4,20 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { uniq } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { IndexPattern, IndexPatternsContract, TimefilterContract } from 'src/plugins/data/public'; -import { IUiSettingsClient } from 'kibana/public'; import moment from 'moment-timezone'; -import { SavedObjectReference } from 'kibana/public'; -import { uniq } from 'lodash'; -import { Document } from './persistence/saved_object_store'; -import { Datasource, DatasourceMap } from './types'; -import { DatasourceStates } from './state_management'; + +import type { + IndexPattern, + IndexPatternsContract, + TimefilterContract, +} from 'src/plugins/data/public'; +import type { IUiSettingsClient } from 'kibana/public'; +import type { SavedObjectReference } from 'kibana/public'; +import type { Document } from './persistence/saved_object_store'; +import type { Datasource, DatasourceMap } from './types'; +import type { DatasourceStates } from './state_management'; export function getVisualizeGeoFieldMessage(fieldType: string) { return i18n.translate('xpack.lens.visualizeGeoFieldMessage', { diff --git a/x-pack/plugins/lens/public/vis_type_alias.ts b/x-pack/plugins/lens/public/vis_type_alias.ts index 5b48ef8b31923..96332e07069b0 100644 --- a/x-pack/plugins/lens/public/vis_type_alias.ts +++ b/x-pack/plugins/lens/public/vis_type_alias.ts @@ -6,8 +6,8 @@ */ import { i18n } from '@kbn/i18n'; -import { VisTypeAlias } from 'src/plugins/visualizations/public'; -import { getBasePath, getEditPath } from '../common'; +import type { VisTypeAlias } from 'src/plugins/visualizations/public'; +import { getBasePath, getEditPath } from '../common/constants'; export const getLensAliasConfig = (): VisTypeAlias => ({ aliasPath: getBasePath(), diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx index 3994aadd9a989..671db4653a88a 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx @@ -22,9 +22,10 @@ import { LayoutDirection, } from '@elastic/charts'; import { PaletteOutput } from 'src/plugins/charts/public'; -import { calculateMinInterval, XYChart, XYChartRenderProps, xyChart } from './expression'; +import { calculateMinInterval, XYChart, XYChartRenderProps } from './expression'; import type { LensMultiTable } from '../../common'; import { layerTypes } from '../../common'; +import { xyChart } from '../../common/expressions'; import { layerConfig, legendConfig, diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 75d14d9b48ee3..b7f1a9dabf3c7 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -74,18 +74,6 @@ type SeriesSpec = InferPropType & InferPropType & InferPropType; -export { - legendConfig, - yAxisConfig, - tickLabelsConfig, - gridlinesConfig, - axisTitlesVisibilityConfig, - axisExtentConfig, - layerConfig, - xyChart, - labelsOrientationConfig, -} from '../../common/expressions'; - export type XYChartRenderProps = XYChartProps & { chartsThemeService: ChartsPluginSetup['theme']; chartsActiveCursorService: ChartsPluginStart['activeCursor']; diff --git a/x-pack/plugins/lens/public/xy_visualization/index.ts b/x-pack/plugins/lens/public/xy_visualization/index.ts index 6823ffedc9d90..f9d48ffaaae37 100644 --- a/x-pack/plugins/lens/public/xy_visualization/index.ts +++ b/x-pack/plugins/lens/public/xy_visualization/index.ts @@ -21,37 +21,14 @@ export interface XyVisualizationPluginSetupPlugins { } export class XyVisualization { - constructor() {} - setup( core: CoreSetup, { expressions, formatFactory, editorFrame }: XyVisualizationPluginSetupPlugins ) { editorFrame.registerVisualization(async () => { - const { - legendConfig, - yAxisConfig, - tickLabelsConfig, - gridlinesConfig, - axisTitlesVisibilityConfig, - axisExtentConfig, - labelsOrientationConfig, - layerConfig, - xyChart, - getXyChartRenderer, - getXyVisualization, - } = await import('../async_services'); + const { getXyChartRenderer, getXyVisualization } = await import('../async_services'); const [, { charts, fieldFormats }] = await core.getStartServices(); const palettes = await charts.palettes.getPalettes(); - expressions.registerFunction(() => legendConfig); - expressions.registerFunction(() => yAxisConfig); - expressions.registerFunction(() => tickLabelsConfig); - expressions.registerFunction(() => axisExtentConfig); - expressions.registerFunction(() => labelsOrientationConfig); - expressions.registerFunction(() => gridlinesConfig); - expressions.registerFunction(() => axisTitlesVisibilityConfig); - expressions.registerFunction(() => layerConfig); - expressions.registerFunction(() => xyChart); expressions.registerRenderer( getXyChartRenderer({ diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index 9d32c2f71c530..4729cfb96f324 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -17,8 +17,8 @@ import { LensIconChartBarHorizontalStacked } from '../assets/chart_bar_horizonta import { LensIconChartBarHorizontalPercentage } from '../assets/chart_bar_horizontal_percentage'; import { LensIconChartLine } from '../assets/chart_line'; -import { VisualizationType } from '../types'; -import { +import type { VisualizationType } from '../types'; +import type { SeriesType, ValueLabelConfig, LegendConfig, diff --git a/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.ts b/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.ts index 86a3a600b58ab..4423d9e659119 100644 --- a/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.ts +++ b/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.ts @@ -9,6 +9,7 @@ import { EmbeddableRegistryDefinition } from 'src/plugins/embeddable/server'; import type { SerializableRecord } from '@kbn/utility-types'; import { DOC_TYPE } from '../../common'; import { + commonMakeReversePaletteAsCustom, commonRemoveTimezoneDateHistogramParam, commonRenameOperationsForFormula, commonUpdateVisLayerType, @@ -17,6 +18,7 @@ import { LensDocShape713, LensDocShape715, LensDocShapePre712, + VisState716, VisStatePre715, } from '../migrations/types'; import { extract, inject } from '../../common/embeddable_factory'; @@ -50,6 +52,14 @@ export const lensEmbeddableFactory = (): EmbeddableRegistryDefinition => { attributes: migratedLensState, } as unknown) as SerializableRecord; }, + '7.16.0': (state) => { + const lensState = (state as unknown) as { attributes: LensDocShape715 }; + const migratedLensState = commonMakeReversePaletteAsCustom(lensState.attributes); + return ({ + ...lensState, + attributes: migratedLensState, + } as unknown) as SerializableRecord; + }, }, extract, inject, diff --git a/x-pack/plugins/lens/server/migrations/common_migrations.ts b/x-pack/plugins/lens/server/migrations/common_migrations.ts index fda4300e03ea9..5755416957440 100644 --- a/x-pack/plugins/lens/server/migrations/common_migrations.ts +++ b/x-pack/plugins/lens/server/migrations/common_migrations.ts @@ -6,6 +6,7 @@ */ import { cloneDeep } from 'lodash'; +import { PaletteOutput } from 'src/plugins/charts/common'; import { LensDocShapePre712, OperationTypePre712, @@ -15,8 +16,9 @@ import { LensDocShape715, VisStatePost715, VisStatePre715, + VisState716, } from './types'; -import { layerTypes } from '../../common'; +import { CustomPaletteParams, layerTypes } from '../../common'; export const commonRenameOperationsForFormula = ( attributes: LensDocShapePre712 @@ -98,3 +100,56 @@ export const commonUpdateVisLayerType = ( } return newAttributes as LensDocShape715; }; + +function moveDefaultPaletteToPercentCustomInPlace(palette?: PaletteOutput) { + if (palette?.params?.reverse && palette.params.name !== 'custom' && palette.params.stops) { + // change to palette type to custom and migrate to a percentage type of mode + palette.name = 'custom'; + palette.params.name = 'custom'; + // we can make strong assumptions here: + // because it was a default palette reversed it means that stops were the default ones + // so when migrating, because there's no access to active data, we could leverage the + // percent rangeType to define colorStops in percent. + // + // Stops should be defined, but reversed, as the previous code was rewriting them on reverse. + // + // The only change the user should notice should be the mode changing from number to percent + // but the final result *must* be identical + palette.params.rangeType = 'percent'; + const steps = palette.params.stops.length; + palette.params.rangeMin = 0; + palette.params.rangeMax = 80; + palette.params.steps = steps; + palette.params.colorStops = palette.params.stops.map(({ color }, index) => ({ + color, + stop: (index * 100) / steps, + })); + palette.params.stops = palette.params.stops.map(({ color }, index) => ({ + color, + stop: ((1 + index) * 100) / steps, + })); + } +} + +export const commonMakeReversePaletteAsCustom = ( + attributes: LensDocShape715 +): LensDocShape715 => { + const newAttributes = cloneDeep(attributes); + const vizState = (newAttributes as LensDocShape715).state.visualization; + if ( + attributes.visualizationType !== 'lnsDatatable' && + attributes.visualizationType !== 'lnsHeatmap' + ) { + return newAttributes; + } + if ('columns' in vizState) { + for (const column of vizState.columns) { + if (column.colorMode && column.colorMode !== 'none') { + moveDefaultPaletteToPercentCustomInPlace(column.palette); + } + } + } else { + moveDefaultPaletteToPercentCustomInPlace(vizState.palette); + } + return newAttributes; +}; diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts index afc6e6c6a590c..c16c5b5169ac5 100644 --- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts @@ -12,8 +12,9 @@ import { SavedObjectMigrationFn, SavedObjectUnsanitizedDoc, } from 'src/core/server'; -import { LensDocShape715, VisStatePost715, VisStatePre715 } from './types'; -import { layerTypes } from '../../common'; +import { LensDocShape715, VisState716, VisStatePost715, VisStatePre715 } from './types'; +import { CustomPaletteParams, layerTypes } from '../../common'; +import { PaletteOutput } from 'src/plugins/charts/common'; describe('Lens migrations', () => { describe('7.7.0 missing dimensions in XY', () => { @@ -1129,4 +1130,276 @@ describe('Lens migrations', () => { } }); }); + + describe('7.16.0 move reversed default palette to custom palette', () => { + const context = ({ log: { warning: () => {} } } as unknown) as SavedObjectMigrationContext; + const example = ({ + type: 'lens', + id: 'mocked-saved-object-id', + attributes: { + savedObjectId: '1', + title: 'MyRenamedOps', + description: '', + visualizationType: null, + state: { + datasourceMetaData: { + filterableIndexPatterns: [], + }, + datasourceStates: { + indexpattern: { + currentIndexPatternId: 'logstash-*', + layers: { + '2': { + columns: { + '3': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { interval: 'auto', timeZone: 'Europe/Berlin' }, + }, + '4': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { interval: 'auto' }, + }, + '5': { + label: '@timestamp', + dataType: 'date', + operationType: 'my_unexpected_operation', + isBucketed: true, + scale: 'interval', + params: { timeZone: 'do not delete' }, + }, + }, + columnOrder: ['3', '4', '5'], + }, + }, + }, + }, + visualization: {}, + query: { query: '', language: 'kuery' }, + filters: [], + }, + }, + } as unknown) as SavedObjectUnsanitizedDoc>; + + it('should just return the same document for XY, partition and metric visualization types', () => { + for (const vizType of ['lnsXY', 'lnsPie', 'lnsMetric']) { + const exampleCopy = cloneDeep(example); + exampleCopy.attributes.visualizationType = vizType; + // add datatable state here, even with another viz (manual change?) + (exampleCopy.attributes as LensDocShape715).state.visualization = ({ + columns: [ + { palette: { type: 'palette', name: 'temperature' }, colorMode: 'cell' }, + { palette: { type: 'palette', name: 'temperature' }, colorMode: 'text' }, + { + palette: { type: 'palette', name: 'temperature', params: { reverse: false } }, + colorMode: 'cell', + }, + ], + } as unknown) as VisState716; + const result = migrations['7.16.0'](exampleCopy, context) as ReturnType< + SavedObjectMigrationFn + >; + expect(result).toEqual(exampleCopy); + } + }); + + it('should not change non reversed default palettes in datatable', () => { + const datatableExample = cloneDeep(example); + datatableExample.attributes.visualizationType = 'lnsDatatable'; + (datatableExample.attributes as LensDocShape715).state.visualization = ({ + columns: [ + { palette: { type: 'palette', name: 'temperature' }, colorMode: 'cell' }, + { palette: { type: 'palette', name: 'temperature' }, colorMode: 'text' }, + { + palette: { type: 'palette', name: 'temperature', params: { reverse: false } }, + colorMode: 'cell', + }, + ], + } as unknown) as VisState716; + const result = migrations['7.16.0'](datatableExample, context) as ReturnType< + SavedObjectMigrationFn + >; + expect(result).toEqual(datatableExample); + }); + + it('should not change custom palettes in datatable', () => { + const datatableExample = cloneDeep(example); + datatableExample.attributes.visualizationType = 'lnsDatatable'; + (datatableExample.attributes as LensDocShape715).state.visualization = ({ + columns: [ + { palette: { type: 'palette', name: 'custom' }, colorMode: 'cell' }, + { palette: { type: 'palette', name: 'custom' }, colorMode: 'text' }, + { + palette: { type: 'palette', name: 'custom', params: { reverse: true } }, + colorMode: 'cell', + }, + ], + } as unknown) as VisState716; + const result = migrations['7.16.0'](datatableExample, context) as ReturnType< + SavedObjectMigrationFn + >; + expect(result).toEqual(datatableExample); + }); + + it('should not change a datatable with no conditional coloring', () => { + const datatableExample = cloneDeep(example); + datatableExample.attributes.visualizationType = 'lnsDatatable'; + (datatableExample.attributes as LensDocShape715).state.visualization = ({ + columns: [{ colorMode: 'none' }, {}], + } as unknown) as VisState716; + const result = migrations['7.16.0'](datatableExample, context) as ReturnType< + SavedObjectMigrationFn + >; + expect(result).toEqual(datatableExample); + }); + + it('should not change default palette if the colorMode is set to "none" in datatable', () => { + const datatableExample = cloneDeep(example); + datatableExample.attributes.visualizationType = 'lnsDatatable'; + (datatableExample.attributes as LensDocShape715).state.visualization = ({ + columns: [ + { palette: { type: 'palette', name: 'temperature' }, colorMode: 'none' }, + { palette: { type: 'palette', name: 'temperature' }, colorMode: 'none' }, + { + palette: { type: 'palette', name: 'temperature', params: { reverse: true } }, + colorMode: 'cell', + }, + ], + } as unknown) as VisState716; + const result = migrations['7.16.0'](datatableExample, context) as ReturnType< + SavedObjectMigrationFn + >; + expect(result).toEqual(datatableExample); + }); + + it('should change a default palette reversed in datatable', () => { + const datatableExample = cloneDeep(example); + datatableExample.attributes.visualizationType = 'lnsDatatable'; + (datatableExample.attributes as LensDocShape715).state.visualization = ({ + columns: [ + { + colorMode: 'cell', + palette: { + type: 'palette', + name: 'temperature1', + params: { + reverse: true, + rangeType: 'number', + stops: [ + { color: 'red', stop: 10 }, + { color: 'blue', stop: 20 }, + { color: 'pink', stop: 50 }, + { color: 'green', stop: 60 }, + { color: 'yellow', stop: 70 }, + ], + }, + }, + }, + { + colorMode: 'text', + palette: { + type: 'palette', + name: 'temperature2', + params: { + reverse: true, + rangeType: 'number', + stops: [ + { color: 'red', stop: 10 }, + { color: 'blue', stop: 20 }, + { color: 'pink', stop: 50 }, + { color: 'green', stop: 60 }, + { color: 'yellow', stop: 70 }, + ], + }, + }, + }, + ], + } as unknown) as VisState716; + const result = migrations['7.16.0'](datatableExample, context) as ReturnType< + SavedObjectMigrationFn + >; + const state = (result.attributes as LensDocShape715< + Extract + >).state.visualization; + for (const column of state.columns) { + expect(column.palette!.name).toBe('custom'); + expect(column.palette!.params!.name).toBe('custom'); + expect(column.palette!.params!.rangeMin).toBe(0); + expect(column.palette!.params!.rangeMax).toBe(80); + expect(column.palette!.params!.reverse).toBeTruthy(); // still true + expect(column.palette!.params!.rangeType).toBe('percent'); + expect(column.palette!.params!.stops).toEqual([ + { color: 'red', stop: 20 }, + { color: 'blue', stop: 40 }, + { color: 'pink', stop: 60 }, + { color: 'green', stop: 80 }, + { color: 'yellow', stop: 100 }, + ]); + expect(column.palette!.params!.colorStops).toEqual([ + { color: 'red', stop: 0 }, + { color: 'blue', stop: 20 }, + { color: 'pink', stop: 40 }, + { color: 'green', stop: 60 }, + { color: 'yellow', stop: 80 }, + ]); + } + }); + + it('should change a default palette reversed in heatmap', () => { + const datatableExample = cloneDeep(example); + datatableExample.attributes.visualizationType = 'lnsHeatmap'; + (datatableExample.attributes as LensDocShape715).state.visualization = ({ + palette: { + type: 'palette', + name: 'temperature1', + params: { + reverse: true, + rangeType: 'number', + stops: [ + { color: 'red', stop: 10 }, + { color: 'blue', stop: 20 }, + { color: 'pink', stop: 50 }, + { color: 'green', stop: 60 }, + { color: 'yellow', stop: 70 }, + ], + }, + }, + } as unknown) as VisState716; + const result = migrations['7.16.0'](datatableExample, context) as ReturnType< + SavedObjectMigrationFn + >; + const state = (result.attributes as LensDocShape715< + Extract }> + >).state.visualization; + expect(state.palette!.name).toBe('custom'); + expect(state.palette!.params!.name).toBe('custom'); + expect(state.palette!.params!.rangeMin).toBe(0); + expect(state.palette!.params!.rangeMax).toBe(80); + expect(state.palette!.params!.reverse).toBeTruthy(); // still true + expect(state.palette!.params!.rangeType).toBe('percent'); + expect(state.palette!.params!.stops).toEqual([ + { color: 'red', stop: 20 }, + { color: 'blue', stop: 40 }, + { color: 'pink', stop: 60 }, + { color: 'green', stop: 80 }, + { color: 'yellow', stop: 100 }, + ]); + expect(state.palette!.params!.colorStops).toEqual([ + { color: 'red', stop: 0 }, + { color: 'blue', stop: 20 }, + { color: 'pink', stop: 40 }, + { color: 'green', stop: 60 }, + { color: 'yellow', stop: 80 }, + ]); + }); + }); }); diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts index 7d08e76841cf5..901f0b5d6e684 100644 --- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts @@ -23,11 +23,13 @@ import { LensDocShape715, VisStatePost715, VisStatePre715, + VisState716, } from './types'; import { commonRenameOperationsForFormula, commonRemoveTimezoneDateHistogramParam, commonUpdateVisLayerType, + commonMakeReversePaletteAsCustom, } from './common_migrations'; interface LensDocShapePre710 { @@ -430,6 +432,14 @@ const addLayerTypeToVisualization: SavedObjectMigrationFn< return { ...newDoc, attributes: commonUpdateVisLayerType(newDoc.attributes) }; }; +const moveDefaultReversedPaletteToCustom: SavedObjectMigrationFn< + LensDocShape715, + LensDocShape715 +> = (doc) => { + const newDoc = cloneDeep(doc); + return { ...newDoc, attributes: commonMakeReversePaletteAsCustom(newDoc.attributes) }; +}; + export const migrations: SavedObjectMigrationMap = { '7.7.0': removeInvalidAccessors, // The order of these migrations matter, since the timefield migration relies on the aggConfigs @@ -442,4 +452,5 @@ export const migrations: SavedObjectMigrationMap = { '7.13.1': renameOperationsForFormula, // duplicate this migration in case a broken by value panel is added to the library '7.14.0': removeTimezoneDateHistogramParam, '7.15.0': addLayerTypeToVisualization, + '7.16.0': moveDefaultReversedPaletteToCustom, }; diff --git a/x-pack/plugins/lens/server/migrations/types.ts b/x-pack/plugins/lens/server/migrations/types.ts index 09b460ff8b8cd..2e6e66aed9949 100644 --- a/x-pack/plugins/lens/server/migrations/types.ts +++ b/x-pack/plugins/lens/server/migrations/types.ts @@ -5,8 +5,9 @@ * 2.0. */ +import type { PaletteOutput } from 'src/plugins/charts/common'; import { Query, Filter } from 'src/plugins/data/public'; -import type { LayerType } from '../../common'; +import type { CustomPaletteParams, LayerType } from '../../common'; export type OperationTypePre712 = | 'avg' @@ -192,3 +193,16 @@ export interface LensDocShape715 { filters: Filter[]; }; } + +export type VisState716 = + // Datatable + | { + columns: Array<{ + palette?: PaletteOutput; + colorMode?: 'none' | 'cell' | 'text'; + }>; + } + // Heatmap + | { + palette?: PaletteOutput; + }; 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 d1690ddfff43d..8e996934ef69e 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 1f4a1ab7c9afa..362b2b341714f 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 caae4385aeec6..93342d1167aeb 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 86c6c14306faf..f3efe4c6e74dd 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/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts index 1403ce2a7b4db..30aae3c0fb550 100644 --- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts @@ -680,7 +680,7 @@ export function jobRoutes({ router, routeGuard }: RouteInitialization) { /** * @apiGroup AnomalyDetectors * - * @api {post} /api/ml/anomaly_detectors/:jobId/model_snapshots/:snapshotId/_update update model snapshot by snapshot ID + * @api {post} /api/ml/anomaly_detectors/:jobId/model_snapshots/:snapshotId/_update Update model snapshot by snapshot ID * @apiName UpdateModelSnapshotsById * @apiDescription Updates the model snapshot for the specified snapshot ID * diff --git a/x-pack/plugins/ml/server/routes/filters.ts b/x-pack/plugins/ml/server/routes/filters.ts index 1535b12f335a6..b4c7d5bf5b109 100644 --- a/x-pack/plugins/ml/server/routes/filters.ts +++ b/x-pack/plugins/ml/server/routes/filters.ts @@ -47,9 +47,9 @@ export function filtersRoutes({ router, routeGuard }: RouteInitialization) { /** * @apiGroup Filters * - * @api {get} /api/ml/filters Gets filters - size limit has been explicitly set to 1000 + * @api {get} /api/ml/filters Get filters * @apiName GetFilters - * @apiDescription Retrieves the list of filters which are used for custom rules in anomaly detection. + * @apiDescription Retrieves the list of filters which are used for custom rules in anomaly detection. Sets the size limit explicitly to return a maximum of 1000. * * @apiSuccess {Boolean} success * @apiSuccess {Object[]} filters list of filters diff --git a/x-pack/plugins/ml/server/routes/job_audit_messages.ts b/x-pack/plugins/ml/server/routes/job_audit_messages.ts index cdef5a9c20dae..d28effae5ca2b 100644 --- a/x-pack/plugins/ml/server/routes/job_audit_messages.ts +++ b/x-pack/plugins/ml/server/routes/job_audit_messages.ts @@ -101,7 +101,7 @@ export function jobAuditMessagesRoutes({ router, routeGuard }: RouteInitializati /** * @apiGroup JobAuditMessages * - * @api {put} /api/ml/job_audit_messages/clear_messages Index annotation + * @api {put} /api/ml/job_audit_messages/clear_messages Clear messages * @apiName ClearJobAuditMessages * @apiDescription Clear the job audit messages. * diff --git a/x-pack/plugins/ml/server/routes/modules.ts b/x-pack/plugins/ml/server/routes/modules.ts index e327d601555ab..097f3f8d67652 100644 --- a/x-pack/plugins/ml/server/routes/modules.ts +++ b/x-pack/plugins/ml/server/routes/modules.ts @@ -533,7 +533,7 @@ export function dataRecognizer({ router, routeGuard }: RouteInitialization) { /** * @apiGroup Modules * - * @api {post} /api/ml/modules/jobs_exist/:moduleId Check if module jobs exist + * @api {get} /api/ml/modules/jobs_exist/:moduleId Check if module jobs exist * @apiName CheckExistingModuleJobs * @apiDescription Check whether the jobs in the module with the specified ID exist in the * current list of jobs. The check runs a test to see if any of the jobs in existence diff --git a/x-pack/plugins/ml/server/routes/results_service.ts b/x-pack/plugins/ml/server/routes/results_service.ts index 2cb34ce357fea..fe1a759caf8e6 100644 --- a/x-pack/plugins/ml/server/routes/results_service.ts +++ b/x-pack/plugins/ml/server/routes/results_service.ts @@ -106,9 +106,9 @@ export function resultsServiceRoutes({ router, routeGuard }: RouteInitialization /** * @apiGroup ResultsService * - * @api {post} /api/ml/results/anomalies_table_data Prepare anomalies records for table display + * @api {post} /api/ml/results/anomalies_table_data Get anomalies records for table display * @apiName GetAnomaliesTableData - * @apiDescription Retrieves anomaly records for an anomaly detection job and formats them for anomalies table display + * @apiDescription Retrieves anomaly records for an anomaly detection job and formats them for anomalies table display. * * @apiSchema (body) anomaliesTableDataSchema */ @@ -138,7 +138,7 @@ export function resultsServiceRoutes({ router, routeGuard }: RouteInitialization /** * @apiGroup ResultsService * - * @api {post} /api/ml/results/category_definition Returns category definition + * @api {post} /api/ml/results/category_definition Get category definition * @apiName GetCategoryDefinition * @apiDescription Returns the definition of the category with the specified ID and job ID * @@ -170,7 +170,7 @@ export function resultsServiceRoutes({ router, routeGuard }: RouteInitialization /** * @apiGroup ResultsService * - * @api {post} /api/ml/results/max_anomaly_score Returns the maximum anomaly_score + * @api {post} /api/ml/results/max_anomaly_score Get the maximum anomaly_score * @apiName GetMaxAnomalyScore * @apiDescription Returns the maximum anomaly score of the bucket results for the request job ID(s) and time range * @@ -202,7 +202,7 @@ export function resultsServiceRoutes({ router, routeGuard }: RouteInitialization /** * @apiGroup ResultsService * - * @api {post} /api/ml/results/category_examples Returns category examples + * @api {post} /api/ml/results/category_examples Get category examples * @apiName GetCategoryExamples * @apiDescription Returns examples for the categories with the specified IDs from the job with the supplied ID * @@ -266,8 +266,10 @@ export function resultsServiceRoutes({ router, routeGuard }: RouteInitialization /** * @apiGroup ResultsService * - * @api {post} /api/ml/results/anomaly_search Performs a search on the anomaly results index + * @api {post} /api/ml/results/anomaly_search Run a search on the anomaly results index * @apiName AnomalySearch + * @apiDescription Runs the supplied query against the anomaly results index for the specified job IDs. + * @apiSchema (body) anomalySearchSchema */ router.post( { @@ -295,7 +297,7 @@ export function resultsServiceRoutes({ router, routeGuard }: RouteInitialization /** * @apiGroup ResultsService * - * @api {get} /api/ml/results/:jobId/categorizer_stats + * @api {get} /api/ml/results/:jobId/categorizer_stats Return categorizer statistics * @apiName GetCategorizerStats * @apiDescription Returns the categorizer stats for the specified job ID * @apiSchema (params) jobIdSchema @@ -327,7 +329,7 @@ export function resultsServiceRoutes({ router, routeGuard }: RouteInitialization /** * @apiGroup ResultsService * - * @api {get} /api/ml/results/category_stopped_partitions + * @api {post} /api/ml/results/category_stopped_partitions Get partitions that have stopped being categorized * @apiName GetCategoryStoppedPartitions * @apiDescription Returns information on the partitions that have stopped being categorized due to the categorization status changing from ok to warn. Can return either the list of stopped partitions for each job, or just the list of job IDs. * @apiSchema (body) getCategorizerStoppedPartitionsSchema diff --git a/x-pack/plugins/ml/server/routes/saved_objects.ts b/x-pack/plugins/ml/server/routes/saved_objects.ts index e9fb748a4c7f8..24140c9253cda 100644 --- a/x-pack/plugins/ml/server/routes/saved_objects.ts +++ b/x-pack/plugins/ml/server/routes/saved_objects.ts @@ -59,10 +59,10 @@ export function savedObjectsRoutes( * * @api {get} /api/ml/saved_objects/sync Sync job saved objects * @apiName SyncJobSavedObjects - * @apiDescription Create saved objects for jobs which are missing them. - * Delete saved objects for jobs which no longer exist. - * Update missing datafeed ids in saved objects for datafeeds which exist. - * Remove datafeed ids for datafeeds which no longer exist. + * @apiDescription Synchronizes saved objects for jobs. Saved objects will be created for jobs which are missing them, + * and saved objects will be deleted for jobs which no longer exist. + * Updates missing datafeed IDs in saved objects for datafeeds which exist, and + * removes datafeed IDs for datafeeds which no longer exist. * */ router.get( @@ -217,9 +217,9 @@ export function savedObjectsRoutes( /** * @apiGroup JobSavedObjects * - * @api {get} /api/ml/saved_objects/jobs_spaces All spaces in all jobs + * @api {get} /api/ml/saved_objects/jobs_spaces Get all jobs and their spaces * @apiName JobsSpaces - * @apiDescription List all jobs and their spaces + * @apiDescription List all jobs and their spaces. * */ router.get( 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 57bb638651d05..a244840e004fd 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 e15ad995ca161..19a367977ffc8 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 f329323bafda8..a7b498ddb88c1 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 0000000000000..3a46840e7d30c --- /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 29aafa09814fb..3461f8650ca6c 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 f54d40ed29a06..8a343a5c61cd6 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 d027298c81c4c..fc1a81cc4dba2 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 e5962b7f80876..9a77b07b96a6e 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/observability/server/index.ts b/x-pack/plugins/observability/server/index.ts index eb08e9f3c258d..9a62602859c54 100644 --- a/x-pack/plugins/observability/server/index.ts +++ b/x-pack/plugins/observability/server/index.ts @@ -29,8 +29,8 @@ export const config = { index: schema.string({ defaultValue: 'observability-annotations' }), }), unsafe: schema.object({ - alertingExperience: schema.object({ enabled: schema.boolean({ defaultValue: true }) }), - cases: schema.object({ enabled: schema.boolean({ defaultValue: true }) }), + alertingExperience: schema.object({ enabled: schema.boolean({ defaultValue: false }) }), + cases: schema.object({ enabled: schema.boolean({ defaultValue: false }) }), }), }), }; diff --git a/x-pack/plugins/reporting/common/types.ts b/x-pack/plugins/reporting/common/types.ts index c324cf363faa1..4051c006a5034 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 d1f0b56cdfa62..8f46ca5616f19 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 735da92fc2e2e..0f7ce3b197435 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, @@ -46,7 +46,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 @@ -94,7 +94,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(','); @@ -154,14 +154,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/report_listing.test.tsx b/x-pack/plugins/reporting/public/management/report_listing.test.tsx index 6b5d5911b7750..973e2879e89d5 100644 --- a/x-pack/plugins/reporting/public/management/report_listing.test.tsx +++ b/x-pack/plugins/reporting/public/management/report_listing.test.tsx @@ -6,10 +6,12 @@ */ import { registerTestBed } from '@kbn/test/jest'; -import { UnwrapPromise } from '@kbn/utility-types'; +import type { SerializableRecord, UnwrapPromise } from '@kbn/utility-types'; +import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import React from 'react'; import { act } from 'react-dom/test-utils'; -import { Observable } from 'rxjs'; +import type { Observable } from 'rxjs'; +import { ListingProps as Props, ReportListing } from '.'; import type { NotificationsSetup } from '../../../../../src/core/public'; import { applicationServiceMock, @@ -18,12 +20,11 @@ import { } from '../../../../../src/core/public/mocks'; import type { LocatorPublic, SharePluginSetup } from '../../../../../src/plugins/share/public'; import type { ILicense } from '../../../licensing/public'; -import { IlmPolicyMigrationStatus, ReportApiJSON } from '../../common/types'; +import type { IlmPolicyMigrationStatus, ReportApiJSON } from '../../common/types'; import { IlmPolicyStatusContextProvider } from '../lib/ilm_policy_status_context'; import { Job } from '../lib/job'; import { InternalApiClientProvider, ReportingAPIClient } from '../lib/reporting_api_client'; import { KibanaContextProvider } from '../shared_imports'; -import { ListingProps as Props, ReportListing } from '.'; import { ReportDownloadLink } from './components'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { @@ -207,11 +208,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: () => ({ @@ -221,8 +222,8 @@ const validCheck = { }; const license$ = { - subscribe: (handler: any) => { - return handler(validCheck); + subscribe: (handler: unknown) => { + return (handler as Function)(validCheck); }, } as Observable; @@ -240,7 +241,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']; @@ -304,7 +305,7 @@ describe('ReportListing', () => { }; ilmLocator = ({ getUrl: jest.fn(), - } as unknown) as LocatorPublic; + } as unknown) as LocatorPublic; urlService = ({ locators: { @@ -326,11 +327,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 }); @@ -345,7 +346,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 45bd20df85660..654d46cdfbcb1 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 8b6e258c06535..eb14e32160869 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 64f1ecddcbb41..59afa91aaa9c3 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 a18ceaf151c7d..e719d720a7895 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/rule_registry/common/assets/lifecycle_policies/default_lifecycle_policy.ts b/x-pack/plugins/rule_registry/common/assets/lifecycle_policies/default_lifecycle_policy.ts index f207087f7aa19..24653dbf07d2d 100644 --- a/x-pack/plugins/rule_registry/common/assets/lifecycle_policies/default_lifecycle_policy.ts +++ b/x-pack/plugins/rule_registry/common/assets/lifecycle_policies/default_lifecycle_policy.ts @@ -11,16 +11,11 @@ export const defaultLifecyclePolicy = { hot: { actions: { rollover: { - max_age: '90d', - max_size: '50gb', + max_age: '30d', + max_primary_shard_size: '50gb', }, }, }, - delete: { - actions: { - delete: {}, - }, - }, }, }, }; diff --git a/x-pack/plugins/rule_registry/server/config.ts b/x-pack/plugins/rule_registry/server/config.ts index 8f98ceb2dd8db..481c5fe3cce8b 100644 --- a/x-pack/plugins/rule_registry/server/config.ts +++ b/x-pack/plugins/rule_registry/server/config.ts @@ -11,7 +11,7 @@ export const config = { schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), write: schema.object({ - enabled: schema.boolean({ defaultValue: true }), + enabled: schema.boolean({ defaultValue: false }), }), unsafe: schema.object({ legacyMultiTenancy: schema.object({ 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 79888d9a97187..61f71e2ee253b 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 52998b090e3b6..901af259a5d2b 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/common/containers/matrix_histogram/index.ts b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts index 233313e47ad66..1be71423899e2 100644 --- a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts @@ -141,7 +141,7 @@ export const useMatrixHistogram = ({ data: response.matrixHistogramData, inspect: getInspectResponse(response, prevResponse.inspect), refetch: refetch.current, - totalCount: response.totalCount, + totalCount: histogramBuckets.reduce((acc, bucket) => bucket.doc_count + acc, 0), buckets: histogramBuckets, })); searchSubscription$.current.unsubscribe(); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx index 51d19651a8efb..c750ca94e633b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import { EuiContextMenuItem } from '@elastic/eui'; @@ -13,11 +13,12 @@ import { useKibana } from '../../../../common/lib/kibana'; import { TimelineId } from '../../../../../common/types/timeline'; import { Ecs } from '../../../../../common/ecs'; import { TimelineNonEcsData } from '../../../../../common/search_strategy/timeline'; -import { timelineActions } from '../../../../timelines/store/timeline'; +import { timelineActions, timelineSelectors } from '../../../../timelines/store/timeline'; import { sendAlertToTimelineAction } from '../actions'; import { dispatchUpdateTimeline } from '../../../../timelines/components/open_timeline/helpers'; import { CreateTimelineProps } from '../types'; import { ACTION_INVESTIGATE_IN_TIMELINE } from '../translations'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { useFetchEcsAlertsData } from '../../../containers/detection_engine/alerts/use_fetch_ecs_alerts_data'; interface UseInvestigateInTimelineActionProps { @@ -34,10 +35,20 @@ export const useInvestigateInTimeline = ({ onInvestigateInTimelineAlertClick, }: UseInvestigateInTimelineActionProps) => { const { - data: { search: searchStrategyClient }, + data: { search: searchStrategyClient, query }, } = useKibana().services; const dispatch = useDispatch(); + const filterManagerBackup = useMemo(() => query.filterManager, [query.filterManager]); + const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []); + const { filterManager: activeFilterManager } = useDeepEqualSelector((state) => + getManageTimeline(state, TimelineId.active ?? '') + ); + const filterManager = useMemo(() => activeFilterManager ?? filterManagerBackup, [ + activeFilterManager, + filterManagerBackup, + ]); + const updateTimelineIsLoading = useCallback( (payload) => dispatch(timelineActions.updateIsLoading(payload)), [dispatch] @@ -53,6 +64,7 @@ export const useInvestigateInTimeline = ({ notes: [], timeline: { ...timeline, + filterManager, // by setting as an empty array, it will default to all in the reducer because of the event type indexNames: [], show: true, @@ -61,7 +73,7 @@ export const useInvestigateInTimeline = ({ ruleNote, })(); }, - [dispatch, updateTimelineIsLoading] + [dispatch, filterManager, updateTimelineIsLoading] ); const showInvestigateInTimelineAction = alertIds != null; 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 24bc670a13ec4..4282a584ea9f3 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 71542e6931489..063dc849027a7 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 4d4ac102ea645..6276d934fed41 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/management/pages/endpoint_hosts/view/details/components/log_entry.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx index 4fe70039d1251..b15c6b9ba0020 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx @@ -45,7 +45,7 @@ const useLogEntryUIProps = ( if (logEntry.type === 'action') { avatarSize = 'm'; commentType = 'regular'; - commentText = logEntry.item.data.data.comment ?? ''; + commentText = logEntry.item.data.data.comment?.trim() ?? ''; displayResponseEvent = false; iconType = 'lockOpen'; username = logEntry.item.data.user_id; @@ -55,7 +55,7 @@ const useLogEntryUIProps = ( iconType = 'lock'; isIsolateAction = true; } - if (data.comment) { + if (commentText) { displayComment = true; } } @@ -154,7 +154,7 @@ export const LogEntry = memo(({ logEntry }: { logEntry: Immutable {displayComment ? ( - +

{commentText}

) : undefined} diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index ea999334ee771..d053da18ce502 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -832,6 +832,15 @@ describe('when on the endpoint list page', () => { }); const actionData = fleetActionGenerator.generate({ agents: [agentId], + data: { + comment: 'some comment', + }, + }); + const isolatedActionData = fleetActionGenerator.generateIsolateAction({ + agents: [agentId], + data: { + comment: ' ', // has space for comment, + }, }); getMockData = () => ({ @@ -854,6 +863,13 @@ describe('when on the endpoint list page', () => { data: actionData, }, }, + { + type: 'action', + item: { + id: 'some_id_3', + data: isolatedActionData, + }, + }, ], }); @@ -890,7 +906,7 @@ describe('when on the endpoint list page', () => { dispatchEndpointDetailsActivityLogChanged('success', getMockData()); }); const logEntries = await renderResult.queryAllByTestId('timelineEntry'); - expect(logEntries.length).toEqual(2); + expect(logEntries.length).toEqual(3); expect(`${logEntries[0]} .euiCommentTimeline__icon--update`).not.toBe(null); expect(`${logEntries[1]} .euiCommentTimeline__icon--regular`).not.toBe(null); }); @@ -947,7 +963,7 @@ describe('when on the endpoint list page', () => { dispatchEndpointDetailsActivityLogChanged('success', getMockData()); }); const logEntries = await renderResult.queryAllByTestId('timelineEntry'); - expect(logEntries.length).toEqual(2); + expect(logEntries.length).toEqual(3); }); it('should display a callout message if no log data', async () => { @@ -975,6 +991,29 @@ describe('when on the endpoint list page', () => { const activityLogCallout = await renderResult.findByTestId('activityLogNoDataCallout'); expect(activityLogCallout).not.toBeNull(); }); + + it('should correctly display non-empty comments only for actions', async () => { + const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); + reactTestingLibrary.act(() => { + history.push( + `${MANAGEMENT_PATH}/endpoints?page_index=0&page_size=10&selected_endpoint=1&show=activity_log` + ); + }); + const changedUrlAction = await userChangedUrlChecker; + expect(changedUrlAction.payload.search).toEqual( + '?page_index=0&page_size=10&selected_endpoint=1&show=activity_log' + ); + await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged'); + reactTestingLibrary.act(() => { + dispatchEndpointDetailsActivityLogChanged('success', getMockData()); + }); + const commentTexts = await renderResult.queryAllByTestId('activityLogCommentText'); + expect(commentTexts.length).toEqual(1); + expect(commentTexts[0].textContent).toEqual('some comment'); + expect(commentTexts[0].parentElement?.parentElement?.className).toContain( + 'euiCommentEvent--regular' + ); + }); }); describe('when showing host Policy Response panel', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts index 1add8bb9d6f76..4d7ca74ca19f8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts @@ -584,6 +584,28 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ } ), }, + { + key: 'windows.advanced.kernel.fileaccess', + first_supported_version: '7.15', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.kernel.fileaccess', + { + defaultMessage: + 'Report limited file access (read) events. Paths are not user-configurable. Default value is true.', + } + ), + }, + { + key: 'windows.advanced.kernel.registryaccess', + first_supported_version: '7.15', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.windows.advanced.kernel.registryaccess', + { + defaultMessage: + 'Report limited registry access (queryvalue, savekey) events. Paths are not user-configurable. Default value is true.', + } + ), + }, { key: 'windows.advanced.diagnostic.enabled', first_supported_version: '7.11', diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/behavior.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/behavior.tsx index f52d8a4c70706..06cf666f2950e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/behavior.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/behavior.tsx @@ -28,13 +28,13 @@ export const BehaviorProtection = React.memo(() => { const protectionLabel = i18n.translate( 'xpack.securitySolution.endpoint.policy.protections.behavior', { - defaultMessage: 'Behavior', + defaultMessage: 'Malicious behavior protections', } ); return ( { const protectionLabel = i18n.translate( 'xpack.securitySolution.endpoint.policy.protections.memory', { - defaultMessage: 'Memory manipulation', + defaultMessage: 'Memory threat protections', } ); return ( { 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 00f5fd5717aeb..52483b4853cbc 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 2071d4b8c27b7..d6599f2686670 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 4518e23bb7fea..07edfce24affd 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 18be9f299c15c..e2a4f9a3f5356 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 f437b2606c35d..e92620eaca93e 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 9c59f69b12113..63bf6fea698ae 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 f0a523fff96d4..0000000000000 --- 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 1aa54dedef5ef..093d5c806c282 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 008f6ac7b874c..5abbbb1b1c6ed 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 358443e675c6e..c04a68171f6f8 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 941fe5cbf5484..e513b5ace737f 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 f2b6364301fe2..cdef729404ceb 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 26d62c2da95b0..9cdf474efb450 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 0a8d10d01469e..3058d95c0237d 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 d29307eddd422..7655afb144b8f 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 6fcac67c5a66b..da34dfd46e577 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 7ea59e77c4e2f..41846f1f71ad6 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 9e7344acb6d99..8eb5c45ac3a65 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 a89091f680287..5f98ebacea980 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 64c13aeb9ce84..532a230a77273 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 9a9d1a0f79857..cdbd8ea096495 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 36ca55901950f..2d139095e81d7 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 a6a26def2a0ee..889b6377b16b5 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 5b1ba3bcc206b..ee2587f61fbc8 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 b05931eab4be6..b4134ddd17395 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 719a96d513c82..523d046d88e35 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 5abf4c7a41ba8..4b921b0b222ea 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 ec2906b274176..62b13ded85bfa 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 4391254d0a8aa..7ee9fe51e59b9 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 a22ea792f50a8..11ee764ea87b6 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 1ff6053126e1c..78944a1712728 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 b90ab198b9c9d..9e3582404bf09 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 93c1b33268bf4..7b5cb3d8e121a 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 71ae3cfcc19e3..6a2cd0eed348a 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 7e6ac78f93327..533a0617d1aff 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 fbc3d7229df6f..2deb0ab6edeb2 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 e41e013df51cc..47b601084ac23 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 0c5709b691ba2..907d689e76ab0 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 132c8a16783e0..2b696db83e0c0 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 07cafef084a61..45062f36bdfbf 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 0d4090f50442c..c74c56723eb8e 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 874cb8cfb5a69..68a314daa121a 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 c89d93f5f5e28..0f0f737e9d21f 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 33e1303b41643..ca38dff4606ad 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/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 2ef5004726eb2..b023ebb44cc38 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -64,6 +64,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/security_api_integration/anonymous_es_anonymous.config.ts'), require.resolve('../test/observability_api_integration/basic/config.ts'), require.resolve('../test/observability_api_integration/trial/config.ts'), + require.resolve('../test/observability_functional/with_rac_write.config.ts'), require.resolve('../test/encrypted_saved_objects_api_integration/config.ts'), require.resolve('../test/spaces_api_integration/spaces_only/config.ts'), require.resolve('../test/spaces_api_integration/security_and_spaces/config_trial.ts'), diff --git a/x-pack/test/api_integration/apis/maps/migrations.js b/x-pack/test/api_integration/apis/maps/migrations.js index d121c79f6cfe1..37dc9c32958ff 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: {}, }) @@ -76,7 +76,7 @@ export default function ({ getService }) { } expect(panels.length).to.be(1); expect(panels[0].type).to.be('map'); - expect(panels[0].version).to.be('7.15.0'); + expect(panels[0].version).to.be('7.16.0'); }); }); }); diff --git a/x-pack/test/apm_api_integration/common/apm_api_supertest.ts b/x-pack/test/apm_api_integration/common/apm_api_supertest.ts index 5034d4fae8fbc..f930cab3b0568 100644 --- a/x-pack/test/apm_api_integration/common/apm_api_supertest.ts +++ b/x-pack/test/apm_api_integration/common/apm_api_supertest.ts @@ -15,15 +15,12 @@ import type { APIClientRequestParamsOf, } from '../../../plugins/apm/public/services/rest/createCallApmApi'; -export function createApmApiSupertest(st: supertest.SuperTest) { +export function createSupertestClient(st: supertest.SuperTest) { return async ( options: { endpoint: TEndpoint; } & APIClientRequestParamsOf & { params?: { query?: { _inspect?: boolean } } } - ): Promise<{ - status: number; - body: APIReturnType; - }> => { + ): Promise> => { const { endpoint } = options; const params = 'params' in options ? (options.params as Record) : {}; @@ -44,7 +41,7 @@ export function createApmApiSupertest(st: supertest.SuperTest) { }; } -export type ApmApiSupertest = ReturnType; +export type ApmApiSupertest = ReturnType; export class ApmApiError extends Error { res: request.Response; @@ -60,3 +57,8 @@ Body: ${JSON.stringify(res.body)}` this.res = res; } } + +export interface SupertestReturnType { + status: number; + body: APIReturnType; +} diff --git a/x-pack/test/apm_api_integration/common/authentication.ts b/x-pack/test/apm_api_integration/common/authentication.ts index 799e78e5646ba..e8c71c54c6fe0 100644 --- a/x-pack/test/apm_api_integration/common/authentication.ts +++ b/x-pack/test/apm_api_integration/common/authentication.ts @@ -18,13 +18,15 @@ export enum ApmUser { apmReadUserWithoutMlAccess = 'apm_read_user_without_ml_access', } +// TODO: Going forward we want to use the built-in roles `viewer` and `editor`. However ML privileges are not included in the built-in roles +// Until https://github.com/elastic/kibana/issues/71422 is closed we have to use the custom roles below const roles = { [ApmUser.noAccessUser]: {}, [ApmUser.apmReadUser]: { kibana: [ { base: [], - feature: { apm: ['read'], ml: ['read'], savedObjectsManagement: ['read'] }, + feature: { ml: ['read'] }, spaces: ['*'], }, ], @@ -51,7 +53,7 @@ const roles = { kibana: [ { base: [], - feature: { apm: ['all'], ml: ['all'], savedObjectsManagement: ['all'] }, + feature: { ml: ['all'] }, spaces: ['*'], }, ], @@ -81,16 +83,16 @@ const users = { roles: [], }, [ApmUser.apmReadUser]: { - roles: ['apm_user', ApmUser.apmReadUser], + roles: ['viewer', ApmUser.apmReadUser], }, [ApmUser.apmReadUserWithoutMlAccess]: { roles: [ApmUser.apmReadUserWithoutMlAccess], }, [ApmUser.apmWriteUser]: { - roles: ['apm_user', ApmUser.apmWriteUser], + roles: ['editor', ApmUser.apmWriteUser], }, [ApmUser.apmAnnotationsWriteUser]: { - roles: ['apm_user', ApmUser.apmWriteUser, ApmUser.apmAnnotationsWriteUser], + roles: ['editor', ApmUser.apmWriteUser, ApmUser.apmAnnotationsWriteUser], }, }; diff --git a/x-pack/test/apm_api_integration/common/config.ts b/x-pack/test/apm_api_integration/common/config.ts index c1ae7bb5f2b75..e8d777814402f 100644 --- a/x-pack/test/apm_api_integration/common/config.ts +++ b/x-pack/test/apm_api_integration/common/config.ts @@ -8,10 +8,12 @@ import { FtrConfigProviderContext } from '@kbn/test'; import supertest from 'supertest'; import { format, UrlObject } from 'url'; +import { SecurityServiceProvider } from 'test/common/services/security'; import { InheritedFtrProviderContext, InheritedServices } from './ftr_provider_context'; import { PromiseReturnType } from '../../../plugins/observability/typings/common'; import { createApmUser, APM_TEST_PASSWORD, ApmUser } from './authentication'; import { APMFtrConfigName } from '../configs'; +import { createSupertestClient } from './apm_api_supertest'; import { registry } from './registry'; interface Config { @@ -20,12 +22,29 @@ interface Config { kibanaConfig?: Record; } -const supertestAsApmUser = (kibanaServer: UrlObject, apmUser: ApmUser) => async ( - context: InheritedFtrProviderContext -) => { - const security = context.getService('security'); - await security.init(); +type SecurityService = PromiseReturnType; +function getLegacySupertestClient(kibanaServer: UrlObject, apmUser: ApmUser) { + return async (context: InheritedFtrProviderContext) => { + const security = context.getService('security'); + await security.init(); + + await createApmUser(security, apmUser); + + const url = format({ + ...kibanaServer, + auth: `${apmUser}:${APM_TEST_PASSWORD}`, + }); + + return supertest(url); + }; +} + +async function getApmApiClient( + kibanaServer: UrlObject, + security: SecurityService, + apmUser: ApmUser +) { await createApmUser(security, apmUser); const url = format({ @@ -33,8 +52,10 @@ const supertestAsApmUser = (kibanaServer: UrlObject, apmUser: ApmUser) => async auth: `${apmUser}:${APM_TEST_PASSWORD}`, }); - return supertest(url); -}; + return createSupertestClient(supertest(url)); +} + +export type CreateTestConfig = ReturnType; export function createTestConfig(config: Config) { const { license, name, kibanaConfig } = config; @@ -46,8 +67,7 @@ export function createTestConfig(config: Config) { const services = xPackAPITestsConfig.get('services') as InheritedServices; const servers = xPackAPITestsConfig.get('servers'); - - const supertestAsApmReadUser = supertestAsApmUser(servers.kibana, ApmUser.apmReadUser); + const kibanaServer = servers.kibana; registry.init(config.name); @@ -56,16 +76,38 @@ export function createTestConfig(config: Config) { servers, services: { ...services, - supertest: supertestAsApmReadUser, - supertestAsApmReadUser, - supertestAsNoAccessUser: supertestAsApmUser(servers.kibana, ApmUser.noAccessUser), - supertestAsApmWriteUser: supertestAsApmUser(servers.kibana, ApmUser.apmWriteUser), - supertestAsApmAnnotationsWriteUser: supertestAsApmUser( - servers.kibana, + + apmApiClient: async (context: InheritedFtrProviderContext) => { + const security = context.getService('security'); + await security.init(); + + return { + noAccessUser: await getApmApiClient(servers.kibana, security, ApmUser.noAccessUser), + readUser: await getApmApiClient(servers.kibana, security, ApmUser.apmReadUser), + writeUser: await getApmApiClient(servers.kibana, security, ApmUser.apmWriteUser), + annotationWriterUser: await getApmApiClient( + servers.kibana, + security, + ApmUser.apmAnnotationsWriteUser + ), + noMlAccessUser: await getApmApiClient( + servers.kibana, + security, + ApmUser.apmReadUserWithoutMlAccess + ), + }; + }, + + // legacy clients + legacySupertestAsNoAccessUser: getLegacySupertestClient(kibanaServer, ApmUser.noAccessUser), + legacySupertestAsApmReadUser: getLegacySupertestClient(kibanaServer, ApmUser.apmReadUser), + legacySupertestAsApmWriteUser: getLegacySupertestClient(kibanaServer, ApmUser.apmWriteUser), + legacySupertestAsApmAnnotationsWriteUser: getLegacySupertestClient( + kibanaServer, ApmUser.apmAnnotationsWriteUser ), - supertestAsApmReadUserWithoutMlAccess: supertestAsApmUser( - servers.kibana, + legacySupertestAsApmReadUserWithoutMlAccess: getLegacySupertestClient( + kibanaServer, ApmUser.apmReadUserWithoutMlAccess ), }, diff --git a/x-pack/test/apm_api_integration/common/registry.ts b/x-pack/test/apm_api_integration/common/registry.ts index 0f0af0bda6ab8..a37cd26f1fc3c 100644 --- a/x-pack/test/apm_api_integration/common/registry.ts +++ b/x-pack/test/apm_api_integration/common/registry.ts @@ -110,7 +110,7 @@ export const registry = { const esArchiver = context.getService('esArchiver'); const logger = context.getService('log'); - const supertest = context.getService('supertestAsApmWriteUser'); + const supertest = context.getService('legacySupertestAsApmWriteUser'); const logWithTimer = () => { const start = process.hrtime(); diff --git a/x-pack/test/apm_api_integration/configs/index.ts b/x-pack/test/apm_api_integration/configs/index.ts index 51bcb30a0c176..ad1f897debe32 100644 --- a/x-pack/test/apm_api_integration/configs/index.ts +++ b/x-pack/test/apm_api_integration/configs/index.ts @@ -6,7 +6,7 @@ */ import { mapValues } from 'lodash'; -import { createTestConfig } from '../common/config'; +import { createTestConfig, CreateTestConfig } from '../common/config'; const apmFtrConfigs = { basic: { @@ -34,9 +34,12 @@ const apmFtrConfigs = { export type APMFtrConfigName = keyof typeof apmFtrConfigs; -export const configs = mapValues(apmFtrConfigs, (value, key) => { - return createTestConfig({ - name: key as APMFtrConfigName, - ...value, - }); -}); +export const configs: Record = mapValues( + apmFtrConfigs, + (value, key) => { + return createTestConfig({ + name: key as APMFtrConfigName, + ...value, + }); + } +); diff --git a/x-pack/test/apm_api_integration/tests/alerts/chart_preview.ts b/x-pack/test/apm_api_integration/tests/alerts/chart_preview.ts index f12256a33ef05..c8bb844238020 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/chart_preview.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/chart_preview.ts @@ -6,13 +6,12 @@ */ import expect from '@kbn/expect'; -import { createApmApiSupertest } from '../../common/apm_api_supertest'; import archives from '../../common/fixtures/es_archiver/archives_metadata'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { - const apmApiSupertest = createApmApiSupertest(getService('supertest')); + const apmApiClient = getService('apmApiClient'); const archiveName = 'apm_8.0.0'; const { end } = archives[archiveName]; const start = new Date(Date.parse(end) - 600000).toISOString(); @@ -32,7 +31,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when(`without data loaded`, { config: 'basic', archives: [] }, () => { it('transaction_error_rate (without data)', async () => { const options = getOptions(); - const response = await apmApiSupertest({ + const response = await apmApiClient.readUser({ endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_rate', ...options, }); @@ -45,7 +44,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const options = getOptions(); options.params.query.transactionType = undefined; - const response = await apmApiSupertest({ + const response = await apmApiClient.readUser({ endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_count', ...options, }); @@ -57,7 +56,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('transaction_duration (without data)', async () => { const options = getOptions(); - const response = await apmApiSupertest({ + const response = await apmApiClient.readUser({ endpoint: 'GET /api/apm/alerts/chart_preview/transaction_duration', ...options, }); @@ -70,7 +69,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when(`with data loaded`, { config: 'basic', archives: [archiveName] }, () => { it('transaction_error_rate (with data)', async () => { const options = getOptions(); - const response = await apmApiSupertest({ + const response = await apmApiClient.readUser({ endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_rate', ...options, }); @@ -87,7 +86,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const options = getOptions(); options.params.query.transactionType = undefined; - const response = await apmApiSupertest({ + const response = await apmApiClient.readUser({ endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_count', ...options, }); @@ -102,7 +101,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('transaction_duration (with data)', async () => { const options = getOptions(); - const response = await apmApiSupertest({ + const response = await apmApiClient.readUser({ ...options, endpoint: 'GET /api/apm/alerts/chart_preview/transaction_duration', }); diff --git a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts index 7cf1fe4c969cc..7f107f127594d 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts @@ -36,7 +36,7 @@ interface Alert { } export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertestAsApmWriteUser'); + const supertest = getService('legacySupertestAsApmWriteUser'); const es = getService('es'); const MAX_POLLS = 10; diff --git a/x-pack/test/apm_api_integration/tests/correlations/errors_failed_transactions.ts b/x-pack/test/apm_api_integration/tests/correlations/errors_failed_transactions.ts index 054ccbfb4996e..b08ced565ec30 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/errors_failed_transactions.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/errors_failed_transactions.ts @@ -13,7 +13,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertest = getService('legacySupertestAsApmReadUser'); const archiveName = 'apm_8.0.0'; const range = archives_metadata[archiveName]; diff --git a/x-pack/test/apm_api_integration/tests/correlations/errors_overall.ts b/x-pack/test/apm_api_integration/tests/correlations/errors_overall.ts index a4e4077a17483..f4e95816a3996 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/errors_overall.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/errors_overall.ts @@ -6,36 +6,37 @@ */ import expect from '@kbn/expect'; -import { format } from 'url'; -import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; +import { SupertestReturnType } from '../../common/apm_api_supertest'; import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const apmApiClient = getService('apmApiClient'); const archiveName = 'apm_8.0.0'; const range = archives_metadata[archiveName]; - const url = format({ - pathname: `/api/apm/correlations/errors/overall_timeseries`, - query: { - start: range.start, - end: range.end, - environment: 'ENVIRONMENT_ALL', - kuery: '', + const urlConfig = { + endpoint: `GET /api/apm/correlations/errors/overall_timeseries` as const, + params: { + query: { + start: range.start, + end: range.end, + environment: 'ENVIRONMENT_ALL', + kuery: '', + }, }, - }); + }; registry.when( 'correlations errors overall without data', { config: 'trial', archives: [] }, () => { it('handles the empty state', async () => { - const response = await supertest.get(url); + const response = await apmApiClient.readUser(urlConfig); expect(response.status).to.be(200); - expect(response.body.response).to.be(undefined); + expect(response.body.overall).to.be(null); }); } ); @@ -44,14 +45,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { 'correlations errors overall with data and default args', { config: 'trial', archives: ['apm_8.0.0'] }, () => { - type ResponseBody = APIReturnType<'GET /api/apm/correlations/errors/overall_timeseries'>; - let response: { - status: number; - body: NonNullable; - }; + let response: SupertestReturnType<'GET /api/apm/correlations/errors/overall_timeseries'>; before(async () => { - response = await supertest.get(url); + response = await apmApiClient.readUser(urlConfig); }); it('returns successfully', () => { 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 94c293cd1a19f..4b484502d5826 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,17 +6,22 @@ */ 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) { const retry = getService('retry'); - const supertest = getService('supertest'); + 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 32ca71694626f..a51c0c8b9d151 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/latency.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/latency.ts @@ -6,31 +6,37 @@ */ 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) { const retry = getService('retry'); - const supertest = getService('supertest'); + 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/apm_api_integration/tests/correlations/latency_overall.ts b/x-pack/test/apm_api_integration/tests/correlations/latency_overall.ts index cfbe63e976655..722a9a2bc4fb7 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/latency_overall.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/latency_overall.ts @@ -13,7 +13,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertest = getService('legacySupertestAsApmReadUser'); const archiveName = 'apm_8.0.0'; const range = archives_metadata[archiveName]; diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency_slow_transactions.ts b/x-pack/test/apm_api_integration/tests/correlations/latency_slow_transactions.ts index dac9ed70bc483..09c092ed1a646 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/latency_slow_transactions.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/latency_slow_transactions.ts @@ -13,7 +13,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertest = getService('legacySupertestAsApmReadUser'); const archiveName = 'apm_8.0.0'; const range = archives_metadata[archiveName]; diff --git a/x-pack/test/apm_api_integration/tests/csm/csm_services.ts b/x-pack/test/apm_api_integration/tests/csm/csm_services.ts index 57018b5012aa2..832ef93e3f721 100644 --- a/x-pack/test/apm_api_integration/tests/csm/csm_services.ts +++ b/x-pack/test/apm_api_integration/tests/csm/csm_services.ts @@ -10,7 +10,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; export default function rumServicesApiTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertest = getService('legacySupertestAsApmReadUser'); registry.when('CSM Services without data', { config: 'trial', archives: [] }, () => { it('returns empty list', async () => { diff --git a/x-pack/test/apm_api_integration/tests/csm/has_rum_data.ts b/x-pack/test/apm_api_integration/tests/csm/has_rum_data.ts index 15ddc04e2414d..3372e43396ed0 100644 --- a/x-pack/test/apm_api_integration/tests/csm/has_rum_data.ts +++ b/x-pack/test/apm_api_integration/tests/csm/has_rum_data.ts @@ -10,7 +10,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; export default function rumHasDataApiTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertest = getService('legacySupertestAsApmReadUser'); registry.when('has_rum_data without data', { config: 'trial', archives: [] }, () => { it('returns empty list', async () => { diff --git a/x-pack/test/apm_api_integration/tests/csm/js_errors.ts b/x-pack/test/apm_api_integration/tests/csm/js_errors.ts index 870c90273d5cc..6346c991373b5 100644 --- a/x-pack/test/apm_api_integration/tests/csm/js_errors.ts +++ b/x-pack/test/apm_api_integration/tests/csm/js_errors.ts @@ -10,7 +10,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; export default function rumJsErrorsApiTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertest = getService('legacySupertestAsApmReadUser'); registry.when('CSM JS errors with data', { config: 'trial', archives: [] }, () => { it('returns no js errors', async () => { diff --git a/x-pack/test/apm_api_integration/tests/csm/long_task_metrics.ts b/x-pack/test/apm_api_integration/tests/csm/long_task_metrics.ts index 86a99325fe9c9..0cb84d1935fa8 100644 --- a/x-pack/test/apm_api_integration/tests/csm/long_task_metrics.ts +++ b/x-pack/test/apm_api_integration/tests/csm/long_task_metrics.ts @@ -10,7 +10,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; export default function rumServicesApiTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertest = getService('legacySupertestAsApmReadUser'); registry.when('CSM long task metrics without data', { config: 'trial', archives: [] }, () => { it('returns empty list', async () => { diff --git a/x-pack/test/apm_api_integration/tests/csm/page_load_dist.ts b/x-pack/test/apm_api_integration/tests/csm/page_load_dist.ts index f26832c8fadbe..8d6a38f27a8c4 100644 --- a/x-pack/test/apm_api_integration/tests/csm/page_load_dist.ts +++ b/x-pack/test/apm_api_integration/tests/csm/page_load_dist.ts @@ -10,7 +10,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; export default function rumServicesApiTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertest = getService('legacySupertestAsApmReadUser'); registry.when('UX page load dist without data', { config: 'trial', archives: [] }, () => { it('returns empty list', async () => { diff --git a/x-pack/test/apm_api_integration/tests/csm/page_views.ts b/x-pack/test/apm_api_integration/tests/csm/page_views.ts index 6732f46011cb1..e5ffd37d3c682 100644 --- a/x-pack/test/apm_api_integration/tests/csm/page_views.ts +++ b/x-pack/test/apm_api_integration/tests/csm/page_views.ts @@ -10,7 +10,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; export default function rumServicesApiTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertest = getService('legacySupertestAsApmReadUser'); registry.when('CSM page views without data', { config: 'trial', archives: [] }, () => { it('returns empty list', async () => { diff --git a/x-pack/test/apm_api_integration/tests/csm/url_search.ts b/x-pack/test/apm_api_integration/tests/csm/url_search.ts index 4d0d120668519..3c63186879788 100644 --- a/x-pack/test/apm_api_integration/tests/csm/url_search.ts +++ b/x-pack/test/apm_api_integration/tests/csm/url_search.ts @@ -10,7 +10,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; export default function rumServicesApiTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertest = getService('legacySupertestAsApmReadUser'); registry.when('CSM url search api without data', { config: 'trial', archives: [] }, () => { it('returns empty list', async () => { diff --git a/x-pack/test/apm_api_integration/tests/csm/web_core_vitals.ts b/x-pack/test/apm_api_integration/tests/csm/web_core_vitals.ts index 19411f44dc771..2c89b13d1b725 100644 --- a/x-pack/test/apm_api_integration/tests/csm/web_core_vitals.ts +++ b/x-pack/test/apm_api_integration/tests/csm/web_core_vitals.ts @@ -10,7 +10,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; export default function rumServicesApiTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertest = getService('legacySupertestAsApmReadUser'); registry.when('CSM web core vitals without data', { config: 'trial', archives: [] }, () => { it('returns empty list', async () => { diff --git a/x-pack/test/apm_api_integration/tests/feature_controls.ts b/x-pack/test/apm_api_integration/tests/feature_controls.ts index 58193726e20f1..18fcf4fef5fec 100644 --- a/x-pack/test/apm_api_integration/tests/feature_controls.ts +++ b/x-pack/test/apm_api_integration/tests/feature_controls.ts @@ -10,7 +10,7 @@ import { FtrProviderContext } from '../common/ftr_provider_context'; import { registry } from '../common/registry'; export default function featureControlsTests({ getService }: FtrProviderContext) { - const supertest = getService('supertestAsApmWriteUser'); + const supertest = getService('legacySupertestAsApmWriteUser'); const supertestWithoutAuth = getService('supertestWithoutAuth'); const security = getService('security'); const spaces = getService('spaces'); diff --git a/x-pack/test/apm_api_integration/tests/inspect/inspect.ts b/x-pack/test/apm_api_integration/tests/inspect/inspect.ts index c2a4dfb77d0e6..95805f4ef4524 100644 --- a/x-pack/test/apm_api_integration/tests/inspect/inspect.ts +++ b/x-pack/test/apm_api_integration/tests/inspect/inspect.ts @@ -8,11 +8,11 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; -import { createApmApiSupertest } from '../../common/apm_api_supertest'; + import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; export default function customLinksTests({ getService }: FtrProviderContext) { - const supertestRead = createApmApiSupertest(getService('supertest')); + const apmApiClient = getService('apmApiClient'); const archiveName = 'apm_8.0.0'; const metadata = archives_metadata[archiveName]; @@ -20,7 +20,7 @@ export default function customLinksTests({ getService }: FtrProviderContext) { registry.when('Inspect feature', { config: 'trial', archives: [archiveName] }, () => { describe('when omitting `_inspect` query param', () => { it('returns response without `_inspect`', async () => { - const { status, body } = await supertestRead({ + const { status, body } = await apmApiClient.readUser({ endpoint: 'GET /api/apm/environments', params: { query: { @@ -38,7 +38,7 @@ export default function customLinksTests({ getService }: FtrProviderContext) { describe('when passing `_inspect` as query param', () => { describe('elasticsearch calls made with end-user auth are returned', () => { it('for environments', async () => { - const { status, body } = await supertestRead({ + const { status, body } = await apmApiClient.readUser({ endpoint: 'GET /api/apm/environments', params: { query: { @@ -66,7 +66,7 @@ export default function customLinksTests({ getService }: FtrProviderContext) { describe('elasticsearch calls made with internal user are not return', () => { it('for custom links', async () => { - const { status, body } = await supertestRead({ + const { status, body } = await apmApiClient.readUser({ endpoint: 'GET /api/apm/settings/custom_links', params: { query: { @@ -82,7 +82,7 @@ export default function customLinksTests({ getService }: FtrProviderContext) { }); it('for agent configs', async () => { - const { status, body } = await supertestRead({ + const { status, body } = await apmApiClient.readUser({ endpoint: 'GET /api/apm/settings/agent-configuration', params: { query: { diff --git a/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.ts b/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.ts index 891334e1c1db2..a3e02984a16de 100644 --- a/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.ts +++ b/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.ts @@ -18,7 +18,7 @@ interface ChartResponse { } export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertest = getService('legacySupertestAsApmReadUser'); registry.when( 'Metrics charts when data is loaded', diff --git a/x-pack/test/apm_api_integration/tests/observability_overview/has_data.ts b/x-pack/test/apm_api_integration/tests/observability_overview/has_data.ts index c6bdce217e229..1b0f8fdcf8736 100644 --- a/x-pack/test/apm_api_integration/tests/observability_overview/has_data.ts +++ b/x-pack/test/apm_api_integration/tests/observability_overview/has_data.ts @@ -6,20 +6,19 @@ */ import expect from '@kbn/expect'; -import { createApmApiSupertest } from '../../common/apm_api_supertest'; + import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const apmApiSupertest = createApmApiSupertest(supertest); + const apmApiClient = getService('apmApiClient'); registry.when( 'Observability overview when data is not loaded', { config: 'basic', archives: [] }, () => { it('returns false when there is no data', async () => { - const response = await apmApiSupertest({ + const response = await apmApiClient.readUser({ endpoint: 'GET /api/apm/observability_overview/has_data', }); expect(response.status).to.be(200); @@ -33,7 +32,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { { config: 'basic', archives: ['observability_overview'] }, () => { it('returns false when there is only onboarding data', async () => { - const response = await apmApiSupertest({ + const response = await apmApiClient.readUser({ endpoint: 'GET /api/apm/observability_overview/has_data', }); expect(response.status).to.be(200); @@ -47,7 +46,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { { config: 'basic', archives: ['apm_8.0.0'] }, () => { it('returns true when there is at least one document on transaction, error or metrics indices', async () => { - const response = await apmApiSupertest({ + const response = await apmApiClient.readUser({ endpoint: 'GET /api/apm/observability_overview/has_data', }); expect(response.status).to.be(200); diff --git a/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.ts b/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.ts index 8760b80f5c737..76a157d72cc6f 100644 --- a/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.ts +++ b/x-pack/test/apm_api_integration/tests/observability_overview/observability_overview.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertest = getService('legacySupertestAsApmReadUser'); const archiveName = 'apm_8.0.0'; const metadata = archives_metadata[archiveName]; diff --git a/x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts b/x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts index 1520ecd644395..816e4e26ef869 100644 --- a/x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts +++ b/x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts @@ -15,8 +15,10 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; export default function serviceMapsApiTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const supertestAsApmReadUserWithoutMlAccess = getService('supertestAsApmReadUserWithoutMlAccess'); + const supertest = getService('legacySupertestAsApmReadUser'); + const supertestAsApmReadUserWithoutMlAccess = getService( + 'legacySupertestAsApmReadUserWithoutMlAccess' + ); const archiveName = 'apm_8.0.0'; const metadata = archives_metadata[archiveName]; diff --git a/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts b/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts index 942378477f04c..4bd9785b31427 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/dependencies/index.ts @@ -9,7 +9,6 @@ import expect from '@kbn/expect'; import { last, omit, pick, sortBy } from 'lodash'; import { ValuesType } from 'utility-types'; import { Node, NodeType } from '../../../../../plugins/apm/common/connections'; -import { createApmApiSupertest } from '../../../common/apm_api_supertest'; import { roundNumber } from '../../../utils'; import { ENVIRONMENT_ALL, @@ -22,7 +21,7 @@ import { registry } from '../../../common/registry'; import { apmDependenciesMapping, createServiceDependencyDocs } from './es_utils'; export default function ApiTest({ getService }: FtrProviderContext) { - const apmApiSupertest = createApmApiSupertest(getService('supertest')); + const apmApiClient = getService('apmApiClient'); const es = getService('es'); const archiveName = 'apm_8.0.0'; @@ -37,7 +36,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { { config: 'basic', archives: [] }, () => { it('handles the empty state', async () => { - const response = await apmApiSupertest({ + const response = await apmApiClient.readUser({ endpoint: `GET /api/apm/services/{serviceName}/dependencies`, params: { path: { serviceName: 'opbeans-java' }, @@ -212,7 +211,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { refresh: 'wait_for', }); - response = await apmApiSupertest({ + response = await apmApiClient.readUser({ endpoint: `GET /api/apm/services/{serviceName}/dependencies`, params: { path: { serviceName: 'opbeans-java' }, @@ -314,7 +313,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }; before(async () => { - response = await apmApiSupertest({ + response = await apmApiClient.readUser({ endpoint: `GET /api/apm/services/{serviceName}/dependencies`, params: { path: { serviceName: 'opbeans-python' }, diff --git a/x-pack/test/apm_api_integration/tests/service_overview/instance_details.ts b/x-pack/test/apm_api_integration/tests/service_overview/instance_details.ts index 52525abe50373..40bfbbb699e65 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/instance_details.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/instance_details.ts @@ -6,18 +6,18 @@ */ import url from 'url'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; import archives from '../../common/fixtures/es_archiver/archives_metadata'; import { registry } from '../../common/registry'; import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; import { getServiceNodeIds } from './get_service_node_ids'; -import { createApmApiSupertest } from '../../common/apm_api_supertest'; +import { createSupertestClient } from '../../common/apm_api_supertest'; type ServiceOverviewInstanceDetails = APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}'>; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const apmApiSupertest = createApmApiSupertest(supertest); + const supertest = getService('legacySupertestAsApmReadUser'); + const apmApiSupertest = createSupertestClient(supertest); const archiveName = 'apm_8.0.0'; const { start, end } = archives[archiveName]; diff --git a/x-pack/test/apm_api_integration/tests/service_overview/instances_detailed_statistics.ts b/x-pack/test/apm_api_integration/tests/service_overview/instances_detailed_statistics.ts index cdea0da2671bb..ffadb7fcf7801 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/instances_detailed_statistics.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/instances_detailed_statistics.ts @@ -14,12 +14,12 @@ import { APIReturnType } from '../../../../plugins/apm/public/services/rest/crea import { FtrProviderContext } from '../../common/ftr_provider_context'; import archives from '../../common/fixtures/es_archiver/archives_metadata'; import { registry } from '../../common/registry'; -import { createApmApiSupertest } from '../../common/apm_api_supertest'; +import { createSupertestClient } from '../../common/apm_api_supertest'; import { getServiceNodeIds } from './get_service_node_ids'; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const apmApiSupertest = createApmApiSupertest(supertest); + const supertest = getService('legacySupertestAsApmReadUser'); + const apmApiSupertest = createSupertestClient(supertest); const archiveName = 'apm_8.0.0'; const { start, end } = archives[archiveName]; diff --git a/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.ts b/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.ts index 52ead7f2b7b81..355778757af3c 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/instances_main_statistics.ts @@ -13,11 +13,11 @@ import { isFiniteNumber } from '../../../../plugins/apm/common/utils/is_finite_n import { FtrProviderContext } from '../../common/ftr_provider_context'; import archives from '../../common/fixtures/es_archiver/archives_metadata'; import { registry } from '../../common/registry'; -import { createApmApiSupertest } from '../../common/apm_api_supertest'; + import { LatencyAggregationType } from '../../../../plugins/apm/common/latency_aggregation_types'; export default function ApiTest({ getService }: FtrProviderContext) { - const apmApiSupertest = createApmApiSupertest(getService('supertest')); + const apmApiClient = getService('apmApiClient'); const archiveName = 'apm_8.0.0'; const { start, end } = archives[archiveName]; @@ -28,7 +28,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { () => { describe('when data is not loaded', () => { it('handles the empty state', async () => { - const response = await apmApiSupertest({ + const response = await apmApiClient.readUser({ endpoint: `GET /api/apm/services/{serviceName}/service_overview_instances/main_statistics`, params: { path: { serviceName: 'opbeans-java' }, @@ -62,7 +62,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }; beforeEach(async () => { - response = await apmApiSupertest({ + response = await apmApiClient.readUser({ endpoint: `GET /api/apm/services/{serviceName}/service_overview_instances/main_statistics`, params: { path: { serviceName: 'opbeans-java' }, @@ -133,7 +133,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }; beforeEach(async () => { - response = await apmApiSupertest({ + response = await apmApiClient.readUser({ endpoint: `GET /api/apm/services/{serviceName}/service_overview_instances/main_statistics`, params: { path: { serviceName: 'opbeans-ruby' }, @@ -201,7 +201,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }; beforeEach(async () => { - response = await apmApiSupertest({ + response = await apmApiClient.readUser({ endpoint: `GET /api/apm/services/{serviceName}/service_overview_instances/main_statistics`, params: { path: { serviceName: 'opbeans-java' }, diff --git a/x-pack/test/apm_api_integration/tests/services/agent.ts b/x-pack/test/apm_api_integration/tests/services/agent.ts index 5fd222c72a3b2..3e44dbe685cd8 100644 --- a/x-pack/test/apm_api_integration/tests/services/agent.ts +++ b/x-pack/test/apm_api_integration/tests/services/agent.ts @@ -11,7 +11,7 @@ import archives from '../../common/fixtures/es_archiver/archives_metadata'; import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertest = getService('legacySupertestAsApmReadUser'); const archiveName = 'apm_8.0.0'; const range = archives[archiveName]; diff --git a/x-pack/test/apm_api_integration/tests/services/annotations.ts b/x-pack/test/apm_api_integration/tests/services/annotations.ts index 0a885301643c6..32ade1036e629 100644 --- a/x-pack/test/apm_api_integration/tests/services/annotations.ts +++ b/x-pack/test/apm_api_integration/tests/services/annotations.ts @@ -14,8 +14,8 @@ import { registry } from '../../common/registry'; const DEFAULT_INDEX_NAME = 'observability-annotations'; export default function annotationApiTests({ getService }: FtrProviderContext) { - const supertestRead = getService('supertestAsApmReadUser'); - const supertestWrite = getService('supertestAsApmAnnotationsWriteUser'); + const supertestRead = getService('legacySupertestAsApmReadUser'); + const supertestWrite = getService('legacySupertestAsApmAnnotationsWriteUser'); const es = getService('es'); function expectContainsObj(source: JsonObject, expected: JsonObject) { diff --git a/x-pack/test/apm_api_integration/tests/services/derived_annotations.ts b/x-pack/test/apm_api_integration/tests/services/derived_annotations.ts index 2ff4eb7e73306..f401d69b1b002 100644 --- a/x-pack/test/apm_api_integration/tests/services/derived_annotations.ts +++ b/x-pack/test/apm_api_integration/tests/services/derived_annotations.ts @@ -7,12 +7,12 @@ import expect from '@kbn/expect'; import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; -import { createApmApiSupertest } from '../../common/apm_api_supertest'; + import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; export default function annotationApiTests({ getService }: FtrProviderContext) { - const supertestRead = createApmApiSupertest(getService('supertestAsApmReadUser')); + const apmApiClient = getService('apmApiClient'); const es = getService('es'); const dates = [ @@ -128,7 +128,7 @@ export default function annotationApiTests({ getService }: FtrProviderContext) { }); response = ( - await supertestRead({ + await apmApiClient.readUser({ endpoint: 'GET /api/apm/services/{serviceName}/annotation/search', params: { path: { diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups_detailed_statistics.ts b/x-pack/test/apm_api_integration/tests/services/error_groups_detailed_statistics.ts index d7eea2d24ddd3..24507c1e42708 100644 --- a/x-pack/test/apm_api_integration/tests/services/error_groups_detailed_statistics.ts +++ b/x-pack/test/apm_api_integration/tests/services/error_groups_detailed_statistics.ts @@ -12,14 +12,14 @@ import archives_metadata from '../../common/fixtures/es_archiver/archives_metada import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; -import { createApmApiSupertest } from '../../common/apm_api_supertest'; +import { createSupertestClient } from '../../common/apm_api_supertest'; import { getErrorGroupIds } from './get_error_group_ids'; type ErrorGroupsDetailedStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/error_groups/detailed_statistics'>; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const apmApiSupertest = createApmApiSupertest(supertest); + const supertest = getService('legacySupertestAsApmReadUser'); + const apmApiSupertest = createSupertestClient(supertest); const archiveName = 'apm_8.0.0'; const metadata = archives_metadata[archiveName]; diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups_main_statistics.ts b/x-pack/test/apm_api_integration/tests/services/error_groups_main_statistics.ts index 1dbd01cd9b4f7..c853bd60e43e0 100644 --- a/x-pack/test/apm_api_integration/tests/services/error_groups_main_statistics.ts +++ b/x-pack/test/apm_api_integration/tests/services/error_groups_main_statistics.ts @@ -15,7 +15,7 @@ import { APIReturnType } from '../../../../plugins/apm/public/services/rest/crea type ErrorGroupsMainStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/error_groups/main_statistics'>; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertest = getService('legacySupertestAsApmReadUser'); const archiveName = 'apm_8.0.0'; const metadata = archives_metadata[archiveName]; diff --git a/x-pack/test/apm_api_integration/tests/services/service_details.ts b/x-pack/test/apm_api_integration/tests/services/service_details.ts index 1f4b4a2c9909b..263aaa504946b 100644 --- a/x-pack/test/apm_api_integration/tests/services/service_details.ts +++ b/x-pack/test/apm_api_integration/tests/services/service_details.ts @@ -12,7 +12,7 @@ import archives from '../../common/fixtures/es_archiver/archives_metadata'; import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertest = getService('legacySupertestAsApmReadUser'); const archiveName = 'apm_8.0.0'; const { start, end } = archives[archiveName]; diff --git a/x-pack/test/apm_api_integration/tests/services/service_icons.ts b/x-pack/test/apm_api_integration/tests/services/service_icons.ts index 5f16ad1d57f2b..619603efc4128 100644 --- a/x-pack/test/apm_api_integration/tests/services/service_icons.ts +++ b/x-pack/test/apm_api_integration/tests/services/service_icons.ts @@ -12,7 +12,7 @@ import archives from '../../common/fixtures/es_archiver/archives_metadata'; import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertest = getService('legacySupertestAsApmReadUser'); const archiveName = 'apm_8.0.0'; const { start, end } = archives[archiveName]; diff --git a/x-pack/test/apm_api_integration/tests/services/services_detailed_statistics.ts b/x-pack/test/apm_api_integration/tests/services/services_detailed_statistics.ts index f19cb71018be0..043a3cdc2c9a3 100644 --- a/x-pack/test/apm_api_integration/tests/services/services_detailed_statistics.ts +++ b/x-pack/test/apm_api_integration/tests/services/services_detailed_statistics.ts @@ -16,7 +16,7 @@ import { isFiniteNumber } from '../../../../plugins/apm/common/utils/is_finite_n type ServicesDetailedStatisticsReturn = APIReturnType<'GET /api/apm/services/detailed_statistics'>; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertest = getService('legacySupertestAsApmReadUser'); const archiveName = 'apm_8.0.0'; const metadata = archives_metadata[archiveName]; diff --git a/x-pack/test/apm_api_integration/tests/services/throughput.ts b/x-pack/test/apm_api_integration/tests/services/throughput.ts index 9134b13e18db1..03815c9947e9a 100644 --- a/x-pack/test/apm_api_integration/tests/services/throughput.ts +++ b/x-pack/test/apm_api_integration/tests/services/throughput.ts @@ -13,19 +13,18 @@ import { APIReturnType } from '../../../../plugins/apm/public/services/rest/crea import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; -import { createApmApiSupertest } from '../../common/apm_api_supertest'; type ThroughputReturn = APIReturnType<'GET /api/apm/services/{serviceName}/throughput'>; export default function ApiTest({ getService }: FtrProviderContext) { - const apmApiSupertest = createApmApiSupertest(getService('supertest')); + const apmApiClient = getService('apmApiClient'); const archiveName = 'apm_8.0.0'; const metadata = archives_metadata[archiveName]; registry.when('Throughput when data is not loaded', { config: 'basic', archives: [] }, () => { it('handles the empty state', async () => { - const response = await apmApiSupertest({ + const response = await apmApiClient.readUser({ endpoint: 'GET /api/apm/services/{serviceName}/throughput', params: { path: { @@ -54,7 +53,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { () => { describe('when querying without kql filter', () => { before(async () => { - const response = await apmApiSupertest({ + const response = await apmApiClient.readUser({ endpoint: 'GET /api/apm/services/{serviceName}/throughput', params: { path: { @@ -108,7 +107,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { describe('with kql filter to force transaction-based UI', () => { before(async () => { - const response = await apmApiSupertest({ + const response = await apmApiClient.readUser({ endpoint: 'GET /api/apm/services/{serviceName}/throughput', params: { path: { @@ -144,7 +143,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { { config: 'basic', archives: [archiveName] }, () => { before(async () => { - const response = await apmApiSupertest({ + const response = await apmApiClient.readUser({ endpoint: 'GET /api/apm/services/{serviceName}/throughput', params: { path: { diff --git a/x-pack/test/apm_api_integration/tests/services/top_services.ts b/x-pack/test/apm_api_integration/tests/services/top_services.ts index 86d5db591a6ba..23b2ca7cfefe9 100644 --- a/x-pack/test/apm_api_integration/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/tests/services/top_services.ts @@ -14,8 +14,10 @@ import archives_metadata from '../../common/fixtures/es_archiver/archives_metada import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const supertestAsApmReadUserWithoutMlAccess = getService('supertestAsApmReadUserWithoutMlAccess'); + const supertest = getService('legacySupertestAsApmReadUser'); + const supertestAsApmReadUserWithoutMlAccess = getService( + 'legacySupertestAsApmReadUserWithoutMlAccess' + ); const archiveName = 'apm_8.0.0'; diff --git a/x-pack/test/apm_api_integration/tests/services/transaction_types.ts b/x-pack/test/apm_api_integration/tests/services/transaction_types.ts index 568d75c3e9cc7..6f574b5c8e997 100644 --- a/x-pack/test/apm_api_integration/tests/services/transaction_types.ts +++ b/x-pack/test/apm_api_integration/tests/services/transaction_types.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertest = getService('legacySupertestAsApmReadUser'); const archiveName = 'apm_8.0.0'; const metadata = archives_metadata[archiveName]; diff --git a/x-pack/test/apm_api_integration/tests/settings/agent_configuration.ts b/x-pack/test/apm_api_integration/tests/settings/agent_configuration.ts index 166c95b5f6de7..377c933144880 100644 --- a/x-pack/test/apm_api_integration/tests/settings/agent_configuration.ts +++ b/x-pack/test/apm_api_integration/tests/settings/agent_configuration.ts @@ -9,52 +9,51 @@ import expect from '@kbn/expect'; import { omit, orderBy } from 'lodash'; import { AgentConfigurationIntake } from '../../../../plugins/apm/common/agent_configuration/configuration_types'; import { AgentConfigSearchParams } from '../../../../plugins/apm/server/routes/settings/agent_configuration'; -import { createApmApiSupertest } from '../../common/apm_api_supertest'; + import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; export default function agentConfigurationTests({ getService }: FtrProviderContext) { - const supertestRead = createApmApiSupertest(getService('supertestAsApmReadUser')); - const supertestWrite = createApmApiSupertest(getService('supertestAsApmWriteUser')); + const apmApiClient = getService('apmApiClient'); const log = getService('log'); const archiveName = 'apm_8.0.0'; function getServices() { - return supertestRead({ + return apmApiClient.readUser({ endpoint: 'GET /api/apm/settings/agent-configuration/services', }); } async function getEnvironments(serviceName: string) { - return supertestRead({ + return apmApiClient.readUser({ endpoint: 'GET /api/apm/settings/agent-configuration/environments', params: { query: { serviceName } }, }); } function getAgentName(serviceName: string) { - return supertestRead({ + return apmApiClient.readUser({ endpoint: 'GET /api/apm/settings/agent-configuration/agent_name', params: { query: { serviceName } }, }); } function searchConfigurations(configuration: AgentConfigSearchParams) { - return supertestRead({ + return apmApiClient.readUser({ endpoint: 'POST /api/apm/settings/agent-configuration/search', params: { body: configuration }, }); } function getAllConfigurations() { - return supertestRead({ endpoint: 'GET /api/apm/settings/agent-configuration' }); + return apmApiClient.readUser({ endpoint: 'GET /api/apm/settings/agent-configuration' }); } function createConfiguration(configuration: AgentConfigurationIntake, { user = 'write' } = {}) { log.debug('creating configuration', configuration.service); - const supertestClient = user === 'read' ? supertestRead : supertestWrite; + const supertestClient = user === 'read' ? apmApiClient.readUser : apmApiClient.writeUser; return supertestClient({ endpoint: 'PUT /api/apm/settings/agent-configuration', @@ -64,7 +63,7 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte function updateConfiguration(config: AgentConfigurationIntake, { user = 'write' } = {}) { log.debug('updating configuration', config.service); - const supertestClient = user === 'read' ? supertestRead : supertestWrite; + const supertestClient = user === 'read' ? apmApiClient.readUser : apmApiClient.writeUser; return supertestClient({ endpoint: 'PUT /api/apm/settings/agent-configuration', @@ -74,7 +73,7 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte function deleteConfiguration({ service }: AgentConfigurationIntake, { user = 'write' } = {}) { log.debug('deleting configuration', service); - const supertestClient = user === 'read' ? supertestRead : supertestWrite; + const supertestClient = user === 'read' ? apmApiClient.readUser : apmApiClient.writeUser; return supertestClient({ endpoint: 'DELETE /api/apm/settings/agent-configuration', diff --git a/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/basic.ts b/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/basic.ts index 9a4968e50bfc2..40708adb754a8 100644 --- a/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/basic.ts +++ b/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/basic.ts @@ -10,9 +10,9 @@ import { registry } from '../../../common/registry'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function apiTest({ getService }: FtrProviderContext) { - const noAccessUser = getService('supertestAsNoAccessUser'); - const readUser = getService('supertestAsApmReadUser'); - const writeUser = getService('supertestAsApmWriteUser'); + const noAccessUser = getService('legacySupertestAsNoAccessUser'); + const readUser = getService('legacySupertestAsApmReadUser'); + const writeUser = getService('legacySupertestAsApmWriteUser'); type SupertestAsUser = typeof noAccessUser | typeof readUser | typeof writeUser; diff --git a/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/no_access_user.ts b/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/no_access_user.ts index 822053e3fc12a..135038755dc6e 100644 --- a/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/no_access_user.ts +++ b/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/no_access_user.ts @@ -10,7 +10,7 @@ import { registry } from '../../../common/registry'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function apiTest({ getService }: FtrProviderContext) { - const noAccessUser = getService('supertestAsNoAccessUser'); + const noAccessUser = getService('legacySupertestAsNoAccessUser'); function getJobs() { return noAccessUser.get(`/api/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); diff --git a/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/read_user.ts b/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/read_user.ts index fb4069eae09d9..3beebb434b317 100644 --- a/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/read_user.ts +++ b/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/read_user.ts @@ -10,7 +10,7 @@ import { registry } from '../../../common/registry'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function apiTest({ getService }: FtrProviderContext) { - const apmReadUser = getService('supertestAsApmReadUser'); + const apmReadUser = getService('legacySupertestAsApmReadUser'); function getJobs() { return apmReadUser.get(`/api/apm/settings/anomaly-detection/jobs`).set('kbn-xsrf', 'foo'); diff --git a/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/write_user.ts b/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/write_user.ts index 322c2a4a049cf..7c13533a14291 100644 --- a/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/write_user.ts +++ b/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/write_user.ts @@ -7,20 +7,19 @@ import expect from '@kbn/expect'; import { countBy } from 'lodash'; -import { createApmApiSupertest } from '../../../common/apm_api_supertest'; import { registry } from '../../../common/registry'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function apiTest({ getService }: FtrProviderContext) { - const apmWriteUser = getService('supertestAsApmWriteUser'); - const apmApiWriteUser = createApmApiSupertest(getService('supertestAsApmWriteUser')); + const apmApiClient = getService('apmApiClient'); + const legacyWriteUserClient = getService('legacySupertestAsApmWriteUser'); function getJobs() { - return apmApiWriteUser({ endpoint: `GET /api/apm/settings/anomaly-detection/jobs` }); + return apmApiClient.writeUser({ endpoint: `GET /api/apm/settings/anomaly-detection/jobs` }); } function createJobs(environments: string[]) { - return apmApiWriteUser({ + return apmApiClient.writeUser({ endpoint: `POST /api/apm/settings/anomaly-detection/jobs`, params: { body: { environments }, @@ -29,7 +28,10 @@ export default function apiTest({ getService }: FtrProviderContext) { } function deleteJobs(jobIds: string[]) { - return apmWriteUser.post(`/api/ml/jobs/delete_jobs`).send({ jobIds }).set('kbn-xsrf', 'foo'); + return legacyWriteUserClient + .post(`/api/ml/jobs/delete_jobs`) + .send({ jobIds }) + .set('kbn-xsrf', 'foo'); } registry.when('ML jobs', { config: 'trial', archives: [] }, () => { diff --git a/x-pack/test/apm_api_integration/tests/settings/custom_link.ts b/x-pack/test/apm_api_integration/tests/settings/custom_link.ts index 7f1fb7df68390..03b2ad4aa3212 100644 --- a/x-pack/test/apm_api_integration/tests/settings/custom_link.ts +++ b/x-pack/test/apm_api_integration/tests/settings/custom_link.ts @@ -9,11 +9,10 @@ import expect from '@kbn/expect'; import { CustomLink } from '../../../../plugins/apm/common/custom_link/custom_link_types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; -import { ApmApiError, createApmApiSupertest } from '../../common/apm_api_supertest'; +import { ApmApiError } from '../../common/apm_api_supertest'; export default function customLinksTests({ getService }: FtrProviderContext) { - const supertestRead = createApmApiSupertest(getService('supertest')); - const supertestWrite = createApmApiSupertest(getService('supertestAsApmWriteUser')); + const apmApiClient = getService('apmApiClient'); const log = getService('log'); const archiveName = 'apm_8.0.0'; @@ -50,6 +49,7 @@ export default function customLinksTests({ getService }: FtrProviderContext) { { key: 'transaction.type', value: 'qux' }, ], } as CustomLink; + await createCustomLink(customLink); }); @@ -125,7 +125,7 @@ export default function customLinksTests({ getService }: FtrProviderContext) { }); it('fetches a transaction sample', async () => { - const response = await supertestRead({ + const response = await apmApiClient.readUser({ endpoint: 'GET /api/apm/settings/custom_links/transaction', params: { query: { @@ -140,7 +140,7 @@ export default function customLinksTests({ getService }: FtrProviderContext) { ); function searchCustomLinks(filters?: any) { - return supertestRead({ + return apmApiClient.readUser({ endpoint: 'GET /api/apm/settings/custom_links', params: { query: filters, @@ -151,7 +151,7 @@ export default function customLinksTests({ getService }: FtrProviderContext) { async function createCustomLink(customLink: CustomLink) { log.debug('creating configuration', customLink); - return supertestWrite({ + return apmApiClient.writeUser({ endpoint: 'POST /api/apm/settings/custom_links', params: { body: customLink, @@ -162,7 +162,7 @@ export default function customLinksTests({ getService }: FtrProviderContext) { async function updateCustomLink(id: string, customLink: CustomLink) { log.debug('updating configuration', id, customLink); - return supertestWrite({ + return apmApiClient.writeUser({ endpoint: 'PUT /api/apm/settings/custom_links/{id}', params: { path: { id }, @@ -174,7 +174,7 @@ export default function customLinksTests({ getService }: FtrProviderContext) { async function deleteCustomLink(id: string) { log.debug('deleting configuration', id); - return supertestWrite({ + return apmApiClient.writeUser({ endpoint: 'DELETE /api/apm/settings/custom_links/{id}', params: { path: { id } }, }); diff --git a/x-pack/test/apm_api_integration/tests/traces/top_traces.ts b/x-pack/test/apm_api_integration/tests/traces/top_traces.ts index 29604bfc990df..705c3d9ff4a15 100644 --- a/x-pack/test/apm_api_integration/tests/traces/top_traces.ts +++ b/x-pack/test/apm_api_integration/tests/traces/top_traces.ts @@ -12,7 +12,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertest = getService('legacySupertestAsApmReadUser'); const archiveName = 'apm_8.0.0'; const metadata = archives_metadata[archiveName]; diff --git a/x-pack/test/apm_api_integration/tests/transactions/breakdown.ts b/x-pack/test/apm_api_integration/tests/transactions/breakdown.ts index 92fdbc3588e39..de23b8fea5363 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/breakdown.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/breakdown.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertest = getService('legacySupertestAsApmReadUser'); const archiveName = 'apm_8.0.0'; const metadata = archives_metadata[archiveName]; diff --git a/x-pack/test/apm_api_integration/tests/transactions/error_rate.ts b/x-pack/test/apm_api_integration/tests/transactions/error_rate.ts index bb6b465c9927c..517812c236c34 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/error_rate.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/error_rate.ts @@ -17,7 +17,7 @@ import { registry } from '../../common/registry'; type ErrorRate = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/error_rate'>; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertest = getService('legacySupertestAsApmReadUser'); const archiveName = 'apm_8.0.0'; diff --git a/x-pack/test/apm_api_integration/tests/transactions/latency.ts b/x-pack/test/apm_api_integration/tests/transactions/latency.ts index 7fa2c76dd54d8..5798da3019982 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/latency.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/latency.ts @@ -17,7 +17,7 @@ import { registry } from '../../common/registry'; type LatencyChartReturnType = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/latency'>; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertest = getService('legacySupertestAsApmReadUser'); const archiveName = 'apm_8.0.0'; diff --git a/x-pack/test/apm_api_integration/tests/transactions/trace_samples.ts b/x-pack/test/apm_api_integration/tests/transactions/trace_samples.ts index 73b1bbfd781d0..fca9222e69bd0 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/trace_samples.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/trace_samples.ts @@ -12,7 +12,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertest = getService('legacySupertestAsApmReadUser'); const archiveName = 'apm_8.0.0'; const metadata = archives_metadata[archiveName]; diff --git a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.ts b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.ts index 3a97195ec587f..965c96bcf287b 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_detailed_statistics.ts @@ -18,7 +18,7 @@ import { removeEmptyCoordinates, roundNumber } from '../../utils'; type TransactionsGroupsDetailedStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups/detailed_statistics'>; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertest = getService('legacySupertestAsApmReadUser'); const archiveName = 'apm_8.0.0'; const { start, end } = archives[archiveName]; diff --git a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_main_statistics.ts b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_main_statistics.ts index d0672946ad019..406d6fa1333d9 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_main_statistics.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/transactions_groups_main_statistics.ts @@ -16,7 +16,7 @@ import { registry } from '../../common/registry'; type TransactionsGroupsPrimaryStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups/main_statistics'>; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertest = getService('legacySupertestAsApmReadUser'); const archiveName = 'apm_8.0.0'; const { start, end } = archives[archiveName]; 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 13c2dd24f9103..8f9428d8a12db 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 06130775ec3cb..be1007e632ef4 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 770502db49dae..d1c3eae785f47 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 85573560177ee..02ecc9570afef 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 6b4d104423144..8c59533ce98dc 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 0000000000000..707b3877a70b5 --- /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 5979ae378c22b..73c9b83de917f 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 fa12e5158ac77..a6e4500d30593 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/functional/config.js b/x-pack/test/functional/config.js index 226cebb9afbfa..8c4ac597f1bf3 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -59,7 +59,6 @@ export default async function ({ readConfigFile }) { resolve(__dirname, './apps/reporting_management'), resolve(__dirname, './apps/management'), resolve(__dirname, './apps/reporting'), - resolve(__dirname, './apps/observability'), // This license_management file must be last because it is destructive. resolve(__dirname, './apps/license_management'), diff --git a/x-pack/test/functional/services/index.ts b/x-pack/test/functional/services/index.ts index 273db212400ab..5e40eb040178b 100644 --- a/x-pack/test/functional/services/index.ts +++ b/x-pack/test/functional/services/index.ts @@ -60,6 +60,7 @@ import { DashboardPanelTimeRangeProvider, } from './dashboard'; import { SearchSessionsService } from './search_sessions'; +import { ObservabilityProvider } from './observability'; // define the name and providers for services that should be // available to your tests. If you don't specify anything here @@ -110,4 +111,5 @@ export const services = { dashboardPanelTimeRange: DashboardPanelTimeRangeProvider, reporting: ReportingFunctionalProvider, searchSessions: SearchSessionsService, + observability: ObservabilityProvider, }; diff --git a/x-pack/test/functional/services/observability/index.ts b/x-pack/test/functional/services/observability/index.ts new file mode 100644 index 0000000000000..14f931d93b56f --- /dev/null +++ b/x-pack/test/functional/services/observability/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { ObservabilityUsersProvider } from './users'; + +export function ObservabilityProvider(context: FtrProviderContext) { + const users = ObservabilityUsersProvider(context); + + return { + users, + }; +} diff --git a/x-pack/test/functional/services/observability/users.ts b/x-pack/test/functional/services/observability/users.ts new file mode 100644 index 0000000000000..78e8b3346cc67 --- /dev/null +++ b/x-pack/test/functional/services/observability/users.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Role } from '../../../../plugins/security/common/model'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +type CreateRolePayload = Pick; + +const OBSERVABILITY_TEST_ROLE_NAME = 'observability-functional-test-role'; + +export function ObservabilityUsersProvider({ getPageObject, getService }: FtrProviderContext) { + const security = getService('security'); + const commonPageObject = getPageObject('common'); + + /** + * Creates a test role and set it as the test user's role. Performs a page + * reload to apply the role change, but doesn't require a re-login. + * + * @arg roleDefinition - the privileges of the test role + */ + const setTestUserRole = async (roleDefinition: CreateRolePayload) => { + // return to neutral grounds to avoid running into permission problems on reload + await commonPageObject.navigateToActualUrl('kibana'); + + await security.role.create(OBSERVABILITY_TEST_ROLE_NAME, roleDefinition); + + await security.testUser.setRoles([OBSERVABILITY_TEST_ROLE_NAME]); // performs a page reload + }; + + /** + * Deletes the test role and restores thedefault test user role. Performs a + * page reload to apply the role change, but doesn't require a re-login. + */ + const restoreDefaultTestUserRole = async () => { + await Promise.all([ + security.role.delete(OBSERVABILITY_TEST_ROLE_NAME), + security.testUser.restoreDefaults(), + ]); + }; + + return { + defineBasicObservabilityRole, + restoreDefaultTestUserRole, + setTestUserRole, + }; +} + +/** + * Generates a combination of Elasticsearch and Kibana privileges for given + * observability features. + */ +const defineBasicObservabilityRole = ( + features: Partial<{ + observabilityCases: string[]; + apm: string[]; + logs: string[]; + infrastructure: string[]; + uptime: string[]; + }> +): CreateRolePayload => { + return { + elasticsearch: { + cluster: ['all'], + indices: [ + ...((features.logs?.length ?? 0) > 0 + ? [{ names: ['filebeat-*', 'logs-*'], privileges: ['all'] }] + : []), + ...((features.infrastructure?.length ?? 0) > 0 + ? [{ names: ['metricbeat-*', 'metrics-*'], privileges: ['all'] }] + : []), + ...((features.apm?.length ?? 0) > 0 ? [{ names: ['apm-*'], privileges: ['all'] }] : []), + ...((features.uptime?.length ?? 0) > 0 + ? [{ names: ['heartbeat-*,synthetics-*'], privileges: ['all'] }] + : []), + ], + run_as: [], + }, + kibana: [ + { + spaces: ['*'], + base: [], + // @ts-expect-error TypeScript doesn't distinguish between missing and + // undefined props yet + feature: features, + }, + ], + }; +}; diff --git a/x-pack/test/functional/apps/observability/alerts/index.ts b/x-pack/test/observability_functional/apps/observability/alerts/index.ts similarity index 100% rename from x-pack/test/functional/apps/observability/alerts/index.ts rename to x-pack/test/observability_functional/apps/observability/alerts/index.ts diff --git a/x-pack/test/functional/apps/observability/feature_controls/index.ts b/x-pack/test/observability_functional/apps/observability/feature_controls/index.ts similarity index 100% rename from x-pack/test/functional/apps/observability/feature_controls/index.ts rename to x-pack/test/observability_functional/apps/observability/feature_controls/index.ts diff --git a/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts b/x-pack/test/observability_functional/apps/observability/feature_controls/observability_security.ts similarity index 62% rename from x-pack/test/functional/apps/observability/feature_controls/observability_security.ts rename to x-pack/test/observability_functional/apps/observability/feature_controls/observability_security.ts index 1d52088ede3da..69bf995c49ae4 100644 --- a/x-pack/test/functional/apps/observability/feature_controls/observability_security.ts +++ b/x-pack/test/observability_functional/apps/observability/feature_controls/observability_security.ts @@ -10,7 +10,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); - const security = getService('security'); + const observability = getService('observability'); const PageObjects = getPageObjects([ 'common', 'observability', @@ -20,6 +20,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ]); const appsMenu = getService('appsMenu'); const testSubjects = getService('testSubjects'); + describe('observability security feature controls', function () { this.tags(['skipFirefox']); before(async () => { @@ -32,39 +33,20 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('observability cases all privileges', () => { before(async () => { - await security.role.create('cases_observability_all_role', { - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: [ - { spaces: ['*'], base: [], feature: { observabilityCases: ['all'], logs: ['all'] } }, - ], - }); - - await security.user.create('cases_observability_all_user', { - password: 'cases_observability_all_user-password', - roles: ['cases_observability_all_role'], - full_name: 'test user', - }); - - await PageObjects.security.forceLogout(); - - await PageObjects.security.login( - 'cases_observability_all_user', - 'cases_observability_all_user-password', - { - expectSpaceSelector: false, - } + await observability.users.setTestUserRole( + observability.users.defineBasicObservabilityRole({ + observabilityCases: ['all'], + logs: ['all'], + }) ); }); after(async () => { - await PageObjects.security.forceLogout(); - await Promise.all([ - security.role.delete('cases_observability_all_role'), - security.user.delete('cases_observability_all_user'), - ]); + await observability.users.restoreDefaultTestUserRole(); }); it('shows observability/cases navlink', async () => { + await PageObjects.common.navigateToActualUrl('observability'); const navLinks = (await appsMenu.readLinks()).map((link) => link.text); expect(navLinks).to.contain('Cases'); }); @@ -101,38 +83,20 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('observability cases read-only privileges', () => { before(async () => { - await security.role.create('cases_observability_read_role', { - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: [ - { - spaces: ['*'], - base: [], - feature: { observabilityCases: ['read'], logs: ['all'] }, - }, - ], - }); - - await security.user.create('cases_observability_read_user', { - password: 'cases_observability_read_user-password', - roles: ['cases_observability_read_role'], - full_name: 'test user', - }); - - await PageObjects.security.login( - 'cases_observability_read_user', - 'cases_observability_read_user-password', - { - expectSpaceSelector: false, - } + await observability.users.setTestUserRole( + observability.users.defineBasicObservabilityRole({ + observabilityCases: ['read'], + logs: ['all'], + }) ); }); after(async () => { - await security.role.delete('cases_observability_read_role'); - await security.user.delete('cases_observability_read_user'); + await observability.users.restoreDefaultTestUserRole(); }); it('shows observability/cases navlink', async () => { + await PageObjects.common.navigateToActualUrl('observability'); const navLinks = (await appsMenu.readLinks()).map((link) => link.text); expect(navLinks).to.contain('Cases'); }); @@ -170,36 +134,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('no observability privileges', () => { before(async () => { - await security.role.create('no_observability_privileges_role', { + await observability.users.setTestUserRole({ elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: [ - { - feature: { - discover: ['all'], - }, - spaces: ['*'], - }, - ], + kibana: [{ spaces: ['*'], base: [], feature: { discover: ['all'] } }], }); - - await security.user.create('no_observability_privileges_user', { - password: 'no_observability_privileges_user-password', - roles: ['no_observability_privileges_role'], - full_name: 'test user', - }); - - await PageObjects.security.login( - 'no_observability_privileges_user', - 'no_observability_privileges_user-password', - { - expectSpaceSelector: false, - } - ); }); after(async () => { - await security.role.delete('no_observability_privileges_role'); - await security.user.delete('no_observability_privileges_user'); + await observability.users.restoreDefaultTestUserRole(); }); it(`returns a 403`, async () => { diff --git a/x-pack/test/functional/apps/observability/index.ts b/x-pack/test/observability_functional/apps/observability/index.ts similarity index 100% rename from x-pack/test/functional/apps/observability/index.ts rename to x-pack/test/observability_functional/apps/observability/index.ts diff --git a/x-pack/test/observability_functional/ftr_provider_context.d.ts b/x-pack/test/observability_functional/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..66d4e37b795ca --- /dev/null +++ b/x-pack/test/observability_functional/ftr_provider_context.d.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { GenericFtrProviderContext } from '@kbn/test'; + +import { pageObjects } from '../functional/page_objects'; +import { services } from '../functional/services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/observability_functional/with_rac_write.config.ts b/x-pack/test/observability_functional/with_rac_write.config.ts new file mode 100644 index 0000000000000..89a0da7857333 --- /dev/null +++ b/x-pack/test/observability_functional/with_rac_write.config.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { readFileSync } from 'fs'; +import { resolve } from 'path'; +import { CA_CERT_PATH } from '@kbn/dev-utils'; +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + + const servers = { + ...xpackFunctionalConfig.get('servers'), + elasticsearch: { + ...xpackFunctionalConfig.get('servers.elasticsearch'), + protocol: 'https', + certificateAuthorities: [readFileSync(CA_CERT_PATH)], + }, + }; + + return { + // default to the xpack functional config + ...xpackFunctionalConfig.getAll(), + servers, + esTestCluster: { + ...xpackFunctionalConfig.get('esTestCluster'), + ssl: true, + }, + kbnTestServer: { + ...xpackFunctionalConfig.get('kbnTestServer'), + serverArgs: [ + ...xpackFunctionalConfig.get('kbnTestServer.serverArgs'), + `--elasticsearch.hosts=https://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, + `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, + // TO DO: Remove feature flags once we're good to go + '--xpack.observability.unsafe.alertingExperience.enabled=true', + '--xpack.observability.unsafe.cases.enabled=true', + '--xpack.ruleRegistry.write.enabled=true', + ], + }, + testFiles: [resolve(__dirname, './apps/observability')], + junit: { + ...xpackFunctionalConfig.get('junit'), + reportName: 'Chrome X-Pack Observability UI Functional Tests', + }, + }; +} 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 5a98011386567..da296e5a4f60a 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();