diff --git a/.github/relabel.yml b/.github/relabel.yml
index a737be356ce81..eb9a2fd557f45 100644
--- a/.github/relabel.yml
+++ b/.github/relabel.yml
@@ -1,3 +1,3 @@
issues:
- missingLabel: needs-team
- regex: ^(\:ml)|(Team:.*)$
\ No newline at end of file
+ regex: (^\:ml$)|(^Team:.+$)|(^EUI$)
diff --git a/.github/workflows/add-fleet-issues-to-ingest-project.yml b/.github/workflows/add-fleet-issues-to-ingest-project.yml
new file mode 100644
index 0000000000000..117ec649cd8a1
--- /dev/null
+++ b/.github/workflows/add-fleet-issues-to-ingest-project.yml
@@ -0,0 +1,51 @@
+name: Add Fleet issue to Platform Ingest project
+
+on:
+ issues:
+ types:
+ - labeled
+
+env:
+ INGEST_PROJECT_ID: 'PVT_kwDOAGc3Zs4AEzn4'
+ FLEET_LABEL: 'Team:Fleet'
+ AREA_FIELD_ID: 'PVTSSF_lADOAGc3Zs4AEzn4zgEgZSo'
+ FLEET_UI_OPTION_ID: '411a7b86'
+
+jobs:
+ add_to_ingest_project:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: octokit/graphql-action@v2.x
+ id: add_to_project
+ if: ${{ github.event.label.name == env.FLEET_LABEL }}
+ with:
+ query: |
+ # Variables have to be snake cased because of https://github.com/octokit/graphql-action/issues/164
+ mutation AddToIngestProject($project_id: ID!, $content_id: ID!) {
+ addProjectV2ItemById(input: { projectId: $project_id, contentId: $content_id }) {
+ item {
+ id
+ }
+ }
+ }
+ project_id: ${{ env.INGEST_PROJECT_ID }}
+ content_id: ${{ github.event.issue.node_id }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.FLEET_TECH_KIBANA_USER_TOKEN }}
+ - uses: octokit/graphql-action@v2.x
+ id: set_fleet_ui_area
+ if: github.event.label.name == env.FLEET_LABEL
+ with:
+ query: |
+ mutation updateIngestArea($item_id: ID!, $project_id: ID!, $area_field_id: ID!, $area_id: String) {
+ updateProjectV2ItemFieldValue(
+ input: { itemId: $item_id, projectId: $project_id, fieldId: $area_field_id, value: { singleSelectOptionId: $area_id } }) {
+ clientMutationId
+ }
+ }
+ item_id: ${{ fromJSON(steps.add_to_project.outputs.data).addProjectV2ItemById.item.id }}
+ project_id: ${{ env.INGEST_PROJECT_ID }}
+ area_field_id: ${{ env.AREA_FIELD_ID }}
+ area_id: ${{ env.FLEET_UI_OPTION_ID }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.FLEET_TECH_KIBANA_USER_TOKEN }}
diff --git a/.github/workflows/add-to-fleet-project.yml b/.github/workflows/add-to-fleet-project.yml
deleted file mode 100644
index e828a3a5b637e..0000000000000
--- a/.github/workflows/add-to-fleet-project.yml
+++ /dev/null
@@ -1,36 +0,0 @@
-name: Add to Fleet:Quality project
-on:
- issues:
- types:
- - labeled
-jobs:
- add_to_project:
- runs-on: ubuntu-latest
- if: |
- contains(github.event.issue.labels.*.name, 'Team:Fleet') && (
- contains(github.event.issue.labels.*.name, 'technical debt') ||
- contains(github.event.issue.labels.*.name, 'bug') ||
- contains(github.event.issue.labels.*.name, 'performance') ||
- contains(github.event.issue.labels.*.name, 'failed-test') ||
- contains(github.event.issue.labels.*.name, 'chore')
- )
- steps:
- - uses: octokit/graphql-action@v2.x
- id: add_to_project
- with:
- headers: '{"GraphQL-Features": "projects_next_graphql"}'
- query: |
- mutation add_to_project($projectid: ID!, $contentid: ID!) {
- addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) {
- projectNextItem {
- id
- }
- }
- }
- projectid: ${{ env.PROJECT_ID }}
- contentid: ${{ github.event.issue.node_id }}
- env:
- # https://github.com/orgs/elastic/projects/763
- PROJECT_ID: "PN_kwDOAGc3Zs4AAsH6"
- # Token with `write:org` access
- GITHUB_TOKEN: ${{ secrets.FLEET_TECH_KIBANA_USER_TOKEN }}
diff --git a/docs/osquery/images/live-query-check-results.png b/docs/osquery/images/live-query-check-results.png
index 6b84a3bf9f7ca..f52a96a3d64f6 100644
Binary files a/docs/osquery/images/live-query-check-results.png and b/docs/osquery/images/live-query-check-results.png differ
diff --git a/docs/osquery/osquery.asciidoc b/docs/osquery/osquery.asciidoc
index e854904b6baf4..c4781affd744d 100644
--- a/docs/osquery/osquery.asciidoc
+++ b/docs/osquery/osquery.asciidoc
@@ -43,7 +43,7 @@ then view the results.
and you'll get suggestions for agents by name, ID, platform, and policy.
. Specify the query or pack to run:
** *Query*: Select a saved query or enter a new one in the text box. After you enter the query, you can expand the **Advanced** section to view or set <> included in the results from the live query. Mapping ECS fields is optional.
-** *Pack*: Select from query packs that have been loaded and activated. After you select a pack, all of the queries in the pack are displayed.
+** *Pack*: Select from available query packs. After you select a pack, all of the queries in the pack are displayed.
+
TIP: Refer to <> to learn about using and managing Elastic prebuilt packs.
+
@@ -173,13 +173,14 @@ For information about the prebuilt packs that are available, refer to <
-
+
diff --git a/examples/guided_onboarding_example/public/components/main.tsx b/examples/guided_onboarding_example/public/components/main.tsx
index a65fd2324d34b..4c9481d423e4c 100644
--- a/examples/guided_onboarding_example/public/components/main.tsx
+++ b/examples/guided_onboarding_example/public/components/main.tsx
@@ -259,6 +259,7 @@ export const Main = (props: MainProps) => {
{ value: 'observability', text: 'observability' },
{ value: 'security', text: 'security' },
{ value: 'search', text: 'search' },
+ { value: 'testGuide', text: 'test guide' },
]}
value={selectedGuide}
onChange={(e) => {
@@ -294,7 +295,7 @@ export const Main = (props: MainProps) => {
@@ -316,6 +317,14 @@ export const Main = (props: MainProps) => {
/>
+
+ history.push('stepThree')}>
+
+
+
>
diff --git a/examples/guided_onboarding_example/public/components/step_one.tsx b/examples/guided_onboarding_example/public/components/step_one.tsx
index 3441b4d8e5d99..fd5cb132b6b91 100644
--- a/examples/guided_onboarding_example/public/components/step_one.tsx
+++ b/examples/guided_onboarding_example/public/components/step_one.tsx
@@ -32,7 +32,7 @@ export const StepOne = ({ guidedOnboarding }: GuidedOnboardingExampleAppDeps) =>
const [isTourStepOpen, setIsTourStepOpen] = useState(false);
const isTourActive = useObservable(
- guidedOnboardingApi!.isGuideStepActive$('search', 'add_data'),
+ guidedOnboardingApi!.isGuideStepActive$('testGuide', 'step1'),
false
);
useEffect(() => {
@@ -45,7 +45,7 @@ export const StepOne = ({ guidedOnboarding }: GuidedOnboardingExampleAppDeps) =>
@@ -56,7 +56,7 @@ export const StepOne = ({ guidedOnboarding }: GuidedOnboardingExampleAppDeps) =>
@@ -72,12 +72,12 @@ export const StepOne = ({ guidedOnboarding }: GuidedOnboardingExampleAppDeps) =>
onFinish={() => setIsTourStepOpen(false)}
step={1}
stepsTotal={1}
- title="Step Add data"
+ title="Step 1"
anchorPosition="rightUp"
>
{
- await guidedOnboardingApi?.completeGuideStep('search', 'add_data');
+ await guidedOnboardingApi?.completeGuideStep('testGuide', 'step1');
}}
>
Complete step 1
diff --git a/examples/guided_onboarding_example/public/components/step_three.tsx b/examples/guided_onboarding_example/public/components/step_three.tsx
index ffe9d87993611..eefb38165beed 100644
--- a/examples/guided_onboarding_example/public/components/step_three.tsx
+++ b/examples/guided_onboarding_example/public/components/step_three.tsx
@@ -30,7 +30,7 @@ export const StepThree = (props: StepThreeProps) => {
useEffect(() => {
const subscription = guidedOnboardingApi
- ?.isGuideStepActive$('search', 'search_experience')
+ ?.isGuideStepActive$('testGuide', 'step3')
.subscribe((isStepActive) => {
setIsTourStepOpen(isStepActive);
});
@@ -53,9 +53,17 @@ export const StepThree = (props: StepThreeProps) => {
+
+
+
@@ -73,12 +81,12 @@ export const StepThree = (props: StepThreeProps) => {
}}
step={1}
stepsTotal={1}
- title="Step Build search experience"
+ title="Step 3"
anchorPosition="rightUp"
>
{
- await guidedOnboardingApi?.completeGuideStep('search', 'search_experience');
+ await guidedOnboardingApi?.completeGuideStep('testGuide', 'step3');
}}
>
Complete step 3
diff --git a/examples/guided_onboarding_example/public/components/step_two.tsx b/examples/guided_onboarding_example/public/components/step_two.tsx
index 07f4fd7e63e0c..89c0c37e46e4a 100644
--- a/examples/guided_onboarding_example/public/components/step_two.tsx
+++ b/examples/guided_onboarding_example/public/components/step_two.tsx
@@ -6,37 +6,17 @@
* Side Public License, v 1.
*/
-import React, { useEffect, useState } from 'react';
+import React from 'react';
-import { EuiButton, EuiSpacer, EuiText, EuiTitle, EuiTourStep } from '@elastic/eui';
+import { EuiText, EuiTitle } from '@elastic/eui';
-import { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public/types';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiPageContentHeader_Deprecated as EuiPageContentHeader,
EuiPageContentBody_Deprecated as EuiPageContentBody,
} from '@elastic/eui';
-interface StepTwoProps {
- guidedOnboarding: GuidedOnboardingPluginStart;
-}
-
-export const StepTwo = (props: StepTwoProps) => {
- const {
- guidedOnboarding: { guidedOnboardingApi },
- } = props;
-
- const [isTourStepOpen, setIsTourStepOpen] = useState(false);
-
- useEffect(() => {
- const subscription = guidedOnboardingApi
- ?.isGuideStepActive$('search', 'browse_docs')
- .subscribe((isStepActive) => {
- setIsTourStepOpen(isStepActive);
- });
- return () => subscription?.unsubscribe();
- }, [guidedOnboardingApi]);
-
+export const StepTwo = () => {
return (
<>
@@ -54,36 +34,11 @@ export const StepTwo = (props: StepTwoProps) => {
-
-
- Click this button to complete step 2.
-
- }
- isStepOpen={isTourStepOpen}
- minWidth={300}
- onFinish={() => {
- setIsTourStepOpen(false);
- }}
- step={1}
- stepsTotal={1}
- title="Step Browse documents"
- anchorPosition="rightUp"
- >
- {
- await guidedOnboardingApi?.completeGuideStep('search', 'browse_docs');
- }}
- >
- Complete step 2
-
-
>
);
diff --git a/package.json b/package.json
index bb7cd79238c33..6b392f582dc14 100644
--- a/package.json
+++ b/package.json
@@ -1319,7 +1319,7 @@
"callsites": "^3.1.0",
"chance": "1.0.18",
"chokidar": "^3.5.3",
- "chromedriver": "^105.0.1",
+ "chromedriver": "^107.0.0",
"clean-webpack-plugin": "^3.0.0",
"compression-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^6.0.2",
@@ -1328,13 +1328,13 @@
"cssnano": "^5.1.12",
"cssnano-preset-default": "^5.2.12",
"csstype": "^3.0.2",
- "cypress": "^10.9.0",
+ "cypress": "^10.10.0",
"cypress-axe": "^1.0.0",
"cypress-file-upload": "^5.0.8",
"cypress-multi-reporters": "^1.6.1",
"cypress-pipe": "^2.0.0",
"cypress-react-selector": "^3.0.0",
- "cypress-real-events": "^1.7.1",
+ "cypress-real-events": "^1.7.2",
"cypress-recurse": "^1.23.0",
"debug": "^2.6.9",
"delete-empty": "^2.0.0",
diff --git a/packages/kbn-apm-config-loader/src/apm_config.ts b/packages/kbn-apm-config-loader/src/apm_config.ts
index 0e7b1b9546288..2127d612d583b 100644
--- a/packages/kbn-apm-config-loader/src/apm_config.ts
+++ b/packages/kbn-apm-config-loader/src/apm_config.ts
@@ -8,14 +8,12 @@
import { schema } from '@kbn/config-schema';
-export const apmConfigSchema = schema.object({
- apm: schema.object(
- {
- active: schema.maybe(schema.boolean()),
- serverUrl: schema.maybe(schema.uri()),
- secretToken: schema.maybe(schema.string()),
- globalLabels: schema.object({}, { unknowns: 'allow' }),
- },
- { unknowns: 'allow' }
- ),
-});
+export const apmConfigSchema = schema.object(
+ {
+ active: schema.maybe(schema.boolean()),
+ serverUrl: schema.maybe(schema.uri()),
+ secretToken: schema.maybe(schema.string()),
+ globalLabels: schema.object({}, { unknowns: 'allow' }),
+ },
+ { unknowns: 'allow' }
+);
diff --git a/packages/kbn-apm-synthtrace/index.ts b/packages/kbn-apm-synthtrace/index.ts
index 170c5ed6206c1..1ff59bdd7d16a 100644
--- a/packages/kbn-apm-synthtrace/index.ts
+++ b/packages/kbn-apm-synthtrace/index.ts
@@ -8,6 +8,7 @@
export { timerange } from './src/lib/timerange';
export { apm } from './src/lib/apm';
+export { dedot } from './src/lib/utils/dedot';
export { stackMonitoring } from './src/lib/stack_monitoring';
export { observer } from './src/lib/agent_config';
export { cleanWriteTargets } from './src/lib/utils/clean_write_targets';
diff --git a/packages/kbn-apm-synthtrace/src/lib/apm/apm_error.ts b/packages/kbn-apm-synthtrace/src/lib/apm/apm_error.ts
index 334c0f296851d..216397f1e1b40 100644
--- a/packages/kbn-apm-synthtrace/src/lib/apm/apm_error.ts
+++ b/packages/kbn-apm-synthtrace/src/lib/apm/apm_error.ts
@@ -27,4 +27,10 @@ export class ApmError extends Serializable {
);
return [data];
}
+
+ timestamp(value: number) {
+ const ret = super.timestamp(value);
+ this.fields['timestamp.us'] = value * 1000;
+ return ret;
+ }
}
diff --git a/packages/kbn-apm-synthtrace/src/lib/apm/base_span.ts b/packages/kbn-apm-synthtrace/src/lib/apm/base_span.ts
index 0cfe5940405a2..b74604c39c242 100644
--- a/packages/kbn-apm-synthtrace/src/lib/apm/base_span.ts
+++ b/packages/kbn-apm-synthtrace/src/lib/apm/base_span.ts
@@ -88,4 +88,10 @@ export class BaseSpan extends Serializable {
});
return this;
}
+
+ override timestamp(timestamp: number) {
+ const ret = super.timestamp(timestamp);
+ this.fields['timestamp.us'] = timestamp * 1000;
+ return ret;
+ }
}
diff --git a/packages/kbn-apm-synthtrace/src/lib/apm/instance.ts b/packages/kbn-apm-synthtrace/src/lib/apm/instance.ts
index f69e54b3e300b..9a7fff73b64a7 100644
--- a/packages/kbn-apm-synthtrace/src/lib/apm/instance.ts
+++ b/packages/kbn-apm-synthtrace/src/lib/apm/instance.ts
@@ -20,24 +20,49 @@ export type SpanParams = {
} & ApmFields;
export class Instance extends Entity {
- transaction({
- transactionName,
- transactionType = 'request',
- }: {
- transactionName: string;
- transactionType?: string;
- }) {
+ transaction(
+ ...options:
+ | [{ transactionName: string; transactionType?: string }]
+ | [string]
+ | [string, string]
+ ) {
+ let transactionName: string;
+ let transactionType: string | undefined;
+ if (options.length === 2) {
+ transactionName = options[0];
+ transactionType = options[1];
+ } else if (typeof options[0] === 'string') {
+ transactionName = options[0];
+ } else {
+ transactionName = options[0].transactionName;
+ transactionType = options[0].transactionType;
+ }
+
return new Transaction({
...this.fields,
'transaction.name': transactionName,
- 'transaction.type': transactionType,
+ 'transaction.type': transactionType || 'request',
});
}
- span({ spanName, spanType, spanSubtype, ...apmFields }: SpanParams) {
+ span(...options: [string, string] | [string, string, string] | [SpanParams]) {
+ let spanName: string;
+ let spanType: string;
+ let spanSubtype: string;
+ let fields: ApmFields;
+
+ if (options.length === 3 || options.length === 2) {
+ spanName = options[0];
+ spanType = options[1];
+ spanSubtype = options[2] || 'unknown';
+ fields = {};
+ } else {
+ ({ spanName, spanType, spanSubtype = 'unknown', ...fields } = options[0]);
+ }
+
return new Span({
...this.fields,
- ...apmFields,
+ ...fields,
'span.name': spanName,
'span.type': spanType,
'span.subtype': spanSubtype,
diff --git a/packages/kbn-apm-synthtrace/src/lib/apm/service.ts b/packages/kbn-apm-synthtrace/src/lib/apm/service.ts
index 0939535a87135..1925c0cdcfd13 100644
--- a/packages/kbn-apm-synthtrace/src/lib/apm/service.ts
+++ b/packages/kbn-apm-synthtrace/src/lib/apm/service.ts
@@ -20,17 +20,18 @@ export class Service extends Entity {
}
}
-export function service({
- name,
- environment,
- agentName,
-}: {
- name: string;
- environment: string;
- agentName: string;
-}) {
+export function service(name: string, environment: string, agentName: string): Service;
+
+export function service(options: { name: string; environment: string; agentName: string }): Service;
+
+export function service(
+ ...args: [{ name: string; environment: string; agentName: string }] | [string, string, string]
+) {
+ const [serviceName, environment, agentName] =
+ args.length === 1 ? [args[0].name, args[0].environment, args[0].agentName] : args;
+
return new Service({
- 'service.name': name,
+ 'service.name': serviceName,
'service.environment': environment,
'agent.name': agentName,
});
diff --git a/packages/kbn-apm-synthtrace/src/lib/stream_processor.ts b/packages/kbn-apm-synthtrace/src/lib/stream_processor.ts
index 0d7d0ff5dfa51..84f0dbb0a62bf 100644
--- a/packages/kbn-apm-synthtrace/src/lib/stream_processor.ts
+++ b/packages/kbn-apm-synthtrace/src/lib/stream_processor.ts
@@ -187,10 +187,7 @@ export class StreamProcessor {
document['service.node.name'] =
document['service.node.name'] || document['container.id'] || document['host.name'];
document['ecs.version'] = '1.4';
- // TODO this non standard field should not be enriched here
- if (document['processor.event'] !== 'metric') {
- document['timestamp.us'] = document['@timestamp']! * 1000;
- }
+
return document;
}
diff --git a/packages/kbn-apm-synthtrace/src/lib/utils/dedot.ts b/packages/kbn-apm-synthtrace/src/lib/utils/dedot.ts
index 4f38a7025f3b5..5d0f57fb5840b 100644
--- a/packages/kbn-apm-synthtrace/src/lib/utils/dedot.ts
+++ b/packages/kbn-apm-synthtrace/src/lib/utils/dedot.ts
@@ -13,4 +13,5 @@ export function dedot(source: Record, target: Record)
const val = source[key as keyof typeof source];
set(target, key, val);
}
+ return target;
}
diff --git a/packages/kbn-apm-synthtrace/src/test/apm_events_to_elasticsearch_output.test.ts b/packages/kbn-apm-synthtrace/src/test/apm_events_to_elasticsearch_output.test.ts
index afafcc0c49665..edb20c4768ee5 100644
--- a/packages/kbn-apm-synthtrace/src/test/apm_events_to_elasticsearch_output.test.ts
+++ b/packages/kbn-apm-synthtrace/src/test/apm_events_to_elasticsearch_output.test.ts
@@ -59,9 +59,6 @@ describe('output apm events to elasticsearch', () => {
"name": "instance-a",
},
},
- "timestamp": Object {
- "us": 1609455600000000,
- },
}
`);
});
diff --git a/packages/kbn-apm-synthtrace/src/test/scenarios/01_simple_trace.test.ts b/packages/kbn-apm-synthtrace/src/test/scenarios/01_simple_trace.test.ts
index a278997ecdf73..a14ae076e8186 100644
--- a/packages/kbn-apm-synthtrace/src/test/scenarios/01_simple_trace.test.ts
+++ b/packages/kbn-apm-synthtrace/src/test/scenarios/01_simple_trace.test.ts
@@ -84,6 +84,7 @@ describe('simple trace', () => {
'service.environment': 'production',
'service.name': 'opbeans-java',
'service.node.name': 'instance-1',
+ 'timestamp.us': 1609459200000000,
'trace.id': '00000000000000000000000000000241',
'transaction.duration.us': 1000000,
'transaction.id': '0000000000000240',
@@ -113,6 +114,7 @@ describe('simple trace', () => {
'span.name': 'GET apm-*/_search',
'span.subtype': 'elasticsearch',
'span.type': 'db',
+ 'timestamp.us': 1609459200050000,
'trace.id': '00000000000000000000000000000301',
'transaction.id': '0000000000000300',
});
diff --git a/packages/kbn-apm-synthtrace/src/test/scenarios/__snapshots__/01_simple_trace.test.ts.snap b/packages/kbn-apm-synthtrace/src/test/scenarios/__snapshots__/01_simple_trace.test.ts.snap
index 1a5fca39e9fd9..8b3306d2d3a4b 100644
--- a/packages/kbn-apm-synthtrace/src/test/scenarios/__snapshots__/01_simple_trace.test.ts.snap
+++ b/packages/kbn-apm-synthtrace/src/test/scenarios/__snapshots__/01_simple_trace.test.ts.snap
@@ -13,6 +13,7 @@ Array [
"service.environment": "production",
"service.name": "opbeans-java",
"service.node.name": "instance-1",
+ "timestamp.us": 1609459200000000,
"trace.id": "00000000000000000000000000000001",
"transaction.duration.us": 1000000,
"transaction.id": "0000000000000000",
@@ -37,6 +38,7 @@ Array [
"span.name": "GET apm-*/_search",
"span.subtype": "elasticsearch",
"span.type": "db",
+ "timestamp.us": 1609459200050000,
"trace.id": "00000000000000000000000000000001",
"transaction.id": "0000000000000000",
},
@@ -51,6 +53,7 @@ Array [
"service.environment": "production",
"service.name": "opbeans-java",
"service.node.name": "instance-1",
+ "timestamp.us": 1609459260000000,
"trace.id": "00000000000000000000000000000005",
"transaction.duration.us": 1000000,
"transaction.id": "0000000000000004",
@@ -75,6 +78,7 @@ Array [
"span.name": "GET apm-*/_search",
"span.subtype": "elasticsearch",
"span.type": "db",
+ "timestamp.us": 1609459260050000,
"trace.id": "00000000000000000000000000000005",
"transaction.id": "0000000000000004",
},
@@ -89,6 +93,7 @@ Array [
"service.environment": "production",
"service.name": "opbeans-java",
"service.node.name": "instance-1",
+ "timestamp.us": 1609459320000000,
"trace.id": "00000000000000000000000000000009",
"transaction.duration.us": 1000000,
"transaction.id": "0000000000000008",
@@ -113,6 +118,7 @@ Array [
"span.name": "GET apm-*/_search",
"span.subtype": "elasticsearch",
"span.type": "db",
+ "timestamp.us": 1609459320050000,
"trace.id": "00000000000000000000000000000009",
"transaction.id": "0000000000000008",
},
@@ -127,6 +133,7 @@ Array [
"service.environment": "production",
"service.name": "opbeans-java",
"service.node.name": "instance-1",
+ "timestamp.us": 1609459380000000,
"trace.id": "00000000000000000000000000000013",
"transaction.duration.us": 1000000,
"transaction.id": "0000000000000012",
@@ -151,6 +158,7 @@ Array [
"span.name": "GET apm-*/_search",
"span.subtype": "elasticsearch",
"span.type": "db",
+ "timestamp.us": 1609459380050000,
"trace.id": "00000000000000000000000000000013",
"transaction.id": "0000000000000012",
},
@@ -165,6 +173,7 @@ Array [
"service.environment": "production",
"service.name": "opbeans-java",
"service.node.name": "instance-1",
+ "timestamp.us": 1609459440000000,
"trace.id": "00000000000000000000000000000017",
"transaction.duration.us": 1000000,
"transaction.id": "0000000000000016",
@@ -189,6 +198,7 @@ Array [
"span.name": "GET apm-*/_search",
"span.subtype": "elasticsearch",
"span.type": "db",
+ "timestamp.us": 1609459440050000,
"trace.id": "00000000000000000000000000000017",
"transaction.id": "0000000000000016",
},
@@ -203,6 +213,7 @@ Array [
"service.environment": "production",
"service.name": "opbeans-java",
"service.node.name": "instance-1",
+ "timestamp.us": 1609459500000000,
"trace.id": "00000000000000000000000000000021",
"transaction.duration.us": 1000000,
"transaction.id": "0000000000000020",
@@ -227,6 +238,7 @@ Array [
"span.name": "GET apm-*/_search",
"span.subtype": "elasticsearch",
"span.type": "db",
+ "timestamp.us": 1609459500050000,
"trace.id": "00000000000000000000000000000021",
"transaction.id": "0000000000000020",
},
@@ -241,6 +253,7 @@ Array [
"service.environment": "production",
"service.name": "opbeans-java",
"service.node.name": "instance-1",
+ "timestamp.us": 1609459560000000,
"trace.id": "00000000000000000000000000000025",
"transaction.duration.us": 1000000,
"transaction.id": "0000000000000024",
@@ -265,6 +278,7 @@ Array [
"span.name": "GET apm-*/_search",
"span.subtype": "elasticsearch",
"span.type": "db",
+ "timestamp.us": 1609459560050000,
"trace.id": "00000000000000000000000000000025",
"transaction.id": "0000000000000024",
},
@@ -279,6 +293,7 @@ Array [
"service.environment": "production",
"service.name": "opbeans-java",
"service.node.name": "instance-1",
+ "timestamp.us": 1609459620000000,
"trace.id": "00000000000000000000000000000029",
"transaction.duration.us": 1000000,
"transaction.id": "0000000000000028",
@@ -303,6 +318,7 @@ Array [
"span.name": "GET apm-*/_search",
"span.subtype": "elasticsearch",
"span.type": "db",
+ "timestamp.us": 1609459620050000,
"trace.id": "00000000000000000000000000000029",
"transaction.id": "0000000000000028",
},
@@ -317,6 +333,7 @@ Array [
"service.environment": "production",
"service.name": "opbeans-java",
"service.node.name": "instance-1",
+ "timestamp.us": 1609459680000000,
"trace.id": "00000000000000000000000000000033",
"transaction.duration.us": 1000000,
"transaction.id": "0000000000000032",
@@ -341,6 +358,7 @@ Array [
"span.name": "GET apm-*/_search",
"span.subtype": "elasticsearch",
"span.type": "db",
+ "timestamp.us": 1609459680050000,
"trace.id": "00000000000000000000000000000033",
"transaction.id": "0000000000000032",
},
@@ -355,6 +373,7 @@ Array [
"service.environment": "production",
"service.name": "opbeans-java",
"service.node.name": "instance-1",
+ "timestamp.us": 1609459740000000,
"trace.id": "00000000000000000000000000000037",
"transaction.duration.us": 1000000,
"transaction.id": "0000000000000036",
@@ -379,6 +398,7 @@ Array [
"span.name": "GET apm-*/_search",
"span.subtype": "elasticsearch",
"span.type": "db",
+ "timestamp.us": 1609459740050000,
"trace.id": "00000000000000000000000000000037",
"transaction.id": "0000000000000036",
},
@@ -393,6 +413,7 @@ Array [
"service.environment": "production",
"service.name": "opbeans-java",
"service.node.name": "instance-1",
+ "timestamp.us": 1609459800000000,
"trace.id": "00000000000000000000000000000041",
"transaction.duration.us": 1000000,
"transaction.id": "0000000000000040",
@@ -417,6 +438,7 @@ Array [
"span.name": "GET apm-*/_search",
"span.subtype": "elasticsearch",
"span.type": "db",
+ "timestamp.us": 1609459800050000,
"trace.id": "00000000000000000000000000000041",
"transaction.id": "0000000000000040",
},
@@ -431,6 +453,7 @@ Array [
"service.environment": "production",
"service.name": "opbeans-java",
"service.node.name": "instance-1",
+ "timestamp.us": 1609459860000000,
"trace.id": "00000000000000000000000000000045",
"transaction.duration.us": 1000000,
"transaction.id": "0000000000000044",
@@ -455,6 +478,7 @@ Array [
"span.name": "GET apm-*/_search",
"span.subtype": "elasticsearch",
"span.type": "db",
+ "timestamp.us": 1609459860050000,
"trace.id": "00000000000000000000000000000045",
"transaction.id": "0000000000000044",
},
@@ -469,6 +493,7 @@ Array [
"service.environment": "production",
"service.name": "opbeans-java",
"service.node.name": "instance-1",
+ "timestamp.us": 1609459920000000,
"trace.id": "00000000000000000000000000000049",
"transaction.duration.us": 1000000,
"transaction.id": "0000000000000048",
@@ -493,6 +518,7 @@ Array [
"span.name": "GET apm-*/_search",
"span.subtype": "elasticsearch",
"span.type": "db",
+ "timestamp.us": 1609459920050000,
"trace.id": "00000000000000000000000000000049",
"transaction.id": "0000000000000048",
},
@@ -507,6 +533,7 @@ Array [
"service.environment": "production",
"service.name": "opbeans-java",
"service.node.name": "instance-1",
+ "timestamp.us": 1609459980000000,
"trace.id": "00000000000000000000000000000053",
"transaction.duration.us": 1000000,
"transaction.id": "0000000000000052",
@@ -531,6 +558,7 @@ Array [
"span.name": "GET apm-*/_search",
"span.subtype": "elasticsearch",
"span.type": "db",
+ "timestamp.us": 1609459980050000,
"trace.id": "00000000000000000000000000000053",
"transaction.id": "0000000000000052",
},
@@ -545,6 +573,7 @@ Array [
"service.environment": "production",
"service.name": "opbeans-java",
"service.node.name": "instance-1",
+ "timestamp.us": 1609460040000000,
"trace.id": "00000000000000000000000000000057",
"transaction.duration.us": 1000000,
"transaction.id": "0000000000000056",
@@ -569,6 +598,7 @@ Array [
"span.name": "GET apm-*/_search",
"span.subtype": "elasticsearch",
"span.type": "db",
+ "timestamp.us": 1609460040050000,
"trace.id": "00000000000000000000000000000057",
"transaction.id": "0000000000000056",
},
diff --git a/packages/kbn-guided-onboarding/src/components/landing_page/use_case_card.tsx b/packages/kbn-guided-onboarding/src/components/landing_page/use_case_card.tsx
index cc16977d966a7..574b9b18bf2b3 100644
--- a/packages/kbn-guided-onboarding/src/components/landing_page/use_case_card.tsx
+++ b/packages/kbn-guided-onboarding/src/components/landing_page/use_case_card.tsx
@@ -9,7 +9,6 @@
import React, { ReactNode } from 'react';
import { EuiCard, EuiText, EuiImage } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { GuideId } from '../../types';
type UseCaseConstants = {
[key in UseCase]: {
@@ -53,7 +52,7 @@ const constants: UseCaseConstants = {
export type UseCase = 'search' | 'observability' | 'security';
export interface UseCaseCardProps {
- useCase: GuideId;
+ useCase: UseCase;
title: string;
description: string;
footer: ReactNode;
diff --git a/packages/kbn-guided-onboarding/src/types.ts b/packages/kbn-guided-onboarding/src/types.ts
index 9a307464cefb8..6b919835da2e7 100644
--- a/packages/kbn-guided-onboarding/src/types.ts
+++ b/packages/kbn-guided-onboarding/src/types.ts
@@ -6,13 +6,14 @@
* Side Public License, v 1.
*/
-export type GuideId = 'observability' | 'security' | 'search';
+export type GuideId = 'observability' | 'security' | 'search' | 'testGuide';
-export type ObservabilityStepIds = 'add_data' | 'view_dashboard' | 'tour_observability';
-export type SecurityStepIds = 'add_data' | 'rules' | 'alertsCases';
-export type SearchStepIds = 'add_data' | 'browse_docs' | 'search_experience';
+type ObservabilityStepIds = 'add_data' | 'view_dashboard' | 'tour_observability';
+type SecurityStepIds = 'add_data' | 'rules' | 'alertsCases';
+type SearchStepIds = 'add_data' | 'browse_docs' | 'search_experience';
+type TestGuideIds = 'step1' | 'step2' | 'step3';
-export type GuideStepIds = ObservabilityStepIds | SecurityStepIds | SearchStepIds;
+export type GuideStepIds = ObservabilityStepIds | SecurityStepIds | SearchStepIds | TestGuideIds;
export interface GuideState {
guideId: GuideId;
diff --git a/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/__snapshots__/heatmap_function.test.ts.snap b/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/__snapshots__/heatmap_function.test.ts.snap
index 96b70e33021f4..1b644ef0a4938 100644
--- a/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/__snapshots__/heatmap_function.test.ts.snap
+++ b/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/__snapshots__/heatmap_function.test.ts.snap
@@ -77,6 +77,7 @@ Object {
"xAccessor": "col-1-2",
"yAccessor": undefined,
},
+ "canNavigateToLens": false,
"data": Object {
"columns": Array [
Object {
diff --git a/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_function.ts b/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_function.ts
index 548d4ec0ab49e..f0c309de19236 100644
--- a/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_function.ts
+++ b/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_function.ts
@@ -232,6 +232,7 @@ export const heatmapFunction = (): HeatmapExpressionFunctionDefinition => ({
},
syncTooltips: handlers?.isSyncTooltipsEnabled?.() ?? false,
syncCursor: handlers?.isSyncCursorEnabled?.() ?? true,
+ canNavigateToLens: Boolean(handlers?.variables?.canNavigateToLens),
},
};
},
diff --git a/src/plugins/chart_expressions/expression_heatmap/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_heatmap/common/types/expression_functions.ts
index 5aa1507f30b03..1bf5fe3bbb36b 100644
--- a/src/plugins/chart_expressions/expression_heatmap/common/types/expression_functions.ts
+++ b/src/plugins/chart_expressions/expression_heatmap/common/types/expression_functions.ts
@@ -94,6 +94,7 @@ export interface HeatmapExpressionProps {
args: HeatmapArguments;
syncTooltips: boolean;
syncCursor: boolean;
+ canNavigateToLens?: boolean;
}
export interface HeatmapRender {
diff --git a/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/heatmap_renderer.tsx b/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/heatmap_renderer.tsx
index 4b813fb93416f..b14ee1382deb2 100644
--- a/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/heatmap_renderer.tsx
+++ b/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/heatmap_renderer.tsx
@@ -61,9 +61,14 @@ export const heatmapRenderer: (
const visualizationType = extractVisualizationType(executionContext);
if (containerType && visualizationType) {
- plugins.usageCollection?.reportUiCounter(containerType, METRIC_TYPE.COUNT, [
+ const events = [
`render_${visualizationType}_${EXPRESSION_HEATMAP_NAME}`,
- ]);
+ config.canNavigateToLens
+ ? `render_${visualizationType}_${EXPRESSION_HEATMAP_NAME}_convertable`
+ : undefined,
+ ].filter((event): event is string => Boolean(event));
+
+ plugins.usageCollection?.reportUiCounter(containerType, METRIC_TYPE.COUNT, events);
}
handlers.done();
diff --git a/src/plugins/guided_onboarding/public/constants/guides_config/index.ts b/src/plugins/guided_onboarding/public/constants/guides_config/index.ts
index 9ce81cf9d4698..e2ab4f7e7747f 100644
--- a/src/plugins/guided_onboarding/public/constants/guides_config/index.ts
+++ b/src/plugins/guided_onboarding/public/constants/guides_config/index.ts
@@ -10,9 +10,11 @@ import type { GuidesConfig } from '../../types';
import { securityConfig } from './security';
import { observabilityConfig } from './observability';
import { searchConfig } from './search';
+import { testGuideConfig } from './test_guide';
export const guidesConfig: GuidesConfig = {
security: securityConfig,
observability: observabilityConfig,
search: searchConfig,
+ testGuide: testGuideConfig,
};
diff --git a/src/plugins/guided_onboarding/public/constants/guides_config/test_guide.ts b/src/plugins/guided_onboarding/public/constants/guides_config/test_guide.ts
new file mode 100644
index 0000000000000..b357ad497c6b4
--- /dev/null
+++ b/src/plugins/guided_onboarding/public/constants/guides_config/test_guide.ts
@@ -0,0 +1,68 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { GuideConfig } from '../../types';
+
+export const testGuideConfig: GuideConfig = {
+ title: 'Test guide for development',
+ description: `This guide is used to test the guided onboarding UI while in development and to run automated tests for the API and UI components.`,
+ guideName: 'Testing example',
+ docs: {
+ text: 'Testing example docs',
+ url: 'example.com',
+ },
+ steps: [
+ {
+ id: 'step1',
+ title: 'Step 1 (completed via an API request)',
+ descriptionList: [
+ `This step is directly completed by clicking the button that uses the API function 'completeGuideStep`,
+ 'Navigate to /guidedOnboardingExample/stepOne to complete the step.',
+ ],
+ location: {
+ appID: 'guidedOnboardingExample',
+ path: 'stepOne',
+ },
+ integration: 'testIntegration',
+ },
+ {
+ id: 'step2',
+ title: 'Step 2 (manual completion after navigation)',
+ descriptionList: [
+ 'This step is set to ready_to_complete on page navigation.',
+ 'After that click the popover on the guide button in the header and mark the step done',
+ ],
+ location: {
+ appID: 'guidedOnboardingExample',
+ path: 'stepTwo',
+ },
+ manualCompletion: {
+ title: 'Manual completion step title',
+ description:
+ 'Mark the step complete by opening the panel and clicking the button "Mark done"',
+ readyToCompleteOnNavigation: true,
+ },
+ },
+ {
+ id: 'step3',
+ title: 'Step 3 (manual completion after click)',
+ descriptionList: [
+ 'This step is completed by clicking a button on the page and then clicking the popover on the guide button in the header and marking the step done',
+ ],
+ manualCompletion: {
+ title: 'Manual completion step title',
+ description:
+ 'Mark the step complete by opening the panel and clicking the button "Mark done"',
+ },
+ location: {
+ appID: 'guidedOnboardingExample',
+ path: 'stepThree',
+ },
+ },
+ ],
+};
diff --git a/src/plugins/guided_onboarding/public/services/api.mocks.ts b/src/plugins/guided_onboarding/public/services/api.mocks.ts
index 21bb257cad68f..2294607f91b38 100644
--- a/src/plugins/guided_onboarding/public/services/api.mocks.ts
+++ b/src/plugins/guided_onboarding/public/services/api.mocks.ts
@@ -6,84 +6,78 @@
* Side Public License, v 1.
*/
-import type { GuideState } from '@kbn/guided-onboarding';
+import type { GuideState, GuideId, GuideStepIds } from '@kbn/guided-onboarding';
-export const searchAddDataActiveState: GuideState = {
- guideId: 'search',
+export const testGuide: GuideId = 'testGuide';
+export const testGuideFirstStep: GuideStepIds = 'step1';
+export const testGuideManualCompletionStep = 'step2';
+export const testGuideLastStep: GuideStepIds = 'step3';
+export const testIntegration = 'testIntegration';
+export const wrongIntegration = 'notTestIntegration';
+
+export const testGuideStep1ActiveState: GuideState = {
+ guideId: 'testGuide',
isActive: true,
status: 'in_progress',
steps: [
{
- id: 'add_data',
+ id: 'step1',
status: 'active',
},
{
- id: 'browse_docs',
+ id: 'step2',
status: 'inactive',
},
{
- id: 'search_experience',
+ id: 'step3',
status: 'inactive',
},
],
};
-export const securityAddDataInProgressState: GuideState = {
- guideId: 'security',
- status: 'in_progress',
- isActive: true,
+export const testGuideStep1InProgressState: GuideState = {
+ ...testGuideStep1ActiveState,
steps: [
{
- id: 'add_data',
- status: 'in_progress',
- },
- {
- id: 'rules',
- status: 'inactive',
- },
- {
- id: 'alertsCases',
- status: 'inactive',
+ id: testGuideStep1ActiveState.steps[0].id,
+ status: 'in_progress', // update the first step status
},
+ testGuideStep1ActiveState.steps[1],
+ testGuideStep1ActiveState.steps[2],
],
};
-export const securityRulesActiveState: GuideState = {
- guideId: 'security',
- isActive: true,
- status: 'in_progress',
+export const testGuideStep2ActiveState: GuideState = {
+ ...testGuideStep1ActiveState,
steps: [
{
- id: 'add_data',
+ ...testGuideStep1ActiveState.steps[0],
status: 'complete',
},
{
- id: 'rules',
+ id: testGuideStep1ActiveState.steps[1].id,
status: 'active',
},
- {
- id: 'alertsCases',
- status: 'inactive',
- },
+ testGuideStep1ActiveState.steps[2],
],
};
-export const noGuideActiveState: GuideState = {
- guideId: 'security',
- status: 'in_progress',
- isActive: false,
+export const testGuideStep2InProgressState: GuideState = {
+ ...testGuideStep1ActiveState,
steps: [
{
- id: 'add_data',
- status: 'in_progress',
- },
- {
- id: 'rules',
- status: 'inactive',
+ ...testGuideStep1ActiveState.steps[0],
+ status: 'complete',
},
{
- id: 'alertsCases',
- status: 'inactive',
+ id: testGuideStep1ActiveState.steps[1].id,
+ status: 'in_progress',
},
+ testGuideStep1ActiveState.steps[2],
],
};
+
+export const testGuideNotActiveState: GuideState = {
+ ...testGuideStep1ActiveState,
+ isActive: false,
+};
diff --git a/src/plugins/guided_onboarding/public/services/api.test.ts b/src/plugins/guided_onboarding/public/services/api.test.ts
index 2296304166648..56a5755f0ee55 100644
--- a/src/plugins/guided_onboarding/public/services/api.test.ts
+++ b/src/plugins/guided_onboarding/public/services/api.test.ts
@@ -12,20 +12,20 @@ import type { GuideState } from '@kbn/guided-onboarding';
import { firstValueFrom, Subscription } from 'rxjs';
import { API_BASE_PATH } from '../../common/constants';
-import { guidesConfig } from '../constants/guides_config';
import { ApiService } from './api';
import {
- noGuideActiveState,
- searchAddDataActiveState,
- securityAddDataInProgressState,
- securityRulesActiveState,
+ testGuide,
+ testGuideFirstStep,
+ testGuideManualCompletionStep,
+ testGuideStep1ActiveState,
+ testGuideStep1InProgressState,
+ testGuideStep2ActiveState,
+ testGuideNotActiveState,
+ testIntegration,
+ wrongIntegration,
+ testGuideStep2InProgressState,
} from './api.mocks';
-const searchGuide = 'search';
-const firstStep = guidesConfig[searchGuide].steps[0].id;
-const endpointIntegration = 'endpoint';
-const kubernetesIntegration = 'kubernetes';
-
describe('GuidedOnboarding ApiService', () => {
let httpClient: jest.Mocked;
let apiService: ApiService;
@@ -34,7 +34,7 @@ describe('GuidedOnboarding ApiService', () => {
beforeEach(() => {
httpClient = httpServiceMock.createStartContract({ basePath: '/base/path' });
httpClient.get.mockResolvedValue({
- state: [securityAddDataInProgressState],
+ state: [testGuideStep1ActiveState],
});
apiService = new ApiService();
apiService.setup(httpClient);
@@ -57,10 +57,10 @@ describe('GuidedOnboarding ApiService', () => {
});
it('broadcasts the updated state', async () => {
- await apiService.activateGuide(searchGuide, searchAddDataActiveState);
+ await apiService.activateGuide(testGuide, testGuideStep1ActiveState);
const state = await firstValueFrom(apiService.fetchActiveGuideState$());
- expect(state).toEqual(searchAddDataActiveState);
+ expect(state).toEqual(testGuideStep1ActiveState);
});
});
@@ -74,12 +74,12 @@ describe('GuidedOnboarding ApiService', () => {
describe('deactivateGuide', () => {
it('deactivates an existing guide', async () => {
- await apiService.deactivateGuide(searchAddDataActiveState);
+ await apiService.deactivateGuide(testGuideStep1ActiveState);
expect(httpClient.put).toHaveBeenCalledTimes(1);
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
body: JSON.stringify({
- ...searchAddDataActiveState,
+ ...testGuideStep1ActiveState,
isActive: false,
}),
});
@@ -88,17 +88,7 @@ describe('GuidedOnboarding ApiService', () => {
describe('updateGuideState', () => {
it('sends a request to the put API', async () => {
- const updatedState: GuideState = {
- ...searchAddDataActiveState,
- steps: [
- {
- id: searchAddDataActiveState.steps[0].id,
- status: 'in_progress', // update the first step status
- },
- searchAddDataActiveState.steps[1],
- searchAddDataActiveState.steps[2],
- ],
- };
+ const updatedState: GuideState = testGuideStep1InProgressState;
await apiService.updateGuideState(updatedState, false);
expect(httpClient.put).toHaveBeenCalledTimes(1);
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
@@ -109,20 +99,11 @@ describe('GuidedOnboarding ApiService', () => {
describe('isGuideStepActive$', () => {
it('returns true if the step has been started', (done) => {
- const updatedState: GuideState = {
- ...searchAddDataActiveState,
- steps: [
- {
- id: searchAddDataActiveState.steps[0].id,
- status: 'in_progress',
- },
- searchAddDataActiveState.steps[1],
- searchAddDataActiveState.steps[2],
- ],
- };
+ const updatedState: GuideState = testGuideStep1InProgressState;
apiService.updateGuideState(updatedState, false);
+
subscription = apiService
- .isGuideStepActive$(searchGuide, firstStep)
+ .isGuideStepActive$(testGuide, testGuideFirstStep)
.subscribe((isStepActive) => {
if (isStepActive) {
done();
@@ -131,9 +112,8 @@ describe('GuidedOnboarding ApiService', () => {
});
it('returns false if the step is not been started', (done) => {
- apiService.updateGuideState(searchAddDataActiveState, false);
subscription = apiService
- .isGuideStepActive$(searchGuide, firstStep)
+ .isGuideStepActive$(testGuide, testGuideFirstStep)
.subscribe((isStepActive) => {
if (!isStepActive) {
done();
@@ -144,56 +124,44 @@ describe('GuidedOnboarding ApiService', () => {
describe('activateGuide', () => {
it('activates a new guide', async () => {
- await apiService.activateGuide(searchGuide);
+ // update the mock to no active guides
+ httpClient.get.mockResolvedValue({
+ state: [],
+ });
+ apiService.setup(httpClient);
+
+ await apiService.activateGuide(testGuide);
expect(httpClient.put).toHaveBeenCalledTimes(1);
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
- body: JSON.stringify({
- isActive: true,
- status: 'not_started',
- steps: [
- {
- id: 'add_data',
- status: 'active',
- },
- {
- id: 'browse_docs',
- status: 'inactive',
- },
- {
- id: 'search_experience',
- status: 'inactive',
- },
- ],
- guideId: searchGuide,
- }),
+ body: JSON.stringify({ ...testGuideStep1ActiveState, status: 'not_started' }),
});
});
it('reactivates a guide that has already been started', async () => {
- await apiService.activateGuide(searchGuide, searchAddDataActiveState);
+ await apiService.activateGuide(testGuide, testGuideStep1ActiveState);
expect(httpClient.put).toHaveBeenCalledTimes(1);
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
- body: JSON.stringify(searchAddDataActiveState),
+ body: JSON.stringify(testGuideStep1ActiveState),
});
});
});
describe('completeGuide', () => {
const readyToCompleteGuideState: GuideState = {
- ...searchAddDataActiveState,
+ ...testGuideStep1ActiveState,
steps: [
{
- id: 'add_data',
+ ...testGuideStep1ActiveState.steps[0],
status: 'complete',
},
{
- id: 'browse_docs',
+ ...testGuideStep1ActiveState.steps[1],
status: 'complete',
},
{
- id: 'search_experience',
+ ...testGuideStep1ActiveState.steps[2],
status: 'complete',
},
],
@@ -204,7 +172,7 @@ describe('GuidedOnboarding ApiService', () => {
});
it('updates the selected guide and marks it as complete', async () => {
- await apiService.completeGuide(searchGuide);
+ await apiService.completeGuide(testGuide);
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
body: JSON.stringify({
@@ -222,51 +190,39 @@ describe('GuidedOnboarding ApiService', () => {
it('returns undefined if the selected guide has uncompleted steps', async () => {
const incompleteGuideState: GuideState = {
- ...searchAddDataActiveState,
+ ...testGuideStep1ActiveState,
steps: [
{
- id: 'add_data',
+ ...testGuideStep1ActiveState.steps[0],
status: 'complete',
},
{
- id: 'browse_docs',
+ ...testGuideStep1ActiveState.steps[1],
status: 'complete',
},
{
- id: 'search_experience',
+ ...testGuideStep1ActiveState.steps[2],
status: 'in_progress',
},
],
};
await apiService.updateGuideState(incompleteGuideState, false);
- const completedState = await apiService.completeGuide(searchGuide);
+ const completedState = await apiService.completeGuide(testGuide);
expect(completedState).not.toBeDefined();
});
});
describe('startGuideStep', () => {
beforeEach(async () => {
- await apiService.updateGuideState(searchAddDataActiveState, false);
+ await apiService.updateGuideState(testGuideStep1ActiveState, false);
});
it('updates the selected step and marks it as in_progress', async () => {
- await apiService.startGuideStep(searchGuide, firstStep);
+ await apiService.startGuideStep(testGuide, testGuideFirstStep);
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
- body: JSON.stringify({
- ...searchAddDataActiveState,
- isActive: true,
- status: 'in_progress',
- steps: [
- {
- id: searchAddDataActiveState.steps[0].id,
- status: 'in_progress',
- },
- searchAddDataActiveState.steps[1],
- searchAddDataActiveState.steps[2],
- ],
- }),
+ body: JSON.stringify(testGuideStep1InProgressState),
});
});
@@ -278,76 +234,35 @@ describe('GuidedOnboarding ApiService', () => {
describe('completeGuideStep', () => {
it(`completes the step when it's in progress`, async () => {
- const updatedState: GuideState = {
- ...searchAddDataActiveState,
- steps: [
- {
- id: searchAddDataActiveState.steps[0].id,
- status: 'in_progress', // Mark a step as in_progress in order to test the "completeGuideStep" behavior
- },
- searchAddDataActiveState.steps[1],
- searchAddDataActiveState.steps[2],
- ],
- };
- await apiService.updateGuideState(updatedState, false);
+ await apiService.updateGuideState(testGuideStep1InProgressState, false);
- await apiService.completeGuideStep(searchGuide, firstStep);
+ await apiService.completeGuideStep(testGuide, testGuideFirstStep);
// Once on update, once on complete
expect(httpClient.put).toHaveBeenCalledTimes(2);
// Verify the completed step now has a "complete" status, and the subsequent step is "active"
expect(httpClient.put).toHaveBeenLastCalledWith(`${API_BASE_PATH}/state`, {
- body: JSON.stringify({
- ...updatedState,
- steps: [
- {
- id: searchAddDataActiveState.steps[0].id,
- status: 'complete',
- },
- {
- id: searchAddDataActiveState.steps[1].id,
- status: 'active',
- },
- searchAddDataActiveState.steps[2],
- ],
- }),
+ body: JSON.stringify({ ...testGuideStep2ActiveState }),
});
});
it(`marks the step as 'ready_to_complete' if it's configured for manual completion`, async () => {
- const securityRulesInProgressState = {
- ...securityRulesActiveState,
- steps: [
- securityRulesActiveState.steps[0],
- {
- id: securityRulesActiveState.steps[1].id,
- status: 'in_progress',
- },
- securityRulesActiveState.steps[2],
- ],
- };
httpClient.get.mockResolvedValue({
- state: [securityRulesInProgressState],
+ state: [testGuideStep2InProgressState],
});
apiService.setup(httpClient);
- await apiService.completeGuideStep('security', 'rules');
+ await apiService.completeGuideStep(testGuide, testGuideManualCompletionStep);
expect(httpClient.put).toHaveBeenCalledTimes(1);
// Verify the completed step now has a "ready_to_complete" status, and the subsequent step is "inactive"
expect(httpClient.put).toHaveBeenLastCalledWith(`${API_BASE_PATH}/state`, {
body: JSON.stringify({
- ...securityRulesInProgressState,
+ ...testGuideStep2InProgressState,
steps: [
- securityRulesInProgressState.steps[0],
- {
- id: securityRulesInProgressState.steps[1].id,
- status: 'ready_to_complete',
- },
- {
- id: securityRulesInProgressState.steps[2].id,
- status: 'inactive',
- },
+ testGuideStep2InProgressState.steps[0],
+ { ...testGuideStep2InProgressState.steps[1], status: 'ready_to_complete' },
+ testGuideStep2InProgressState.steps[2],
],
}),
});
@@ -359,12 +274,8 @@ describe('GuidedOnboarding ApiService', () => {
});
it('does nothing if the step is not in progress', async () => {
- httpClient.get.mockResolvedValue({
- state: [searchAddDataActiveState],
- });
- apiService.setup(httpClient);
-
- await apiService.completeGuideStep(searchGuide, firstStep);
+ // by default the state set in beforeEach is test guide, step 1 active
+ await apiService.completeGuideStep(testGuide, testGuideFirstStep);
expect(httpClient.put).toHaveBeenCalledTimes(0);
});
});
@@ -372,11 +283,11 @@ describe('GuidedOnboarding ApiService', () => {
describe('isGuidedOnboardingActiveForIntegration$', () => {
it('returns true if the integration is part of the active step', (done) => {
httpClient.get.mockResolvedValue({
- state: [securityAddDataInProgressState],
+ state: [testGuideStep1InProgressState],
});
apiService.setup(httpClient);
subscription = apiService
- .isGuidedOnboardingActiveForIntegration$(endpointIntegration)
+ .isGuidedOnboardingActiveForIntegration$(testIntegration)
.subscribe((isIntegrationInGuideStep) => {
if (isIntegrationInGuideStep) {
done();
@@ -384,13 +295,13 @@ describe('GuidedOnboarding ApiService', () => {
});
});
- it('returns false if another integration is part of the active step', (done) => {
+ it('returns false if the current step has a different integration', (done) => {
httpClient.get.mockResolvedValue({
- state: [securityAddDataInProgressState],
+ state: [testGuideStep1InProgressState],
});
apiService.setup(httpClient);
subscription = apiService
- .isGuidedOnboardingActiveForIntegration$(kubernetesIntegration)
+ .isGuidedOnboardingActiveForIntegration$(wrongIntegration)
.subscribe((isIntegrationInGuideStep) => {
if (!isIntegrationInGuideStep) {
done();
@@ -400,11 +311,11 @@ describe('GuidedOnboarding ApiService', () => {
it('returns false if no guide is active', (done) => {
httpClient.get.mockResolvedValue({
- state: [noGuideActiveState],
+ state: [testGuideNotActiveState],
});
apiService.setup(httpClient);
subscription = apiService
- .isGuidedOnboardingActiveForIntegration$(endpointIntegration)
+ .isGuidedOnboardingActiveForIntegration$(testIntegration)
.subscribe((isIntegrationInGuideStep) => {
if (!isIntegrationInGuideStep) {
done();
@@ -416,35 +327,35 @@ describe('GuidedOnboarding ApiService', () => {
describe('completeGuidedOnboardingForIntegration', () => {
it(`completes the step if it's active for the integration`, async () => {
httpClient.get.mockResolvedValue({
- state: [securityAddDataInProgressState],
+ state: [testGuideStep1InProgressState],
});
apiService.setup(httpClient);
- await apiService.completeGuidedOnboardingForIntegration(endpointIntegration);
+ await apiService.completeGuidedOnboardingForIntegration(testIntegration);
expect(httpClient.put).toHaveBeenCalledTimes(1);
// this assertion depends on the guides config
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
- body: JSON.stringify(securityRulesActiveState),
+ body: JSON.stringify(testGuideStep2ActiveState),
});
});
it(`does nothing if the step has a different integration`, async () => {
httpClient.get.mockResolvedValue({
- state: [securityAddDataInProgressState],
+ state: [testGuideStep1InProgressState],
});
apiService.setup(httpClient);
- await apiService.completeGuidedOnboardingForIntegration(kubernetesIntegration);
+ await apiService.completeGuidedOnboardingForIntegration(wrongIntegration);
expect(httpClient.put).not.toHaveBeenCalled();
});
it(`does nothing if no guide is active`, async () => {
httpClient.get.mockResolvedValue({
- state: [noGuideActiveState],
+ state: [testGuideNotActiveState],
});
apiService.setup(httpClient);
- await apiService.completeGuidedOnboardingForIntegration(endpointIntegration);
+ await apiService.completeGuidedOnboardingForIntegration(testIntegration);
expect(httpClient.put).not.toHaveBeenCalled();
});
});
diff --git a/src/plugins/guided_onboarding/public/services/api.ts b/src/plugins/guided_onboarding/public/services/api.ts
index 688e72fa83243..cd33f9505c546 100644
--- a/src/plugins/guided_onboarding/public/services/api.ts
+++ b/src/plugins/guided_onboarding/public/services/api.ts
@@ -147,10 +147,10 @@ export class ApiService implements GuidedOnboardingApi {
});
const updatedGuide: GuideState = {
+ guideId,
isActive: true,
status: 'not_started',
steps: updatedSteps,
- guideId,
};
return await this.updateGuideState(updatedGuide, true);
diff --git a/src/plugins/guided_onboarding/public/services/helpers.test.ts b/src/plugins/guided_onboarding/public/services/helpers.test.ts
index 9dc7519a02019..82720c4f9d223 100644
--- a/src/plugins/guided_onboarding/public/services/helpers.test.ts
+++ b/src/plugins/guided_onboarding/public/services/helpers.test.ts
@@ -6,51 +6,50 @@
* Side Public License, v 1.
*/
-import { guidesConfig } from '../constants/guides_config';
import { isIntegrationInGuideStep, isLastStep } from './helpers';
import {
- noGuideActiveState,
- securityAddDataInProgressState,
- securityRulesActiveState,
+ testGuide,
+ testGuideFirstStep,
+ testGuideLastStep,
+ testGuideNotActiveState,
+ testGuideStep1InProgressState,
+ testGuideStep2InProgressState,
+ testIntegration,
+ wrongIntegration,
} from './api.mocks';
-const searchGuide = 'search';
-const firstStep = guidesConfig[searchGuide].steps[0].id;
-const lastStep = guidesConfig[searchGuide].steps[guidesConfig[searchGuide].steps.length - 1].id;
-
describe('GuidedOnboarding ApiService helpers', () => {
- // this test suite depends on the guides config
describe('isLastStepActive', () => {
it('returns true if the passed params are for the last step', () => {
- const result = isLastStep(searchGuide, lastStep);
+ const result = isLastStep(testGuide, testGuideLastStep);
expect(result).toBe(true);
});
it('returns false if the passed params are not for the last step', () => {
- const result = isLastStep(searchGuide, firstStep);
+ const result = isLastStep(testGuide, testGuideFirstStep);
expect(result).toBe(false);
});
});
describe('isIntegrationInGuideStep', () => {
it('return true if the integration is defined in the guide step config', () => {
- const result = isIntegrationInGuideStep(securityAddDataInProgressState, 'endpoint');
+ const result = isIntegrationInGuideStep(testGuideStep1InProgressState, testIntegration);
expect(result).toBe(true);
});
it('returns false if a different integration is defined in the guide step', () => {
- const result = isIntegrationInGuideStep(securityAddDataInProgressState, 'kubernetes');
+ const result = isIntegrationInGuideStep(testGuideStep1InProgressState, wrongIntegration);
expect(result).toBe(false);
});
it('returns false if no integration is defined in the guide step', () => {
- const result = isIntegrationInGuideStep(securityRulesActiveState, 'endpoint');
+ const result = isIntegrationInGuideStep(testGuideStep2InProgressState, testIntegration);
expect(result).toBe(false);
});
it('returns false if no guide is active', () => {
- const result = isIntegrationInGuideStep(noGuideActiveState, 'endpoint');
+ const result = isIntegrationInGuideStep(testGuideNotActiveState, testIntegration);
expect(result).toBe(false);
});
it('returns false if no integration passed', () => {
- const result = isIntegrationInGuideStep(securityAddDataInProgressState);
+ const result = isIntegrationInGuideStep(testGuideStep1InProgressState);
expect(result).toBe(false);
});
});
diff --git a/src/plugins/kibana_react/public/index.ts b/src/plugins/kibana_react/public/index.ts
index 3311f42bff55d..0d01bd9e0dcd2 100644
--- a/src/plugins/kibana_react/public/index.ts
+++ b/src/plugins/kibana_react/public/index.ts
@@ -72,6 +72,7 @@ export { ValidatedDualRange } from './validated_range';
export type { ToastInput, KibanaReactNotifications } from './notifications';
export { createNotifications } from './notifications';
+/** @deprecated use `Markdown` from `@kbn/shared-ux-markdown` */
export { Markdown, MarkdownSimple } from './markdown';
export { reactToUiComponent, uiToReactComponent } from './adapters';
diff --git a/src/plugins/kibana_react/public/markdown/index.tsx b/src/plugins/kibana_react/public/markdown/index.tsx
index 99da8a3c8898c..d0c72d8db8d76 100644
--- a/src/plugins/kibana_react/public/markdown/index.tsx
+++ b/src/plugins/kibana_react/public/markdown/index.tsx
@@ -17,6 +17,7 @@ const Fallback = () => (
);
+/** @deprecated use `Markdown` from `@kbn/shared-ux-markdown` */
const LazyMarkdownSimple = React.lazy(() => import('./markdown_simple'));
export const MarkdownSimple = (props: MarkdownSimpleProps) => (
}>
@@ -24,6 +25,7 @@ export const MarkdownSimple = (props: MarkdownSimpleProps) => (
);
+/** @deprecated use `Markdown` from `@kbn/shared-ux-markdown` */
const LazyMarkdown = React.lazy(() => import('./markdown'));
export const Markdown = (props: MarkdownProps) => (
}>
diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts
index 41df488839358..22b2a5de751f5 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts
@@ -546,6 +546,10 @@ export const stackManagementSchema: MakeSchemaFrom = {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
+ 'observability:apmEnableCriticalPath': {
+ type: 'boolean',
+ _meta: { description: 'Non-default value of setting.' },
+ },
'observability:enableInfrastructureHostsView': {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts
index 2bd59dc69084f..6957323103545 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts
@@ -147,6 +147,7 @@ export interface UsageStats {
'observability:apmServiceGroupMaxNumberOfServices': number;
'observability:apmServiceInventoryOptimizedSorting': boolean;
'observability:apmTraceExplorerTab': boolean;
+ 'observability:apmEnableCriticalPath': boolean;
'securitySolution:enableGroupedNav': boolean;
'securitySolution:showRelatedIntegrations': boolean;
'visualization:visualize:legacyGaugeChartsLibrary': boolean;
diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json
index 1a97586dffa62..14db4bca74d4a 100644
--- a/src/plugins/telemetry/schema/oss_plugins.json
+++ b/src/plugins/telemetry/schema/oss_plugins.json
@@ -8864,6 +8864,12 @@
"description": "Non-default value of setting."
}
},
+ "observability:apmEnableCriticalPath": {
+ "type": "boolean",
+ "_meta": {
+ "description": "Non-default value of setting."
+ }
+ },
"observability:enableInfrastructureHostsView": {
"type": "boolean",
"_meta": {
diff --git a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx
index c70f1df820252..07d35b78b58a2 100755
--- a/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx
+++ b/src/plugins/unified_field_list/public/components/field_stats/field_stats.tsx
@@ -75,7 +75,7 @@ export interface FieldStatsProps {
'data-test-subj'?: string;
overrideMissingContent?: (params: {
element: JSX.Element;
- noDataFound?: boolean;
+ reason: 'no-data' | 'unsupported';
}) => JSX.Element | null;
overrideFooter?: (params: {
element: JSX.Element;
@@ -304,7 +304,7 @@ const FieldStatsComponent: React.FC = ({
return overrideMissingContent
? overrideMissingContent({
- noDataFound: false,
+ reason: 'unsupported',
element: messageNoAnalysis,
})
: messageNoAnalysis;
@@ -338,7 +338,7 @@ const FieldStatsComponent: React.FC = ({
return overrideMissingContent
? overrideMissingContent({
- noDataFound: true,
+ reason: 'no-data',
element: messageNoData,
})
: messageNoData;
@@ -358,12 +358,14 @@ const FieldStatsComponent: React.FC = ({
defaultMessage: 'Top values',
}),
id: 'topValues',
+ 'data-test-subj': `${dataTestSubject}-buttonGroup-topValuesButton`,
},
{
label: i18n.translate('unifiedFieldList.fieldStats.fieldDistributionLabel', {
defaultMessage: 'Distribution',
}),
id: 'histogram',
+ 'data-test-subj': `${dataTestSubject}-buttonGroup-distributionButton`,
},
]}
onChange={(optionId: string) => {
diff --git a/src/plugins/vis_types/heatmap/kibana.json b/src/plugins/vis_types/heatmap/kibana.json
index c8df98e2b343a..b7f4a3bacbb90 100644
--- a/src/plugins/vis_types/heatmap/kibana.json
+++ b/src/plugins/vis_types/heatmap/kibana.json
@@ -1,14 +1,27 @@
{
- "id": "visTypeHeatmap",
- "version": "kibana",
- "ui": true,
- "server": true,
- "requiredPlugins": ["charts", "data", "expressions", "visualizations", "usageCollection", "fieldFormats"],
- "requiredBundles": ["visDefaultEditor"],
- "extraPublicDirs": ["common/index"],
- "owner": {
- "name": "Vis Editors",
- "githubTeam": "kibana-vis-editors"
- },
- "description": "Contains the heatmap implementation using the elastic-charts library. The goal is to eventually deprecate the old implementation and keep only this. Until then, the library used is defined by the Legacy heatmap charts library advanced setting."
- }
+ "id": "visTypeHeatmap",
+ "version": "kibana",
+ "ui": true,
+ "server": true,
+ "requiredPlugins": [
+ "charts",
+ "data",
+ "expressions",
+ "visualizations",
+ "usageCollection",
+ "fieldFormats",
+ "dataViews"
+ ],
+ "requiredBundles": [
+ "visDefaultEditor",
+ "kibanaUtils"
+ ],
+ "extraPublicDirs": [
+ "common/index"
+ ],
+ "owner": {
+ "name": "Vis Editors",
+ "githubTeam": "kibana-vis-editors"
+ },
+ "description": "Contains the heatmap implementation using the elastic-charts library. The goal is to eventually deprecate the old implementation and keep only this. Until then, the library used is defined by the Legacy heatmap charts library advanced setting."
+}
\ No newline at end of file
diff --git a/src/plugins/vis_types/heatmap/public/convert_to_lens/configurations/index.test.ts b/src/plugins/vis_types/heatmap/public/convert_to_lens/configurations/index.test.ts
new file mode 100644
index 0000000000000..3f60b6fde0a94
--- /dev/null
+++ b/src/plugins/vis_types/heatmap/public/convert_to_lens/configurations/index.test.ts
@@ -0,0 +1,114 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { AvgColumn, DateHistogramColumn } from '@kbn/visualizations-plugin/common/convert_to_lens';
+import { Vis } from '@kbn/visualizations-plugin/public';
+import { getConfiguration } from '.';
+import { sampleHeatmapVis } from '../../sample_vis.test.mocks';
+import { HeatmapVisParams } from '../../types';
+
+describe('getConfiguration', () => {
+ const layerId = 'layer-id';
+ let vis: Vis;
+
+ const metric: AvgColumn = {
+ sourceField: 'price',
+ columnId: 'column-1',
+ operationType: 'average',
+ isBucketed: false,
+ isSplit: false,
+ dataType: 'string',
+ params: {},
+ };
+ const xColumn: DateHistogramColumn = {
+ sourceField: 'price',
+ columnId: 'column-2',
+ operationType: 'date_histogram',
+ isBucketed: true,
+ isSplit: false,
+ dataType: 'string',
+ params: {
+ interval: '1h',
+ },
+ };
+
+ const yColumn: DateHistogramColumn = {
+ sourceField: 'price',
+ columnId: 'column-3',
+ operationType: 'date_histogram',
+ isBucketed: true,
+ isSplit: true,
+ dataType: 'string',
+ params: {
+ interval: '1h',
+ },
+ };
+
+ beforeEach(() => {
+ vis = sampleHeatmapVis as unknown as Vis;
+ });
+
+ test('should return valid configuration', async () => {
+ const result = await getConfiguration(layerId, vis, {
+ metrics: [metric.columnId],
+ buckets: [xColumn.columnId, yColumn.columnId],
+ });
+ expect(result).toEqual({
+ gridConfig: {
+ isCellLabelVisible: true,
+ isXAxisLabelVisible: true,
+ isXAxisTitleVisible: true,
+ isYAxisLabelVisible: true,
+ isYAxisTitleVisible: true,
+ type: 'heatmap_grid',
+ },
+ layerId,
+ layerType: 'data',
+ legend: { isVisible: undefined, position: 'right', type: 'heatmap_legend' },
+ palette: {
+ accessor: 'column-1',
+ name: 'custom',
+ params: {
+ colorStops: [
+ { color: '#F7FBFF', stop: 0 },
+ { color: '#DEEBF7', stop: 12.5 },
+ { color: '#C3DBEE', stop: 25 },
+ { color: '#9CC8E2', stop: 37.5 },
+ { color: '#6DAED5', stop: 50 },
+ { color: '#4391C6', stop: 62.5 },
+ { color: '#2271B3', stop: 75 },
+ { color: '#0D5097', stop: 87.5 },
+ ],
+ continuity: 'none',
+ maxSteps: 5,
+ name: 'custom',
+ progression: 'fixed',
+ rangeMax: 100,
+ rangeMin: 0,
+ rangeType: 'number',
+ reverse: false,
+ stops: [
+ { color: '#F7FBFF', stop: 12.5 },
+ { color: '#DEEBF7', stop: 25 },
+ { color: '#C3DBEE', stop: 37.5 },
+ { color: '#9CC8E2', stop: 50 },
+ { color: '#6DAED5', stop: 62.5 },
+ { color: '#4391C6', stop: 75 },
+ { color: '#2271B3', stop: 87.5 },
+ { color: '#0D5097', stop: 100 },
+ ],
+ },
+ type: 'palette',
+ },
+ shape: 'heatmap',
+ valueAccessor: metric.columnId,
+ xAccessor: xColumn.columnId,
+ yAccessor: yColumn.columnId,
+ });
+ });
+});
diff --git a/src/plugins/vis_types/heatmap/public/convert_to_lens/configurations/index.ts b/src/plugins/vis_types/heatmap/public/convert_to_lens/configurations/index.ts
new file mode 100644
index 0000000000000..2e7a3f161514a
--- /dev/null
+++ b/src/plugins/vis_types/heatmap/public/convert_to_lens/configurations/index.ts
@@ -0,0 +1,54 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { HeatmapConfiguration } from '@kbn/visualizations-plugin/common';
+import { Vis } from '@kbn/visualizations-plugin/public';
+import { HeatmapVisParams } from '../../types';
+import { getPaletteForHeatmap } from './palette';
+
+export const getConfiguration = async (
+ layerId: string,
+ vis: Vis,
+ {
+ metrics,
+ buckets,
+ }: {
+ metrics: string[];
+ buckets: string[];
+ }
+): Promise => {
+ const [valueAccessor] = metrics;
+ const [xAccessor, yAccessor] = buckets;
+
+ const { params, uiState } = vis;
+ const state = uiState.get('vis', {}) ?? {};
+
+ const palette = await getPaletteForHeatmap(params);
+ return {
+ layerId,
+ layerType: 'data',
+ shape: 'heatmap',
+ legend: {
+ type: 'heatmap_legend',
+ isVisible: state.legendOpen,
+ position: params.legendPosition,
+ },
+ gridConfig: {
+ type: 'heatmap_grid',
+ isCellLabelVisible: params.valueAxes?.[0].labels.show ?? false,
+ isXAxisLabelVisible: true,
+ isYAxisLabelVisible: true,
+ isYAxisTitleVisible: true,
+ isXAxisTitleVisible: true,
+ },
+ valueAccessor,
+ xAccessor,
+ yAccessor,
+ palette: palette ? { ...palette, accessor: valueAccessor } : undefined,
+ };
+};
diff --git a/src/plugins/vis_types/heatmap/public/convert_to_lens/configurations/palette.ts b/src/plugins/vis_types/heatmap/public/convert_to_lens/configurations/palette.ts
new file mode 100644
index 0000000000000..32187e184d4ef
--- /dev/null
+++ b/src/plugins/vis_types/heatmap/public/convert_to_lens/configurations/palette.ts
@@ -0,0 +1,52 @@
+/*
+ * Copyright 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 { Range } from '@kbn/expressions-plugin/common';
+import { convertToLensModule } from '@kbn/visualizations-plugin/public';
+import { HeatmapVisParams } from '../../types';
+import { getStopsWithColorsFromColorsNumber } from '../../utils/palette';
+
+type HeatmapVisParamsWithRanges = Omit & {
+ colorsRange: Exclude;
+};
+
+const isHeatmapVisParamsWithRanges = (
+ params: HeatmapVisParams | HeatmapVisParamsWithRanges
+): params is HeatmapVisParamsWithRanges => {
+ return Boolean(params.setColorRange && params.colorsRange && params.colorsRange.length);
+};
+
+export const getPaletteForHeatmap = async (params: HeatmapVisParams) => {
+ const { getPalette, getPaletteFromStopsWithColors, getPercentageModeConfig } =
+ await convertToLensModule;
+
+ if (isHeatmapVisParamsWithRanges(params)) {
+ const percentageModeConfig = getPercentageModeConfig(params, false);
+ return getPalette(params, percentageModeConfig, params.percentageMode);
+ }
+
+ const { color, stop = [] } = getStopsWithColorsFromColorsNumber(
+ params.colorsNumber,
+ params.colorSchema,
+ params.invertColors,
+ true
+ );
+ const colorsRange: Range[] = [{ from: stop[0], to: stop[stop.length - 1], type: 'range' }];
+ const { colorSchema, invertColors, percentageMode } = params;
+ const percentageModeConfig = getPercentageModeConfig(
+ {
+ colorsRange,
+ colorSchema,
+ invertColors,
+ percentageMode,
+ },
+ false
+ );
+
+ return getPaletteFromStopsWithColors({ color, stop: stop ?? [] }, percentageModeConfig, true);
+};
diff --git a/src/plugins/vis_types/heatmap/public/convert_to_lens/index.test.ts b/src/plugins/vis_types/heatmap/public/convert_to_lens/index.test.ts
new file mode 100644
index 0000000000000..ef86b3829c248
--- /dev/null
+++ b/src/plugins/vis_types/heatmap/public/convert_to_lens/index.test.ts
@@ -0,0 +1,166 @@
+/*
+ * Copyright 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 { ColorSchemas } from '@kbn/charts-plugin/common';
+import { Vis } from '@kbn/visualizations-plugin/public';
+import { convertToLens } from '.';
+import { HeatmapVisParams } from '../types';
+
+const mockGetColumnsFromVis = jest.fn();
+const mockGetConfiguration = jest.fn().mockReturnValue({});
+const mockGetDataViewByIndexPatternId = jest.fn();
+const mockConvertToFiltersColumn = jest.fn();
+
+jest.mock('../services', () => ({
+ getDataViewsStart: jest.fn(() => ({ get: () => ({}), getDefault: () => ({}) })),
+}));
+
+jest.mock('@kbn/visualizations-plugin/public', () => ({
+ convertToLensModule: Promise.resolve({
+ getColumnsFromVis: jest.fn(() => mockGetColumnsFromVis()),
+ convertToFiltersColumn: jest.fn(() => mockConvertToFiltersColumn()),
+ }),
+ getDataViewByIndexPatternId: jest.fn(() => mockGetDataViewByIndexPatternId()),
+}));
+
+jest.mock('./configurations', () => ({
+ getConfiguration: jest.fn(() => mockGetConfiguration()),
+}));
+
+const params: HeatmapVisParams = {
+ addTooltip: false,
+ addLegend: false,
+ enableHover: true,
+ legendPosition: 'bottom',
+ lastRangeIsRightOpen: false,
+ percentageMode: false,
+ valueAxes: [],
+ colorSchema: ColorSchemas.Blues,
+ invertColors: false,
+ colorsNumber: 4,
+ setColorRange: true,
+};
+
+const vis = {
+ isHierarchical: () => false,
+ type: {},
+ params,
+ data: {},
+} as unknown as Vis;
+
+const timefilter = {
+ getAbsoluteTime: () => {},
+} as any;
+
+describe('convertToLens', () => {
+ beforeEach(() => {
+ mockGetDataViewByIndexPatternId.mockReturnValue({ id: 'index-pattern' });
+ mockConvertToFiltersColumn.mockReturnValue({ columnId: 'column-id-1' });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('should return null if timefilter is undefined', async () => {
+ const result = await convertToLens(vis);
+ expect(result).toBeNull();
+ });
+
+ test('should return null if mockGetDataViewByIndexPatternId returns null', async () => {
+ mockGetDataViewByIndexPatternId.mockReturnValue(null);
+ const result = await convertToLens(vis, timefilter);
+ expect(mockGetDataViewByIndexPatternId).toBeCalledTimes(1);
+ expect(mockGetColumnsFromVis).toBeCalledTimes(0);
+ expect(result).toBeNull();
+ });
+
+ test('should return null if getColumnsFromVis returns null', async () => {
+ mockGetColumnsFromVis.mockReturnValue(null);
+ const result = await convertToLens(vis, timefilter);
+ expect(mockGetColumnsFromVis).toBeCalledTimes(1);
+ expect(result).toBeNull();
+ });
+
+ test('should return null if metrics count is more than 1', async () => {
+ mockGetColumnsFromVis.mockReturnValue([
+ {
+ metrics: ['1', '2'],
+ buckets: { all: [] },
+ columns: [{ columnId: '2' }, { columnId: '1' }],
+ },
+ ]);
+ const result = await convertToLens(vis, timefilter);
+ expect(mockGetColumnsFromVis).toBeCalledTimes(1);
+ expect(result).toBeNull();
+ });
+
+ test('should return empty filters for x-axis if no buckets are specified', async () => {
+ mockGetColumnsFromVis.mockReturnValue([
+ {
+ metrics: ['1'],
+ buckets: { all: [] },
+ columns: [{ columnId: '1', dataType: 'number' }],
+ columnsWithoutReferenced: [
+ { columnId: '1', meta: { aggId: 'agg-1' } },
+ { columnId: '2', meta: { aggId: 'agg-2' } },
+ { columnId: 'column-id-1' },
+ ],
+ },
+ ]);
+ const result = await convertToLens(vis, timefilter);
+ expect(mockGetColumnsFromVis).toBeCalledTimes(1);
+ expect(result).toEqual(
+ expect.objectContaining({
+ configuration: {},
+ indexPatternIds: ['index-pattern'],
+ layers: [
+ expect.objectContaining({
+ columnOrder: [],
+ columns: [{ columnId: '1', dataType: 'number' }, { columnId: 'column-id-1' }],
+ indexPatternId: 'index-pattern',
+ }),
+ ],
+ type: 'lnsHeatmap',
+ })
+ );
+ });
+
+ test('should return correct state for valid vis', async () => {
+ const config = {
+ layerType: 'data',
+ };
+
+ mockGetColumnsFromVis.mockReturnValue([
+ {
+ metrics: ['1'],
+ buckets: { all: ['2'] },
+ columns: [{ columnId: '1', dataType: 'number' }],
+ columnsWithoutReferenced: [
+ { columnId: '1', meta: { aggId: 'agg-1' } },
+ { columnId: '2', meta: { aggId: 'agg-2' } },
+ ],
+ },
+ ]);
+ mockGetConfiguration.mockReturnValue(config);
+
+ const result = await convertToLens(vis, timefilter);
+ expect(mockGetColumnsFromVis).toBeCalledTimes(1);
+ expect(mockGetConfiguration).toBeCalledTimes(1);
+ expect(result?.type).toEqual('lnsHeatmap');
+ expect(result?.layers.length).toEqual(1);
+ expect(result?.layers[0]).toEqual(
+ expect.objectContaining({
+ columnOrder: [],
+ columns: [{ columnId: '1', dataType: 'number' }, { columnId: 'column-id-1' }],
+ indexPatternId: 'index-pattern',
+ })
+ );
+ expect(result?.configuration).toEqual(config);
+ });
+});
diff --git a/src/plugins/vis_types/heatmap/public/convert_to_lens/index.ts b/src/plugins/vis_types/heatmap/public/convert_to_lens/index.ts
new file mode 100644
index 0000000000000..546d497e80560
--- /dev/null
+++ b/src/plugins/vis_types/heatmap/public/convert_to_lens/index.ts
@@ -0,0 +1,97 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { Column, ColumnWithMeta } from '@kbn/visualizations-plugin/common';
+import {
+ convertToLensModule,
+ getDataViewByIndexPatternId,
+} from '@kbn/visualizations-plugin/public';
+import uuid from 'uuid';
+import { getDataViewsStart } from '../services';
+import { getConfiguration } from './configurations';
+import { ConvertHeatmapToLensVisualization } from './types';
+
+export const isColumnWithMeta = (column: Column): column is ColumnWithMeta => {
+ if ((column as ColumnWithMeta).meta) {
+ return true;
+ }
+ return false;
+};
+
+export const excludeMetaFromColumn = (column: Column) => {
+ if (isColumnWithMeta(column)) {
+ const { meta, ...rest } = column;
+ return rest;
+ }
+ return column;
+};
+
+export const convertToLens: ConvertHeatmapToLensVisualization = async (vis, timefilter) => {
+ if (!timefilter) {
+ return null;
+ }
+
+ const dataViews = getDataViewsStart();
+ const dataView = await getDataViewByIndexPatternId(vis.data.indexPattern?.id, dataViews);
+
+ if (!dataView) {
+ return null;
+ }
+
+ const { getColumnsFromVis, convertToFiltersColumn } = await convertToLensModule;
+ const layers = getColumnsFromVis(vis, timefilter, dataView, {
+ buckets: ['segment'],
+ splits: ['group'],
+ unsupported: ['split_row', 'split_column'],
+ });
+
+ if (layers === null) {
+ return null;
+ }
+
+ const [layerConfig] = layers;
+
+ const xColumn = layerConfig.columns.find(({ isBucketed, isSplit }) => isBucketed && !isSplit);
+ const xAxisColumn =
+ xColumn ??
+ convertToFiltersColumn(uuid(), { filters: [{ input: { language: 'lucene', query: '*' } }] })!;
+
+ if (xColumn?.columnId !== xAxisColumn?.columnId) {
+ layerConfig.buckets.all.push(xAxisColumn.columnId);
+ layerConfig.columns.push(xAxisColumn);
+ }
+ const yColumn = layerConfig.columns.find(({ isBucketed, isSplit }) => isBucketed && isSplit);
+
+ if (!layerConfig.buckets.all.length || layerConfig.metrics.length > 1) {
+ return null;
+ }
+
+ const layerId = uuid();
+
+ const indexPatternId = dataView.id!;
+ const configuration = await getConfiguration(layerId, vis, {
+ metrics: layerConfig.metrics,
+ buckets: [xAxisColumn.columnId, yColumn?.columnId].filter((c): c is string =>
+ Boolean(c)
+ ),
+ });
+
+ return {
+ type: 'lnsHeatmap',
+ layers: [
+ {
+ indexPatternId,
+ layerId,
+ columns: layerConfig.columns.map(excludeMetaFromColumn),
+ columnOrder: [],
+ },
+ ],
+ configuration,
+ indexPatternIds: [indexPatternId],
+ };
+};
diff --git a/src/plugins/vis_types/heatmap/public/convert_to_lens/types.ts b/src/plugins/vis_types/heatmap/public/convert_to_lens/types.ts
new file mode 100644
index 0000000000000..732b977dd7b59
--- /dev/null
+++ b/src/plugins/vis_types/heatmap/public/convert_to_lens/types.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { TimefilterContract } from '@kbn/data-plugin/public';
+import { NavigateToLensContext, HeatmapConfiguration } from '@kbn/visualizations-plugin/common';
+import { Vis } from '@kbn/visualizations-plugin/public';
+import { HeatmapVisParams } from '../types';
+
+export type ConvertHeatmapToLensVisualization = (
+ vis: Vis,
+ timefilter?: TimefilterContract
+) => Promise | null>;
diff --git a/src/plugins/vis_types/heatmap/public/plugin.ts b/src/plugins/vis_types/heatmap/public/plugin.ts
index 44357cceaa86b..ee7349145e7c6 100644
--- a/src/plugins/vis_types/heatmap/public/plugin.ts
+++ b/src/plugins/vis_types/heatmap/public/plugin.ts
@@ -6,14 +6,16 @@
* Side Public License, v 1.
*/
-import { CoreSetup } from '@kbn/core/public';
+import { CoreSetup, CoreStart } from '@kbn/core/public';
import type { VisualizationsSetup } from '@kbn/visualizations-plugin/public';
import type { ChartsPluginSetup } from '@kbn/charts-plugin/public';
+import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { LEGACY_HEATMAP_CHARTS_LIBRARY } from '../common';
import { heatmapVisType } from './vis_type';
+import { setDataViewsStart } from './services';
/** @internal */
export interface VisTypeHeatmapSetupDependencies {
@@ -28,6 +30,11 @@ export interface VisTypeHeatmapPluginStartDependencies {
fieldFormats: FieldFormatsStart;
}
+/** @internal */
+export interface VisTypeHeatmapStartDependencies {
+ dataViews: DataViewsPublicPluginStart;
+}
+
export class VisTypeHeatmapPlugin {
setup(
core: CoreSetup,
@@ -44,5 +51,7 @@ export class VisTypeHeatmapPlugin {
return {};
}
- start() {}
+ start(core: CoreStart, { dataViews }: VisTypeHeatmapStartDependencies) {
+ setDataViewsStart(dataViews);
+ }
}
diff --git a/src/plugins/vis_types/heatmap/public/sample_vis.test.mocks.ts b/src/plugins/vis_types/heatmap/public/sample_vis.test.mocks.ts
index 6a33feb853221..89ede55b951ef 100644
--- a/src/plugins/vis_types/heatmap/public/sample_vis.test.mocks.ts
+++ b/src/plugins/vis_types/heatmap/public/sample_vis.test.mocks.ts
@@ -5,7 +5,9 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-export const sampleAreaVis = {
+
+const mockUiStateGet = jest.fn().mockReturnValue(() => {});
+export const sampleHeatmapVis = {
type: {
name: 'heatmap',
title: 'Heatmap',
@@ -1788,5 +1790,10 @@ export const sampleAreaVis = {
},
},
isHierarchical: () => false,
- uiState: {},
+ uiState: {
+ vis: {
+ legendOpen: false,
+ },
+ get: mockUiStateGet,
+ },
};
diff --git a/src/plugins/vis_types/heatmap/public/services.ts b/src/plugins/vis_types/heatmap/public/services.ts
new file mode 100644
index 0000000000000..736ad70d49419
--- /dev/null
+++ b/src/plugins/vis_types/heatmap/public/services.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 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 { createGetterSetter } from '@kbn/kibana-utils-plugin/public';
+import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
+
+export const [getDataViewsStart, setDataViewsStart] =
+ createGetterSetter('dataViews');
diff --git a/src/plugins/vis_types/heatmap/public/to_ast.test.ts b/src/plugins/vis_types/heatmap/public/to_ast.test.ts
index d1e312755cf49..07585d9f2332f 100644
--- a/src/plugins/vis_types/heatmap/public/to_ast.test.ts
+++ b/src/plugins/vis_types/heatmap/public/to_ast.test.ts
@@ -7,7 +7,7 @@
*/
import { Vis } from '@kbn/visualizations-plugin/public';
-import { sampleAreaVis } from './sample_vis.test.mocks';
+import { sampleHeatmapVis } from './sample_vis.test.mocks';
import { buildExpression } from '@kbn/expressions-plugin/public';
import { toExpressionAst } from './to_ast';
@@ -33,7 +33,7 @@ describe('heatmap vis toExpressionAst function', () => {
} as any;
beforeEach(() => {
- vis = sampleAreaVis as any;
+ vis = sampleHeatmapVis as any;
});
it('should match basic snapshot', () => {
diff --git a/src/plugins/vis_types/heatmap/public/utils/palette.ts b/src/plugins/vis_types/heatmap/public/utils/palette.ts
index aa978a2954e90..29109a55fd1e7 100644
--- a/src/plugins/vis_types/heatmap/public/utils/palette.ts
+++ b/src/plugins/vis_types/heatmap/public/utils/palette.ts
@@ -27,13 +27,20 @@ const getColor = (
export const getStopsWithColorsFromColorsNumber = (
colorsNumber: number | '',
colorSchema: ColorSchemas,
- invertColors: boolean = false
+ invertColors: boolean = false,
+ includeZeroElement: boolean = false
) => {
const colors = [];
const stops = [];
if (!colorsNumber) {
return { color: [] };
}
+
+ if (includeZeroElement) {
+ colors.push(TRANSPARENT);
+ stops.push(0);
+ }
+
const step = 100 / colorsNumber;
for (let i = 0; i < colorsNumber; i++) {
colors.push(getColor(i, colorsNumber, colorSchema, invertColors));
diff --git a/src/plugins/vis_types/heatmap/public/vis_type/heatmap.tsx b/src/plugins/vis_types/heatmap/public/vis_type/heatmap.tsx
index e5a92ca03f5cc..336da6e2d8041 100644
--- a/src/plugins/vis_types/heatmap/public/vis_type/heatmap.tsx
+++ b/src/plugins/vis_types/heatmap/public/vis_type/heatmap.tsx
@@ -16,6 +16,7 @@ import { HeatmapTypeProps, HeatmapVisParams, AxisType, ScaleType } from '../type
import { toExpressionAst } from '../to_ast';
import { getHeatmapOptions } from '../editor/components';
import { SplitTooltip } from './split_tooltip';
+import { convertToLens } from '../convert_to_lens';
export const getHeatmapVisTypeDefinition = ({
showElasticChartsOptions = false,
@@ -154,4 +155,10 @@ export const getHeatmapVisTypeDefinition = ({
],
},
requiresSearch: true,
+ navigateToLens: async (vis, timefilter) => (vis ? convertToLens(vis, timefilter) : null),
+ getExpressionVariables: async (vis, timeFilter) => {
+ return {
+ canNavigateToLens: Boolean(vis?.params ? await convertToLens(vis, timeFilter) : null),
+ };
+ },
});
diff --git a/src/plugins/vis_types/pie/kibana.json b/src/plugins/vis_types/pie/kibana.json
index 4c5ee6b50579e..d9dca861e33be 100644
--- a/src/plugins/vis_types/pie/kibana.json
+++ b/src/plugins/vis_types/pie/kibana.json
@@ -1,14 +1,27 @@
{
- "id": "visTypePie",
- "version": "kibana",
- "ui": true,
- "server": true,
- "requiredPlugins": ["charts", "data", "expressions", "visualizations", "usageCollection", "expressionPartitionVis", "dataViews"],
- "requiredBundles": ["visDefaultEditor", "kibanaUtils"],
- "extraPublicDirs": ["common/index"],
- "owner": {
- "name": "Vis Editors",
- "githubTeam": "kibana-vis-editors"
- },
- "description": "Contains the pie chart implementation using the elastic-charts library. The goal is to eventually deprecate the old implementation and keep only this. Until then, the library used is defined by the Legacy charts library advanced setting."
- }
+ "id": "visTypePie",
+ "version": "kibana",
+ "ui": true,
+ "server": true,
+ "requiredPlugins": [
+ "charts",
+ "data",
+ "expressions",
+ "visualizations",
+ "usageCollection",
+ "expressionPartitionVis",
+ "dataViews"
+ ],
+ "requiredBundles": [
+ "visDefaultEditor",
+ "kibanaUtils"
+ ],
+ "extraPublicDirs": [
+ "common/index"
+ ],
+ "owner": {
+ "name": "Vis Editors",
+ "githubTeam": "kibana-vis-editors"
+ },
+ "description": "Contains the pie chart implementation using the elastic-charts library. The goal is to eventually deprecate the old implementation and keep only this. Until then, the library used is defined by the Legacy charts library advanced setting."
+}
\ No newline at end of file
diff --git a/src/plugins/vis_types/table/public/convert_to_lens/index.ts b/src/plugins/vis_types/table/public/convert_to_lens/index.ts
index e69faccbfd7ec..ed23d612cb68c 100644
--- a/src/plugins/vis_types/table/public/convert_to_lens/index.ts
+++ b/src/plugins/vis_types/table/public/convert_to_lens/index.ts
@@ -73,6 +73,7 @@ export const convertToLens: ConvertTableToLensVisualization = async (vis, timefi
return null;
}
const percentageColumn = getPercentageColumnFormulaColumn({
+ visType: vis.type.name,
agg: metricAgg as SchemaConfig,
dataView,
aggs: visSchemas.metric as Array>,
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/buckets/index.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/buckets/index.test.ts
index f0a8e4d32f7c3..02a6140625c07 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/buckets/index.test.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/buckets/index.test.ts
@@ -8,7 +8,7 @@
import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub';
import { BUCKET_TYPES, METRIC_TYPES } from '@kbn/data-plugin/common';
-import { convertBucketToColumns } from '.';
+import { BucketAggs, convertBucketToColumns } from '.';
import { DateHistogramColumn, FiltersColumn, RangeColumn, TermsColumn } from '../../types';
import { AggBasedColumn, SchemaConfig } from '../../..';
@@ -27,7 +27,7 @@ jest.mock('../convert', () => ({
describe('convertBucketToColumns', () => {
const field = stubLogstashDataView.fields[0].name;
const dateField = stubLogstashDataView.fields.find((f) => f.type === 'date')!.name;
- const bucketAggs: SchemaConfig[] = [
+ const bucketAggs: Array> = [
{
accessor: 0,
label: '',
@@ -152,6 +152,7 @@ describe('convertBucketToColumns', () => {
},
},
];
+ const visType = 'heatmap';
afterEach(() => {
jest.clearAllMocks();
@@ -167,7 +168,7 @@ describe('convertBucketToColumns', () => {
>([
[
'null if bucket agg type is not supported',
- [{ dataView: stubLogstashDataView, agg: bucketAggs[6], aggs, metricColumns }],
+ [{ dataView: stubLogstashDataView, agg: bucketAggs[6], aggs, metricColumns, visType }],
() => {},
null,
],
@@ -179,6 +180,7 @@ describe('convertBucketToColumns', () => {
agg: { ...bucketAggs[0], aggParams: undefined },
aggs,
metricColumns,
+ visType,
},
],
() => {},
@@ -186,7 +188,7 @@ describe('convertBucketToColumns', () => {
],
[
'filters column if bucket agg is valid filters agg',
- [{ dataView: stubLogstashDataView, agg: bucketAggs[0], aggs, metricColumns }],
+ [{ dataView: stubLogstashDataView, agg: bucketAggs[0], aggs, metricColumns, visType }],
() => {
mockConvertToFiltersColumn.mockReturnValue({
operationType: 'filters',
@@ -198,7 +200,7 @@ describe('convertBucketToColumns', () => {
],
[
'date histogram column if bucket agg is valid date histogram agg',
- [{ dataView: stubLogstashDataView, agg: bucketAggs[1], aggs, metricColumns }],
+ [{ dataView: stubLogstashDataView, agg: bucketAggs[1], aggs, metricColumns, visType }],
() => {
mockConvertToDateHistogramColumn.mockReturnValue({
operationType: 'date_histogram',
@@ -210,7 +212,7 @@ describe('convertBucketToColumns', () => {
],
[
'date histogram column if bucket agg is valid terms agg with date field',
- [{ dataView: stubLogstashDataView, agg: bucketAggs[3], aggs, metricColumns }],
+ [{ dataView: stubLogstashDataView, agg: bucketAggs[3], aggs, metricColumns, visType }],
() => {
mockConvertToDateHistogramColumn.mockReturnValue({
operationType: 'date_histogram',
@@ -222,7 +224,7 @@ describe('convertBucketToColumns', () => {
],
[
'terms column if bucket agg is valid terms agg with no date field',
- [{ dataView: stubLogstashDataView, agg: bucketAggs[2], aggs, metricColumns }],
+ [{ dataView: stubLogstashDataView, agg: bucketAggs[2], aggs, metricColumns, visType }],
() => {
mockConvertToTermsColumn.mockReturnValue({
operationType: 'terms',
@@ -234,7 +236,7 @@ describe('convertBucketToColumns', () => {
],
[
'range column if bucket agg is valid histogram agg',
- [{ dataView: stubLogstashDataView, agg: bucketAggs[4], aggs, metricColumns }],
+ [{ dataView: stubLogstashDataView, agg: bucketAggs[4], aggs, metricColumns, visType }],
() => {
mockConvertToRangeColumn.mockReturnValue({
operationType: 'range',
@@ -246,7 +248,7 @@ describe('convertBucketToColumns', () => {
],
[
'range column if bucket agg is valid range agg',
- [{ dataView: stubLogstashDataView, agg: bucketAggs[5], aggs, metricColumns }],
+ [{ dataView: stubLogstashDataView, agg: bucketAggs[5], aggs, metricColumns, visType }],
() => {
mockConvertToRangeColumn.mockReturnValue({
operationType: 'range',
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/buckets/index.ts b/src/plugins/visualizations/common/convert_to_lens/lib/buckets/index.ts
index 0f929189f3369..db02b1e09fdce 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/buckets/index.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/buckets/index.ts
@@ -9,9 +9,8 @@
import { BUCKET_TYPES, IAggConfig, METRIC_TYPES } from '@kbn/data-plugin/common';
import type { DataView } from '@kbn/data-views-plugin/common';
import { convertToSchemaConfig } from '../../../vis_schemas';
-import { SchemaConfig } from '../../..';
+import { AggBasedColumn, SchemaConfig } from '../../..';
import {
- AggBasedColumn,
CommonBucketConverterArgs,
convertToDateHistogramColumn,
convertToFiltersColumn,
@@ -26,6 +25,7 @@ export type BucketAggs =
| BUCKET_TYPES.FILTERS
| BUCKET_TYPES.RANGE
| BUCKET_TYPES.HISTOGRAM;
+
const SUPPORTED_BUCKETS: string[] = [
BUCKET_TYPES.TERMS,
BUCKET_TYPES.DATE_HISTOGRAM,
@@ -39,7 +39,7 @@ const isSupportedBucketAgg = (agg: SchemaConfig): agg is SchemaConfig,
+ { agg, dataView, metricColumns, aggs, visType }: CommonBucketConverterArgs,
{
label,
isSplit = false,
@@ -76,7 +76,7 @@ export const getBucketColumns = (
if (field.type !== 'date') {
return convertToTermsColumn(
agg.aggId ?? '',
- { agg, dataView, metricColumns, aggs },
+ { agg, dataView, metricColumns, aggs, visType },
label,
isSplit
);
@@ -102,7 +102,9 @@ export const convertBucketToColumns = (
dataView,
metricColumns,
aggs,
+ visType,
}: {
+ visType: string;
agg: SchemaConfig | IAggConfig;
dataView: DataView;
metricColumns: AggBasedColumn[];
@@ -116,7 +118,7 @@ export const convertBucketToColumns = (
return null;
}
return getBucketColumns(
- { agg: currentAgg, dataView, metricColumns, aggs },
+ { agg: currentAgg, dataView, metricColumns, aggs, visType },
{
label: getLabel(currentAgg),
isSplit,
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/configurations/index.ts b/src/plugins/visualizations/common/convert_to_lens/lib/configurations/index.ts
index c4592f50836c5..b4934d0bb0c85 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/configurations/index.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/configurations/index.ts
@@ -6,5 +6,5 @@
* Side Public License, v 1.
*/
-export { getPalette } from './palette';
+export { getPalette, getPaletteFromStopsWithColors } from './palette';
export { getPercentageModeConfig } from './percentage_mode';
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/configurations/palette.ts b/src/plugins/visualizations/common/convert_to_lens/lib/configurations/palette.ts
index a89177c914996..3f81291fab201 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/configurations/palette.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/configurations/palette.ts
@@ -74,6 +74,21 @@ const convertToPercentColorStops = (
return { ...colorStops, stop };
};
+export const getPaletteFromStopsWithColors = (
+ config: PaletteConfig,
+ percentageModeConfig: PercentageModeConfig,
+ isPercentPaletteSupported: boolean = false
+) => {
+ const percentStopsWithColors = percentageModeConfig.isPercentageMode
+ ? convertToPercentColorStops(config, percentageModeConfig, isPercentPaletteSupported)
+ : config;
+
+ return buildCustomPalette(
+ buildPaletteParams(percentStopsWithColors),
+ isPercentPaletteSupported && percentageModeConfig.isPercentageMode
+ );
+};
+
export const getPalette = (
params: PaletteParams,
percentageModeConfig: PercentageModeConfig,
@@ -86,12 +101,10 @@ export const getPalette = (
}
const stopsWithColors = getStopsWithColorsFromRanges(colorsRange, colorSchema, invertColors);
- const percentStopsWithColors = percentageModeConfig.isPercentageMode
- ? convertToPercentColorStops(stopsWithColors, percentageModeConfig, isPercentPaletteSupported)
- : stopsWithColors;
- return buildCustomPalette(
- buildPaletteParams(percentStopsWithColors),
- isPercentPaletteSupported && percentageModeConfig.isPercentageMode
+ return getPaletteFromStopsWithColors(
+ stopsWithColors,
+ percentageModeConfig,
+ isPercentPaletteSupported
);
};
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/formula.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/formula.ts
index 0ad2a4072e19d..e79be2ba51516 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/convert/formula.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/formula.ts
@@ -21,6 +21,7 @@ export const createFormulaColumn = (formula: string, agg: SchemaConfig): Formula
operationType: 'formula',
...createColumn(agg),
references: [],
+ dataType: 'number',
params: { ...params, ...getFormat() },
timeShift: agg.aggParams?.timeShift,
meta: { aggId: createAggregationId(agg) },
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/last_value.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/last_value.test.ts
index 55ba1e8b5e09d..c46055ca6a9ab 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/convert/last_value.test.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/last_value.test.ts
@@ -22,6 +22,7 @@ jest.mock('../utils', () => ({
}));
describe('convertToLastValueColumn', () => {
+ const visType = 'heatmap';
const dataView = stubLogstashDataView;
const sortField = dataView.fields[0];
@@ -59,7 +60,13 @@ describe('convertToLastValueColumn', () => {
test.each<[string, Parameters, Partial | null]>([
[
'null if top hits size is more than 1',
- [{ agg: { ...topHitAgg, aggParams: { ...topHitAgg.aggParams!, size: 2 } }, dataView }],
+ [
+ {
+ agg: { ...topHitAgg, aggParams: { ...topHitAgg.aggParams!, size: 2 } },
+ dataView,
+ visType,
+ },
+ ],
null,
],
[
@@ -74,6 +81,7 @@ describe('convertToLastValueColumn', () => {
},
},
dataView,
+ visType,
},
],
null,
@@ -88,7 +96,7 @@ describe('convertToLastValueColumn', () => {
test('should skip if top hit field is not specified', () => {
mockGetFieldNameFromField.mockReturnValue(null);
- expect(convertToLastValueColumn({ agg: topHitAgg, dataView })).toBeNull();
+ expect(convertToLastValueColumn({ agg: topHitAgg, dataView, visType })).toBeNull();
expect(mockGetFieldNameFromField).toBeCalledTimes(1);
expect(dataView.getFieldByName).toBeCalledTimes(0);
});
@@ -97,14 +105,14 @@ describe('convertToLastValueColumn', () => {
mockGetFieldByName.mockReturnValue(null);
dataView.getFieldByName = mockGetFieldByName;
- expect(convertToLastValueColumn({ agg: topHitAgg, dataView })).toBeNull();
+ expect(convertToLastValueColumn({ agg: topHitAgg, dataView, visType })).toBeNull();
expect(mockGetFieldNameFromField).toBeCalledTimes(1);
expect(dataView.getFieldByName).toBeCalledTimes(1);
expect(mockGetLabel).toBeCalledTimes(0);
});
test('should return top hit column if top hit field is not present in index pattern', () => {
- expect(convertToLastValueColumn({ agg: topHitAgg, dataView })).toEqual(
+ expect(convertToLastValueColumn({ agg: topHitAgg, dataView, visType })).toEqual(
expect.objectContaining({
dataType: 'number',
label: 'someLabel',
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/last_value.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/last_value.ts
index 3162cf14e71c3..9525f4b41b7eb 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/convert/last_value.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/last_value.ts
@@ -25,7 +25,11 @@ const convertToLastValueParams = (
};
export const convertToLastValueColumn = (
- { agg, dataView }: CommonColumnConverterArgs,
+ {
+ visType,
+ agg,
+ dataView,
+ }: CommonColumnConverterArgs,
reducedTimeRange?: string
): LastValueColumn | null => {
const { aggParams } = agg;
@@ -43,7 +47,7 @@ export const convertToLastValueColumn = (
}
const field = dataView.getFieldByName(fieldName);
- if (!isFieldValid(field, SUPPORTED_METRICS[agg.aggType])) {
+ if (!isFieldValid(visType, field, SUPPORTED_METRICS[agg.aggType])) {
return null;
}
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/metric.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/metric.test.ts
index 3be17abc46ac1..a0419d46df6b5 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/convert/metric.test.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/metric.test.ts
@@ -16,6 +16,7 @@ const mockGetFieldByName = jest.fn();
describe('convertToLastValueColumn', () => {
const dataView = stubLogstashDataView;
+ const visType = 'heatmap';
const agg: SchemaConfig = {
accessor: 0,
@@ -42,6 +43,7 @@ describe('convertToLastValueColumn', () => {
convertMetricAggregationColumnWithoutSpecialParams(SUPPORTED_METRICS[METRIC_TYPES.TOP_HITS], {
agg,
dataView,
+ visType,
})
).toBeNull();
});
@@ -54,6 +56,7 @@ describe('convertToLastValueColumn', () => {
convertMetricAggregationColumnWithoutSpecialParams(SUPPORTED_METRICS[METRIC_TYPES.AVG], {
agg,
dataView,
+ visType,
})
).toBeNull();
expect(dataView.getFieldByName).toBeCalledTimes(1);
@@ -67,6 +70,7 @@ describe('convertToLastValueColumn', () => {
convertMetricAggregationColumnWithoutSpecialParams(SUPPORTED_METRICS[METRIC_TYPES.COUNT], {
agg,
dataView,
+ visType,
})
).toEqual(expect.objectContaining({ operationType: 'count' }));
expect(dataView.getFieldByName).toBeCalledTimes(1);
@@ -80,6 +84,7 @@ describe('convertToLastValueColumn', () => {
convertMetricAggregationColumnWithoutSpecialParams(SUPPORTED_METRICS[METRIC_TYPES.AVG], {
agg,
dataView,
+ visType,
})
).toEqual(
expect.objectContaining({
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/metric.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/metric.ts
index eb21b9f0fe91d..dd6c8b02687b0 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/convert/metric.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/metric.ts
@@ -78,7 +78,7 @@ export const isMetricWithField = (
export const convertMetricAggregationColumnWithoutSpecialParams = (
aggregation: SupportedMetric,
- { agg, dataView }: CommonColumnConverterArgs,
+ { visType, agg, dataView }: CommonColumnConverterArgs,
reducedTimeRange?: string
): MetricAggregationColumnWithoutSpecialParams | null => {
if (!isSupportedAggregationWithoutParams(aggregation.name)) {
@@ -94,7 +94,7 @@ export const convertMetricAggregationColumnWithoutSpecialParams = (
}
const field = dataView.getFieldByName(sourceField);
- if (!isFieldValid(field, aggregation)) {
+ if (!isFieldValid(visType, field, aggregation)) {
return null;
}
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/parent_pipeline.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/parent_pipeline.test.ts
index c28324533c837..65dd1cf40aaef 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/convert/parent_pipeline.test.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/parent_pipeline.test.ts
@@ -40,6 +40,7 @@ jest.mock('../metrics', () => ({
}));
describe('convertToOtherParentPipelineAggColumns', () => {
+ const visType = 'heatmap';
const field = stubLogstashDataView.fields[0].name;
const aggs: Array> = [
{
@@ -81,6 +82,7 @@ describe('convertToOtherParentPipelineAggColumns', () => {
dataView: stubLogstashDataView,
aggs,
agg: aggs[1] as SchemaConfig,
+ visType,
},
],
() => {
@@ -95,6 +97,7 @@ describe('convertToOtherParentPipelineAggColumns', () => {
dataView: stubLogstashDataView,
aggs,
agg: aggs[1] as SchemaConfig,
+ visType,
},
],
() => {
@@ -112,6 +115,7 @@ describe('convertToOtherParentPipelineAggColumns', () => {
dataView: stubLogstashDataView,
aggs,
agg: aggs[1] as SchemaConfig,
+ visType,
},
],
() => {
@@ -129,6 +133,7 @@ describe('convertToOtherParentPipelineAggColumns', () => {
dataView: stubLogstashDataView,
aggs,
agg: aggs[1] as SchemaConfig,
+ visType,
},
],
() => {
@@ -147,6 +152,7 @@ describe('convertToOtherParentPipelineAggColumns', () => {
dataView: stubLogstashDataView,
aggs,
agg: aggs[1] as SchemaConfig,
+ visType,
},
],
() => {
@@ -170,6 +176,7 @@ describe('convertToOtherParentPipelineAggColumns', () => {
dataView: stubLogstashDataView,
aggs,
agg: aggs[1] as SchemaConfig,
+ visType,
},
],
() => {
@@ -188,6 +195,7 @@ describe('convertToOtherParentPipelineAggColumns', () => {
dataView: stubLogstashDataView,
aggs,
agg: aggs[1] as SchemaConfig,
+ visType,
},
],
() => {
@@ -229,6 +237,7 @@ describe('convertToOtherParentPipelineAggColumns', () => {
});
describe('convertToCumulativeSumAggColumn', () => {
+ const visType = 'heatmap';
const field = stubLogstashDataView.fields[0].name;
const aggs: Array> = [
{
@@ -280,6 +289,7 @@ describe('convertToCumulativeSumAggColumn', () => {
dataView: stubLogstashDataView,
aggs,
agg: { ...aggs[1], aggParams: undefined } as SchemaConfig,
+ visType,
},
],
() => {
@@ -294,6 +304,7 @@ describe('convertToCumulativeSumAggColumn', () => {
dataView: stubLogstashDataView,
aggs,
agg: aggs[1] as SchemaConfig,
+ visType,
},
],
() => {
@@ -308,6 +319,7 @@ describe('convertToCumulativeSumAggColumn', () => {
dataView: stubLogstashDataView,
aggs,
agg: aggs[1] as SchemaConfig,
+ visType,
},
],
() => {
@@ -325,6 +337,7 @@ describe('convertToCumulativeSumAggColumn', () => {
dataView: stubLogstashDataView,
aggs,
agg: aggs[1] as SchemaConfig,
+ visType,
},
],
() => {
@@ -342,6 +355,7 @@ describe('convertToCumulativeSumAggColumn', () => {
dataView: stubLogstashDataView,
aggs,
agg: aggs[1] as SchemaConfig,
+ visType,
},
],
() => {
@@ -360,6 +374,7 @@ describe('convertToCumulativeSumAggColumn', () => {
dataView: stubLogstashDataView,
aggs,
agg: aggs[1] as SchemaConfig,
+ visType,
},
],
() => {
@@ -383,6 +398,7 @@ describe('convertToCumulativeSumAggColumn', () => {
dataView: stubLogstashDataView,
aggs,
agg: aggs[1] as SchemaConfig,
+ visType,
},
],
() => {
@@ -401,6 +417,7 @@ describe('convertToCumulativeSumAggColumn', () => {
dataView: stubLogstashDataView,
aggs,
agg: aggs[1] as SchemaConfig,
+ visType,
},
],
() => {
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/parent_pipeline.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/parent_pipeline.ts
index ab41ceb259adb..0e0aef11316b2 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/convert/parent_pipeline.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/parent_pipeline.ts
@@ -38,7 +38,7 @@ export const convertToMovingAverageParams = (
});
export const convertToOtherParentPipelineAggColumns = (
- { agg, dataView, aggs }: ExtendedColumnConverterArgs,
+ { agg, dataView, aggs, visType }: ExtendedColumnConverterArgs,
reducedTimeRange?: string
): FormulaColumn | [ParentPipelineAggColumn, AggBasedColumn] | null => {
const { aggType } = agg;
@@ -63,7 +63,7 @@ export const convertToOtherParentPipelineAggColumns = (
}
if (PIPELINE_AGGS.includes(metric.aggType)) {
- const formula = getFormulaForPipelineAgg({ agg, aggs, dataView });
+ const formula = getFormulaForPipelineAgg({ agg, aggs, dataView, visType });
if (!formula) {
return null;
}
@@ -71,7 +71,7 @@ export const convertToOtherParentPipelineAggColumns = (
return createFormulaColumn(formula, agg);
}
- const subMetric = convertMetricToColumns(metric, dataView, aggs);
+ const subMetric = convertMetricToColumns({ agg: metric, dataView, aggs, visType });
if (subMetric === null) {
return null;
@@ -90,7 +90,7 @@ export const convertToOtherParentPipelineAggColumns = (
};
export const convertToCumulativeSumAggColumn = (
- { agg, dataView, aggs }: ExtendedColumnConverterArgs,
+ { agg, dataView, aggs, visType }: ExtendedColumnConverterArgs,
reducedTimeRange?: string
):
| FormulaColumn
@@ -119,7 +119,7 @@ export const convertToCumulativeSumAggColumn = (
// create column for sum or count
const subMetric = convertMetricAggregationColumnWithoutSpecialParams(
subAgg,
- { agg: metric as SchemaConfig, dataView },
+ { agg: metric as SchemaConfig, dataView, visType },
reducedTimeRange
);
@@ -144,7 +144,7 @@ export const convertToCumulativeSumAggColumn = (
subMetric,
];
} else {
- const formula = getFormulaForPipelineAgg({ agg, aggs, dataView });
+ const formula = getFormulaForPipelineAgg({ agg, aggs, dataView, visType });
if (!formula) {
return null;
}
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/percentage_mode.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/percentage_mode.test.ts
index 3b7e8ad7e797f..0ef5d07236d60 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/convert/percentage_mode.test.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/percentage_mode.test.ts
@@ -18,6 +18,7 @@ jest.mock('../metrics/formula', () => ({
}));
describe('convertToColumnInPercentageMode', () => {
+ const visType = 'heatmap';
const formula = 'average(some_field)';
const dataView = stubLogstashDataView;
@@ -42,7 +43,7 @@ describe('convertToColumnInPercentageMode', () => {
test('should return null if it is not possible to build the valid formula', () => {
mockGetFormulaForAgg.mockReturnValue(null);
- expect(convertToColumnInPercentageMode({ agg, dataView, aggs: [agg] }, {})).toBeNull();
+ expect(convertToColumnInPercentageMode({ agg, dataView, aggs: [agg], visType }, {})).toBeNull();
});
test('should return percentage mode over range formula if min and max was passed', () => {
@@ -51,7 +52,7 @@ describe('convertToColumnInPercentageMode', () => {
params: { format: { id: 'percent' }, formula: `((${formula}) - 0) / (100 - 0)` },
};
expect(
- convertToColumnInPercentageMode({ agg, dataView, aggs: [agg] }, { min: 0, max: 100 })
+ convertToColumnInPercentageMode({ agg, dataView, aggs: [agg], visType }, { min: 0, max: 100 })
).toEqual(expect.objectContaining(formulaColumn));
});
@@ -60,7 +61,7 @@ describe('convertToColumnInPercentageMode', () => {
operationType: 'formula',
params: { format: { id: 'percent' }, formula: `(${formula}) / 10000` },
};
- expect(convertToColumnInPercentageMode({ agg, dataView, aggs: [agg] }, {})).toEqual(
+ expect(convertToColumnInPercentageMode({ agg, dataView, aggs: [agg], visType }, {})).toEqual(
expect.objectContaining(formulaColumn)
);
});
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/percentile.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/percentile.test.ts
index b4cf7f141e928..adfab7f55d1c4 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/convert/percentile.test.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/percentile.test.ts
@@ -24,6 +24,7 @@ jest.mock('../utils', () => ({
}));
describe('convertToPercentileColumn', () => {
+ const visType = 'heatmap';
const dataView = stubLogstashDataView;
const field = dataView.fields[0].displayName;
const aggId = 'pr.10';
@@ -67,23 +68,27 @@ describe('convertToPercentileColumn', () => {
test.each<
[string, Parameters, Partial | null]
>([
- ['null if no percents', [{ agg: { ...agg, aggId: 'pr' }, dataView }], null],
+ ['null if no percents', [{ agg: { ...agg, aggId: 'pr' }, dataView, visType }], null],
[
'null if no value',
- [{ agg: { ...singlePercentileRankAgg, aggParams: undefined }, dataView }],
+ [{ agg: { ...singlePercentileRankAgg, aggParams: undefined }, dataView, visType }],
+ null,
+ ],
+ ['null if no aggId', [{ agg: { ...agg, aggId: undefined }, dataView, visType }], null],
+ ['null if no aggParams', [{ agg: { ...agg, aggParams: undefined }, dataView, visType }], null],
+ [
+ 'null if aggId is invalid',
+ [{ agg: { ...agg, aggId: 'pr.invalid' }, dataView, visType }],
null,
],
- ['null if no aggId', [{ agg: { ...agg, aggId: undefined }, dataView }], null],
- ['null if no aggParams', [{ agg: { ...agg, aggParams: undefined }, dataView }], null],
- ['null if aggId is invalid', [{ agg: { ...agg, aggId: 'pr.invalid' }, dataView }], null],
[
'null if values are undefined',
- [{ agg: { ...agg, aggParams: { percents: undefined, field } }, dataView }],
+ [{ agg: { ...agg, aggParams: { percents: undefined, field } }, dataView, visType }],
null,
],
[
'null if values are empty',
- [{ agg: { ...agg, aggParams: { percents: [], field } }, dataView }],
+ [{ agg: { ...agg, aggParams: { percents: [], field } }, dataView, visType }],
null,
],
])('should return %s', (_, input, expected) => {
@@ -96,7 +101,7 @@ describe('convertToPercentileColumn', () => {
test('should return null if field is not specified', () => {
mockGetFieldNameFromField.mockReturnValue(null);
- expect(convertToPercentileColumn({ agg, dataView })).toBeNull();
+ expect(convertToPercentileColumn({ agg, dataView, visType })).toBeNull();
expect(mockGetFieldNameFromField).toBeCalledTimes(1);
expect(dataView.getFieldByName).toBeCalledTimes(0);
});
@@ -105,13 +110,13 @@ describe('convertToPercentileColumn', () => {
mockGetFieldByName.mockReturnValueOnce(null);
dataView.getFieldByName = mockGetFieldByName;
- expect(convertToPercentileColumn({ agg, dataView })).toBeNull();
+ expect(convertToPercentileColumn({ agg, dataView, visType })).toBeNull();
expect(mockGetFieldNameFromField).toBeCalledTimes(1);
expect(dataView.getFieldByName).toBeCalledTimes(1);
});
test('should return percentile rank column for percentiles', () => {
- expect(convertToPercentileColumn({ agg, dataView })).toEqual(
+ expect(convertToPercentileColumn({ agg, dataView, visType })).toEqual(
expect.objectContaining({
dataType: 'number',
label: 'someOtherLabel',
@@ -126,7 +131,7 @@ describe('convertToPercentileColumn', () => {
});
test('should return percentile rank column for single percentile', () => {
- expect(convertToPercentileColumn({ agg: singlePercentileRankAgg, dataView })).toEqual(
+ expect(convertToPercentileColumn({ agg: singlePercentileRankAgg, dataView, visType })).toEqual(
expect.objectContaining({
dataType: 'number',
label: 'someOtherLabel',
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/percentile.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/percentile.ts
index de9d4e088b636..9989db1c5dda7 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/convert/percentile.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/percentile.ts
@@ -51,6 +51,7 @@ const getPercent = (
export const convertToPercentileColumn = (
{
+ visType,
agg,
dataView,
}: CommonColumnConverterArgs,
@@ -74,7 +75,7 @@ export const convertToPercentileColumn = (
}
const field = dataView.getFieldByName(fieldName);
- if (!isFieldValid(field, SUPPORTED_METRICS[agg.aggType])) {
+ if (!isFieldValid(visType, field, SUPPORTED_METRICS[agg.aggType])) {
return null;
}
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/percentile_rank.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/percentile_rank.test.ts
index 8a696d51d871b..afeaa9899d107 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/convert/percentile_rank.test.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/percentile_rank.test.ts
@@ -24,6 +24,7 @@ jest.mock('../utils', () => ({
}));
describe('convertToPercentileRankColumn', () => {
+ const visType = 'heatmap';
const dataView = stubLogstashDataView;
const field = dataView.fields[0].displayName;
const aggId = 'pr.10';
@@ -71,23 +72,27 @@ describe('convertToPercentileRankColumn', () => {
Partial | null
]
>([
- ['null if no percents', [{ agg: { ...agg, aggId: 'pr' }, dataView }], null],
+ ['null if no percents', [{ agg: { ...agg, aggId: 'pr' }, dataView, visType }], null],
[
'null if no value',
- [{ agg: { ...singlePercentileRankAgg, aggParams: undefined }, dataView }],
+ [{ agg: { ...singlePercentileRankAgg, aggParams: undefined }, dataView, visType }],
+ null,
+ ],
+ ['null if no aggId', [{ agg: { ...agg, aggId: undefined }, dataView, visType }], null],
+ ['null if no aggParams', [{ agg: { ...agg, aggParams: undefined }, dataView, visType }], null],
+ [
+ 'null if aggId is invalid',
+ [{ agg: { ...agg, aggId: 'pr.invalid' }, dataView, visType }],
null,
],
- ['null if no aggId', [{ agg: { ...agg, aggId: undefined }, dataView }], null],
- ['null if no aggParams', [{ agg: { ...agg, aggParams: undefined }, dataView }], null],
- ['null if aggId is invalid', [{ agg: { ...agg, aggId: 'pr.invalid' }, dataView }], null],
[
'null if values are undefined',
- [{ agg: { ...agg, aggParams: { values: undefined, field } }, dataView }],
+ [{ agg: { ...agg, aggParams: { values: undefined, field } }, dataView, visType }],
null,
],
[
'null if values are empty',
- [{ agg: { ...agg, aggParams: { values: [], field } }, dataView }],
+ [{ agg: { ...agg, aggParams: { values: [], field } }, dataView, visType }],
null,
],
])('should return %s', (_, input, expected) => {
@@ -100,7 +105,7 @@ describe('convertToPercentileRankColumn', () => {
test('should return null if field is not specified', () => {
mockGetFieldNameFromField.mockReturnValue(null);
- expect(convertToPercentileRankColumn({ agg, dataView })).toBeNull();
+ expect(convertToPercentileRankColumn({ agg, dataView, visType })).toBeNull();
expect(mockGetFieldNameFromField).toBeCalledTimes(1);
expect(dataView.getFieldByName).toBeCalledTimes(0);
});
@@ -109,13 +114,13 @@ describe('convertToPercentileRankColumn', () => {
mockGetFieldByName.mockReturnValueOnce(null);
dataView.getFieldByName = mockGetFieldByName;
- expect(convertToPercentileRankColumn({ agg, dataView })).toBeNull();
+ expect(convertToPercentileRankColumn({ agg, dataView, visType })).toBeNull();
expect(mockGetFieldNameFromField).toBeCalledTimes(1);
expect(dataView.getFieldByName).toBeCalledTimes(1);
});
test('should return percentile rank column for percentile ranks', () => {
- expect(convertToPercentileRankColumn({ agg, dataView })).toEqual(
+ expect(convertToPercentileRankColumn({ agg, dataView, visType })).toEqual(
expect.objectContaining({
dataType: 'number',
label: 'someOtherLabel',
@@ -130,7 +135,9 @@ describe('convertToPercentileRankColumn', () => {
});
test('should return percentile rank column for single percentile rank', () => {
- expect(convertToPercentileRankColumn({ agg: singlePercentileRankAgg, dataView })).toEqual(
+ expect(
+ convertToPercentileRankColumn({ agg: singlePercentileRankAgg, dataView, visType })
+ ).toEqual(
expect.objectContaining({
dataType: 'number',
label: 'someOtherLabel',
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/percentile_rank.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/percentile_rank.ts
index 5124a26543552..8fb55789dd6a7 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/convert/percentile_rank.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/percentile_rank.ts
@@ -50,6 +50,7 @@ const getPercent = (
export const convertToPercentileRankColumn = (
{
+ visType,
agg,
dataView,
}: CommonColumnConverterArgs,
@@ -69,7 +70,7 @@ export const convertToPercentileRankColumn = (
}
const field = dataView.getFieldByName(fieldName);
- if (!isFieldValid(field, SUPPORTED_METRICS[agg.aggType])) {
+ if (!isFieldValid(visType, field, SUPPORTED_METRICS[agg.aggType])) {
return null;
}
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/range.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/range.test.ts
index 8f535c28c8264..5a754fd1c9466 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/convert/range.test.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/range.test.ts
@@ -60,7 +60,6 @@ describe('convertToRangeColumn', () => {
params: {
type: RANGE_MODES.Histogram,
maxBars: 'auto',
- ranges: [],
},
},
],
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/range.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/range.ts
index 6a9f96fd5ad1e..98200c321935c 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/convert/range.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/range.ts
@@ -27,18 +27,17 @@ export const convertToRangeParams = (
return {
type: RANGE_MODES.Histogram,
maxBars: aggParams.maxBars ?? 'auto',
- ranges: [],
+ includeEmptyRows: aggParams.min_doc_count,
};
} else {
return {
type: RANGE_MODES.Range,
maxBars: 'auto',
- ranges:
- aggParams.ranges?.map((range) => ({
- label: range.label,
- from: range.from ?? null,
- to: range.to ?? null,
- })) ?? [],
+ ranges: aggParams.ranges?.map((range) => ({
+ label: range.label,
+ from: range.from ?? null,
+ to: range.to ?? null,
+ })),
};
}
};
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/sibling_pipeline.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/sibling_pipeline.test.ts
index 759620650b8a6..6adde7004b69a 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/convert/sibling_pipeline.test.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/sibling_pipeline.test.ts
@@ -23,6 +23,7 @@ jest.mock('../../../vis_schemas', () => ({
}));
describe('convertToSiblingPipelineColumns', () => {
+ const visType = 'heatmap';
const dataView = stubLogstashDataView;
const aggId = 'agg-id-1';
const agg: SchemaConfig = {
@@ -46,7 +47,12 @@ describe('convertToSiblingPipelineColumns', () => {
test('should return null if aggParams are not defined', () => {
expect(
- convertToSiblingPipelineColumns({ agg: { ...agg, aggParams: undefined }, aggs: [], dataView })
+ convertToSiblingPipelineColumns({
+ agg: { ...agg, aggParams: undefined },
+ aggs: [],
+ dataView,
+ visType,
+ })
).toBeNull();
expect(mockConvertMetricToColumns).toBeCalledTimes(0);
});
@@ -57,6 +63,7 @@ describe('convertToSiblingPipelineColumns', () => {
agg: { ...agg, aggParams: { customMetric: undefined } },
aggs: [],
dataView,
+ visType,
})
).toBeNull();
expect(mockConvertMetricToColumns).toBeCalledTimes(0);
@@ -64,7 +71,7 @@ describe('convertToSiblingPipelineColumns', () => {
test('should return null if sibling agg is not supported', () => {
mockConvertMetricToColumns.mockReturnValue(null);
- expect(convertToSiblingPipelineColumns({ agg, aggs: [], dataView })).toBeNull();
+ expect(convertToSiblingPipelineColumns({ agg, aggs: [], dataView, visType })).toBeNull();
expect(mockConvertToSchemaConfig).toBeCalledTimes(1);
expect(mockConvertMetricToColumns).toBeCalledTimes(1);
});
@@ -72,7 +79,7 @@ describe('convertToSiblingPipelineColumns', () => {
test('should return column', () => {
const column = { operationType: 'formula' };
mockConvertMetricToColumns.mockReturnValue([column]);
- expect(convertToSiblingPipelineColumns({ agg, aggs: [], dataView })).toEqual(column);
+ expect(convertToSiblingPipelineColumns({ agg, aggs: [], dataView, visType })).toEqual(column);
expect(mockConvertToSchemaConfig).toBeCalledTimes(1);
expect(mockConvertMetricToColumns).toBeCalledTimes(1);
});
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/sibling_pipeline.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/sibling_pipeline.ts
index a8389cb8601e4..c77500a55d5d1 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/convert/sibling_pipeline.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/sibling_pipeline.ts
@@ -22,11 +22,12 @@ export const convertToSiblingPipelineColumns = (
return null;
}
- const customMetricColumn = convertMetricToColumns(
- { ...convertToSchemaConfig(aggParams.customMetric), label, aggId },
- columnConverterArgs.dataView,
- columnConverterArgs.aggs
- );
+ const customMetricColumn = convertMetricToColumns({
+ agg: { ...convertToSchemaConfig(aggParams.customMetric), label, aggId },
+ dataView: columnConverterArgs.dataView,
+ aggs: columnConverterArgs.aggs,
+ visType: columnConverterArgs.visType,
+ });
if (!customMetricColumn) {
return null;
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/std_deviation.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/std_deviation.test.ts
index cbb1f03a6dc2e..c786d6b8c3a6f 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/convert/std_deviation.test.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/std_deviation.test.ts
@@ -22,6 +22,7 @@ jest.mock('../utils', () => ({
}));
describe('convertToStdDeviationFormulaColumns', () => {
+ const visType = 'heatmap';
const dataView = stubLogstashDataView;
const stdLowerAggId = 'agg-id.std_lower';
const stdUpperAggId = 'agg-id.std_upper';
@@ -51,22 +52,25 @@ describe('convertToStdDeviationFormulaColumns', () => {
test.each<
[string, Parameters, Partial | null]
- >([['null if no aggId is passed', [{ agg: { ...agg, aggId: undefined }, dataView }], null]])(
- 'should return %s',
- (_, input, expected) => {
- if (expected === null) {
- expect(convertToStdDeviationFormulaColumns(...input)).toBeNull();
- } else {
- expect(convertToStdDeviationFormulaColumns(...input)).toEqual(
- expect.objectContaining(expected)
- );
- }
+ >([
+ [
+ 'null if no aggId is passed',
+ [{ agg: { ...agg, aggId: undefined }, dataView, visType }],
+ null,
+ ],
+ ])('should return %s', (_, input, expected) => {
+ if (expected === null) {
+ expect(convertToStdDeviationFormulaColumns(...input)).toBeNull();
+ } else {
+ expect(convertToStdDeviationFormulaColumns(...input)).toEqual(
+ expect.objectContaining(expected)
+ );
}
- );
+ });
test('should return null if field is not present', () => {
mockGetFieldNameFromField.mockReturnValue(null);
- expect(convertToStdDeviationFormulaColumns({ agg, dataView })).toBeNull();
+ expect(convertToStdDeviationFormulaColumns({ agg, dataView, visType })).toBeNull();
expect(mockGetFieldNameFromField).toBeCalledTimes(1);
expect(dataView.getFieldByName).toBeCalledTimes(0);
});
@@ -74,14 +78,14 @@ describe('convertToStdDeviationFormulaColumns', () => {
test("should return null if field doesn't exist in dataView", () => {
mockGetFieldByName.mockReturnValue(null);
dataView.getFieldByName = mockGetFieldByName;
- expect(convertToStdDeviationFormulaColumns({ agg, dataView })).toBeNull();
+ expect(convertToStdDeviationFormulaColumns({ agg, dataView, visType })).toBeNull();
expect(mockGetFieldNameFromField).toBeCalledTimes(1);
expect(dataView.getFieldByName).toBeCalledTimes(1);
});
test('should return null if agg id is invalid', () => {
expect(
- convertToStdDeviationFormulaColumns({ agg: { ...agg, aggId: 'some-id' }, dataView })
+ convertToStdDeviationFormulaColumns({ agg: { ...agg, aggId: 'some-id' }, dataView, visType })
).toBeNull();
expect(mockGetFieldNameFromField).toBeCalledTimes(1);
expect(dataView.getFieldByName).toBeCalledTimes(1);
@@ -89,7 +93,11 @@ describe('convertToStdDeviationFormulaColumns', () => {
test('should return formula column for lower std deviation', () => {
expect(
- convertToStdDeviationFormulaColumns({ agg: { ...agg, aggId: stdLowerAggId }, dataView })
+ convertToStdDeviationFormulaColumns({
+ agg: { ...agg, aggId: stdLowerAggId },
+ dataView,
+ visType,
+ })
).toEqual(
expect.objectContaining({
label,
@@ -102,7 +110,11 @@ describe('convertToStdDeviationFormulaColumns', () => {
test('should return formula column for upper std deviation', () => {
expect(
- convertToStdDeviationFormulaColumns({ agg: { ...agg, aggId: stdUpperAggId }, dataView })
+ convertToStdDeviationFormulaColumns({
+ agg: { ...agg, aggId: stdUpperAggId },
+ dataView,
+ visType,
+ })
).toEqual(
expect.objectContaining({
label,
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/std_deviation.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/std_deviation.ts
index f2c218d429bdf..fe4e854759d8f 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/convert/std_deviation.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/std_deviation.ts
@@ -50,7 +50,7 @@ export const getStdDeviationFormula = (
};
export const convertToStdDeviationFormulaColumns = (
- { agg, dataView }: CommonColumnConverterArgs,
+ { visType, agg, dataView }: CommonColumnConverterArgs,
reducedTimeRange?: string
) => {
const { aggId } = agg;
@@ -64,7 +64,7 @@ export const convertToStdDeviationFormulaColumns = (
return null;
}
const field = dataView.getFieldByName(fieldName);
- if (!isFieldValid(field, SUPPORTED_METRICS[agg.aggType])) {
+ if (!isFieldValid(visType, field, SUPPORTED_METRICS[agg.aggType])) {
return null;
}
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/supported_metrics.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/supported_metrics.ts
index 17a8ccf26c369..61f3f3961b6dc 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/convert/supported_metrics.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/supported_metrics.ts
@@ -18,10 +18,12 @@ interface AggWithFormula {
formula: string;
}
+type SupportedDataTypes = { [key: string]: readonly string[] } & { default: readonly string[] };
+
export type AggOptions = {
isFullReference: boolean;
isFieldRequired: boolean;
- supportedDataTypes: readonly string[];
+ supportedDataTypes: SupportedDataTypes;
} & (T extends Exclude ? Agg : AggWithFormula);
// list of supported TSVB aggregation types in Lens
@@ -62,9 +64,9 @@ export type SupportedMetrics = LocalSupportedMetrics & {
[Key in UnsupportedSupportedMetrics]?: null;
};
-const supportedDataTypesWithDate = ['number', 'date', 'histogram'] as const;
-const supportedDataTypes = ['number', 'histogram'] as const;
-const extendedSupportedDataTypes = [
+const supportedDataTypesWithDate: readonly string[] = ['number', 'date', 'histogram'];
+const supportedDataTypes: readonly string[] = ['number', 'histogram'];
+const extendedSupportedDataTypes: readonly string[] = [
'string',
'boolean',
'number',
@@ -74,44 +76,44 @@ const extendedSupportedDataTypes = [
'date',
'date_range',
'murmur3',
-] as const;
+];
export const SUPPORTED_METRICS: SupportedMetrics = {
avg: {
name: 'average',
isFullReference: false,
isFieldRequired: true,
- supportedDataTypes: ['number'],
+ supportedDataTypes: { default: ['number'] },
},
cardinality: {
name: 'unique_count',
isFullReference: false,
isFieldRequired: true,
- supportedDataTypes: extendedSupportedDataTypes,
+ supportedDataTypes: { default: extendedSupportedDataTypes },
},
count: {
name: 'count',
isFullReference: false,
isFieldRequired: false,
- supportedDataTypes: [],
+ supportedDataTypes: { default: ['number'] },
},
moving_avg: {
name: 'moving_average',
isFullReference: true,
isFieldRequired: true,
- supportedDataTypes: ['number'],
+ supportedDataTypes: { default: ['number'] },
},
derivative: {
name: 'differences',
isFullReference: true,
isFieldRequired: true,
- supportedDataTypes: ['number'],
+ supportedDataTypes: { default: ['number'] },
},
cumulative_sum: {
name: 'cumulative_sum',
isFullReference: true,
isFieldRequired: true,
- supportedDataTypes: ['number'],
+ supportedDataTypes: { default: ['number'] },
},
avg_bucket: {
name: 'formula',
@@ -119,7 +121,7 @@ export const SUPPORTED_METRICS: SupportedMetrics = {
isFieldRequired: true,
isFormula: true,
formula: 'overall_average',
- supportedDataTypes: ['number'],
+ supportedDataTypes: { default: ['number'] },
},
max_bucket: {
name: 'formula',
@@ -127,7 +129,7 @@ export const SUPPORTED_METRICS: SupportedMetrics = {
isFieldRequired: true,
isFormula: true,
formula: 'overall_max',
- supportedDataTypes: ['number'],
+ supportedDataTypes: { default: ['number'] },
},
min_bucket: {
name: 'formula',
@@ -135,7 +137,7 @@ export const SUPPORTED_METRICS: SupportedMetrics = {
isFieldRequired: true,
isFormula: true,
formula: 'overall_min',
- supportedDataTypes: ['number'],
+ supportedDataTypes: { default: ['number'] },
},
sum_bucket: {
name: 'formula',
@@ -143,79 +145,91 @@ export const SUPPORTED_METRICS: SupportedMetrics = {
isFieldRequired: true,
isFormula: true,
formula: 'overall_sum',
- supportedDataTypes: ['number'],
+ supportedDataTypes: { default: ['number'] },
},
max: {
name: 'max',
isFullReference: false,
isFieldRequired: true,
- supportedDataTypes: supportedDataTypesWithDate,
+ supportedDataTypes: {
+ default: ['number'],
+ heatmap: ['number'],
+ line: ['number'],
+ area: ['number'],
+ histogram: ['number'],
+ },
},
min: {
name: 'min',
isFullReference: false,
isFieldRequired: true,
- supportedDataTypes: supportedDataTypesWithDate,
+ supportedDataTypes: {
+ default: supportedDataTypesWithDate,
+ heatmap: ['number'],
+ line: ['number'],
+ area: ['number'],
+ histogram: ['number'],
+ },
},
percentiles: {
name: 'percentile',
isFullReference: false,
isFieldRequired: true,
- supportedDataTypes,
+ supportedDataTypes: { default: supportedDataTypes },
},
single_percentile: {
name: 'percentile',
isFullReference: false,
isFieldRequired: true,
- supportedDataTypes,
+ supportedDataTypes: { default: supportedDataTypes },
},
percentile_ranks: {
name: 'percentile_rank',
isFullReference: false,
isFieldRequired: true,
- supportedDataTypes,
+ supportedDataTypes: { default: supportedDataTypes },
},
single_percentile_rank: {
name: 'percentile_rank',
isFullReference: false,
isFieldRequired: true,
- supportedDataTypes,
+ supportedDataTypes: { default: supportedDataTypes },
},
sum: {
name: 'sum',
isFullReference: false,
isFieldRequired: true,
- supportedDataTypes,
+ supportedDataTypes: { default: supportedDataTypes },
},
top_hits: {
name: 'last_value',
isFullReference: false,
isFieldRequired: true,
- supportedDataTypes: extendedSupportedDataTypes,
+ supportedDataTypes: { default: extendedSupportedDataTypes },
},
top_metrics: {
name: 'last_value',
isFullReference: false,
isFieldRequired: true,
- supportedDataTypes: extendedSupportedDataTypes,
+ supportedDataTypes: { default: extendedSupportedDataTypes },
},
value_count: {
name: 'count',
isFullReference: false,
isFieldRequired: true,
- supportedDataTypes: extendedSupportedDataTypes,
+ supportedDataTypes: { default: extendedSupportedDataTypes },
},
std_dev: {
name: 'standard_deviation',
isFullReference: false,
isFieldRequired: true,
- supportedDataTypes,
+ supportedDataTypes: { default: supportedDataTypes },
},
median: {
name: 'median',
isFullReference: false,
isFieldRequired: true,
- supportedDataTypes,
+ supportedDataTypes: { default: supportedDataTypes },
},
} as const;
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/terms.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/terms.test.ts
index d214ec74b09b1..516ad6b196095 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/convert/terms.test.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/terms.test.ts
@@ -23,6 +23,7 @@ jest.mock('../../../vis_schemas', () => ({
}));
describe('convertToDateHistogramColumn', () => {
+ const visType = 'heatmap';
const aggId = `some-id`;
const aggParams: AggParamsTerms = {
field: stubLogstashDataView.fields[0].name,
@@ -79,6 +80,7 @@ describe('convertToDateHistogramColumn', () => {
dataView: stubLogstashDataView,
aggs,
metricColumns,
+ visType,
},
'',
false,
@@ -95,6 +97,7 @@ describe('convertToDateHistogramColumn', () => {
dataView: stubLogstashDataView,
aggs,
metricColumns,
+ visType,
},
'',
false,
@@ -107,6 +110,8 @@ describe('convertToDateHistogramColumn', () => {
size: 5,
include: [],
exclude: [],
+ includeIsRegex: false,
+ excludeIsRegex: false,
parentFormat: { id: 'terms' },
orderBy: { type: 'alphabetical' },
orderDirection: 'asc',
@@ -123,6 +128,7 @@ describe('convertToDateHistogramColumn', () => {
dataView: stubLogstashDataView,
aggs,
metricColumns,
+ visType,
},
'',
false,
@@ -135,6 +141,8 @@ describe('convertToDateHistogramColumn', () => {
size: 5,
include: [],
exclude: [],
+ includeIsRegex: false,
+ excludeIsRegex: false,
parentFormat: { id: 'terms' },
orderBy: { type: 'column', columnId: metricColumns[0].columnId },
orderAgg: metricColumns[0],
@@ -152,6 +160,7 @@ describe('convertToDateHistogramColumn', () => {
dataView: stubLogstashDataView,
aggs,
metricColumns,
+ visType,
},
'',
false,
@@ -170,6 +179,7 @@ describe('convertToDateHistogramColumn', () => {
dataView: stubLogstashDataView,
aggs,
metricColumns,
+ visType,
},
'',
false,
@@ -188,6 +198,7 @@ describe('convertToDateHistogramColumn', () => {
dataView: stubLogstashDataView,
aggs,
metricColumns,
+ visType,
},
'',
false,
@@ -208,6 +219,7 @@ describe('convertToDateHistogramColumn', () => {
dataView: stubLogstashDataView,
aggs,
metricColumns,
+ visType,
},
'',
false,
@@ -220,6 +232,8 @@ describe('convertToDateHistogramColumn', () => {
size: 5,
include: [],
exclude: [],
+ includeIsRegex: false,
+ excludeIsRegex: false,
parentFormat: { id: 'terms' },
orderBy: { type: 'custom' },
orderAgg: metricColumns[0],
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/terms.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/terms.ts
index 0a50390ec469e..a54a3857e20f6 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/convert/terms.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/terms.ts
@@ -23,6 +23,7 @@ const getOrderByWithAgg = ({
agg,
dataView,
aggs,
+ visType,
metricColumns,
}: CommonBucketConverterArgs): OrderByWithAgg | null => {
if (!agg.aggParams) {
@@ -37,11 +38,12 @@ const getOrderByWithAgg = ({
if (!agg.aggParams.orderAgg) {
return null;
}
- const orderMetricColumn = convertMetricToColumns(
- convertToSchemaConfig(agg.aggParams.orderAgg),
+ const orderMetricColumn = convertMetricToColumns({
+ agg: convertToSchemaConfig(agg.aggParams.orderAgg),
dataView,
- aggs
- );
+ aggs,
+ visType,
+ });
if (!orderMetricColumn) {
return null;
}
@@ -68,35 +70,43 @@ const getOrderByWithAgg = ({
};
};
+const filterOutEmptyValues = (values: string | Array): number[] | string[] => {
+ if (typeof values === 'string') {
+ return Boolean(values) ? [values] : [];
+ }
+
+ return values.filter((v): v is string | number => {
+ if (typeof v === 'string') {
+ return Boolean(v);
+ }
+ return true;
+ }) as string[] | number[];
+};
+
export const convertToTermsParams = ({
agg,
dataView,
aggs,
metricColumns,
+ visType,
}: CommonBucketConverterArgs): TermsParams | null => {
if (!agg.aggParams) {
return null;
}
- const orderByWithAgg = getOrderByWithAgg({ agg, dataView, aggs, metricColumns });
+ const orderByWithAgg = getOrderByWithAgg({ agg, dataView, aggs, metricColumns, visType });
if (orderByWithAgg === null) {
return null;
}
+ const exclude = agg.aggParams.exclude ? filterOutEmptyValues(agg.aggParams.exclude) : [];
+ const include = agg.aggParams.include ? filterOutEmptyValues(agg.aggParams.include) : [];
return {
size: agg.aggParams.size ?? 10,
- include: agg.aggParams.include
- ? Array.isArray(agg.aggParams.include)
- ? agg.aggParams.include
- : [agg.aggParams.include]
- : [],
- includeIsRegex: agg.aggParams.includeIsRegex,
- exclude: agg.aggParams.exclude
- ? Array.isArray(agg.aggParams.exclude)
- ? agg.aggParams.exclude
- : [agg.aggParams.exclude]
- : [],
- excludeIsRegex: agg.aggParams.excludeIsRegex,
+ include,
+ exclude,
+ includeIsRegex: Boolean(include.length && agg.aggParams.includeIsRegex),
+ excludeIsRegex: Boolean(exclude.length && agg.aggParams.excludeIsRegex),
otherBucket: agg.aggParams.otherBucket,
orderDirection: agg.aggParams.order?.value ?? 'desc',
parentFormat: { id: 'terms' },
@@ -107,7 +117,7 @@ export const convertToTermsParams = ({
export const convertToTermsColumn = (
aggId: string,
- { agg, dataView, aggs, metricColumns }: CommonBucketConverterArgs,
+ { agg, dataView, aggs, metricColumns, visType }: CommonBucketConverterArgs,
label: string,
isSplit: boolean
): TermsColumn | null => {
@@ -121,7 +131,7 @@ export const convertToTermsColumn = (
return null;
}
- const params = convertToTermsParams({ agg, dataView, aggs, metricColumns });
+ const params = convertToTermsParams({ agg, dataView, aggs, metricColumns, visType });
if (!params) {
return null;
}
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/convert/types.ts b/src/plugins/visualizations/common/convert_to_lens/lib/convert/types.ts
index 8e6f9ec9443bb..97ccba39303fc 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/convert/types.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/convert/types.ts
@@ -64,6 +64,7 @@ export interface CommonColumnConverterArgs<
> {
agg: SchemaConfig;
dataView: DataView;
+ visType: string;
}
export interface ExtendedColumnConverterArgs<
@@ -75,6 +76,7 @@ export interface ExtendedColumnConverterArgs<
export interface CommonBucketConverterArgs<
Agg extends SupportedAggregation = SupportedAggregation
> {
+ visType: string;
agg: SchemaConfig;
dataView: DataView;
metricColumns: AggBasedColumn[];
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/metrics/formula.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/metrics/formula.test.ts
index 95e128e22b092..72cd07ba03f7c 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/metrics/formula.test.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/metrics/formula.test.ts
@@ -29,7 +29,7 @@ jest.mock('../utils', () => ({
}));
const dataView = stubLogstashDataView;
-
+const visType = 'heatmap';
const field = stubLogstashDataView.fields[0].name;
const aggs: Array> = [
{
@@ -97,7 +97,7 @@ describe('getFormulaForPipelineAgg', () => {
test.each<[string, Parameters, () => void, string | null]>([
[
'null if custom metric is invalid',
- [{ agg: aggs[0] as SchemaConfig, aggs, dataView }],
+ [{ agg: aggs[0] as SchemaConfig, aggs, dataView, visType }],
() => {
mockGetMetricFromParentPipelineAgg.mockReturnValue(null);
},
@@ -105,7 +105,7 @@ describe('getFormulaForPipelineAgg', () => {
],
[
'null if custom metric type is not supported',
- [{ agg: aggs[0] as SchemaConfig, aggs, dataView }],
+ [{ agg: aggs[0] as SchemaConfig, aggs, dataView, visType }],
() => {
mockGetMetricFromParentPipelineAgg.mockReturnValue({
aggType: METRIC_TYPES.GEO_BOUNDS,
@@ -115,7 +115,7 @@ describe('getFormulaForPipelineAgg', () => {
],
[
'correct formula if agg is parent pipeline agg and custom metric is valid and supported pipeline agg',
- [{ agg: aggs[0] as SchemaConfig, aggs, dataView }],
+ [{ agg: aggs[0] as SchemaConfig, aggs, dataView, visType }],
() => {
mockGetMetricFromParentPipelineAgg
.mockReturnValueOnce({
@@ -135,7 +135,7 @@ describe('getFormulaForPipelineAgg', () => {
],
[
'correct formula if agg is parent pipeline agg and custom metric is valid and supported not pipeline agg',
- [{ agg: aggs[0] as SchemaConfig, aggs, dataView }],
+ [{ agg: aggs[0] as SchemaConfig, aggs, dataView, visType }],
() => {
mockGetMetricFromParentPipelineAgg.mockReturnValueOnce({
aggType: METRIC_TYPES.AVG,
@@ -149,7 +149,7 @@ describe('getFormulaForPipelineAgg', () => {
],
[
'correct formula if agg is parent pipeline agg and custom metric is valid and supported percentile rank agg',
- [{ agg: aggs[0] as SchemaConfig, aggs, dataView }],
+ [{ agg: aggs[0] as SchemaConfig, aggs, dataView, visType }],
() => {
mockGetMetricFromParentPipelineAgg.mockReturnValueOnce({
aggType: METRIC_TYPES.PERCENTILE_RANKS,
@@ -163,7 +163,7 @@ describe('getFormulaForPipelineAgg', () => {
],
[
'correct formula if agg is sibling pipeline agg and custom metric is valid and supported agg',
- [{ agg: aggs[1] as SchemaConfig, aggs, dataView }],
+ [{ agg: aggs[1] as SchemaConfig, aggs, dataView, visType }],
() => {
mockGetMetricFromParentPipelineAgg.mockReturnValueOnce({
aggType: METRIC_TYPES.AVG,
@@ -212,6 +212,7 @@ describe('getFormulaForPipelineAgg', () => {
agg: aggs[1] as SchemaConfig,
aggs,
dataView,
+ visType,
});
expect(agg).toBeNull();
});
@@ -244,6 +245,7 @@ describe('getFormulaForPipelineAgg', () => {
agg: aggs[1] as SchemaConfig,
aggs,
dataView,
+ visType,
});
expect(agg).toBeNull();
});
@@ -270,6 +272,7 @@ describe('getFormulaForAgg', () => {
agg: { ...aggs[0], aggType: METRIC_TYPES.GEO_BOUNDS, aggParams: { field } },
aggs,
dataView,
+ visType,
},
],
() => {},
@@ -277,7 +280,7 @@ describe('getFormulaForAgg', () => {
],
[
'correct pipeline formula if agg is valid pipeline agg',
- [{ agg: aggs[0], aggs, dataView }],
+ [{ agg: aggs[0], aggs, dataView, visType }],
() => {
mockIsPipeline.mockReturnValue(true);
mockGetMetricFromParentPipelineAgg.mockReturnValueOnce({
@@ -292,7 +295,7 @@ describe('getFormulaForAgg', () => {
],
[
'correct percentile formula if agg is valid percentile agg',
- [{ agg: aggs[2], aggs, dataView }],
+ [{ agg: aggs[2], aggs, dataView, visType }],
() => {
mockIsPercentileAgg.mockReturnValue(true);
},
@@ -300,7 +303,7 @@ describe('getFormulaForAgg', () => {
],
[
'correct percentile rank formula if agg is valid percentile rank agg',
- [{ agg: aggs[3], aggs, dataView }],
+ [{ agg: aggs[3], aggs, dataView, visType }],
() => {
mockIsPercentileRankAgg.mockReturnValue(true);
},
@@ -308,7 +311,7 @@ describe('getFormulaForAgg', () => {
],
[
'correct standart deviation formula if agg is valid standart deviation agg',
- [{ agg: aggs[4], aggs, dataView }],
+ [{ agg: aggs[4], aggs, dataView, visType }],
() => {
mockIsStdDevAgg.mockReturnValue(true);
},
@@ -316,7 +319,7 @@ describe('getFormulaForAgg', () => {
],
[
'correct metric formula if agg is valid other metric agg',
- [{ agg: aggs[5], aggs, dataView }],
+ [{ agg: aggs[5], aggs, dataView, visType }],
() => {},
'average(bytes)',
],
@@ -395,6 +398,7 @@ describe('getFormulaForAgg', () => {
>,
aggs,
dataView,
+ visType,
});
expect(result).toBeNull();
});
@@ -467,6 +471,7 @@ describe('getFormulaForAgg', () => {
>,
aggs,
dataView,
+ visType,
});
expect(result).toBeNull();
}
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/metrics/formula.ts b/src/plugins/visualizations/common/convert_to_lens/lib/metrics/formula.ts
index 276ac54e2fc3d..4492cd58ac230 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/metrics/formula.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/metrics/formula.ts
@@ -66,7 +66,7 @@ const isDataViewField = (field: string | DataViewField): field is DataViewField
return false;
};
-const isValidAgg = (agg: SchemaConfig, dataView: DataView) => {
+const isValidAgg = (visType: string, agg: SchemaConfig, dataView: DataView) => {
const aggregation = SUPPORTED_METRICS[agg.aggType];
if (!aggregation) {
return false;
@@ -77,7 +77,7 @@ const isValidAgg = (agg: SchemaConfig, dataView: DataView) => {
}
const sourceField = getFieldNameFromField(agg.aggParams?.field);
const field = dataView.getFieldByName(sourceField!);
- if (!isFieldValid(field, aggregation)) {
+ if (!isFieldValid(visType, field, aggregation)) {
return false;
}
}
@@ -86,13 +86,14 @@ const isValidAgg = (agg: SchemaConfig, dataView: DataView) => {
};
const getFormulaForAggsWithoutParams = (
+ visType: string,
agg: SchemaConfig,
dataView: DataView,
selector: string | undefined,
reducedTimeRange?: string
) => {
const op = SUPPORTED_METRICS[agg.aggType];
- if (!isValidAgg(agg, dataView) || !op) {
+ if (!isValidAgg(visType, agg, dataView) || !op) {
return null;
}
@@ -101,6 +102,7 @@ const getFormulaForAggsWithoutParams = (
};
const getFormulaForPercentileRanks = (
+ visType: string,
agg: SchemaConfig,
dataView: DataView,
selector: string | undefined,
@@ -108,7 +110,7 @@ const getFormulaForPercentileRanks = (
) => {
const value = Number(agg.aggId?.split('.')[1]);
const op = SUPPORTED_METRICS[agg.aggType];
- if (!isValidAgg(agg, dataView) || !op) {
+ if (!isValidAgg(visType, agg, dataView) || !op) {
return null;
}
@@ -117,6 +119,7 @@ const getFormulaForPercentileRanks = (
};
const getFormulaForPercentile = (
+ visType: string,
agg: SchemaConfig,
dataView: DataView,
selector: string,
@@ -124,7 +127,7 @@ const getFormulaForPercentile = (
) => {
const percentile = Number(agg.aggId?.split('.')[1]);
const op = SUPPORTED_METRICS[agg.aggType];
- if (!isValidAgg(agg, dataView) || !op) {
+ if (!isValidAgg(visType, agg, dataView) || !op) {
return null;
}
@@ -138,6 +141,7 @@ const getFormulaForSubMetric = ({
agg,
dataView,
aggs,
+ visType,
}: ExtendedColumnConverterArgs): string | null => {
const op = SUPPORTED_METRICS[agg.aggType];
if (!op) {
@@ -148,12 +152,13 @@ const getFormulaForSubMetric = ({
PARENT_PIPELINE_OPS.includes(op.name) ||
SIBLING_PIPELINE_AGGS.includes(agg.aggType as METRIC_TYPES)
) {
- return getFormulaForPipelineAgg({ agg: agg as PipelineAggs, aggs, dataView });
+ return getFormulaForPipelineAgg({ agg: agg as PipelineAggs, aggs, dataView, visType });
}
if (METRIC_OPS_WITHOUT_PARAMS.includes(op.name)) {
const metricAgg = agg as MetricAggsWithoutParams;
return getFormulaForAggsWithoutParams(
+ visType,
metricAgg,
dataView,
metricAgg.aggParams && 'field' in metricAgg.aggParams
@@ -168,6 +173,7 @@ const getFormulaForSubMetric = ({
const percentileRanksAgg = agg as SchemaConfig;
return getFormulaForPercentileRanks(
+ visType,
percentileRanksAgg,
dataView,
percentileRanksAgg.aggParams?.field
@@ -181,6 +187,7 @@ export const getFormulaForPipelineAgg = ({
agg,
dataView,
aggs,
+ visType,
}: ExtendedColumnConverterArgs<
| METRIC_TYPES.CUMULATIVE_SUM
| METRIC_TYPES.DERIVATIVE
@@ -205,6 +212,7 @@ export const getFormulaForPipelineAgg = ({
agg: metricAgg,
aggs,
dataView,
+ visType,
});
if (subFormula === null) {
return null;
@@ -222,13 +230,15 @@ export const getFormulaForAgg = ({
agg,
aggs,
dataView,
+ visType,
}: ExtendedColumnConverterArgs) => {
if (isPipeline(agg)) {
- return getFormulaForPipelineAgg({ agg, aggs, dataView });
+ return getFormulaForPipelineAgg({ agg, aggs, dataView, visType });
}
if (isPercentileAgg(agg)) {
return getFormulaForPercentile(
+ visType,
agg,
dataView,
getFieldNameFromField(agg.aggParams?.field) ?? ''
@@ -237,6 +247,7 @@ export const getFormulaForAgg = ({
if (isPercentileRankAgg(agg)) {
return getFormulaForPercentileRanks(
+ visType,
agg,
dataView,
getFieldNameFromField(agg.aggParams?.field) ?? ''
@@ -244,13 +255,14 @@ export const getFormulaForAgg = ({
}
if (isStdDevAgg(agg) && agg.aggId) {
- if (!isValidAgg(agg, dataView)) {
+ if (!isValidAgg(visType, agg, dataView)) {
return null;
}
return getStdDeviationFormula(agg.aggId, getFieldNameFromField(agg.aggParams?.field) ?? '');
}
return getFormulaForAggsWithoutParams(
+ visType,
agg,
dataView,
isMetricWithField(agg) ? getFieldNameFromField(agg.aggParams?.field) ?? '' : ''
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/metrics/metrics.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/metrics/metrics.test.ts
index 1cf3ff0b84064..c7674bf6603c0 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/metrics/metrics.test.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/metrics/metrics.test.ts
@@ -9,6 +9,7 @@
import { METRIC_TYPES } from '@kbn/data-plugin/common';
import { stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub';
import { SchemaConfig } from '../../..';
+import { ExtendedColumnConverterArgs } from '../convert';
import { convertMetricToColumns } from './metrics';
const mockConvertMetricAggregationColumnWithoutSpecialParams = jest.fn();
@@ -37,6 +38,8 @@ jest.mock('../convert', () => ({
convertToColumnInPercentageMode: jest.fn(() => mockConvertToColumnInPercentageMode()),
}));
+const visType = 'heatmap';
+
describe('convertMetricToColumns invalid cases', () => {
const dataView = stubLogstashDataView;
@@ -55,13 +58,18 @@ describe('convertMetricToColumns invalid cases', () => {
mockConvertToCumulativeSumAggColumn.mockReturnValue(null);
});
+ const aggs: ExtendedColumnConverterArgs['aggs'] = [];
+
test.each<[string, Parameters, null, jest.Mock | undefined]>([
[
'null if agg is not supported',
[
- { aggType: METRIC_TYPES.GEO_BOUNDS } as unknown as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.GEO_BOUNDS } as unknown as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
null,
@@ -70,9 +78,12 @@ describe('convertMetricToColumns invalid cases', () => {
[
'null if supported agg AVG is not valid',
[
- { aggType: METRIC_TYPES.AVG } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.AVG } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
null,
@@ -81,9 +92,12 @@ describe('convertMetricToColumns invalid cases', () => {
[
'null if supported agg MIN is not valid',
[
- { aggType: METRIC_TYPES.MIN } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.MIN } as SchemaConfig,
+ dataView,
+ aggs: [],
+ visType,
+ },
{ isPercentageMode: false },
],
null,
@@ -92,9 +106,12 @@ describe('convertMetricToColumns invalid cases', () => {
[
'null if supported agg MAX is not valid',
[
- { aggType: METRIC_TYPES.MAX } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.MAX } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
null,
@@ -103,9 +120,12 @@ describe('convertMetricToColumns invalid cases', () => {
[
'null if supported agg SUM is not valid',
[
- { aggType: METRIC_TYPES.SUM } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.SUM } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
null,
@@ -114,9 +134,12 @@ describe('convertMetricToColumns invalid cases', () => {
[
'null if supported agg COUNT is not valid',
[
- { aggType: METRIC_TYPES.COUNT } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.COUNT } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
null,
@@ -125,9 +148,12 @@ describe('convertMetricToColumns invalid cases', () => {
[
'null if supported agg CARDINALITY is not valid',
[
- { aggType: METRIC_TYPES.CARDINALITY } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.CARDINALITY } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
null,
@@ -136,9 +162,12 @@ describe('convertMetricToColumns invalid cases', () => {
[
'null if supported agg VALUE_COUNT is not valid',
[
- { aggType: METRIC_TYPES.VALUE_COUNT } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.VALUE_COUNT } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
null,
@@ -147,9 +176,12 @@ describe('convertMetricToColumns invalid cases', () => {
[
'null if supported agg MEDIAN is not valid',
[
- { aggType: METRIC_TYPES.MEDIAN } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.MEDIAN } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
null,
@@ -158,9 +190,12 @@ describe('convertMetricToColumns invalid cases', () => {
[
'null if supported agg STD_DEV is not valid',
[
- { aggType: METRIC_TYPES.STD_DEV } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.STD_DEV } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
null,
@@ -169,9 +204,12 @@ describe('convertMetricToColumns invalid cases', () => {
[
'null if supported agg PERCENTILES is not valid',
[
- { aggType: METRIC_TYPES.PERCENTILES } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.PERCENTILES } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
null,
@@ -180,9 +218,12 @@ describe('convertMetricToColumns invalid cases', () => {
[
'null if supported agg SINGLE_PERCENTILE is not valid',
[
- { aggType: METRIC_TYPES.SINGLE_PERCENTILE } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.SINGLE_PERCENTILE } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
null,
@@ -191,9 +232,12 @@ describe('convertMetricToColumns invalid cases', () => {
[
'null if supported agg PERCENTILE_RANKS is not valid',
[
- { aggType: METRIC_TYPES.PERCENTILE_RANKS } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.PERCENTILE_RANKS } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
null,
@@ -202,9 +246,12 @@ describe('convertMetricToColumns invalid cases', () => {
[
'null if supported agg SINGLE_PERCENTILE_RANK is not valid',
[
- { aggType: METRIC_TYPES.SINGLE_PERCENTILE_RANK } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.SINGLE_PERCENTILE_RANK } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
null,
@@ -213,9 +260,12 @@ describe('convertMetricToColumns invalid cases', () => {
[
'null if supported agg TOP_HITS is not valid',
[
- { aggType: METRIC_TYPES.TOP_HITS } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.TOP_HITS } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
null,
@@ -224,9 +274,12 @@ describe('convertMetricToColumns invalid cases', () => {
[
'null if supported agg TOP_METRICS is not valid',
[
- { aggType: METRIC_TYPES.TOP_METRICS } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.TOP_METRICS } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
null,
@@ -235,9 +288,12 @@ describe('convertMetricToColumns invalid cases', () => {
[
'null if supported agg CUMULATIVE_SUM is not valid',
[
- { aggType: METRIC_TYPES.CUMULATIVE_SUM } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.CUMULATIVE_SUM } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
null,
@@ -246,9 +302,12 @@ describe('convertMetricToColumns invalid cases', () => {
[
'null if supported agg DERIVATIVE is not valid',
[
- { aggType: METRIC_TYPES.DERIVATIVE } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.DERIVATIVE } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
null,
@@ -257,9 +316,12 @@ describe('convertMetricToColumns invalid cases', () => {
[
'null if supported agg MOVING_FN is not valid',
[
- { aggType: METRIC_TYPES.MOVING_FN } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.MOVING_FN } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
null,
@@ -268,9 +330,12 @@ describe('convertMetricToColumns invalid cases', () => {
[
'null if supported agg SUM_BUCKET is not valid',
[
- { aggType: METRIC_TYPES.SUM_BUCKET } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.SUM_BUCKET } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
null,
@@ -279,9 +344,12 @@ describe('convertMetricToColumns invalid cases', () => {
[
'null if supported agg MIN_BUCKET is not valid',
[
- { aggType: METRIC_TYPES.MIN_BUCKET } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.MIN_BUCKET } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
null,
@@ -290,9 +358,12 @@ describe('convertMetricToColumns invalid cases', () => {
[
'null if supported agg MAX_BUCKET is not valid',
[
- { aggType: METRIC_TYPES.MAX_BUCKET } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.MAX_BUCKET } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
null,
@@ -301,9 +372,12 @@ describe('convertMetricToColumns invalid cases', () => {
[
'null if supported agg AVG_BUCKET is not valid',
[
- { aggType: METRIC_TYPES.AVG_BUCKET } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.AVG_BUCKET } as SchemaConfig,
+ dataView,
+ aggs: [],
+ visType,
+ },
{ isPercentageMode: false },
],
null,
@@ -312,9 +386,12 @@ describe('convertMetricToColumns invalid cases', () => {
[
'null if supported agg SERIAL_DIFF is not valid',
[
- { aggType: METRIC_TYPES.SERIAL_DIFF } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.SERIAL_DIFF } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
null,
@@ -330,6 +407,7 @@ describe('convertMetricToColumns invalid cases', () => {
});
describe('convertMetricToColumns valid cases', () => {
const dataView = stubLogstashDataView;
+ const aggs: ExtendedColumnConverterArgs['aggs'] = [];
beforeEach(() => {
jest.clearAllMocks();
@@ -353,9 +431,12 @@ describe('convertMetricToColumns valid cases', () => {
[
'array of columns if supported agg AVG is valid',
[
- { aggType: METRIC_TYPES.AVG } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.AVG } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
result,
@@ -364,9 +445,12 @@ describe('convertMetricToColumns valid cases', () => {
[
'array of columns if supported agg MIN is valid',
[
- { aggType: METRIC_TYPES.MIN } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.MIN } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
result,
@@ -375,9 +459,12 @@ describe('convertMetricToColumns valid cases', () => {
[
'array of columns if supported agg MAX is valid',
[
- { aggType: METRIC_TYPES.MAX } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.MAX } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
result,
@@ -386,9 +473,12 @@ describe('convertMetricToColumns valid cases', () => {
[
'array of columns if supported agg SUM is valid',
[
- { aggType: METRIC_TYPES.SUM } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.SUM } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
result,
@@ -397,9 +487,12 @@ describe('convertMetricToColumns valid cases', () => {
[
'array of columns if supported agg COUNT is valid',
[
- { aggType: METRIC_TYPES.COUNT } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.COUNT } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
result,
@@ -408,9 +501,12 @@ describe('convertMetricToColumns valid cases', () => {
[
'array of columns if supported agg CARDINALITY is valid',
[
- { aggType: METRIC_TYPES.CARDINALITY } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.CARDINALITY } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
result,
@@ -419,9 +515,12 @@ describe('convertMetricToColumns valid cases', () => {
[
'array of columns if supported agg VALUE_COUNT is valid',
[
- { aggType: METRIC_TYPES.VALUE_COUNT } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.VALUE_COUNT } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
result,
@@ -430,9 +529,12 @@ describe('convertMetricToColumns valid cases', () => {
[
'array of columns if supported agg MEDIAN is valid',
[
- { aggType: METRIC_TYPES.MEDIAN } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.MEDIAN } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
result,
@@ -441,9 +543,12 @@ describe('convertMetricToColumns valid cases', () => {
[
'array of columns if supported agg STD_DEV is valid',
[
- { aggType: METRIC_TYPES.STD_DEV } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.STD_DEV } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
result,
@@ -452,9 +557,12 @@ describe('convertMetricToColumns valid cases', () => {
[
'array of columns if supported agg PERCENTILES is valid',
[
- { aggType: METRIC_TYPES.PERCENTILES } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.PERCENTILES } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
result,
@@ -463,9 +571,12 @@ describe('convertMetricToColumns valid cases', () => {
[
'array of columns if supported agg SINGLE_PERCENTILE is valid',
[
- { aggType: METRIC_TYPES.SINGLE_PERCENTILE } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.SINGLE_PERCENTILE } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
result,
@@ -474,9 +585,12 @@ describe('convertMetricToColumns valid cases', () => {
[
'array of columns if supported agg PERCENTILE_RANKS is valid',
[
- { aggType: METRIC_TYPES.PERCENTILE_RANKS } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.PERCENTILE_RANKS } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
result,
@@ -485,9 +599,12 @@ describe('convertMetricToColumns valid cases', () => {
[
'array of columns if supported agg SINGLE_PERCENTILE_RANK is valid',
[
- { aggType: METRIC_TYPES.SINGLE_PERCENTILE_RANK } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.SINGLE_PERCENTILE_RANK } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
result,
@@ -496,9 +613,12 @@ describe('convertMetricToColumns valid cases', () => {
[
'array of columns if supported agg TOP_HITS is valid',
[
- { aggType: METRIC_TYPES.TOP_HITS } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.TOP_HITS } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
result,
@@ -507,9 +627,12 @@ describe('convertMetricToColumns valid cases', () => {
[
'array of columns if supported agg TOP_METRICS is valid',
[
- { aggType: METRIC_TYPES.TOP_METRICS } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.TOP_METRICS } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
result,
@@ -518,9 +641,12 @@ describe('convertMetricToColumns valid cases', () => {
[
'array of columns if supported agg CUMULATIVE_SUM is valid',
[
- { aggType: METRIC_TYPES.CUMULATIVE_SUM } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.CUMULATIVE_SUM } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
result,
@@ -529,9 +655,12 @@ describe('convertMetricToColumns valid cases', () => {
[
'array of columns if supported agg DERIVATIVE is valid',
[
- { aggType: METRIC_TYPES.DERIVATIVE } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.DERIVATIVE } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
result,
@@ -540,9 +669,12 @@ describe('convertMetricToColumns valid cases', () => {
[
'array of columns if supported agg MOVING_FN is valid',
[
- { aggType: METRIC_TYPES.MOVING_FN } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.MOVING_FN } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
result,
@@ -551,9 +683,12 @@ describe('convertMetricToColumns valid cases', () => {
[
'array of columns if supported agg SUM_BUCKET is valid',
[
- { aggType: METRIC_TYPES.SUM_BUCKET } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.SUM_BUCKET } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
result,
@@ -562,9 +697,12 @@ describe('convertMetricToColumns valid cases', () => {
[
'array of columns if supported agg MIN_BUCKET is valid',
[
- { aggType: METRIC_TYPES.MIN_BUCKET } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.MIN_BUCKET } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
result,
@@ -573,9 +711,12 @@ describe('convertMetricToColumns valid cases', () => {
[
'array of columns if supported agg MAX_BUCKET is valid',
[
- { aggType: METRIC_TYPES.MAX_BUCKET } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.MAX_BUCKET } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
result,
@@ -584,9 +725,12 @@ describe('convertMetricToColumns valid cases', () => {
[
'array of columns if supported agg AVG_BUCKET is valid',
[
- { aggType: METRIC_TYPES.AVG_BUCKET } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.AVG_BUCKET } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: false },
],
result,
@@ -595,9 +739,12 @@ describe('convertMetricToColumns valid cases', () => {
[
'column in percentage mode without range if percentageMode is enabled ',
[
- { aggType: METRIC_TYPES.AVG_BUCKET } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.AVG_BUCKET } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: true, min: 0, max: 100 },
],
result,
@@ -606,9 +753,12 @@ describe('convertMetricToColumns valid cases', () => {
[
'column in percentage mode with range if percentageMode is enabled ',
[
- { aggType: METRIC_TYPES.AVG_BUCKET } as SchemaConfig,
- dataView,
- [],
+ {
+ agg: { aggType: METRIC_TYPES.AVG_BUCKET } as SchemaConfig,
+ dataView,
+ aggs,
+ visType,
+ },
{ isPercentageMode: true, min: 0, max: 100 },
],
result,
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/metrics/metrics.ts b/src/plugins/visualizations/common/convert_to_lens/lib/metrics/metrics.ts
index be4c92cd4ec7f..5d765a6f286ba 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/metrics/metrics.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/metrics/metrics.ts
@@ -7,8 +7,7 @@
*/
import { METRIC_TYPES } from '@kbn/data-plugin/common';
-import type { DataView } from '@kbn/data-views-plugin/common';
-import { PercentageModeConfig, SchemaConfig } from '../../..';
+import { PercentageModeConfig } from '../../..';
import {
convertMetricAggregationColumnWithoutSpecialParams,
convertToOtherParentPipelineAggColumns,
@@ -20,14 +19,13 @@ import {
convertToCumulativeSumAggColumn,
AggBasedColumn,
convertToColumnInPercentageMode,
+ ExtendedColumnConverterArgs,
} from '../convert';
import { SUPPORTED_METRICS } from '../convert/supported_metrics';
import { getValidColumns } from '../utils';
export const convertMetricToColumns = (
- agg: SchemaConfig,
- dataView: DataView,
- aggs: Array>,
+ { agg, dataView, aggs, visType }: ExtendedColumnConverterArgs,
percentageModeConfig: PercentageModeConfig = { isPercentageMode: false }
): AggBasedColumn[] | null => {
const supportedAgg = SUPPORTED_METRICS[agg.aggType];
@@ -38,7 +36,7 @@ export const convertMetricToColumns = (
if (percentageModeConfig.isPercentageMode) {
const { isPercentageMode, ...minMax } = percentageModeConfig;
- const formulaColumn = convertToColumnInPercentageMode({ agg, dataView, aggs }, minMax);
+ const formulaColumn = convertToColumnInPercentageMode({ agg, dataView, aggs, visType }, minMax);
return getValidColumns(formulaColumn);
}
@@ -54,6 +52,7 @@ export const convertMetricToColumns = (
const columns = convertMetricAggregationColumnWithoutSpecialParams(supportedAgg, {
agg,
dataView,
+ visType,
});
return getValidColumns(columns);
}
@@ -61,6 +60,7 @@ export const convertMetricToColumns = (
const columns = convertToStdDeviationFormulaColumns({
agg,
dataView,
+ visType,
});
return getValidColumns(columns);
}
@@ -68,6 +68,7 @@ export const convertMetricToColumns = (
const columns = convertToPercentileColumn({
agg,
dataView,
+ visType,
});
return getValidColumns(columns);
}
@@ -75,6 +76,7 @@ export const convertMetricToColumns = (
const columns = convertToPercentileColumn({
agg,
dataView,
+ visType,
});
return getValidColumns(columns);
}
@@ -82,6 +84,7 @@ export const convertMetricToColumns = (
const columns = convertToPercentileRankColumn({
agg,
dataView,
+ visType,
});
return getValidColumns(columns);
}
@@ -89,6 +92,7 @@ export const convertMetricToColumns = (
const columns = convertToPercentileRankColumn({
agg,
dataView,
+ visType,
});
return getValidColumns(columns);
}
@@ -97,6 +101,7 @@ export const convertMetricToColumns = (
const columns = convertToLastValueColumn({
agg,
dataView,
+ visType,
});
return getValidColumns(columns);
}
@@ -105,6 +110,7 @@ export const convertMetricToColumns = (
agg,
dataView,
aggs,
+ visType,
});
return getValidColumns(columns);
}
@@ -114,6 +120,7 @@ export const convertMetricToColumns = (
agg,
dataView,
aggs,
+ visType,
});
return getValidColumns(columns);
}
@@ -125,6 +132,7 @@ export const convertMetricToColumns = (
agg,
dataView,
aggs,
+ visType,
});
return getValidColumns(columns);
}
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/metrics/percentage_formula.test.ts b/src/plugins/visualizations/common/convert_to_lens/lib/metrics/percentage_formula.test.ts
index 9855ce44b6602..fe6204d1fb2a1 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/metrics/percentage_formula.test.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/metrics/percentage_formula.test.ts
@@ -24,6 +24,7 @@ jest.mock('../convert', () => ({
}));
describe('getPercentageColumnFormulaColumn', () => {
+ const visType = 'heatmap';
const dataView = stubLogstashDataView;
const field = stubLogstashDataView.fields[0].name;
const aggs: Array> = [
@@ -52,7 +53,7 @@ describe('getPercentageColumnFormulaColumn', () => {
>([
[
'null if cannot build formula for provided agg',
- [{ agg: aggs[0], aggs, dataView }],
+ [{ agg: aggs[0], aggs, dataView, visType }],
() => {
mockGetFormulaForAgg.mockReturnValue(null);
},
@@ -60,7 +61,7 @@ describe('getPercentageColumnFormulaColumn', () => {
],
[
'null if cannot create formula column for provided arguments',
- [{ agg: aggs[0], aggs, dataView }],
+ [{ agg: aggs[0], aggs, dataView, visType }],
() => {
mockGetFormulaForAgg.mockReturnValue('test-formula');
mockCreateFormulaColumn.mockReturnValue(null);
@@ -69,7 +70,7 @@ describe('getPercentageColumnFormulaColumn', () => {
],
[
'formula column if provided arguments are valid',
- [{ agg: aggs[0], aggs, dataView }],
+ [{ agg: aggs[0], aggs, dataView, visType }],
() => {
mockGetFormulaForAgg.mockReturnValue('test-formula');
mockCreateFormulaColumn.mockImplementation((formula) => ({
diff --git a/src/plugins/visualizations/common/convert_to_lens/lib/metrics/percentage_formula.ts b/src/plugins/visualizations/common/convert_to_lens/lib/metrics/percentage_formula.ts
index 773851a770db4..8d7194d5c25df 100644
--- a/src/plugins/visualizations/common/convert_to_lens/lib/metrics/percentage_formula.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/lib/metrics/percentage_formula.ts
@@ -14,8 +14,9 @@ export const getPercentageColumnFormulaColumn = ({
agg,
aggs,
dataView,
+ visType,
}: ExtendedColumnConverterArgs): FormulaColumn | null => {
- const metricFormula = getFormulaForAgg({ agg, aggs, dataView });
+ const metricFormula = getFormulaForAgg({ agg, aggs, dataView, visType });
if (!metricFormula) {
return null;
}
diff --git a/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts b/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts
index f62f61f0c50ab..8a6e70669dcf4 100644
--- a/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/types/configurations.ts
@@ -28,6 +28,9 @@ import {
GaugeCentralMajorModes,
CollapseFunctions,
} from '../constants';
+import { ExpressionValueVisDimension } from '../../expression_functions';
+
+export type ChartShapes = 'heatmap';
export type CollapseFunction = typeof CollapseFunctions[number];
@@ -277,9 +280,63 @@ export type GaugeVisConfiguration = GaugeState & {
layerType: typeof LayerTypes.DATA;
};
+export interface HeatmapLegendConfig {
+ isVisible: boolean;
+ position: Position;
+ maxLines?: number;
+ shouldTruncate?: boolean;
+ legendSize?: LegendSize;
+ type: 'heatmap_legend';
+}
+
+export interface HeatmapGridConfig {
+ strokeWidth?: number;
+ strokeColor?: string;
+ isCellLabelVisible: boolean;
+ isYAxisLabelVisible: boolean;
+ isYAxisTitleVisible: boolean;
+ yTitle?: string;
+ isXAxisLabelVisible: boolean;
+ isXAxisTitleVisible: boolean;
+ xTitle?: string;
+ type: 'heatmap_grid';
+}
+export interface HeatmapArguments {
+ percentageMode?: boolean;
+ lastRangeIsRightOpen?: boolean;
+ showTooltip?: boolean;
+ highlightInHover?: boolean;
+ palette?: PaletteOutput;
+ xAccessor?: string | ExpressionValueVisDimension;
+ yAccessor?: string | ExpressionValueVisDimension;
+ valueAccessor?: string | ExpressionValueVisDimension;
+ splitRowAccessor?: string | ExpressionValueVisDimension;
+ splitColumnAccessor?: string | ExpressionValueVisDimension;
+ legend: HeatmapLegendConfig;
+ gridConfig: HeatmapGridConfig;
+ ariaLabel?: string;
+}
+
+export type HeatmapLayerState = HeatmapArguments & {
+ layerId: string;
+ layerType: LayerType;
+ valueAccessor?: string;
+ xAccessor?: string;
+ yAccessor?: string;
+ shape: ChartShapes;
+};
+
+export type Palette = PaletteOutput & { accessor: string };
+
+export type HeatmapConfiguration = HeatmapLayerState & {
+ // need to store the current accessor to reset the color stops at accessor change
+ palette?: Palette;
+};
+
export type Configuration =
| XYConfiguration
| TableVisConfiguration
| PartitionVisConfiguration
| MetricVisConfiguration
- | GaugeVisConfiguration;
+ | GaugeVisConfiguration
+ | HeatmapConfiguration;
diff --git a/src/plugins/visualizations/common/convert_to_lens/types/params.ts b/src/plugins/visualizations/common/convert_to_lens/types/params.ts
index d66822921fb19..4623506496382 100644
--- a/src/plugins/visualizations/common/convert_to_lens/types/params.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/types/params.ts
@@ -55,7 +55,7 @@ interface Range {
export interface RangeParams extends FormatParams {
type: RangeMode;
maxBars: 'auto' | number;
- ranges: Range[];
+ ranges?: Range[];
includeEmptyRows?: boolean;
parentFormat?: {
id: string;
diff --git a/src/plugins/visualizations/common/convert_to_lens/utils.ts b/src/plugins/visualizations/common/convert_to_lens/utils.ts
index 6a875bf63bea4..88c2802c421ec 100644
--- a/src/plugins/visualizations/common/convert_to_lens/utils.ts
+++ b/src/plugins/visualizations/common/convert_to_lens/utils.ts
@@ -18,7 +18,17 @@ export const isAnnotationsLayer = (
export const getIndexPatternIds = (layers: Layer[]) =>
layers.map(({ indexPatternId }) => indexPatternId);
+const isValidFieldType = (
+ visType: string,
+ { supportedDataTypes }: SupportedMetric,
+ field: DataViewField
+) => {
+ const availableDataTypes = supportedDataTypes[visType] ?? supportedDataTypes.default;
+ return availableDataTypes.includes(field.type);
+};
+
export const isFieldValid = (
+ visType: string,
field: DataViewField | undefined,
aggregation: SupportedMetric
): field is DataViewField => {
@@ -26,7 +36,7 @@ export const isFieldValid = (
return false;
}
- if (field && (!field.aggregatable || !aggregation.supportedDataTypes.includes(field.type))) {
+ if (field && (!field.aggregatable || !isValidFieldType(visType, aggregation, field))) {
return false;
}
diff --git a/src/plugins/visualizations/public/convert_to_lens/index.ts b/src/plugins/visualizations/public/convert_to_lens/index.ts
index 73509d49157ae..46fca64199ae1 100644
--- a/src/plugins/visualizations/public/convert_to_lens/index.ts
+++ b/src/plugins/visualizations/public/convert_to_lens/index.ts
@@ -8,8 +8,10 @@
export { getColumnsFromVis } from './schemas';
export {
+ convertToFiltersColumn,
getPercentageColumnFormulaColumn,
getPalette,
+ getPaletteFromStopsWithColors,
getPercentageModeConfig,
createStaticValueColumn,
} from '../../common/convert_to_lens/lib';
diff --git a/src/plugins/visualizations/public/convert_to_lens/schemas.test.ts b/src/plugins/visualizations/public/convert_to_lens/schemas.test.ts
index 54975d08b8486..aa338db367988 100644
--- a/src/plugins/visualizations/public/convert_to_lens/schemas.test.ts
+++ b/src/plugins/visualizations/public/convert_to_lens/schemas.test.ts
@@ -70,7 +70,9 @@ describe('getColumnsFromVis', () => {
);
const aggConfig = new AggConfig(aggConfigs, {} as AggConfigOptions);
- const vis = {} as Vis;
+ const vis = {
+ type: { name: 'heatmap' },
+ } as Vis;
beforeEach(() => {
jest.clearAllMocks();
mockGetVisSchemas.mockReturnValue({});
diff --git a/src/plugins/visualizations/public/convert_to_lens/schemas.ts b/src/plugins/visualizations/public/convert_to_lens/schemas.ts
index 3a225e540faae..1b44f7cdffda1 100644
--- a/src/plugins/visualizations/public/convert_to_lens/schemas.ts
+++ b/src/plugins/visualizations/public/convert_to_lens/schemas.ts
@@ -33,6 +33,7 @@ const areVisSchemasValid = (visSchemas: Schemas, unsupported: Array>,
metricsForLayer: Array>,
@@ -52,7 +53,7 @@ const createLayer = (
dropEmptyRowsInDateHistogram?: boolean
) => {
const metricColumns = metricsForLayer.flatMap((m) =>
- convertMetricToColumns(m, dataView, allMetrics, percentageModeConfig)
+ convertMetricToColumns({ agg: m, dataView, aggs: allMetrics, visType }, percentageModeConfig)
);
if (metricColumns.includes(null)) {
return null;
@@ -60,6 +61,7 @@ const createLayer = (
const metricColumnsWithoutNull = metricColumns as AggBasedColumn[];
const { customBucketColumns, customBucketsMap } = getCustomBucketColumns(
+ visType,
customBucketsWithMetricIds,
metricColumnsWithoutNull,
dataView,
@@ -72,6 +74,7 @@ const createLayer = (
}
const bucketColumns = getBucketColumns(
+ visType,
visSchemas,
buckets,
dataView,
@@ -84,6 +87,7 @@ const createLayer = (
}
const splitBucketColumns = getBucketColumns(
+ visType,
visSchemas,
splits,
dataView,
@@ -181,6 +185,7 @@ export const getColumnsFromVis = (
c.metricIds.some((m) => metricAggIds.includes(m))
);
const layer = createLayer(
+ vis.type.name,
visSchemas,
aggs,
metrics,
@@ -197,6 +202,7 @@ export const getColumnsFromVis = (
}
} else {
const layer = createLayer(
+ vis.type.name,
visSchemas,
aggs,
aggs,
diff --git a/src/plugins/visualizations/public/convert_to_lens/utils.test.ts b/src/plugins/visualizations/public/convert_to_lens/utils.test.ts
index 50f667430a8cb..8c36b28452271 100644
--- a/src/plugins/visualizations/public/convert_to_lens/utils.test.ts
+++ b/src/plugins/visualizations/public/convert_to_lens/utils.test.ts
@@ -213,6 +213,7 @@ describe('getBucketCollapseFn', () => {
describe('getBucketColumns', () => {
const dataView = stubLogstashDataView;
+ const visType = 'heatmap';
beforeEach(() => {
jest.clearAllMocks();
@@ -228,7 +229,7 @@ describe('getBucketColumns', () => {
[bucketKey]: [],
};
- expect(getBucketColumns(visSchemas, keys, dataView, false, [])).toEqual([]);
+ expect(getBucketColumns(visType, visSchemas, keys, dataView, false, [])).toEqual([]);
expect(mockConvertBucketToColumns).toBeCalledTimes(0);
});
@@ -254,7 +255,7 @@ describe('getBucketColumns', () => {
};
mockConvertBucketToColumns.mockReturnValueOnce(null);
- expect(getBucketColumns(visSchemas, keys, dataView, false, [])).toBeNull();
+ expect(getBucketColumns(visType, visSchemas, keys, dataView, false, [])).toBeNull();
expect(mockConvertBucketToColumns).toBeCalledTimes(1);
});
@@ -280,7 +281,7 @@ describe('getBucketColumns', () => {
};
mockConvertBucketToColumns.mockReturnValueOnce([null]);
- expect(getBucketColumns(visSchemas, keys, dataView, false, [])).toBeNull();
+ expect(getBucketColumns(visType, visSchemas, keys, dataView, false, [])).toBeNull();
expect(mockConvertBucketToColumns).toBeCalledTimes(1);
});
test('should return columns', () => {
@@ -319,7 +320,7 @@ describe('getBucketColumns', () => {
mockConvertBucketToColumns.mockReturnValue(returnValue);
- expect(getBucketColumns(visSchemas, keys, dataView, false, [])).toEqual([
+ expect(getBucketColumns(visType, visSchemas, keys, dataView, false, [])).toEqual([
...returnValue,
...returnValue,
]);
@@ -592,6 +593,8 @@ describe('sortColumns', () => {
});
describe('getColumnIds', () => {
+ const visType = 'heatmap';
+
const colId1 = '0_agg_id';
const colId2 = '1_agg_id';
const colId3 = '2_agg_id';
@@ -694,6 +697,7 @@ describe('getColumnIds', () => {
});
expect(
getCustomBucketColumns(
+ visType,
customBucketsWithMetricIds,
[
{ columnId: 'col-3', meta: { aggId: '3' } },
diff --git a/src/plugins/visualizations/public/convert_to_lens/utils.ts b/src/plugins/visualizations/public/convert_to_lens/utils.ts
index ba05d29cdeea9..531746ff86d87 100644
--- a/src/plugins/visualizations/public/convert_to_lens/utils.ts
+++ b/src/plugins/visualizations/public/convert_to_lens/utils.ts
@@ -63,6 +63,7 @@ export const getBucketCollapseFn = (
};
export const getBucketColumns = (
+ visType: string,
visSchemas: Schemas,
keys: Array,
dataView: DataView,
@@ -78,6 +79,7 @@ export const getBucketColumns = (
{
agg: m,
dataView,
+ visType,
metricColumns,
aggs: visSchemas.metric as Array>,
},
@@ -154,6 +156,7 @@ export const sortColumns = (
export const getColumnIds = (columns: AggBasedColumn[]) => columns.map(({ columnId }) => columnId);
export const getCustomBucketColumns = (
+ visType: string,
customBucketsWithMetricIds: Array<{
customBucket: IAggConfig;
metricIds: string[];
@@ -167,7 +170,7 @@ export const getCustomBucketColumns = (
const customBucketsMap: Record = {};
customBucketsWithMetricIds.forEach((customBucketWithMetricIds) => {
const customBucketColumn = convertBucketToColumns(
- { agg: customBucketWithMetricIds.customBucket, dataView, metricColumns, aggs },
+ { agg: customBucketWithMetricIds.customBucket, dataView, metricColumns, aggs, visType },
true,
dropEmptyRowsInDateHistogram
);
diff --git a/src/plugins/visualizations/public/visualize_app/components/visualize_byvalue_editor.tsx b/src/plugins/visualizations/public/visualize_app/components/visualize_byvalue_editor.tsx
index b9ff8d98f2ced..8cc220e77c8bc 100644
--- a/src/plugins/visualizations/public/visualize_app/components/visualize_byvalue_editor.tsx
+++ b/src/plugins/visualizations/public/visualize_app/components/visualize_byvalue_editor.tsx
@@ -110,6 +110,7 @@ export const VisualizeByValueEditor = ({ onAppLeave }: VisualizeAppProps) => {
visEditorRef={visEditorRef}
embeddableId={embeddableId}
onAppLeave={onAppLeave}
+ eventEmitter={eventEmitter}
/>
);
};
diff --git a/src/plugins/visualizations/public/visualize_app/components/visualize_editor.tsx b/src/plugins/visualizations/public/visualize_app/components/visualize_editor.tsx
index 480f0c3d36ee1..221cdcc9d8e10 100644
--- a/src/plugins/visualizations/public/visualize_app/components/visualize_editor.tsx
+++ b/src/plugins/visualizations/public/visualize_app/components/visualize_editor.tsx
@@ -110,6 +110,7 @@ export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => {
visEditorRef={visEditorRef}
onAppLeave={onAppLeave}
embeddableId={embeddableIdValue}
+ eventEmitter={eventEmitter}
/>
);
};
diff --git a/src/plugins/visualizations/public/visualize_app/components/visualize_editor_common.tsx b/src/plugins/visualizations/public/visualize_app/components/visualize_editor_common.tsx
index 4598d2d23e613..7fa6418aa261b 100644
--- a/src/plugins/visualizations/public/visualize_app/components/visualize_editor_common.tsx
+++ b/src/plugins/visualizations/public/visualize_app/components/visualize_editor_common.tsx
@@ -7,6 +7,7 @@
*/
import './visualize_editor.scss';
+import { EventEmitter } from 'events';
import React, { RefObject, useCallback, useEffect } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
@@ -48,6 +49,7 @@ interface VisualizeEditorCommonProps {
originatingPath?: string;
visualizationIdFromUrl?: string;
embeddableId?: string;
+ eventEmitter?: EventEmitter;
}
export const VisualizeEditorCommon = ({
@@ -66,6 +68,7 @@ export const VisualizeEditorCommon = ({
visualizationIdFromUrl,
embeddableId,
visEditorRef,
+ eventEmitter,
}: VisualizeEditorCommonProps) => {
const { services } = useKibana();
@@ -148,6 +151,7 @@ export const VisualizeEditorCommon = ({
visualizationIdFromUrl={visualizationIdFromUrl}
embeddableId={embeddableId}
onAppLeave={onAppLeave}
+ eventEmitter={eventEmitter}
/>
)}
{visInstance?.vis?.type?.stage === 'experimental' &&
diff --git a/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx b/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx
index 0111c9026397d..2deffa0c511b3 100644
--- a/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx
+++ b/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx
@@ -7,7 +7,7 @@
*/
import React, { memo, useCallback, useMemo, useState, useEffect } from 'react';
-
+import { EventEmitter } from 'events';
import { AppMountParameters, OverlayRef } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import useLocalStorage from 'react-use/lib/useLocalStorage';
@@ -40,6 +40,7 @@ interface VisualizeTopNavProps {
visualizationIdFromUrl?: string;
embeddableId?: string;
onAppLeave: AppMountParameters['onAppLeave'];
+ eventEmitter?: EventEmitter;
}
const TopNav = ({
@@ -57,6 +58,7 @@ const TopNav = ({
visualizationIdFromUrl,
embeddableId,
onAppLeave,
+ eventEmitter,
}: VisualizeTopNavProps) => {
const { services } = useKibana();
const { TopNavMenu } = services.navigation.ui;
@@ -116,6 +118,7 @@ const TopNav = ({
uiStateJSON?.vis,
uiStateJSON?.table,
vis.data.indexPattern,
+ eventEmitter,
]);
const displayEditInLensItem = Boolean(vis.type.navigateToLens && editInLensConfig);
@@ -140,6 +143,7 @@ const TopNav = ({
hideLensBadge,
setNavigateToLens,
showBadge: !hideTryInLensBadge && displayEditInLensItem,
+ eventEmitter,
},
services
);
@@ -162,6 +166,7 @@ const TopNav = ({
displayEditInLensItem,
hideLensBadge,
hideTryInLensBadge,
+ eventEmitter,
]);
const [indexPatterns, setIndexPatterns] = useState([]);
const showDatePicker = () => {
diff --git a/src/plugins/visualizations/public/visualize_app/utils/get_top_nav_config.tsx b/src/plugins/visualizations/public/visualize_app/utils/get_top_nav_config.tsx
index 36b92585f1096..cab3d41ff8266 100644
--- a/src/plugins/visualizations/public/visualize_app/utils/get_top_nav_config.tsx
+++ b/src/plugins/visualizations/public/visualize_app/utils/get_top_nav_config.tsx
@@ -8,6 +8,7 @@
import React from 'react';
import moment from 'moment';
+import EventEmitter from 'events';
import { i18n } from '@kbn/i18n';
import { EuiBetaBadgeProps } from '@elastic/eui';
import { parse } from 'query-string';
@@ -71,6 +72,7 @@ export interface TopNavConfigParams {
hideLensBadge: () => void;
setNavigateToLens: (flag: boolean) => void;
showBadge: boolean;
+ eventEmitter?: EventEmitter;
}
const SavedObjectSaveModalDashboard = withSuspense(LazySavedObjectSaveModalDashboard);
@@ -102,6 +104,7 @@ export const getTopNavConfig = (
hideLensBadge,
setNavigateToLens,
showBadge,
+ eventEmitter,
}: TopNavConfigParams,
{
data,
@@ -301,6 +304,10 @@ export const getTopNavConfig = (
},
}),
run: async () => {
+ // lens doesn't support saved searches, should unlink before transition
+ if (eventEmitter && visInstance.vis.data.savedSearchId) {
+ eventEmitter.emit('unlinkFromSavedSearch', false);
+ }
const updatedWithMeta = {
...editInLensConfig,
savedObjectId: visInstance.vis.id,
diff --git a/src/plugins/visualizations/public/visualize_app/utils/use/use_linked_search_updates.ts b/src/plugins/visualizations/public/visualize_app/utils/use/use_linked_search_updates.ts
index 8d7f2a8ef61f4..ffd23ec06aea6 100644
--- a/src/plugins/visualizations/public/visualize_app/utils/use/use_linked_search_updates.ts
+++ b/src/plugins/visualizations/public/visualize_app/utils/use/use_linked_search_updates.ts
@@ -29,7 +29,7 @@ export const useLinkedSearchUpdates = (
// SearchSource is a promise-based stream of search results that can inherit from other search sources.
const { searchSource } = visInstance.vis.data;
- const unlinkFromSavedSearch = () => {
+ const unlinkFromSavedSearch = (showToast: boolean = true) => {
const searchSourceParent = savedSearch.searchSource;
const searchSourceGrandparent = searchSourceParent?.getParent();
const currentIndex = searchSourceParent?.getField('index');
@@ -44,14 +44,16 @@ export const useLinkedSearchUpdates = (
parentFilters: (searchSourceParent?.getOwnField('filter') as Filter[]) || [],
});
- services.toastNotifications.addSuccess(
- i18n.translate('visualizations.linkedToSearch.unlinkSuccessNotificationText', {
- defaultMessage: `Unlinked from saved search '{searchTitle}'`,
- values: {
- searchTitle: savedSearch.title,
- },
- })
- );
+ if (showToast) {
+ services.toastNotifications.addSuccess(
+ i18n.translate('visualizations.linkedToSearch.unlinkSuccessNotificationText', {
+ defaultMessage: `Unlinked from saved search '{searchTitle}'`,
+ values: {
+ searchTitle: savedSearch.title,
+ },
+ })
+ );
+ }
};
eventEmitter.on('unlinkFromSavedSearch', unlinkFromSavedSearch);
diff --git a/x-pack/examples/screenshotting_example/server/plugin.ts b/x-pack/examples/screenshotting_example/server/plugin.ts
index 9ca74c6e16353..16a766558ff3f 100644
--- a/x-pack/examples/screenshotting_example/server/plugin.ts
+++ b/x-pack/examples/screenshotting_example/server/plugin.ts
@@ -38,6 +38,7 @@ export class ScreenshottingExamplePlugin implements Plugin {
);
return response.ok({
+ headers: { 'content-type': 'application/json' },
body: JSON.stringify({
metrics,
image: results[0]?.screenshots[0]?.data.toString('base64'),
diff --git a/x-pack/plugins/aiops/server/routes/queries/fetch_frequent_items.ts b/x-pack/plugins/aiops/server/routes/queries/fetch_frequent_items.ts
index aaf9af283c3e1..362cae07273e5 100644
--- a/x-pack/plugins/aiops/server/routes/queries/fetch_frequent_items.ts
+++ b/x-pack/plugins/aiops/server/routes/queries/fetch_frequent_items.ts
@@ -13,6 +13,8 @@ import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
import type { Logger } from '@kbn/logging';
import type { ChangePoint, FieldValuePair } from '@kbn/ml-agg-utils';
+const FREQUENT_ITEMS_FIELDS_LIMIT = 15;
+
interface FrequentItemsAggregation extends estypes.AggregationsSamplerAggregation {
fi: {
buckets: Array<{ key: Record; doc_count: number; support: number }>;
@@ -59,10 +61,19 @@ export async function fetchFrequentItems(
emitError: (m: string) => void,
abortSignal?: AbortSignal
) {
- // get unique fields from change points
- const fields = [...new Set(changePoints.map((t) => t.fieldName))];
+ // Sort change points by ascending p-value, necessary to apply the field limit correctly.
+ const sortedChangePoints = changePoints.slice().sort((a, b) => {
+ return (a.pValue ?? 0) - (b.pValue ?? 0);
+ });
+
+ // Get up to 15 unique fields from change points with retained order
+ const fields = sortedChangePoints.reduce((p, c) => {
+ if (p.length < FREQUENT_ITEMS_FIELDS_LIMIT && !p.some((d) => d === c.fieldName)) {
+ p.push(c.fieldName);
+ }
+ return p;
+ }, []);
- // TODO add query params
const query = {
bool: {
minimum_should_match: 2,
@@ -77,7 +88,7 @@ export async function fetchFrequentItems(
},
},
],
- should: changePoints.map((t) => {
+ should: sortedChangePoints.map((t) => {
return { term: { [t.fieldName]: t.fieldValue } };
}),
},
@@ -117,16 +128,18 @@ export async function fetchFrequentItems(
},
};
+ const esBody = {
+ query,
+ aggs,
+ size: 0,
+ track_total_hits: true,
+ };
+
const body = await client.search(
{
index,
size: 0,
- body: {
- query,
- aggs,
- size: 0,
- track_total_hits: true,
- },
+ body: esBody,
},
{ signal: abortSignal, maxRetries: 0 }
);
@@ -167,7 +180,7 @@ export async function fetchFrequentItems(
Object.entries(fis.key).forEach(([key, value]) => {
result.set[key] = value[0];
- const pValue = changePoints.find(
+ const pValue = sortedChangePoints.find(
(t) => t.fieldName === key && t.fieldValue === value[0]
)?.pValue;
diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts
index f1917a079a26d..9326f30dd7828 100644
--- a/x-pack/plugins/alerting/server/types.ts
+++ b/x-pack/plugins/alerting/server/types.ts
@@ -90,21 +90,21 @@ export interface RuleExecutorOptions<
InstanceContext extends AlertInstanceContext = never,
ActionGroupIds extends string = never
> {
- alertId: string;
+ alertId: string; // Is actually the Rule ID. Will be updated as part of https://github.com/elastic/kibana/issues/100115
+ createdBy: string | null;
executionId: string;
- startedAt: Date;
- previousStartedAt: Date | null;
- services: RuleExecutorServices;
+ logger: Logger;
+ name: string;
params: Params;
- state: State;
+ previousStartedAt: Date | null;
rule: SanitizedRuleConfig;
+ services: RuleExecutorServices;
spaceId: string;
- namespace?: string;
- name: string;
+ startedAt: Date;
+ state: State;
tags: string[];
- createdBy: string | null;
updatedBy: string | null;
- logger: Logger;
+ namespace?: string;
}
export interface RuleParamsAndRefs {
diff --git a/x-pack/plugins/apm/common/critical_path/get_critical_path.test.ts b/x-pack/plugins/apm/common/critical_path/get_critical_path.test.ts
new file mode 100644
index 0000000000000..38d1b0a3da1ca
--- /dev/null
+++ b/x-pack/plugins/apm/common/critical_path/get_critical_path.test.ts
@@ -0,0 +1,274 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { apm, ApmFields, dedot } from '@kbn/apm-synthtrace';
+import { getWaterfall } from '../../public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers';
+import { Span } from '../../typings/es_schemas/ui/span';
+import { Transaction } from '../../typings/es_schemas/ui/transaction';
+import { getCriticalPath } from './get_critical_path';
+
+describe('getCriticalPath', () => {
+ function getCriticalPathFromEvents(events: ApmFields[]) {
+ const waterfall = getWaterfall(
+ {
+ traceDocs: events.map(
+ (event) => dedot(event, {}) as Transaction | Span
+ ),
+ errorDocs: [],
+ exceedsMax: false,
+ linkedChildrenOfSpanCountBySpanId: {},
+ },
+ events[0]['transaction.id']!
+ );
+
+ return {
+ waterfall,
+ criticalPath: getCriticalPath(waterfall),
+ };
+ }
+ it('adds the only active span to the critical path', () => {
+ const service = apm.service('a', 'development', 'java').instance('a');
+
+ const {
+ criticalPath: { segments },
+ waterfall,
+ } = getCriticalPathFromEvents(
+ service
+ .transaction('/service-a')
+ .timestamp(1)
+ .duration(100)
+ .children(
+ service.span('foo', 'external', 'db').duration(100).timestamp(1)
+ )
+ .serialize()
+ );
+
+ expect(segments).toEqual([
+ { self: false, duration: 100000, item: waterfall.items[0], offset: 0 },
+ { self: false, duration: 100000, item: waterfall.items[1], offset: 0 },
+ { self: true, duration: 100000, item: waterfall.items[1], offset: 0 },
+ ]);
+ });
+
+ it('adds the span that ended last', () => {
+ const service = apm.service('a', 'development', 'java').instance('a');
+
+ const {
+ criticalPath: { segments },
+ waterfall,
+ } = getCriticalPathFromEvents(
+ service
+ .transaction('/service-a')
+ .timestamp(1)
+ .duration(100)
+ .children(
+ service.span('foo', 'external', 'db').duration(99).timestamp(1),
+ service.span('bar', 'external', 'db').duration(100).timestamp(1)
+ )
+ .serialize()
+ );
+
+ const longerSpan = waterfall.items.find(
+ (item) => (item.doc as Span).span?.name === 'bar'
+ );
+
+ expect(segments).toEqual([
+ { self: false, duration: 100000, item: waterfall.items[0], offset: 0 },
+ {
+ self: false,
+ duration: 100000,
+ item: longerSpan,
+ offset: 0,
+ },
+ { self: true, duration: 100000, item: longerSpan, offset: 0 },
+ ]);
+ });
+
+ it('adds segment for uninstrumented gaps in the parent', () => {
+ const service = apm.service('a', 'development', 'java').instance('a');
+
+ const {
+ criticalPath: { segments },
+ waterfall,
+ } = getCriticalPathFromEvents(
+ service
+ .transaction('/service-a')
+ .timestamp(1)
+ .duration(100)
+ .children(
+ service.span('foo', 'external', 'db').duration(50).timestamp(11)
+ )
+ .serialize()
+ );
+
+ expect(
+ segments.map((segment) => ({
+ self: segment.self,
+ duration: segment.duration,
+ id: segment.item.id,
+ offset: segment.offset,
+ }))
+ ).toEqual([
+ { self: false, duration: 100000, id: waterfall.items[0].id, offset: 0 },
+ {
+ self: true,
+ duration: 40000,
+ id: waterfall.items[0].id,
+ offset: 60000,
+ },
+ {
+ self: false,
+ duration: 50000,
+ id: waterfall.items[1].id,
+ offset: 10000,
+ },
+ {
+ self: true,
+ duration: 50000,
+ id: waterfall.items[1].id,
+ offset: 10000,
+ },
+ {
+ self: true,
+ duration: 10000,
+ offset: 0,
+ id: waterfall.items[0].id,
+ },
+ ]);
+ });
+
+ it('only considers a single child to be active at the same time', () => {
+ const service = apm.service('a', 'development', 'java').instance('a');
+
+ const {
+ criticalPath: { segments },
+ waterfall,
+ } = getCriticalPathFromEvents(
+ service
+ .transaction('s1')
+ .timestamp(1)
+ .duration(100)
+ .children(
+ service.span('s2', 'external', 'db').duration(1).timestamp(1),
+ service.span('s3', 'external', 'db').duration(1).timestamp(2),
+ service.span('s4', 'external', 'db').duration(98).timestamp(3),
+ service
+ .span('s5', 'external', 'db')
+ .duration(98)
+ .timestamp(1)
+ .children(
+ service.span('s6', 'external', 'db').duration(30).timestamp(5),
+ service.span('s7', 'external', 'db').duration(30).timestamp(35)
+ )
+ )
+ .serialize()
+ );
+
+ const [_s1, s2, _s5, _s6, _s7, s3, s4] = waterfall.items;
+
+ expect(
+ segments
+ .map((segment) => ({
+ self: segment.self,
+ duration: segment.duration,
+ id: segment.item.id,
+ offset: segment.offset,
+ }))
+ .filter((segment) => segment.self)
+ .map((segment) => segment.id)
+ ).toEqual([s4.id, s3.id, s2.id]);
+ });
+
+ // https://www.uber.com/en-NL/blog/crisp-critical-path-analysis-for-microservice-architectures/
+ it('correctly returns the critical path for the CRISP example', () => {
+ const service = apm.service('a', 'development', 'java').instance('a');
+
+ const {
+ criticalPath: { segments },
+ waterfall,
+ } = getCriticalPathFromEvents(
+ service
+ .transaction('s1')
+ .timestamp(1)
+ .duration(100)
+ .children(
+ service.span('s2', 'external', 'db').duration(25).timestamp(6),
+ service
+ .span('s3', 'external', 'db')
+ .duration(50)
+ .timestamp(41)
+ .children(
+ service.span('s4', 'external', 'db').duration(20).timestamp(61),
+ service.span('s5', 'external', 'db').duration(30).timestamp(51)
+ )
+ )
+ .serialize()
+ );
+
+ const [s1, s2, s3, s5, _s4] = waterfall.items;
+
+ expect(
+ segments
+ .map((segment) => ({
+ self: segment.self,
+ duration: segment.duration,
+ id: segment.item.id,
+ offset: segment.offset,
+ }))
+ .filter((segment) => segment.self)
+ ).toEqual([
+ // T9-T10
+ {
+ self: true,
+ duration: 10000,
+ id: s1.id,
+ offset: 90000,
+ },
+ // T8-T9
+ {
+ self: true,
+ duration: 10000,
+ id: s3.id,
+ offset: 80000,
+ },
+ // T5-T8
+ {
+ self: true,
+ duration: s5.duration,
+ id: s5.id,
+ offset: s5.offset,
+ },
+ // T4-T5
+ {
+ self: true,
+ duration: 10000,
+ id: s3.id,
+ offset: 40000,
+ },
+ // T3-T4
+ {
+ self: true,
+ duration: 10000,
+ id: s1.id,
+ offset: 30000,
+ },
+ // T2-T3
+ {
+ self: true,
+ duration: 25000,
+ id: s2.id,
+ offset: 5000,
+ },
+ // T1-T2
+ {
+ duration: 5000,
+ id: s1.id,
+ offset: 0,
+ self: true,
+ },
+ ]);
+ });
+});
diff --git a/x-pack/plugins/apm/common/critical_path/get_critical_path.ts b/x-pack/plugins/apm/common/critical_path/get_critical_path.ts
new file mode 100644
index 0000000000000..c517548bf3d1f
--- /dev/null
+++ b/x-pack/plugins/apm/common/critical_path/get_critical_path.ts
@@ -0,0 +1,134 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type {
+ IWaterfall,
+ IWaterfallSpanOrTransaction,
+} from '../../public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers';
+import { CriticalPath, CriticalPathSegment } from './types';
+
+export function getCriticalPath(waterfall: IWaterfall): CriticalPath {
+ const segments: CriticalPathSegment[] = [];
+
+ function scan({
+ item,
+ start,
+ end,
+ }: {
+ item: IWaterfallSpanOrTransaction;
+ start: number;
+ end: number;
+ }): void {
+ segments.push({
+ self: false,
+ duration: end - start,
+ item,
+ offset: start,
+ });
+ const directChildren = waterfall.childrenByParentId[item.id];
+
+ if (directChildren && directChildren.length > 0) {
+ // We iterate over all the item's direct children. The one that
+ // ends last is the first item in the array.
+ const orderedChildren = directChildren.concat().sort((a, b) => {
+ const endTimeA = a.offset + a.skew + a.duration;
+ const endTimeB = b.offset + b.skew + b.duration;
+ return endTimeB - endTimeA;
+ });
+
+ // For each point in time, determine what child is on the critical path.
+ // We start scanning at the end. Once we've decided what the child on the
+ // critical path is, scan its children, from the start time of that span
+ // until the end. The next scan time is the start time of the child that was
+ // on the critical path.
+ let scanTime = end;
+
+ orderedChildren.forEach((child) => {
+ const normalizedChildStart = Math.max(child.offset + child.skew, start);
+ const childEnd = child.offset + child.skew + child.duration;
+
+ // if a span ends before the current scan time, use the current
+ // scan time as when the child ended. We don't want to scan further
+ // than the scan time. This prevents overlap in the critical path.
+ const normalizedChildEnd = Math.min(childEnd, scanTime);
+
+ const isOnCriticalPath = !(
+ // A span/tx is NOT on the critical path if:
+ // - The start time is equal to or greater than the current scan time.
+ // Otherwise, spans that started at the same time will all contribute to
+ // the critical path, but we only want one to contribute.
+ // - The span/tx ends before the start of the initial scan period.
+ // - The span ends _after_ the current scan time.
+
+ (
+ normalizedChildStart >= scanTime ||
+ normalizedChildEnd < start ||
+ childEnd > scanTime
+ )
+ );
+
+ if (!isOnCriticalPath) {
+ return;
+ }
+
+ if (normalizedChildEnd < scanTime - 1000) {
+ // This span is on the critical path, but it ended before the scan time.
+ // This means that there is a gap, so we add a segment to the critical path
+ // for the _parent_. There's a slight offset because we don't want really small
+ // segments that can be reasonably attributed to clock skew.
+ segments.push({
+ item,
+ duration: scanTime - normalizedChildEnd,
+ offset: normalizedChildEnd,
+ self: true,
+ });
+ }
+
+ // scan this child for the period we're considering it to be on the critical path
+ scan({
+ start: normalizedChildStart,
+ end: childEnd,
+ item: child,
+ });
+
+ // set the scan time to the start of the span, and scan the next child
+ scanTime = normalizedChildStart;
+ });
+
+ // there's an unattributed gap at the start, so add a segment for the parent as well
+ if (scanTime > start) {
+ segments.push({
+ item,
+ offset: start,
+ duration: scanTime - start,
+ self: true,
+ });
+ }
+ } else {
+ // for the entire scan period, add this item to the critical path
+ segments.push({
+ item,
+ offset: start,
+ duration: end - start,
+ self: true,
+ });
+ }
+ }
+
+ if (waterfall.entryWaterfallTransaction) {
+ const start =
+ waterfall.entryWaterfallTransaction.skew +
+ waterfall.entryWaterfallTransaction.offset;
+ scan({
+ item: waterfall.entryWaterfallTransaction,
+ start,
+ end: start + waterfall.entryWaterfallTransaction.duration,
+ });
+ }
+
+ return { segments };
+}
diff --git a/x-pack/plugins/apm/common/critical_path/types.ts b/x-pack/plugins/apm/common/critical_path/types.ts
new file mode 100644
index 0000000000000..56f3db04e866f
--- /dev/null
+++ b/x-pack/plugins/apm/common/critical_path/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 { IWaterfallSpanOrTransaction } from '../../public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers';
+
+export interface CriticalPathSegment {
+ item: IWaterfallSpanOrTransaction;
+ offset: number;
+ duration: number;
+ self: boolean;
+}
+
+export interface CriticalPath {
+ segments: CriticalPathSegment[];
+}
diff --git a/x-pack/plugins/apm/public/components/app/dependency_detail_operations/dependency_detail_operations_list/index.tsx b/x-pack/plugins/apm/public/components/app/dependency_detail_operations/dependency_detail_operations_list/index.tsx
index ecef396bb0c5d..792f3f0aece25 100644
--- a/x-pack/plugins/apm/public/components/app/dependency_detail_operations/dependency_detail_operations_list/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/dependency_detail_operations/dependency_detail_operations_list/index.tsx
@@ -23,6 +23,7 @@ import { ITableColumn, ManagedTable } from '../../../shared/managed_table';
import { getComparisonEnabled } from '../../../shared/time_comparison/get_comparison_enabled';
import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip';
import { DependencyOperationDetailLink } from '../../dependency_operation_detail_view/dependency_operation_detail_link';
+import { TransactionTab } from '../../transaction_details/waterfall_with_summary/transaction_tabs';
interface OperationStatisticsItem extends SpanMetricGroup {
spanName: string;
@@ -35,7 +36,14 @@ function OperationLink({ spanName }: { spanName: string }) {
}
+ content={
+
+ }
/>
);
}
diff --git a/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/dependency_operation_detail_trace_list.tsx b/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/dependency_operation_detail_trace_list.tsx
index da4c603ff283e..4dde4af56ccf3 100644
--- a/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/dependency_operation_detail_trace_list.tsx
+++ b/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/dependency_operation_detail_trace_list.tsx
@@ -9,22 +9,28 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiLink,
+ EuiRadio,
EuiText,
EuiTitle,
RIGHT_ALIGNMENT,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
+import { useHistory } from 'react-router-dom';
import { ValuesType } from 'utility-types';
import { EventOutcome } from '../../../../common/event_outcome';
import { asMillisecondDuration } from '../../../../common/utils/formatters';
import { useApmParams } from '../../../hooks/use_apm_params';
import { useApmRouter } from '../../../hooks/use_apm_router';
-import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
+import { FetcherResult, FETCH_STATUS } from '../../../hooks/use_fetcher';
import { useTheme } from '../../../hooks/use_theme';
-import { useTimeRange } from '../../../hooks/use_time_range';
import { APIReturnType } from '../../../services/rest/create_call_apm_api';
-import { ITableColumn, ManagedTable } from '../../shared/managed_table';
+import { push } from '../../shared/links/url_helpers';
+import {
+ ITableColumn,
+ ManagedTable,
+ SortFunction,
+} from '../../shared/managed_table';
import { ServiceLink } from '../../shared/service_link';
import { TimestampTooltip } from '../../shared/timestamp_tooltip';
@@ -32,15 +38,23 @@ type DependencySpan = ValuesType<
APIReturnType<'GET /internal/apm/dependencies/operations/spans'>['spans']
>;
-export function DependencyOperationDetailTraceList() {
+export function DependencyOperationDetailTraceList({
+ spanFetch,
+ sortFn,
+}: {
+ spanFetch: FetcherResult<
+ APIReturnType<'GET /internal/apm/dependencies/operations/spans'>
+ >;
+ sortFn: SortFunction;
+}) {
const router = useApmRouter();
+ const history = useHistory();
+
const theme = useTheme();
const {
query: {
- dependencyName,
- spanName,
comparisonEnabled,
environment,
offset,
@@ -49,8 +63,11 @@ export function DependencyOperationDetailTraceList() {
refreshInterval,
refreshPaused,
kuery,
- sampleRangeFrom,
- sampleRangeTo,
+ sortField = '@timestamp',
+ sortDirection = 'desc',
+ pageSize = 10,
+ page = 1,
+ spanId,
},
} = useApmParams('/dependencies/operation');
@@ -84,6 +101,7 @@ export function DependencyOperationDetailTraceList() {
traceId,
transactionId,
transactionType,
+ showCriticalPath: false,
},
})
: router.link('/link-to/trace/{traceId}', {
@@ -99,9 +117,24 @@ export function DependencyOperationDetailTraceList() {
return href;
}
- const { start, end } = useTimeRange({ rangeFrom, rangeTo });
-
const columns: Array> = [
+ {
+ name: '',
+ field: 'spanId',
+ render: (_, { spanId: itemSpanId }) => {
+ return (
+ {
+ push(history, {
+ query: { spanId: value ? itemSpanId : '' },
+ });
+ }}
+ checked={itemSpanId === spanId}
+ />
+ );
+ },
+ },
{
name: i18n.translate(
'xpack.apm.dependencyOperationDetailTraceListOutcomeColumn',
@@ -121,38 +154,6 @@ export function DependencyOperationDetailTraceList() {
return {outcome};
},
},
- {
- name: i18n.translate(
- 'xpack.apm.dependencyOperationDetailTraceListTraceIdColumn',
- { defaultMessage: 'Trace' }
- ),
- field: 'traceId',
- truncateText: true,
- render: (
- _,
- {
- serviceName,
- traceId,
- transactionId,
- transactionName,
- transactionType,
- }
- ) => {
- const href = getTraceLink({
- serviceName,
- traceId,
- transactionId,
- transactionType,
- transactionName,
- });
-
- return (
-
- {traceId.substr(0, 6)}
-
- );
- },
- },
{
name: i18n.translate(
'xpack.apm.dependencyOperationDetailTraceListServiceNameColumn',
@@ -190,6 +191,7 @@ export function DependencyOperationDetailTraceList() {
),
field: 'transactionName',
truncateText: true,
+ width: '60%',
render: (
_,
{
@@ -239,35 +241,6 @@ export function DependencyOperationDetailTraceList() {
},
];
- const { data = { spans: [] }, status } = useFetcher(
- (callApmApi) => {
- return callApmApi('GET /internal/apm/dependencies/operations/spans', {
- params: {
- query: {
- dependencyName,
- spanName,
- start,
- end,
- environment,
- kuery,
- sampleRangeFrom,
- sampleRangeTo,
- },
- },
- });
- },
- [
- dependencyName,
- spanName,
- start,
- end,
- environment,
- kuery,
- sampleRangeFrom,
- sampleRangeTo,
- ]
- );
-
return (
@@ -281,15 +254,18 @@ export function DependencyOperationDetailTraceList() {
diff --git a/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/dependendecy_operation_distribution_chart.tsx b/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/dependency_operation_distribution_chart.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/dependendecy_operation_distribution_chart.tsx
rename to x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/dependency_operation_distribution_chart.tsx
diff --git a/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/index.tsx b/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/index.tsx
index c824a61f019b2..9acd060f5fe68 100644
--- a/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/index.tsx
@@ -6,25 +6,142 @@
*/
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import React from 'react';
+import { omit, orderBy } from 'lodash';
+import React, { useEffect, useMemo, useRef } from 'react';
+import { useHistory } from 'react-router-dom';
+import type { DependencySpan } from '../../../../server/routes/dependencies/get_top_dependency_spans';
import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context';
import { useApmParams } from '../../../hooks/use_apm_params';
import { useApmRouter } from '../../../hooks/use_apm_router';
import { useDependencyDetailOperationsBreadcrumb } from '../../../hooks/use_dependency_detail_operations_breadcrumb';
+import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
+import { useTimeRange } from '../../../hooks/use_time_range';
import { DependencyMetricCharts } from '../../shared/dependency_metric_charts';
import { DetailViewHeader } from '../../shared/detail_view_header';
-import { DependencyOperationDistributionChart } from './dependendecy_operation_distribution_chart';
+import { ResettingHeightRetainer } from '../../shared/height_retainer/resetting_height_container';
+import { push, replace } from '../../shared/links/url_helpers';
+import { SortFunction } from '../../shared/managed_table';
+import { useWaterfallFetcher } from '../transaction_details/use_waterfall_fetcher';
+import { WaterfallWithSummary } from '../transaction_details/waterfall_with_summary';
import { DependencyOperationDetailTraceList } from './dependency_operation_detail_trace_list';
+import { DependencyOperationDistributionChart } from './dependency_operation_distribution_chart';
+import { maybeRedirectToAvailableSpanSample } from './maybe_redirect_to_available_span_sample';
export function DependencyOperationDetailView() {
const router = useApmRouter();
+ const history = useHistory();
+
const {
- query: { spanName, ...query },
+ query,
+ query: {
+ spanName,
+ dependencyName,
+ sampleRangeFrom,
+ sampleRangeTo,
+ kuery,
+ environment,
+ rangeFrom,
+ rangeTo,
+ spanId,
+ waterfallItemId,
+ detailTab,
+ sortField = '@timestamp',
+ sortDirection = 'desc',
+ showCriticalPath,
+ },
} = useApmParams('/dependencies/operation');
useDependencyDetailOperationsBreadcrumb();
+ const { start, end } = useTimeRange({ rangeFrom, rangeTo });
+
+ const queryWithoutSpanName = omit(query, 'spanName');
+
+ const spanFetch = useFetcher(
+ (callApmApi) => {
+ return callApmApi('GET /internal/apm/dependencies/operations/spans', {
+ params: {
+ query: {
+ dependencyName,
+ spanName,
+ start,
+ end,
+ environment,
+ kuery,
+ sampleRangeFrom,
+ sampleRangeTo,
+ },
+ },
+ });
+ },
+ [
+ dependencyName,
+ spanName,
+ start,
+ end,
+ environment,
+ kuery,
+ sampleRangeFrom,
+ sampleRangeTo,
+ ]
+ );
+
+ const getSortedSamples: SortFunction = (
+ items,
+ localSortField,
+ localSortDirection
+ ) => {
+ return orderBy(items, localSortField, localSortDirection);
+ };
+
+ const samples = useMemo(() => {
+ return (
+ getSortedSamples(
+ spanFetch.data?.spans ?? [],
+ sortField,
+ sortDirection
+ ).map((span) => ({
+ spanId: span.spanId,
+ traceId: span.traceId,
+ transactionId: span.transactionId,
+ })) || []
+ );
+ }, [spanFetch.data?.spans, sortField, sortDirection]);
+
+ const selectedSample = useMemo(() => {
+ return samples.find((sample) => sample.spanId === spanId);
+ }, [samples, spanId]);
+
+ const waterfallFetch = useWaterfallFetcher({
+ traceId: selectedSample?.traceId,
+ transactionId: selectedSample?.transactionId,
+ start,
+ end,
+ });
+
+ const queryRef = useRef(query);
+
+ queryRef.current = query;
+
+ useEffect(() => {
+ maybeRedirectToAvailableSpanSample({
+ history,
+ page: queryRef.current.page ?? 0,
+ pageSize: queryRef.current.pageSize ?? 10,
+ replace,
+ samples,
+ spanFetchStatus: spanFetch.status,
+ spanId,
+ });
+ }, [samples, spanId, history, queryRef, router, spanFetch.status]);
+
+ const isWaterfallLoading =
+ spanFetch.status === FETCH_STATUS.NOT_INITIATED ||
+ (spanFetch.status === FETCH_STATUS.LOADING && samples.length === 0) ||
+ waterfallFetch.status === FETCH_STATUS.LOADING ||
+ !waterfallFetch.waterfall.entryWaterfallTransaction;
+
return (
@@ -33,7 +150,9 @@ export function DependencyOperationDetailView() {
'xpack.apm.dependecyOperationDetailView.header.backLinkLabel',
{ defaultMessage: 'All operations' }
)}
- backHref={router.link('/dependencies/operations', { query })}
+ backHref={router.link('/dependencies/operations', {
+ query: queryWithoutSpanName,
+ })}
title={spanName}
/>
@@ -50,7 +169,47 @@ export function DependencyOperationDetailView() {
-
+
+
+
+
+
+
+ {
+ push(history, { query: { spanId: sample.spanId } });
+ }}
+ onTabClick={(tab) => {
+ push(history, {
+ query: {
+ detailTab: tab,
+ },
+ });
+ }}
+ serviceName={
+ waterfallFetch.waterfall.entryWaterfallTransaction?.doc.service
+ .name
+ }
+ waterfallItemId={waterfallItemId}
+ detailTab={detailTab}
+ selectedSample={selectedSample || null}
+ showCriticalPath={showCriticalPath}
+ onShowCriticalPathChange={(nextShowCriticalPath) => {
+ push(history, {
+ query: {
+ showCriticalPath: nextShowCriticalPath ? 'true' : 'false',
+ },
+ });
+ }}
+ />
+
diff --git a/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/maybe_redirect_to_available_span_sample.test.ts b/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/maybe_redirect_to_available_span_sample.test.ts
new file mode 100644
index 0000000000000..a7c2b2b669bf3
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/maybe_redirect_to_available_span_sample.test.ts
@@ -0,0 +1,108 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { range } from 'lodash';
+import { maybeRedirectToAvailableSpanSample } from './maybe_redirect_to_available_span_sample';
+import { replace as urlHelpersReplace } from '../../shared/links/url_helpers';
+import { History } from 'history';
+import { FETCH_STATUS } from '../../../hooks/use_fetcher';
+
+describe('maybeRedirectToAvailableSpanSample', () => {
+ const samples: Array<{
+ spanId: string;
+ traceId: string;
+ transactionId: string;
+ }> = range(11).map((_, index) => ({
+ spanId: (index + 1).toString(),
+ traceId: '',
+ transactionId: '',
+ }));
+
+ let defaultParams: Omit<
+ Parameters[0],
+ 'replace'
+ > & { replace: jest.MockedFunction };
+
+ beforeEach(() => {
+ defaultParams = {
+ samples,
+ page: 0,
+ pageSize: 10,
+ history: {
+ location: {
+ search: '',
+ },
+ } as History,
+ spanFetchStatus: FETCH_STATUS.SUCCESS,
+ replace: jest.fn(),
+ };
+ });
+
+ it('does not redirect while loading', () => {
+ maybeRedirectToAvailableSpanSample({
+ ...defaultParams,
+ spanId: undefined,
+ spanFetchStatus: FETCH_STATUS.LOADING,
+ });
+ expect(defaultParams.replace).not.toHaveBeenCalled();
+ });
+
+ it('redirects to the first available span if no span is selected', () => {
+ maybeRedirectToAvailableSpanSample({
+ ...defaultParams,
+ spanId: undefined,
+ page: 1,
+ spanFetchStatus: FETCH_STATUS.SUCCESS,
+ });
+ expect(defaultParams.replace).toHaveBeenCalled();
+
+ expect(defaultParams.replace.mock.calls[0][1].query).toEqual({
+ spanId: samples[0].spanId,
+ page: '0',
+ });
+ });
+
+ it('redirects to the first available span if the currently selected sample is not found', () => {
+ maybeRedirectToAvailableSpanSample({
+ ...defaultParams,
+ page: 1,
+ spanId: '12',
+ spanFetchStatus: FETCH_STATUS.SUCCESS,
+ });
+ expect(defaultParams.replace).toHaveBeenCalled();
+
+ expect(defaultParams.replace.mock.calls[0][1].query).toEqual({
+ spanId: samples[0].spanId,
+ page: '0',
+ });
+ });
+
+ it('does not redirect if the sample is found', () => {
+ maybeRedirectToAvailableSpanSample({
+ ...defaultParams,
+ page: 0,
+ spanId: '1',
+ spanFetchStatus: FETCH_STATUS.SUCCESS,
+ });
+ expect(defaultParams.replace).not.toHaveBeenCalled();
+ });
+
+ it('redirects to the page of the currently selected sample', () => {
+ maybeRedirectToAvailableSpanSample({
+ ...defaultParams,
+ page: 0,
+ spanId: '11',
+ spanFetchStatus: FETCH_STATUS.SUCCESS,
+ });
+
+ expect(defaultParams.replace).toHaveBeenCalled();
+
+ expect(defaultParams.replace.mock.calls[0][1].query).toEqual({
+ page: '1',
+ spanId: '11',
+ });
+ });
+});
diff --git a/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/maybe_redirect_to_available_span_sample.ts b/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/maybe_redirect_to_available_span_sample.ts
new file mode 100644
index 0000000000000..00ce95a255ea2
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/maybe_redirect_to_available_span_sample.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { History } from 'history';
+import { FETCH_STATUS } from '../../../hooks/use_fetcher';
+import { replace as urlHelpersReplace } from '../../shared/links/url_helpers';
+
+export function maybeRedirectToAvailableSpanSample({
+ spanFetchStatus,
+ spanId,
+ pageSize,
+ page,
+ replace,
+ samples,
+ history,
+}: {
+ spanFetchStatus: FETCH_STATUS;
+ spanId?: string;
+ pageSize: number;
+ page: number;
+ replace: typeof urlHelpersReplace;
+ history: History;
+ samples: Array<{ spanId: string; traceId: string; transactionId: string }>;
+}) {
+ if (spanFetchStatus !== FETCH_STATUS.SUCCESS) {
+ // we're still loading, don't do anything
+ return;
+ }
+
+ const nextSpanId =
+ samples.find((sample) => sample.spanId === spanId)?.spanId ||
+ samples[0]?.spanId ||
+ '';
+
+ const indexOfNextSample =
+ samples.findIndex((sample) => sample.spanId === nextSpanId) ?? 0;
+
+ const nextPageIndex = Math.floor((indexOfNextSample + 1) / (pageSize ?? 10));
+
+ if (page !== nextPageIndex || (spanId ?? '') !== nextSpanId) {
+ replace(history, {
+ query: { spanId: nextSpanId, page: nextPageIndex.toString() },
+ });
+ }
+}
diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.tsx
index 220f276f62152..4b41099240f54 100644
--- a/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.tsx
@@ -99,6 +99,7 @@ export function DetailView({ errorGroup, urlParams, kuery }: Props) {
const traceExplorerLink = router.link('/traces/explorer', {
query: {
...query,
+ showCriticalPath: false,
query: `${ERROR_GROUP_ID}:${groupId}`,
type: TraceSearchType.kql,
traceId: '',
diff --git a/x-pack/plugins/apm/public/components/app/service_map/popover/edge_contents.tsx b/x-pack/plugins/apm/public/components/app/service_map/popover/edge_contents.tsx
index 4bcc8bb8ca53b..add6c48fef8e5 100644
--- a/x-pack/plugins/apm/public/components/app/service_map/popover/edge_contents.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_map/popover/edge_contents.tsx
@@ -58,6 +58,7 @@ export function EdgeContents({ elementData }: ContentsProps) {
traceId: '',
transactionId: '',
detailTab: TransactionTab.timeline,
+ showCriticalPath: false,
},
});
diff --git a/x-pack/plugins/apm/public/components/app/trace_explorer/index.tsx b/x-pack/plugins/apm/public/components/app/trace_explorer/index.tsx
index 47287084386ad..1b6c35adffc04 100644
--- a/x-pack/plugins/apm/public/components/app/trace_explorer/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/trace_explorer/index.tsx
@@ -20,10 +20,6 @@ import { useWaterfallFetcher } from '../transaction_details/use_waterfall_fetche
import { WaterfallWithSummary } from '../transaction_details/waterfall_with_summary';
import { TraceSearchBox } from './trace_search_box';
-const INITIAL_DATA = {
- traceSamples: [],
-};
-
export function TraceExplorer() {
const [query, setQuery] = useState({
query: '',
@@ -41,6 +37,7 @@ export function TraceExplorer() {
transactionId,
waterfallItemId,
detailTab,
+ showCriticalPath,
},
} = useApmParams('/traces/explorer');
@@ -58,11 +55,7 @@ export function TraceExplorer() {
rangeTo,
});
- const {
- data = INITIAL_DATA,
- status,
- error,
- } = useFetcher(
+ const { data, status, error } = useFetcher(
(callApmApi) => {
return callApmApi('GET /internal/apm/traces/find', {
params: {
@@ -80,7 +73,7 @@ export function TraceExplorer() {
);
useEffect(() => {
- const nextSample = data.traceSamples[0];
+ const nextSample = data?.traceSamples[0];
const nextWaterfallItemId = '';
history.replace({
...history.location,
@@ -141,7 +134,8 @@ export function TraceExplorer() {
{
push(history, {
@@ -165,6 +159,14 @@ export function TraceExplorer() {
waterfallFetchResult.waterfall.entryWaterfallTransaction?.doc
.service.name
}
+ showCriticalPath={showCriticalPath}
+ onShowCriticalPathChange={(nextShowCriticalPath) => {
+ push(history, {
+ query: {
+ showCriticalPath: nextShowCriticalPath ? 'true' : 'false',
+ },
+ });
+ }}
/>
diff --git a/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx
index 1093a74f6bc2f..4c176527d49f6 100644
--- a/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx
@@ -44,6 +44,7 @@ export function TraceOverview({ children }: { children: React.ReactElement }) {
traceId: '',
transactionId: '',
detailTab: TransactionTab.timeline,
+ showCriticalPath: 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 73b224d60aaa4..72bf5a048e9e7 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
@@ -21,7 +21,7 @@ import { useApmParams } from '../../../../hooks/use_apm_params';
import { useTimeRange } from '../../../../hooks/use_time_range';
import { DurationDistributionChartWithScrubber } from '../../../shared/charts/duration_distribution_chart_with_scrubber';
import { HeightRetainer } from '../../../shared/height_retainer';
-import { fromQuery, toQuery } from '../../../shared/links/url_helpers';
+import { fromQuery, push, toQuery } from '../../../shared/links/url_helpers';
import { TransactionTab } from '../waterfall_with_summary/transaction_tabs';
import { useTransactionDistributionChartData } from './use_transaction_distribution_chart_data';
import { TraceSamplesFetchResult } from '../../../../hooks/use_transaction_trace_samples_fetcher';
@@ -43,7 +43,7 @@ export function TransactionDistribution({
const { traceId, transactionId } = urlParams;
const {
- query: { rangeFrom, rangeTo },
+ query: { rangeFrom, rangeTo, showCriticalPath },
} = useApmParams('/services/{serviceName}/transactions/view');
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
@@ -117,7 +117,16 @@ export function TransactionDistribution({
waterfallItemId={waterfallItemId}
detailTab={detailTab as TransactionTab | undefined}
waterfallFetchResult={waterfallFetchResult}
- traceSamplesFetchResult={traceSamplesFetchResult}
+ traceSamplesFetchStatus={traceSamplesFetchResult.status}
+ traceSamples={traceSamplesFetchResult.data?.traceSamples}
+ showCriticalPath={showCriticalPath}
+ onShowCriticalPathChange={(nextShowCriticalPath) => {
+ push(history, {
+ query: {
+ showCriticalPath: nextShowCriticalPath ? 'true' : 'false',
+ },
+ });
+ }}
/>
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx
index 57d630393f9a9..4ef0bb54319a0 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx
@@ -22,55 +22,71 @@ import { MaybeViewTraceLink } from './maybe_view_trace_link';
import { TransactionTab, TransactionTabs } from './transaction_tabs';
import { Environment } from '../../../../../common/environment_rt';
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
-import { TraceSamplesFetchResult } from '../../../../hooks/use_transaction_trace_samples_fetcher';
import { WaterfallFetchResult } from '../use_waterfall_fetcher';
-interface Props {
+interface Props {
waterfallFetchResult: WaterfallFetchResult;
- traceSamplesFetchResult: TraceSamplesFetchResult;
+ traceSamples?: TSample[];
+ traceSamplesFetchStatus: FETCH_STATUS;
environment: Environment;
- onSampleClick: (sample: { transactionId: string; traceId: string }) => void;
- onTabClick: (tab: string) => void;
+ onSampleClick: (sample: TSample) => void;
+ onTabClick: (tab: TransactionTab) => void;
serviceName?: string;
waterfallItemId?: string;
detailTab?: TransactionTab;
+ showCriticalPath: boolean;
+ onShowCriticalPathChange: (showCriticalPath: boolean) => void;
+ selectedSample?: TSample | null;
}
-export function WaterfallWithSummary({
+export function WaterfallWithSummary({
waterfallFetchResult,
- traceSamplesFetchResult,
+ traceSamples,
+ traceSamplesFetchStatus,
environment,
onSampleClick,
onTabClick,
serviceName,
waterfallItemId,
detailTab,
-}: Props) {
+ showCriticalPath,
+ onShowCriticalPathChange,
+ selectedSample,
+}: Props) {
const [sampleActivePage, setSampleActivePage] = useState(0);
+ const isControlled = selectedSample !== undefined;
+
+ const isLoading =
+ waterfallFetchResult.status === FETCH_STATUS.LOADING ||
+ traceSamplesFetchStatus === FETCH_STATUS.LOADING;
+ const isSucceded =
+ waterfallFetchResult.status === FETCH_STATUS.SUCCESS &&
+ traceSamplesFetchStatus === FETCH_STATUS.SUCCESS;
+
useEffect(() => {
- setSampleActivePage(0);
- }, [traceSamplesFetchResult.data.traceSamples]);
+ if (!isControlled) {
+ setSampleActivePage(0);
+ }
+ }, [traceSamples, isControlled]);
const goToSample = (index: number) => {
- setSampleActivePage(index);
- const sample = traceSamplesFetchResult.data.traceSamples[index];
+ const sample = traceSamples![index];
+ if (!isControlled) {
+ setSampleActivePage(index);
+ }
onSampleClick(sample);
};
+ const samplePageIndex = isControlled
+ ? selectedSample
+ ? traceSamples?.indexOf(selectedSample)
+ : 0
+ : sampleActivePage;
+
const { entryWaterfallTransaction } = waterfallFetchResult.waterfall;
- const isLoading =
- waterfallFetchResult.status === FETCH_STATUS.LOADING ||
- traceSamplesFetchResult.status === FETCH_STATUS.LOADING;
- const isSucceded =
- waterfallFetchResult.status === FETCH_STATUS.SUCCESS &&
- traceSamplesFetchResult.status === FETCH_STATUS.SUCCESS;
- if (
- !entryWaterfallTransaction &&
- traceSamplesFetchResult.data.traceSamples.length === 0 &&
- isSucceded
- ) {
+ if (!entryWaterfallTransaction && traceSamples?.length === 0 && isSucceded) {
return (
- {traceSamplesFetchResult.data.traceSamples.length > 0 && (
+ {!!traceSamples?.length && (
@@ -159,6 +175,8 @@ export function WaterfallWithSummary({
onTabClick={onTabClick}
waterfall={waterfallFetchResult.waterfall}
isLoading={isLoading}
+ showCriticalPath={showCriticalPath}
+ onShowCriticalPathChange={onShowCriticalPathChange}
/>
>
);
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/maybe_view_trace_link.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/maybe_view_trace_link.tsx
index 1621ea72b39a1..40b1944605b58 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/maybe_view_trace_link.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/maybe_view_trace_link.tsx
@@ -53,7 +53,8 @@ export function MaybeViewTraceLink({
query: { comparisonEnabled, offset },
} = useAnyOfApmParams(
'/services/{serviceName}/transactions/view',
- '/traces/explorer'
+ '/traces/explorer',
+ '/dependencies/operation'
);
const latencyAggregationType =
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx
index e3fdaeea24846..85e8b36942936 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx
@@ -22,6 +22,8 @@ interface Props {
serviceName?: string;
waterfallItemId?: string;
onTabClick: (tab: TransactionTab) => void;
+ showCriticalPath: boolean;
+ onShowCriticalPathChange: (showCriticalPath: boolean) => void;
}
export function TransactionTabs({
@@ -32,6 +34,8 @@ export function TransactionTabs({
waterfallItemId,
serviceName,
onTabClick,
+ showCriticalPath,
+ onShowCriticalPathChange,
}: Props) {
const tabs = [timelineTab, metadataTab, logsTab];
const currentTab = tabs.find(({ key }) => key === detailTab) ?? timelineTab;
@@ -64,6 +68,8 @@ export function TransactionTabs({
serviceName={serviceName}
waterfall={waterfall}
transaction={transaction}
+ showCriticalPath={showCriticalPath}
+ onShowCriticalPathChange={onShowCriticalPathChange}
/>
)}
>
@@ -104,16 +110,22 @@ function TimelineTabContent({
waterfall,
waterfallItemId,
serviceName,
+ showCriticalPath,
+ onShowCriticalPathChange,
}: {
waterfallItemId?: string;
serviceName?: string;
waterfall: IWaterfall;
+ showCriticalPath: boolean;
+ onShowCriticalPathChange: (showCriticalPath: boolean) => void;
}) {
return (
);
}
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/index.tsx
index 2dd74aeae3eef..b9f149c32e491 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/index.tsx
@@ -5,26 +5,36 @@
* 2.0.
*/
-import React from 'react';
+import { EuiFlexGroup, EuiFlexItem, EuiSwitch } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { keyBy } from 'lodash';
+import React from 'react';
+import { useCriticalPathFeatureEnabledSetting } from '../../../../../hooks/use_critical_path_feature_enabled_setting';
+import { TechnicalPreviewBadge } from '../../../../shared/technical_preview_badge';
+import { Waterfall } from './waterfall';
import {
IWaterfall,
WaterfallLegendType,
} from './waterfall/waterfall_helpers/waterfall_helpers';
-import { Waterfall } from './waterfall';
import { WaterfallLegends } from './waterfall_legends';
interface Props {
waterfallItemId?: string;
serviceName?: string;
waterfall: IWaterfall;
+ showCriticalPath: boolean;
+ onShowCriticalPathChange: (showCriticalPath: boolean) => void;
}
export function WaterfallContainer({
serviceName,
waterfallItemId,
waterfall,
+ showCriticalPath,
+ onShowCriticalPathChange,
}: Props) {
+ const isCriticalPathFeatureEnabled = useCriticalPathFeatureEnabledSetting();
+
if (!waterfall) {
return null;
}
@@ -74,9 +84,40 @@ export function WaterfallContainer({
});
return (
-
-
-
-
+
+ {isCriticalPathFeatureEnabled ? (
+
+
+
+ {i18n.translate('xpack.apm.waterfall.showCriticalPath', {
+ defaultMessage: 'Show critical path',
+ })}
+
+
+
+
+
+ }
+ checked={showCriticalPath}
+ onChange={(event) => {
+ onShowCriticalPathChange(event.target.checked);
+ }}
+ />
+
+ ) : null}
+
+
+
+
+
+
+
);
}
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/accordion_waterfall.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/accordion_waterfall.tsx
index c0932e041de1a..3b996bfb3cdd1 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/accordion_waterfall.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/accordion_waterfall.tsx
@@ -15,12 +15,16 @@ import {
} from '@elastic/eui';
import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { euiStyled } from '@kbn/kibana-react-plugin/common';
+import { groupBy } from 'lodash';
+import { transparentize } from 'polished';
import { Margins } from '../../../../../shared/charts/timeline';
import {
IWaterfall,
IWaterfallSpanOrTransaction,
} from './waterfall_helpers/waterfall_helpers';
import { WaterfallItem } from './waterfall_item';
+import { getCriticalPath } from '../../../../../../../common/critical_path/get_critical_path';
+import { useTheme } from '../../../../../../hooks/use_theme';
interface AccordionWaterfallProps {
isOpen: boolean;
@@ -32,6 +36,7 @@ interface AccordionWaterfallProps {
waterfall: IWaterfall;
timelineMargins: Margins;
onClickWaterfallItem: (item: IWaterfallSpanOrTransaction) => void;
+ showCriticalPath: boolean;
}
const ACCORDION_HEIGHT = '48px';
@@ -85,8 +90,11 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) {
setMaxLevel,
timelineMargins,
onClickWaterfallItem,
+ showCriticalPath,
} = props;
+ const theme = useTheme();
+
const [isOpen, setIsOpen] = useState(props.isOpen);
const [nextLevel] = useState(level + 1);
@@ -94,7 +102,26 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) {
setMaxLevel(nextLevel);
}, [nextLevel, setMaxLevel]);
- const children = waterfall.childrenByParentId[item.id] || [];
+ let children = waterfall.childrenByParentId[item.id] || [];
+
+ const criticalPath = showCriticalPath
+ ? getCriticalPath(waterfall)
+ : undefined;
+
+ const criticalPathSegmentsById = groupBy(
+ criticalPath?.segments,
+ (segment) => segment.item.id
+ );
+
+ let displayedColor = item.color;
+
+ if (showCriticalPath) {
+ children = children.filter(
+ (child) => criticalPathSegmentsById[child.id]?.length
+ );
+ displayedColor = transparentize(0.5, item.color);
+ }
+
const errorCount = waterfall.getErrorCount(item.id);
// To indent the items creating the parent/child tree
@@ -131,7 +158,7 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) {
{
onClickWaterfallItem(item);
}}
+ segments={criticalPathSegmentsById[item.id]
+ ?.filter((segment) => segment.self)
+ .map((segment) => ({
+ color: theme.eui.euiColorAccent,
+ left:
+ (segment.offset - item.offset - item.skew) / item.duration,
+ width: segment.duration / item.duration,
+ }))}
/>
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/index.tsx
index 04c3734eebaff..d117cb2d982c1 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/index.tsx
@@ -52,8 +52,14 @@ const WaterfallItemsContainer = euiStyled.div`
interface Props {
waterfallItemId?: string;
waterfall: IWaterfall;
+ showCriticalPath: boolean;
}
-export function Waterfall({ waterfall, waterfallItemId }: Props) {
+
+export function Waterfall({
+ waterfall,
+ waterfallItemId,
+ showCriticalPath,
+}: Props) {
const history = useHistory();
const [isAccordionOpen, setIsAccordionOpen] = useState(true);
const itemContainerHeight = 58; // TODO: This is a nasty way to calculate the height of the svg element. A better approach should be found
@@ -119,6 +125,7 @@ export function Waterfall({ waterfall, waterfallItemId }: Props) {
onClickWaterfallItem={(item: IWaterfallItem) =>
toggleFlyout({ history, item })
}
+ showCriticalPath={showCriticalPath}
/>
)}
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/sticky_span_properties.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/sticky_span_properties.tsx
index 0b500cb79a746..59abacb1c325c 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/sticky_span_properties.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/sticky_span_properties.tsx
@@ -33,7 +33,8 @@ interface Props {
export function StickySpanProperties({ span, transaction }: Props) {
const { query } = useAnyOfApmParams(
'/services/{serviceName}/transactions/view',
- '/traces/explorer'
+ '/traces/explorer',
+ '/dependencies/operation'
);
const { environment, comparisonEnabled, offset } = query;
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx
index 9bfff6a4ea89b..8057ee3a32b7d 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx
@@ -83,6 +83,31 @@ const ItemText = euiStyled.span`
}
`;
+const CriticalPathItemBar = euiStyled.div`
+ box-sizing: border-box;
+ position: relative;
+ height: ${({ theme }) => theme.eui.euiSizeS};
+ top : ${({ theme }) => theme.eui.euiSizeS};
+ min-width: 2px;
+ background-color: transparent;
+ display: flex;
+ flex-direction: row;
+`;
+
+const CriticalPathItemSegment = euiStyled.div<{
+ left: number;
+ width: number;
+ color: string;
+}>`
+ box-sizing: border-box;
+ position: absolute;
+ height: ${({ theme }) => theme.eui.euiSizeS};
+ left: ${(props) => props.left * 100}%;
+ width: ${(props) => props.width * 100}%;
+ min-width: 2px;
+ background-color: ${(props) => props.color};
+`;
+
interface IWaterfallItemProps {
timelineMargins: Margins;
totalDuration?: number;
@@ -92,6 +117,11 @@ interface IWaterfallItemProps {
isSelected: boolean;
errorCount: number;
marginLeftLevel: number;
+ segments?: Array<{
+ left: number;
+ width: number;
+ color: string;
+ }>;
onClick: () => unknown;
}
@@ -194,6 +224,7 @@ export function WaterfallItem({
errorCount,
marginLeftLevel,
onClick,
+ segments,
}: IWaterfallItemProps) {
const [widthFactor, setWidthFactor] = useState(1);
const waterfallItemRef: React.RefObject = useRef(null);
@@ -217,7 +248,9 @@ export function WaterfallItem({
100;
const isCompositeSpan = item.docType === 'span' && item.doc.span.composite;
+
const itemBarStyle = getItemBarStyle(item, color, width, left);
+
const isServerlessColdstart =
item.docType === 'transaction' && item.doc.faas?.coldstart;
@@ -237,7 +270,19 @@ export function WaterfallItem({
style={itemBarStyle}
color={isCompositeSpan ? 'transparent' : color}
type={item.docType}
- />
+ >
+ {segments?.length ? (
+
+ {segments?.map((segment) => (
+
+ ))}
+
+ ) : null}
+
@@ -277,7 +322,8 @@ function RelatedErrors({
const theme = useTheme();
const { query } = useAnyOfApmParams(
'/services/{serviceName}/transactions/view',
- '/traces/explorer'
+ '/traces/explorer',
+ '/dependencies/operation'
);
let kuery = `${TRACE_ID} : "${item.doc.trace.id}"`;
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.tsx
index a10518ab58e4c..0a08dcb166048 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.tsx
@@ -8,6 +8,7 @@
import { Meta, Story } from '@storybook/react';
import React, { ComponentProps } from 'react';
import { MemoryRouter } from 'react-router-dom';
+import { noop } from 'lodash';
import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context';
import { WaterfallContainer } from '.';
import { getWaterfall } from './waterfall/waterfall_helpers/waterfall_helpers';
@@ -59,6 +60,8 @@ export const Example: Story = ({
serviceName={serviceName}
waterfallItemId={waterfallItemId}
waterfall={waterfall}
+ showCriticalPath={false}
+ onShowCriticalPathChange={noop}
/>
);
};
@@ -76,6 +79,8 @@ export const WithErrors: Story = ({
serviceName={serviceName}
waterfallItemId={waterfallItemId}
waterfall={waterfall}
+ showCriticalPath={false}
+ onShowCriticalPathChange={noop}
/>
);
};
@@ -93,6 +98,8 @@ export const ChildStartsBeforeParent: Story = ({
serviceName={serviceName}
waterfallItemId={waterfallItemId}
waterfall={waterfall}
+ showCriticalPath={false}
+ onShowCriticalPathChange={noop}
/>
);
};
@@ -110,6 +117,8 @@ export const InferredSpans: Story = ({
serviceName={serviceName}
waterfallItemId={waterfallItemId}
waterfall={waterfall}
+ showCriticalPath={false}
+ onShowCriticalPathChange={noop}
/>
);
};
@@ -127,6 +136,8 @@ export const ManyChildrenWithSameLength: Story = ({
serviceName={serviceName}
waterfallItemId={waterfallItemId}
waterfall={waterfall}
+ showCriticalPath={false}
+ onShowCriticalPathChange={noop}
/>
);
};
diff --git a/x-pack/plugins/apm/public/components/routing/home/dependencies.tsx b/x-pack/plugins/apm/public/components/routing/home/dependencies.tsx
index a20634c31912c..7b0d93d7550e1 100644
--- a/x-pack/plugins/apm/public/components/routing/home/dependencies.tsx
+++ b/x-pack/plugins/apm/public/components/routing/home/dependencies.tsx
@@ -20,6 +20,7 @@ import { DependencyDetailView } from '../../app/dependency_detail_view';
import { DependenciesInventory } from '../../app/dependencies_inventory';
import { DependencyOperationDetailView } from '../../app/dependency_operation_detail_view';
import { useApmParams } from '../../../hooks/use_apm_params';
+import { TransactionTab } from '../../app/transaction_details/waterfall_with_summary/transaction_tabs';
export const DependenciesInventoryTitle = i18n.translate(
'xpack.apm.views.dependenciesInventory.title',
@@ -73,13 +74,27 @@ export const dependencies = {
query: t.intersection([
t.type({
spanName: t.string,
+ detailTab: t.union([
+ t.literal(TransactionTab.timeline),
+ t.literal(TransactionTab.metadata),
+ t.literal(TransactionTab.logs),
+ ]),
+ showCriticalPath: toBooleanRt,
}),
t.partial({
+ spanId: t.string,
sampleRangeFrom: toNumberRt,
sampleRangeTo: toNumberRt,
+ waterfallItemId: t.string,
}),
]),
}),
+ defaults: {
+ query: {
+ detailTab: TransactionTab.timeline,
+ showCriticalPath: '',
+ },
+ },
element: ,
},
'/dependencies/overview': {
diff --git a/x-pack/plugins/apm/public/components/routing/home/index.tsx b/x-pack/plugins/apm/public/components/routing/home/index.tsx
index 51a68488f9d81..36ead4f7b36c7 100644
--- a/x-pack/plugins/apm/public/components/routing/home/index.tsx
+++ b/x-pack/plugins/apm/public/components/routing/home/index.tsx
@@ -213,6 +213,7 @@ export const home = {
t.literal(TransactionTab.metadata),
t.literal(TransactionTab.logs),
]),
+ showCriticalPath: toBooleanRt,
}),
}),
defaults: {
@@ -223,6 +224,7 @@ export const home = {
traceId: '',
transactionId: '',
detailTab: TransactionTab.timeline,
+ showCriticalPath: '',
},
},
},
diff --git a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx
index fdd1aedfa0022..7cc2f7b113fe9 100644
--- a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx
+++ b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx
@@ -180,6 +180,7 @@ export const serviceDetail = {
t.type({
transactionName: t.string,
comparisonEnabled: toBooleanRt,
+ showCriticalPath: toBooleanRt,
}),
t.partial({
traceId: t.string,
@@ -188,6 +189,11 @@ export const serviceDetail = {
offsetRt,
]),
}),
+ defaults: {
+ query: {
+ showCriticalPath: '',
+ },
+ },
},
'/services/{serviceName}/transactions': {
element: ,
diff --git a/x-pack/plugins/apm/public/components/shared/height_retainer/resetting_height_container.tsx b/x-pack/plugins/apm/public/components/shared/height_retainer/resetting_height_container.tsx
new file mode 100644
index 0000000000000..00a1b9dd40c31
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/height_retainer/resetting_height_container.tsx
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useRef } from 'react';
+
+export function ResettingHeightRetainer(
+ props: React.DetailedHTMLProps<
+ React.HTMLAttributes,
+ HTMLDivElement
+ > & { reset?: boolean }
+) {
+ const { reset, ...containerProps } = props;
+ const resetRef = useRef(reset);
+ const containerRef = useRef(null);
+
+ const minHeightRef = useRef(0);
+
+ if (resetRef.current !== reset) {
+ minHeightRef.current = reset ? 0 : containerRef.current?.clientHeight ?? 0;
+
+ resetRef.current = reset;
+ }
+
+ return (
+