From 0092f0ac532798f15d1a8aae10a403afa96c82e5 Mon Sep 17 00:00:00 2001
From: Walter Rafelsberger
Date: Thu, 27 Oct 2022 14:35:33 +0200
Subject: [PATCH 01/28] [ML] Explain Log Rate Spikes: Limit fields for
frequent_items agg. (#143974)
Limits the fields we pass on to the frequent_items aggregation to 15. This is a trade off between speed and quality of the grouping result. The amount of fields we pass on to the agg grow the time it takes to get frequent items more than linearly and we risk timeouts with more fields.
---
.../routes/queries/fetch_frequent_items.ts | 35 +++++++++++++------
1 file changed, 24 insertions(+), 11 deletions(-)
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;
From bbbf9f89854b628a27aa304ca32af0009466c995 Mon Sep 17 00:00:00 2001
From: Kurt
Date: Thu, 27 Oct 2022 08:52:35 -0400
Subject: [PATCH 02/28] Adding content type (#143800)
Co-authored-by: Larry Gregory
---
x-pack/examples/screenshotting_example/server/plugin.ts | 1 +
1 file changed, 1 insertion(+)
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'),
From 460cf89d5f1bd148958c7d92c9aa6b1e9b2e76d8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Yulia=20=C4=8Cech?=
<6585477+yuliacech@users.noreply.github.com>
Date: Thu, 27 Oct 2022 15:19:01 +0200
Subject: [PATCH 03/28] [Guided onboarding] Add a config for test guide
(#143973)
* [Guided onboarding] Add a guide config for testing and update the example plugin to use it instead of search
* [Guided onboarding] Update the API tests with the test guide config
* [Guided onboarding] Address CR comments
* [Guided onboarding] Delete unneeded code in examples
* Update src/plugins/guided_onboarding/public/constants/guides_config/test_guide.ts
Co-authored-by: Alison Goryachev
Co-authored-by: Alison Goryachev
---
.../public/components/app.tsx | 2 +-
.../public/components/main.tsx | 11 +-
.../public/components/step_one.tsx | 10 +-
.../public/components/step_three.tsx | 20 +-
.../public/components/step_two.tsx | 55 +----
.../components/landing_page/use_case_card.tsx | 3 +-
packages/kbn-guided-onboarding/src/types.ts | 11 +-
.../public/constants/guides_config/index.ts | 2 +
.../constants/guides_config/test_guide.ts | 68 ++++++
.../public/services/api.mocks.ts | 78 +++---
.../public/services/api.test.ts | 225 ++++++------------
.../guided_onboarding/public/services/api.ts | 2 +-
.../public/services/helpers.test.ts | 31 ++-
13 files changed, 232 insertions(+), 286 deletions(-)
create mode 100644 src/plugins/guided_onboarding/public/constants/guides_config/test_guide.ts
diff --git a/examples/guided_onboarding_example/public/components/app.tsx b/examples/guided_onboarding_example/public/components/app.tsx
index a5252920c27fa..ae55f3d3811dc 100755
--- a/examples/guided_onboarding_example/public/components/app.tsx
+++ b/examples/guided_onboarding_example/public/components/app.tsx
@@ -59,7 +59,7 @@ export const GuidedOnboardingExampleApp = (props: GuidedOnboardingExampleAppDeps
-
+
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/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/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);
});
});
From 7bc63e07afb19760ee9a258cbc2f57d0aa5abaae Mon Sep 17 00:00:00 2001
From: Coen Warmer
Date: Thu, 27 Oct 2022 15:20:43 +0200
Subject: [PATCH 04/28] Add context.alertDetailsUrl to connector template when
configuring a Rule (#142854)
---
x-pack/plugins/alerting/server/types.ts | 18 +--
.../lib/adapters/framework/adapter_types.ts | 8 +-
.../server/lib/alerting/common/messages.ts | 10 +-
.../infra/server/lib/alerting/common/utils.ts | 51 ++++++--
.../inventory_metric_threshold_executor.ts | 111 +++++++++++-------
...er_inventory_metric_threshold_rule_type.ts | 12 +-
.../metric_threshold_executor.ts | 61 ++++++----
.../register_metric_threshold_rule_type.ts | 12 +-
.../alerting/metric_threshold/test_mocks.ts | 4 -
.../plugins/infra/server/lib/infra_types.ts | 10 +-
x-pack/plugins/infra/server/plugin.ts | 1 +
x-pack/plugins/observability/server/plugin.ts | 3 +
.../server/utils/create_lifecycle_executor.ts | 38 ++++--
.../utils/create_lifecycle_rule_type.test.ts | 33 +++---
.../utils/lifecycle_alert_services.mock.ts | 1 +
15 files changed, 250 insertions(+), 123 deletions(-)
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/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts
index 64d389a1c0bf7..55b847d33f87d 100644
--- a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts
+++ b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts
@@ -22,16 +22,18 @@ import { SpacesPluginSetup } from '@kbn/spaces-plugin/server';
import { PluginSetupContract as AlertingPluginContract } from '@kbn/alerting-plugin/server';
import { MlPluginSetup } from '@kbn/ml-plugin/server';
import { RuleRegistryPluginSetupContract } from '@kbn/rule-registry-plugin/server';
+import { ObservabilityPluginSetup } from '@kbn/observability-plugin/server';
export interface InfraServerPluginSetupDeps {
+ alerting: AlertingPluginContract;
data: DataPluginSetup;
home: HomeServerPluginSetup;
+ features: FeaturesPluginSetup;
+ ruleRegistry: RuleRegistryPluginSetupContract;
+ observability: ObservabilityPluginSetup;
spaces: SpacesPluginSetup;
usageCollection: UsageCollectionSetup;
visTypeTimeseries: VisTypeTimeseriesSetup;
- features: FeaturesPluginSetup;
- alerting: AlertingPluginContract;
- ruleRegistry: RuleRegistryPluginSetupContract;
ml?: MlPluginSetup;
}
diff --git a/x-pack/plugins/infra/server/lib/alerting/common/messages.ts b/x-pack/plugins/infra/server/lib/alerting/common/messages.ts
index 80dae7ffac959..644c31813deae 100644
--- a/x-pack/plugins/infra/server/lib/alerting/common/messages.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/common/messages.ts
@@ -169,6 +169,14 @@ export const alertStateActionVariableDescription = i18n.translate(
}
);
+export const alertDetailUrlActionVariableDescription = i18n.translate(
+ 'xpack.infra.metrics.alerting.alertDetailUrlActionVariableDescription',
+ {
+ defaultMessage:
+ 'Link to the view within Elastic that shows further details and context surrounding this alert',
+ }
+);
+
export const reasonActionVariableDescription = i18n.translate(
'xpack.infra.metrics.alerting.reasonActionVariableDescription',
{
@@ -211,7 +219,7 @@ export const viewInAppUrlActionVariableDescription = i18n.translate(
'xpack.infra.metrics.alerting.viewInAppUrlActionVariableDescription',
{
defaultMessage:
- 'Link to the view or feature within Elastic that can be used to investigate the alert and its context further',
+ 'Link to the view or feature within Elastic that can assist with further investigation',
}
);
diff --git a/x-pack/plugins/infra/server/lib/alerting/common/utils.ts b/x-pack/plugins/infra/server/lib/alerting/common/utils.ts
index 2618af72168cd..ced80c75a3ef1 100644
--- a/x-pack/plugins/infra/server/lib/alerting/common/utils.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/common/utils.ts
@@ -9,8 +9,11 @@ import { isEmpty, isError } from 'lodash';
import { schema } from '@kbn/config-schema';
import { Logger, LogMeta } from '@kbn/logging';
import type { IBasePath } from '@kbn/core/server';
+import { addSpaceIdToPath } from '@kbn/spaces-plugin/common';
+import { ObservabilityConfig } from '@kbn/observability-plugin/server';
import { ALERT_RULE_PARAMETERS, TIMESTAMP } from '@kbn/rule-data-utils';
import { parseTechnicalFields } from '@kbn/rule-registry-plugin/common/parse_technical_fields';
+import { LINK_TO_METRICS_EXPLORER } from '../../../../common/alerting/metrics';
import { getInventoryViewInAppUrl } from '../../../../common/alerting/metrics/alert_link';
import {
AlertExecutionDetails,
@@ -83,18 +86,30 @@ export const createScopedLogger = (
};
};
-export const getViewInAppUrl = (basePath: IBasePath, relativeViewInAppUrl: string) =>
- basePath.publicBaseUrl
- ? new URL(basePath.prepend(relativeViewInAppUrl), basePath.publicBaseUrl).toString()
- : relativeViewInAppUrl;
+export const getAlertDetailsPageEnabledForApp = (
+ config: ObservabilityConfig['unsafe']['alertDetails'] | null,
+ appName: keyof ObservabilityConfig['unsafe']['alertDetails']
+): boolean => {
+ if (!config) return false;
-export const getViewInAppUrlInventory = (
- criteria: InventoryMetricConditions[],
- nodeType: string,
- timestamp: string,
- basePath: IBasePath
-) => {
+ return config[appName].enabled;
+};
+
+export const getViewInInventoryAppUrl = ({
+ basePath,
+ criteria,
+ nodeType,
+ spaceId,
+ timestamp,
+}: {
+ basePath: IBasePath;
+ criteria: InventoryMetricConditions[];
+ nodeType: string;
+ spaceId: string;
+ timestamp: string;
+}) => {
const { metric, customMetric } = criteria[0];
+
const fields = {
[`${ALERT_RULE_PARAMETERS}.criteria.metric`]: [metric],
[`${ALERT_RULE_PARAMETERS}.criteria.customMetric.id`]: [customMetric?.id],
@@ -104,6 +119,18 @@ export const getViewInAppUrlInventory = (
[TIMESTAMP]: timestamp,
};
- const relativeViewInAppUrl = getInventoryViewInAppUrl(parseTechnicalFields(fields, true));
- return getViewInAppUrl(basePath, relativeViewInAppUrl);
+ return addSpaceIdToPath(
+ basePath.publicBaseUrl,
+ spaceId,
+ getInventoryViewInAppUrl(parseTechnicalFields(fields, true))
+ );
};
+
+export const getViewInMetricsAppUrl = (basePath: IBasePath, spaceId: string) =>
+ addSpaceIdToPath(basePath.publicBaseUrl, spaceId, LINK_TO_METRICS_EXPLORER);
+
+export const getAlertDetailsUrl = (
+ basePath: IBasePath,
+ spaceId: string,
+ alertUuid: string | null
+) => addSpaceIdToPath(basePath.publicBaseUrl, spaceId, `/app/observability/alerts/${alertUuid}`);
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
index 2e51d2e8291b5..20b804a2cc7db 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
@@ -30,7 +30,12 @@ import {
buildNoDataAlertReason,
stateToAlertMessage,
} from '../common/messages';
-import { createScopedLogger, getViewInAppUrlInventory } from '../common/utils';
+import {
+ createScopedLogger,
+ getAlertDetailsUrl,
+ getViewInInventoryAppUrl,
+ UNGROUPED_FACTORY_KEY,
+} from '../common/utils';
import { evaluateCondition, ConditionResult } from './evaluate_condition';
type InventoryMetricThresholdAllowedActionGroups = ActionGroupIdsOf<
@@ -61,12 +66,18 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
InventoryMetricThresholdAlertState,
InventoryMetricThresholdAlertContext,
InventoryMetricThresholdAllowedActionGroups
- >(async ({ services, params, alertId, executionId, startedAt }) => {
+ >(async ({ services, params, alertId, executionId, spaceId, startedAt }) => {
const startTime = Date.now();
+
const { criteria, filterQuery, sourceId = 'default', nodeType, alertOnNoData } = params;
+
if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions');
+
const logger = createScopedLogger(libs.logger, 'inventoryRule', { alertId, executionId });
- const { alertWithLifecycle, savedObjectsClient, getAlertStartedDate } = services;
+
+ const esClient = services.scopedClusterClient.asCurrentUser;
+
+ const { alertWithLifecycle, savedObjectsClient, getAlertStartedDate, getAlertUuid } = services;
const alertFactory: InventoryMetricThresholdAlertFactory = (id, reason, additionalContext) =>
alertWithLifecycle({
id,
@@ -85,23 +96,28 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
logger.error(e.message);
const actionGroupId = FIRED_ACTIONS.id; // Change this to an Error action group when able
const reason = buildInvalidQueryAlertReason(params.filterQueryText);
- const alert = alertFactory('*', reason);
- const indexedStartedDate = getAlertStartedDate('*') ?? startedAt.toISOString();
- const viewInAppUrl = getViewInAppUrlInventory(
- criteria,
- nodeType,
- indexedStartedDate,
- libs.basePath
- );
+ const alert = alertFactory(UNGROUPED_FACTORY_KEY, reason);
+ const indexedStartedDate =
+ getAlertStartedDate(UNGROUPED_FACTORY_KEY) ?? startedAt.toISOString();
+ const alertUuid = getAlertUuid(UNGROUPED_FACTORY_KEY);
+
alert.scheduleActions(actionGroupId, {
- group: '*',
+ alertDetailsUrl: getAlertDetailsUrl(libs.basePath, spaceId, alertUuid),
alertState: stateToAlertMessage[AlertStates.ERROR],
+ group: UNGROUPED_FACTORY_KEY,
+ metric: mapToConditionsLookup(criteria, (c) => c.metric),
reason,
timestamp: startedAt.toISOString(),
- viewInAppUrl,
value: null,
- metric: mapToConditionsLookup(criteria, (c) => c.metric),
+ viewInAppUrl: getViewInInventoryAppUrl({
+ basePath: libs.basePath,
+ criteria,
+ nodeType,
+ timestamp: indexedStartedDate,
+ spaceId,
+ }),
});
+
return {};
}
}
@@ -109,7 +125,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
const [, , { logViews }] = await libs.getStartServices();
const logQueryFields: LogQueryFields | undefined = await logViews
- .getClient(savedObjectsClient, services.scopedClusterClient.asCurrentUser)
+ .getClient(savedObjectsClient, esClient)
.getResolvedLogView(sourceId)
.then(
({ indices }) => ({ indexPattern: indices }),
@@ -120,18 +136,19 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
const results = await Promise.all(
criteria.map((condition) =>
evaluateCondition({
- condition,
- nodeType,
- source,
- logQueryFields,
- esClient: services.scopedClusterClient.asCurrentUser,
compositeSize,
- filterQuery,
+ condition,
+ esClient,
executionTimestamp: startedAt,
+ filterQuery,
logger,
+ logQueryFields,
+ nodeType,
+ source,
})
)
);
+
let scheduledActionsCount = 0;
const inventoryItems = Object.keys(first(results)!);
for (const group of inventoryItems) {
@@ -190,25 +207,28 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
const alert = alertFactory(group, reason, additionalContext);
const indexedStartedDate = getAlertStartedDate(group) ?? startedAt.toISOString();
- const viewInAppUrl = getViewInAppUrlInventory(
- criteria,
- nodeType,
- indexedStartedDate,
- libs.basePath
- );
+ const alertUuid = getAlertUuid(group);
+
scheduledActionsCount++;
const context = {
- group,
+ alertDetailsUrl: getAlertDetailsUrl(libs.basePath, spaceId, alertUuid),
alertState: stateToAlertMessage[nextState],
+ group,
reason,
+ metric: mapToConditionsLookup(criteria, (c) => c.metric),
timestamp: startedAt.toISOString(),
- viewInAppUrl,
+ threshold: mapToConditionsLookup(criteria, (c) => c.threshold),
value: mapToConditionsLookup(results, (result) =>
formatMetric(result[group].metric, result[group].currentValue)
),
- threshold: mapToConditionsLookup(criteria, (c) => c.threshold),
- metric: mapToConditionsLookup(criteria, (c) => c.metric),
+ viewInAppUrl: getViewInInventoryAppUrl({
+ basePath: libs.basePath,
+ criteria,
+ nodeType,
+ timestamp: indexedStartedDate,
+ spaceId,
+ }),
...additionalContext,
};
alert.scheduleActions(actionGroupId, context);
@@ -217,24 +237,27 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
const { getRecoveredAlerts } = services.alertFactory.done();
const recoveredAlerts = getRecoveredAlerts();
+
for (const alert of recoveredAlerts) {
const recoveredAlertId = alert.getId();
const indexedStartedDate = getAlertStartedDate(recoveredAlertId) ?? startedAt.toISOString();
- const viewInAppUrl = getViewInAppUrlInventory(
- criteria,
- nodeType,
- indexedStartedDate,
- libs.basePath
- );
- const context = {
- group: recoveredAlertId,
+ const alertUuid = getAlertUuid(recoveredAlertId);
+
+ alert.setContext({
+ alertDetailsUrl: getAlertDetailsUrl(libs.basePath, spaceId, alertUuid),
alertState: stateToAlertMessage[AlertStates.OK],
- timestamp: startedAt.toISOString(),
- viewInAppUrl,
- threshold: mapToConditionsLookup(criteria, (c) => c.threshold),
+ group: recoveredAlertId,
metric: mapToConditionsLookup(criteria, (c) => c.metric),
- };
- alert.setContext(context);
+ threshold: mapToConditionsLookup(criteria, (c) => c.threshold),
+ timestamp: startedAt.toISOString(),
+ viewInAppUrl: getViewInInventoryAppUrl({
+ basePath: libs.basePath,
+ criteria,
+ nodeType,
+ timestamp: indexedStartedDate,
+ spaceId,
+ }),
+ });
}
const stopTime = Date.now();
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_rule_type.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_rule_type.ts
index b3b1f5ba21c65..030628f59ad38 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_rule_type.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_rule_type.ts
@@ -24,6 +24,7 @@ import {
} from '../../../../common/inventory_models/types';
import { InfraBackendLibs } from '../../infra_types';
import {
+ alertDetailUrlActionVariableDescription,
alertStateActionVariableDescription,
cloudActionVariableDescription,
containerActionVariableDescription,
@@ -39,7 +40,11 @@ import {
valueActionVariableDescription,
viewInAppUrlActionVariableDescription,
} from '../common/messages';
-import { oneOfLiterals, validateIsStringElasticsearchJSONFilter } from '../common/utils';
+import {
+ getAlertDetailsPageEnabledForApp,
+ oneOfLiterals,
+ validateIsStringElasticsearchJSONFilter,
+} from '../common/utils';
import {
createInventoryMetricThresholdExecutor,
FIRED_ACTIONS,
@@ -72,6 +77,8 @@ export async function registerMetricInventoryThresholdRuleType(
alertingPlugin: PluginSetupContract,
libs: InfraBackendLibs
) {
+ const config = libs.getAlertDetailsConfig();
+
alertingPlugin.registerType({
id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
name: i18n.translate('xpack.infra.metrics.inventory.alertName', {
@@ -102,6 +109,9 @@ export async function registerMetricInventoryThresholdRuleType(
context: [
{ name: 'group', description: groupActionVariableDescription },
{ name: 'alertState', description: alertStateActionVariableDescription },
+ ...(getAlertDetailsPageEnabledForApp(config, 'metrics')
+ ? [{ name: 'alertDetailsUrl', description: alertDetailUrlActionVariableDescription }]
+ : []),
{ name: 'reason', description: reasonActionVariableDescription },
{ name: 'timestamp', description: timestampActionVariableDescription },
{ name: 'value', description: valueActionVariableDescription },
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
index 14b5fe8e75614..200ad68aa81d1 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
@@ -26,8 +26,12 @@ import {
// buildRecoveredAlertReason,
stateToAlertMessage,
} from '../common/messages';
-import { UNGROUPED_FACTORY_KEY, getViewInAppUrl, createScopedLogger } from '../common/utils';
-import { LINK_TO_METRICS_EXPLORER } from '../../../../common/alerting/metrics';
+import {
+ createScopedLogger,
+ getAlertDetailsUrl,
+ getViewInMetricsAppUrl,
+ UNGROUPED_FACTORY_KEY,
+} from '../common/utils';
import { EvaluatedRuleParams, evaluateRule } from './lib/evaluate_rule';
@@ -67,11 +71,16 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) =>
MetricThresholdAllowedActionGroups
>(async function (options) {
const startTime = Date.now();
- const { services, params, state, startedAt, alertId, executionId } = options;
+
+ const { services, params, state, startedAt, alertId, executionId, spaceId } = options;
+
const { criteria } = params;
if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions');
+
const logger = createScopedLogger(libs.logger, 'metricThresholdRule', { alertId, executionId });
- const { alertWithLifecycle, savedObjectsClient } = services;
+
+ const { alertWithLifecycle, savedObjectsClient, getAlertUuid } = services;
+
const alertFactory: MetricThresholdAlertFactory = (id, reason) =>
alertWithLifecycle({
id,
@@ -100,15 +109,19 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) =>
const actionGroupId = FIRED_ACTIONS.id; // Change this to an Error action group when able
const reason = buildInvalidQueryAlertReason(params.filterQueryText);
const alert = alertFactory(UNGROUPED_FACTORY_KEY, reason);
+ const alertUuid = getAlertUuid(UNGROUPED_FACTORY_KEY);
+
alert.scheduleActions(actionGroupId, {
- group: UNGROUPED_FACTORY_KEY,
+ alertDetailsUrl: getAlertDetailsUrl(libs.basePath, spaceId, alertUuid),
alertState: stateToAlertMessage[AlertStates.ERROR],
+ group: UNGROUPED_FACTORY_KEY,
+ metric: mapToConditionsLookup(criteria, (c) => c.metric),
reason,
- viewInAppUrl: getViewInAppUrl(libs.basePath, LINK_TO_METRICS_EXPLORER),
timestamp,
value: null,
- metric: mapToConditionsLookup(criteria, (c) => c.metric),
+ viewInAppUrl: getViewInMetricsAppUrl(libs.basePath, spaceId),
});
+
return {
lastRunTimestamp: startedAt.valueOf(),
missingGroups: [],
@@ -157,6 +170,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) =>
const hasGroups = !isEqual(groups, [UNGROUPED_FACTORY_KEY]);
let scheduledActionsCount = 0;
+ // The key of `groups` is the alert instance ID.
for (const group of groups) {
// AND logic; all criteria must be across the threshold
const shouldAlertFire = alertResults.every((result) => result[group]?.shouldFire);
@@ -227,40 +241,45 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) =>
? WARNING_ACTIONS.id
: FIRED_ACTIONS.id;
const alert = alertFactory(`${group}`, reason);
+ const alertUuid = getAlertUuid(group);
scheduledActionsCount++;
+
alert.scheduleActions(actionGroupId, {
- group,
+ alertDetailsUrl: getAlertDetailsUrl(libs.basePath, spaceId, alertUuid),
alertState: stateToAlertMessage[nextState],
+ group,
+ metric: mapToConditionsLookup(criteria, (c) => c.metric),
reason,
- viewInAppUrl: getViewInAppUrl(libs.basePath, LINK_TO_METRICS_EXPLORER),
+ threshold: mapToConditionsLookup(
+ alertResults,
+ (result) => formatAlertResult(result[group]).threshold
+ ),
timestamp,
value: mapToConditionsLookup(
alertResults,
(result) => formatAlertResult(result[group]).currentValue
),
- threshold: mapToConditionsLookup(
- alertResults,
- (result) => formatAlertResult(result[group]).threshold
- ),
- metric: mapToConditionsLookup(criteria, (c) => c.metric),
+ viewInAppUrl: getViewInMetricsAppUrl(libs.basePath, spaceId),
});
}
}
const { getRecoveredAlerts } = services.alertFactory.done();
const recoveredAlerts = getRecoveredAlerts();
+
for (const alert of recoveredAlerts) {
const recoveredAlertId = alert.getId();
- const viewInAppUrl = getViewInAppUrl(libs.basePath, LINK_TO_METRICS_EXPLORER);
- const context = {
- group: recoveredAlertId,
+ const alertUuid = getAlertUuid(recoveredAlertId);
+
+ alert.setContext({
+ alertDetailsUrl: getAlertDetailsUrl(libs.basePath, spaceId, alertUuid),
alertState: stateToAlertMessage[AlertStates.OK],
+ group: recoveredAlertId,
+ metric: mapToConditionsLookup(criteria, (c) => c.metric),
timestamp: startedAt.toISOString(),
- viewInAppUrl,
threshold: mapToConditionsLookup(criteria, (c) => c.threshold),
- metric: mapToConditionsLookup(criteria, (c) => c.metric),
- };
- alert.setContext(context);
+ viewInAppUrl: getViewInMetricsAppUrl(libs.basePath, spaceId),
+ });
}
const stopTime = Date.now();
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_rule_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_rule_type.ts
index 0ebb427819f74..6538fb25b6c8c 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_rule_type.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_rule_type.ts
@@ -13,6 +13,7 @@ import { Comparator, METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../common/a
import { METRIC_EXPLORER_AGGREGATIONS } from '../../../../common/http_api';
import { InfraBackendLibs } from '../../infra_types';
import {
+ alertDetailUrlActionVariableDescription,
alertStateActionVariableDescription,
groupActionVariableDescription,
metricActionVariableDescription,
@@ -22,7 +23,11 @@ import {
valueActionVariableDescription,
viewInAppUrlActionVariableDescription,
} from '../common/messages';
-import { oneOfLiterals, validateIsStringElasticsearchJSONFilter } from '../common/utils';
+import {
+ getAlertDetailsPageEnabledForApp,
+ oneOfLiterals,
+ validateIsStringElasticsearchJSONFilter,
+} from '../common/utils';
import {
createMetricThresholdExecutor,
FIRED_ACTIONS,
@@ -41,6 +46,8 @@ export async function registerMetricThresholdRuleType(
alertingPlugin: PluginSetupContract,
libs: InfraBackendLibs
) {
+ const config = libs.getAlertDetailsConfig();
+
const baseCriterion = {
threshold: schema.arrayOf(schema.number()),
comparator: oneOfLiterals(Object.values(Comparator)),
@@ -93,6 +100,9 @@ export async function registerMetricThresholdRuleType(
actionVariables: {
context: [
{ name: 'group', description: groupActionVariableDescription },
+ ...(getAlertDetailsPageEnabledForApp(config, 'metrics')
+ ? [{ name: 'alertDetailsUrl', description: alertDetailUrlActionVariableDescription }]
+ : []),
{ name: 'alertState', description: alertStateActionVariableDescription },
{ name: 'reason', description: reasonActionVariableDescription },
{ name: 'timestamp', description: timestampActionVariableDescription },
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts
index 37e59700b0488..3d3c7a17cd1dd 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts
@@ -4,10 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import * as utils from '../common/utils';
-jest
- .spyOn(utils, 'getViewInAppUrl')
- .mockReturnValue('http://localhost:5601/eyg/app/metrics/explorer');
const bucketsA = (from: number) => [
{
diff --git a/x-pack/plugins/infra/server/lib/infra_types.ts b/x-pack/plugins/infra/server/lib/infra_types.ts
index a4636daa7986e..4801fb49651f6 100644
--- a/x-pack/plugins/infra/server/lib/infra_types.ts
+++ b/x-pack/plugins/infra/server/lib/infra_types.ts
@@ -8,6 +8,7 @@
import { Logger } from '@kbn/logging';
import type { IBasePath } from '@kbn/core/server';
import { handleEsError } from '@kbn/es-ui-shared-plugin/server';
+import { ObservabilityConfig } from '@kbn/observability-plugin/server';
import { RulesServiceSetup } from '../services/rules';
import { InfraConfig, InfraPluginStartServicesAccessor } from '../types';
import { KibanaFramework } from './adapters/framework/kibana_framework_adapter';
@@ -24,14 +25,15 @@ export interface InfraDomainLibs {
}
export interface InfraBackendLibs extends InfraDomainLibs {
+ basePath: IBasePath;
configuration: InfraConfig;
framework: KibanaFramework;
- sources: InfraSources;
- sourceStatus: InfraSourceStatus;
- handleEsError: typeof handleEsError;
logsRules: RulesServiceSetup;
metricsRules: RulesServiceSetup;
+ sources: InfraSources;
+ sourceStatus: InfraSourceStatus;
+ getAlertDetailsConfig: () => ObservabilityConfig['unsafe']['alertDetails'];
getStartServices: InfraPluginStartServicesAccessor;
+ handleEsError: typeof handleEsError;
logger: Logger;
- basePath: IBasePath;
}
diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts
index bfd7113ec4dc0..a7fa9ceacd3c9 100644
--- a/x-pack/plugins/infra/server/plugin.ts
+++ b/x-pack/plugins/infra/server/plugin.ts
@@ -170,6 +170,7 @@ export class InfraServerPlugin
logsRules: this.logsRules.setup(core, plugins),
metricsRules: this.metricsRules.setup(core, plugins),
getStartServices: () => core.getStartServices(),
+ getAlertDetailsConfig: () => plugins.observability.getAlertDetailsConfig(),
logger: this.logger,
basePath: core.http.basePath,
};
diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts
index ff5fd246bea1b..dd2a07f848db3 100644
--- a/x-pack/plugins/observability/server/plugin.ts
+++ b/x-pack/plugins/observability/server/plugin.ts
@@ -157,6 +157,9 @@ export class ObservabilityPlugin implements Plugin {
});
return {
+ getAlertDetailsConfig() {
+ return config.unsafe.alertDetails;
+ },
getScopedAnnotationsClient: async (...args: Parameters) => {
const api = await annotationsApiPromise;
return api?.getScopedAnnotationsClient(...args);
diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts
index 160e06d03e92a..615201fe44d99 100644
--- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts
+++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts
@@ -68,7 +68,8 @@ export interface LifecycleAlertServices<
ActionGroupIds extends string = never
> {
alertWithLifecycle: LifecycleAlertService;
- getAlertStartedDate: (alertId: string) => string | null;
+ getAlertStartedDate: (alertInstanceId: string) => string | null;
+ getAlertUuid: (alertInstanceId: string) => string | null;
}
export type LifecycleRuleExecutor<
@@ -88,6 +89,12 @@ export type LifecycleRuleExecutor<
>
) => Promise;
+/*
+ `alertId` will at some point be renamed to `ruleId` as that more
+ accurately describes the meaning of the variable.
+ See https://github.com/elastic/kibana/issues/100115
+*/
+
const trackedAlertStateRt = rt.type({
alertId: rt.string,
alertUuid: rt.string,
@@ -159,6 +166,8 @@ export const createLifecycleExecutor =
const currentAlerts: Record = {};
+ const newAlertUuids: Record = {};
+
const lifecycleAlertServices: LifecycleAlertServices<
InstanceState,
InstanceContext,
@@ -169,6 +178,15 @@ export const createLifecycleExecutor =
return alertFactory.create(id);
},
getAlertStartedDate: (alertId: string) => state.trackedAlerts[alertId]?.started ?? null,
+ getAlertUuid: (alertId: string) => {
+ if (!state.trackedAlerts[alertId]) {
+ const alertUuid = v4();
+ newAlertUuids[alertId] = alertUuid;
+ return alertUuid;
+ }
+
+ return state.trackedAlerts[alertId].alertUuid;
+ },
};
const nextWrappedState = await wrappedExecutor({
@@ -203,9 +221,9 @@ export const createLifecycleExecutor =
commonRuleFields
);
result.forEach((hit) => {
- const alertId = hit._source ? hit._source[ALERT_INSTANCE_ID] : void 0;
- if (alertId && hit._source) {
- trackedAlertsDataMap[alertId] = {
+ const alertInstanceId = hit._source ? hit._source[ALERT_INSTANCE_ID] : void 0;
+ if (alertInstanceId && hit._source) {
+ trackedAlertsDataMap[alertInstanceId] = {
indexName: hit._index,
fields: hit._source,
};
@@ -226,10 +244,12 @@ export const createLifecycleExecutor =
const isRecovered = !currentAlerts[alertId];
const isActive = !isRecovered;
- const { alertUuid, started } = state.trackedAlerts[alertId] ?? {
- alertUuid: v4(),
- started: commonRuleFields[TIMESTAMP],
- };
+ const { alertUuid, started } = !isNew
+ ? state.trackedAlerts[alertId]
+ : {
+ alertUuid: newAlertUuids[alertId] || v4(),
+ started: commonRuleFields[TIMESTAMP],
+ };
const event: ParsedTechnicalFields & ParsedExperimentalFields = {
...alertData?.fields,
@@ -249,8 +269,8 @@ export const createLifecycleExecutor =
[ALERT_WORKFLOW_STATUS]: alertData?.fields[ALERT_WORKFLOW_STATUS] ?? 'open',
[EVENT_KIND]: 'signal',
[EVENT_ACTION]: isNew ? 'open' : isActive ? 'active' : 'close',
- [VERSION]: ruleDataClient.kibanaVersion,
[TAGS]: options.tags,
+ [VERSION]: ruleDataClient.kibanaVersion,
...(isRecovered ? { [ALERT_END]: commonRuleFields[TIMESTAMP] } : {}),
};
diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts
index f71c7391cec77..132eb096e0aaa 100644
--- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts
+++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts
@@ -40,6 +40,11 @@ function createRule(shouldWriteAlerts: boolean = true) {
name: 'warning',
},
],
+ actionVariables: {
+ context: [],
+ params: [],
+ state: [],
+ },
defaultActionGroupId: 'warning',
executor: async ({ services }) => {
nextAlerts.forEach((alert) => {
@@ -48,17 +53,17 @@ function createRule(shouldWriteAlerts: boolean = true) {
nextAlerts = [];
},
id: 'ruleTypeId',
- minimumLicenseRequired: 'basic',
isExportable: true,
+ minimumLicenseRequired: 'basic',
name: 'ruleTypeName',
producer: 'producer',
- actionVariables: {
- context: [],
- params: [],
- state: [],
- },
validate: {
- params: schema.object({}, { unknowns: 'allow' }),
+ params: schema.object(
+ {},
+ {
+ unknowns: 'allow',
+ }
+ ),
},
});
@@ -92,10 +97,12 @@ function createRule(shouldWriteAlerts: boolean = true) {
state = ((await type.executor({
alertId: 'alertId',
createdBy: 'createdBy',
+ executionId: 'b33f65d7-6e8b-4aae-8d20-c93613dec9f9',
+ logger: loggerMock.create(),
name: 'name',
+ namespace: 'namespace',
params: {},
previousStartedAt,
- startedAt,
rule: {
actions: [],
consumer: 'consumer',
@@ -118,20 +125,18 @@ function createRule(shouldWriteAlerts: boolean = true) {
services: {
alertFactory,
savedObjectsClient: {} as any,
- uiSettingsClient: {} as any,
scopedClusterClient: {} as any,
- shouldWriteAlerts: () => shouldWriteAlerts,
- shouldStopExecution: () => false,
search: {} as any,
searchSourceClient: {} as ISearchStartSearchSource,
+ shouldStopExecution: () => false,
+ shouldWriteAlerts: () => shouldWriteAlerts,
+ uiSettingsClient: {} as any,
},
spaceId: 'spaceId',
+ startedAt,
state,
tags: ['tags'],
updatedBy: 'updatedBy',
- namespace: 'namespace',
- executionId: 'b33f65d7-6e8b-4aae-8d20-c93613dec9f9',
- logger: loggerMock.create(),
})) ?? {}) as Record;
previousStartedAt = startedAt;
diff --git a/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services.mock.ts b/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services.mock.ts
index 5465e7a7922c5..a383110394da7 100644
--- a/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services.mock.ts
+++ b/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services.mock.ts
@@ -36,4 +36,5 @@ export const createLifecycleAlertServicesMock = <
): LifecycleAlertServices => ({
alertWithLifecycle: ({ id }) => alertServices.alertFactory.create(id),
getAlertStartedDate: jest.fn((id: string) => null),
+ getAlertUuid: jest.fn((id: string) => null),
});
From 2efad9d15abecfc4a24f453af797125196c25c88 Mon Sep 17 00:00:00 2001
From: Marco Liberati
Date: Thu, 27 Oct 2022 15:32:11 +0200
Subject: [PATCH 05/28] [Lens][Unified Field list] Add functional tests to
fields lists and summary popover (#143747)
* :recycle: Add testId handlers
* :white_check_mark: Add functional tests
* :lipstick: Wrap unsupported messages with testId
* :wrench: Enable creation of dataViews without timefield
* :white_check_mark: Extends tests for other dataview types + runtime fields
* :white_check_mark: Add more checks on top values charts
* :ok_hand: Integrated feedback
* :bug: Fix testIds and added some logging
---
.../components/field_stats/field_stats.tsx | 8 +-
.../datasources/form_based/field_item.tsx | 37 +--
.../form_based/fields_accordion.tsx | 8 +-
.../text_based/fields_accordion.tsx | 8 +-
.../apps/lens/group1/fields_list.ts | 233 ++++++++++++++++++
.../test/functional/apps/lens/group1/index.ts | 1 +
.../test/functional/page_objects/lens_page.ts | 16 +-
7 files changed, 287 insertions(+), 24 deletions(-)
create mode 100644 x-pack/test/functional/apps/lens/group1/fields_list.ts
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/x-pack/plugins/lens/public/datasources/form_based/field_item.tsx b/x-pack/plugins/lens/public/datasources/form_based/field_item.tsx
index 5ebfecc4cc95a..e2ee0559b3808 100644
--- a/x-pack/plugins/lens/public/datasources/form_based/field_item.tsx
+++ b/x-pack/plugins/lens/public/datasources/form_based/field_item.tsx
@@ -190,6 +190,9 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
initialFocus=".lnsFieldItem__fieldPanel"
className="lnsFieldItem__popoverAnchor"
data-test-subj="lnsFieldListPanelField"
+ panelProps={{
+ 'data-test-subj': 'lnsFieldListPanelFieldContent',
+ }}
container={document.querySelector('.application') || undefined}
button={
{
- if (params?.noDataFound) {
+ if (params.reason === 'no-data') {
// TODO: should we replace this with a default message "Analysis is not available for this field?"
const isUsingSampling = core.uiSettings.get('lens:useFieldExistenceSampling');
return (
- <>
-
- {isUsingSampling
- ? i18n.translate('xpack.lens.indexPattern.fieldStatsSamplingNoData', {
- defaultMessage:
- 'Lens is unable to create visualizations with this field because it does not contain data in the first 500 documents that match your filters. To create a visualization, drag and drop a different field.',
- })
- : i18n.translate('xpack.lens.indexPattern.fieldStatsNoData', {
- defaultMessage:
- 'Lens is unable to create visualizations with this field because it does not contain data. To create a visualization, drag and drop a different field.',
- })}
-
- >
+
+ {isUsingSampling
+ ? i18n.translate('xpack.lens.indexPattern.fieldStatsSamplingNoData', {
+ defaultMessage:
+ 'Lens is unable to create visualizations with this field because it does not contain data in the first 500 documents that match your filters. To create a visualization, drag and drop a different field.',
+ })
+ : i18n.translate('xpack.lens.indexPattern.fieldStatsNoData', {
+ defaultMessage:
+ 'Lens is unable to create visualizations with this field because it does not contain data. To create a visualization, drag and drop a different field.',
+ })}
+
+ );
+ }
+ if (params.reason === 'unsupported') {
+ return (
+
+ {params.element}
+
);
}
-
return params.element;
}}
/>
diff --git a/x-pack/plugins/lens/public/datasources/form_based/fields_accordion.tsx b/x-pack/plugins/lens/public/datasources/form_based/fields_accordion.tsx
index 105c9583e300d..d6b4c73b51082 100644
--- a/x-pack/plugins/lens/public/datasources/form_based/fields_accordion.tsx
+++ b/x-pack/plugins/lens/public/datasources/form_based/fields_accordion.tsx
@@ -169,14 +169,18 @@ export const FieldsAccordion = memo(function InnerFieldsAccordion({
}
if (hasLoaded) {
return (
-
+
{fieldsCount}
);
}
return ;
- }, [showExistenceFetchError, showExistenceFetchTimeout, hasLoaded, isFiltered, fieldsCount]);
+ }, [showExistenceFetchError, showExistenceFetchTimeout, hasLoaded, isFiltered, id, fieldsCount]);
return (
{
if (hasLoaded) {
return (
-
+
{fields.length}
);
}
return ;
- }, [fields.length, hasLoaded, isFiltered]);
+ }, [fields.length, hasLoaded, id, isFiltered]);
return (
<>
diff --git a/x-pack/test/functional/apps/lens/group1/fields_list.ts b/x-pack/test/functional/apps/lens/group1/fields_list.ts
new file mode 100644
index 0000000000000..3d571483bf9ac
--- /dev/null
+++ b/x-pack/test/functional/apps/lens/group1/fields_list.ts
@@ -0,0 +1,233 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../../ftr_provider_context';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']);
+ const find = getService('find');
+ const log = getService('log');
+ const testSubjects = getService('testSubjects');
+ const filterBar = getService('filterBar');
+ const fieldEditor = getService('fieldEditor');
+ const retry = getService('retry');
+
+ describe('lens fields list tests', () => {
+ for (const datasourceType of ['form-based', 'ad-hoc', 'ad-hoc-no-timefield']) {
+ describe(`${datasourceType} datasource`, () => {
+ before(async () => {
+ await PageObjects.visualize.navigateToNewVisualization();
+ await PageObjects.visualize.clickVisType('lens');
+
+ if (datasourceType !== 'form-based') {
+ await PageObjects.lens.createAdHocDataView(
+ '*stash*',
+ datasourceType !== 'ad-hoc-no-timefield'
+ );
+ retry.try(async () => {
+ const selectedPattern = await PageObjects.lens.getDataPanelIndexPattern();
+ expect(selectedPattern).to.eql('*stash*');
+ });
+ }
+
+ if (datasourceType !== 'ad-hoc-no-timefield') {
+ await PageObjects.lens.goToTimeRange();
+ }
+
+ await retry.try(async () => {
+ await PageObjects.lens.clickAddField();
+ await fieldEditor.setName('runtime_string');
+ await fieldEditor.enableValue();
+ await fieldEditor.typeScript("emit('abc')");
+ await fieldEditor.save();
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ });
+ });
+
+ it('should show all fields as available', async () => {
+ expect(
+ await (await testSubjects.find('lnsIndexPatternAvailableFields-count')).getVisibleText()
+ ).to.eql(53);
+ });
+
+ it('should show a histogram and top values popover for numeric field', async () => {
+ const [fieldId] = await PageObjects.lens.findFieldIdsByType('number');
+ await log.debug(`Opening field stats for ${fieldId}`);
+ await testSubjects.click(fieldId);
+ // check for popover
+ await testSubjects.exists('lnsFieldListPanel-title');
+ // check for top values chart
+ await testSubjects.existOrFail('lnsFieldListPanel-topValues');
+ const topValuesRows = await testSubjects.findAll('lnsFieldListPanel-topValues-bucket');
+ expect(topValuesRows.length).to.eql(11);
+ // check for the Other entry
+ expect(await topValuesRows[10].getVisibleText()).to.eql('Other\n96.7%');
+ // switch to date histogram
+ await testSubjects.click('lnsFieldListPanel-buttonGroup-distributionButton');
+ // check for date histogram chart
+ expect(
+ await find.existsByCssSelector(
+ '[data-test-subj="lnsFieldListPanelFieldContent"] .echChart'
+ )
+ ).to.eql(true);
+ });
+
+ it('should show a top values popover for a keyword field', async () => {
+ const [fieldId] = await PageObjects.lens.findFieldIdsByType('string');
+ await log.debug(`Opening field stats for ${fieldId}`);
+ await testSubjects.click(fieldId);
+ // check for popover
+ await testSubjects.exists('lnsFieldListPanel-title');
+ // check for top values chart
+ await testSubjects.existOrFail('lnsFieldListPanel-topValues');
+ const topValuesRows = await testSubjects.findAll('lnsFieldListPanel-topValues-bucket');
+ expect(topValuesRows.length).to.eql(11);
+ // check for the Other entry
+ expect(await topValuesRows[10].getVisibleText()).to.eql('Other\n99.9%');
+ // check no date histogram
+ expect(
+ await find.existsByCssSelector(
+ '[data-test-subj="lnsFieldListPanelFieldContent"] .echChart'
+ )
+ ).to.eql(false);
+ });
+
+ it('should show a date histogram popover for a date field', async () => {
+ const [fieldId] = await PageObjects.lens.findFieldIdsByType('date');
+ await log.debug(`Opening field stats for ${fieldId}`);
+ await testSubjects.click(fieldId);
+ // check for popover
+ await testSubjects.exists('lnsFieldListPanel-title');
+ // check for date histogram chart
+ expect(
+ await find.existsByCssSelector(
+ '[data-test-subj="lnsFieldListPanelFieldContent"] .echChart'
+ )
+ ).to.eql(true);
+ // check no top values chart
+ await testSubjects.missingOrFail('lnsFieldListPanel-buttonGroup-topValuesButton');
+ });
+
+ it('should show a placeholder message about geo points field', async () => {
+ const [fieldId] = await PageObjects.lens.findFieldIdsByType('geo_point');
+ await log.debug(`Opening field stats for ${fieldId}`);
+ await testSubjects.click(fieldId);
+ const message = await testSubjects.getVisibleText('lnsFieldListPanel-missingFieldStats');
+ expect(message).to.eql('Analysis is not available for this field.');
+ });
+
+ it('should show stats for a numeric runtime field', async () => {
+ await PageObjects.lens.searchField('runtime');
+ await PageObjects.lens.waitForField('runtime_number');
+ const [fieldId] = await PageObjects.lens.findFieldIdsByType('number');
+ await log.debug(`Opening field stats for ${fieldId}`);
+ await testSubjects.click(fieldId);
+ // check for popover
+ await testSubjects.exists('lnsFieldListPanel-title');
+ // check for top values chart
+ await testSubjects.existOrFail('lnsFieldListPanel-topValues');
+ // check values
+ const topValuesRows = await testSubjects.findAll('lnsFieldListPanel-topValues-bucket');
+ expect(topValuesRows.length).to.eql(11);
+ // check for the Other entry
+ expect(await topValuesRows[10].getVisibleText()).to.eql('Other\n96.7%');
+ // switch to date histogram
+ await testSubjects.click('lnsFieldListPanel-buttonGroup-distributionButton');
+ // check for date histogram chart
+ expect(
+ await find.existsByCssSelector(
+ '[data-test-subj="lnsFieldListPanelFieldContent"] .echChart'
+ )
+ ).to.eql(true);
+ });
+
+ it('should show stats for a keyword runtime field', async () => {
+ await PageObjects.lens.searchField('runtime');
+ await PageObjects.lens.waitForField('runtime_string');
+ const [fieldId] = await PageObjects.lens.findFieldIdsByType('string');
+ await log.debug(`Opening field stats for ${fieldId}`);
+ await testSubjects.click(fieldId);
+ // check for popover
+ await testSubjects.exists('lnsFieldListPanel-title');
+ // check for top values chart
+ await testSubjects.existOrFail('lnsFieldListPanel-topValues');
+ // check no date histogram
+ expect(
+ await find.existsByCssSelector(
+ '[data-test-subj="lnsFieldListPanelFieldContent"] .echChart'
+ )
+ ).to.eql(false);
+ await PageObjects.lens.searchField('');
+ });
+
+ it('should change popover content if user defines a filter that affects field values', async () => {
+ // check the current records count for stats
+ const [fieldId] = await PageObjects.lens.findFieldIdsByType('string');
+ await log.debug(`Opening field stats for ${fieldId}`);
+ await testSubjects.click(fieldId);
+ const valuesCount = parseInt(
+ (await testSubjects.getVisibleText('lnsFieldListPanel-statsFooter'))
+ .replaceAll(/(Calculated from | records\.)/g, '')
+ .replace(',', ''),
+ 10
+ );
+ // define a filter
+ await filterBar.addFilter('geo.src', 'is', 'CN');
+ await retry.waitFor('Wait for the filter to take effect', async () => {
+ await testSubjects.click(fieldId);
+ // check for top values chart has changed compared to the previous test
+ const newValuesCount = parseInt(
+ (await testSubjects.getVisibleText('lnsFieldListPanel-statsFooter'))
+ .replaceAll(/(Calculated from | records\.)/g, '')
+ .replace(',', ''),
+ 10
+ );
+ return newValuesCount < valuesCount;
+ });
+ });
+
+ // One Fields cap's limitation is to not know when an index has no fields based on filters
+ it('should detect fields have no data in popup if filter excludes them', async () => {
+ await filterBar.removeAllFilters();
+ await filterBar.addFilter('bytes', 'is', '-1');
+ // check via popup fields have no data
+ const [fieldId] = await PageObjects.lens.findFieldIdsByType('string');
+ await log.debug(`Opening field stats for ${fieldId}`);
+ await retry.try(async () => {
+ await testSubjects.click(fieldId);
+ expect(await testSubjects.find('lnsFieldListPanel-missingFieldStats')).to.be.ok();
+ // close the popover
+ await testSubjects.click(fieldId);
+ });
+ });
+
+ if (datasourceType !== 'ad-hoc-no-timefield') {
+ it('should move some fields as empty when the time range excludes them', async () => {
+ // remove the filter
+ await filterBar.removeAllFilters();
+ // tweak the time range to 17 Sept 2015 to 18 Sept 2015
+ await PageObjects.lens.goToTimeRange(
+ 'Sep 17, 2015 @ 06:31:44.000',
+ 'Sep 18, 2015 @ 06:31:44.000'
+ );
+ // check all fields are empty now
+ expect(
+ await (await testSubjects.find('lnsIndexPatternEmptyFields-count')).getVisibleText()
+ ).to.eql(52);
+ // check avaialble count is 0
+ expect(
+ await (
+ await testSubjects.find('lnsIndexPatternAvailableFields-count')
+ ).getVisibleText()
+ ).to.eql(1);
+ });
+ }
+ });
+ }
+ });
+}
diff --git a/x-pack/test/functional/apps/lens/group1/index.ts b/x-pack/test/functional/apps/lens/group1/index.ts
index 47f08a59e7341..302289319adbf 100644
--- a/x-pack/test/functional/apps/lens/group1/index.ts
+++ b/x-pack/test/functional/apps/lens/group1/index.ts
@@ -79,6 +79,7 @@ export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext
loadTestFile(require.resolve('./table_dashboard'));
loadTestFile(require.resolve('./table'));
loadTestFile(require.resolve('./text_based_languages'));
+ loadTestFile(require.resolve('./fields_list'));
loadTestFile(require.resolve('./layer_actions'));
}
});
diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts
index 8dd95aa107929..c814b5b161fcd 100644
--- a/x-pack/test/functional/page_objects/lens_page.ts
+++ b/x-pack/test/functional/page_objects/lens_page.ts
@@ -1335,9 +1335,9 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
await testSubjects.click('indexPattern-add-field');
},
- async createAdHocDataView(name: string) {
+ async createAdHocDataView(name: string, hasTimeField?: boolean) {
await testSubjects.click('lns-dataView-switch-link');
- await PageObjects.unifiedSearch.createNewDataView(name, true);
+ await PageObjects.unifiedSearch.createNewDataView(name, true, hasTimeField);
},
async switchToTextBasedLanguage(language: string) {
@@ -1638,5 +1638,17 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
})
);
},
+
+ async findFieldIdsByType(
+ type: 'string' | 'number' | 'date' | 'geo_point' | 'ip_range',
+ group: 'available' | 'empty' | 'meta' = 'available'
+ ) {
+ const groupCapitalized = `${group[0].toUpperCase()}${group.slice(1).toLowerCase()}`;
+ const allFieldsForType = await find.allByCssSelector(
+ `[data-test-subj="lnsIndexPattern${groupCapitalized}Fields"] .lnsFieldItem--${type}`
+ );
+ // map to testSubjId
+ return Promise.all(allFieldsForType.map((el) => el.getAttribute('data-test-subj')));
+ },
});
}
From eff4ce0cd531f832fc7019574e02281ccfac0a7d Mon Sep 17 00:00:00 2001
From: Carlos Crespo
Date: Thu, 27 Oct 2022 15:56:54 +0200
Subject: [PATCH 06/28] [Infrastructure UI] Add unified search to hosts table
(#143850)
* Add unified search to hosts table
* Add saved query support
* Adjust error handling
* Minor refactoring and unit tests
* Revert changes to translations
* CR fixes
---
.../hosts/components/hosts_container.tsx | 39 ++++++
.../metrics/hosts/components/hosts_table.tsx | 82 +++++++++++-
.../hosts/components/unified_search_bar.tsx | 77 +++++++++++
.../metrics/hosts/hooks/use_data_view.test.ts | 85 +++++++++++++
.../metrics/hosts/hooks/use_data_view.ts | 34 ++++-
.../metrics/hosts/hooks/use_unified_search.ts | 105 +++++++++++++++
.../pages/metrics/hosts/hosts_content.tsx | 120 ------------------
.../public/pages/metrics/hosts/index.tsx | 8 +-
.../translations/translations/fr-FR.json | 14 +-
.../translations/translations/ja-JP.json | 14 +-
.../translations/translations/zh-CN.json | 14 +-
11 files changed, 435 insertions(+), 157 deletions(-)
create mode 100644 x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx
create mode 100644 x-pack/plugins/infra/public/pages/metrics/hosts/components/unified_search_bar.tsx
create mode 100644 x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_data_view.test.ts
create mode 100644 x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts
delete mode 100644 x-pack/plugins/infra/public/pages/metrics/hosts/hosts_content.tsx
diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx
new file mode 100644
index 0000000000000..036d22d8b7c5f
--- /dev/null
+++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React from 'react';
+import { EuiSpacer } from '@elastic/eui';
+
+import { i18n } from '@kbn/i18n';
+import { InfraLoadingPanel } from '../../../../components/loading';
+import { useMetricsDataViewContext } from '../hooks/use_data_view';
+import { UnifiedSearchBar } from './unified_search_bar';
+import { HostsTable } from './hosts_table';
+
+export const HostContainer = () => {
+ const { metricsDataView, isDataViewLoading, hasFailedLoadingDataView } =
+ useMetricsDataViewContext();
+
+ if (isDataViewLoading) {
+ return (
+
+ );
+ }
+
+ return hasFailedLoadingDataView || !metricsDataView ? null : (
+ <>
+
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx
index d045c594f0ee6..759c65ca84b2e 100644
--- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx
@@ -7,16 +7,86 @@
import React from 'react';
import { EuiInMemoryTable } from '@elastic/eui';
-import type { SnapshotNode } from '../../../../../common/http_api';
+import { i18n } from '@kbn/i18n';
import { HostsTableColumns } from './hosts_table_columns';
+import { NoData } from '../../../../components/empty_states';
+import { InfraLoadingPanel } from '../../../../components/loading';
import { useHostTable } from '../hooks/use_host_table';
+import { useSnapshot } from '../../inventory_view/hooks/use_snaphot';
+import type { SnapshotMetricType } from '../../../../../common/inventory_models/types';
+import type { InfraTimerangeInput } from '../../../../../common/http_api';
+import { useUnifiedSearchContext } from '../hooks/use_unified_search';
+import { useSourceContext } from '../../../../containers/metrics_source';
-interface Props {
- nodes: SnapshotNode[];
-}
+const HOST_METRICS: Array<{ type: SnapshotMetricType }> = [
+ { type: 'rx' },
+ { type: 'tx' },
+ { type: 'memory' },
+ { type: 'cpuCores' },
+ { type: 'memoryTotal' },
+];
+
+export const HostsTable = () => {
+ const { sourceId } = useSourceContext();
+ const { esQuery, dateRangeTimestamp } = useUnifiedSearchContext();
+
+ const timeRange: InfraTimerangeInput = {
+ from: dateRangeTimestamp.from,
+ to: dateRangeTimestamp.to,
+ interval: '1m',
+ ignoreLookback: true,
+ };
+
+ // Snapshot endpoint internally uses the indices stored in source.configuration.metricAlias.
+ // For the Unified Search, we create a data view, which for now will be built off of source.configuration.metricAlias too
+ // if we introduce data view selection, we'll have to change this hook and the endpoint to accept a new parameter for the indices
+ const { loading, nodes, reload } = useSnapshot(
+ esQuery && JSON.stringify(esQuery),
+ HOST_METRICS,
+ [],
+ 'host',
+ sourceId,
+ dateRangeTimestamp.to,
+ '',
+ '',
+ true,
+ timeRange
+ );
-export const HostsTable: React.FunctionComponent = ({ nodes }) => {
const items = useHostTable(nodes);
+ const noData = items.length === 0;
- return ;
+ return (
+ <>
+ {loading ? (
+
+ ) : noData ? (
+
+ {
+ reload();
+ }}
+ testString="noMetricsDataPrompt"
+ />
+
+ ) : (
+
+ )}
+ >
+ );
};
diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/unified_search_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/unified_search_bar.tsx
new file mode 100644
index 0000000000000..ec9879579908e
--- /dev/null
+++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/unified_search_bar.tsx
@@ -0,0 +1,77 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { useKibana } from '@kbn/kibana-react-plugin/public';
+import type { Filter, Query, TimeRange } from '@kbn/es-query';
+import type { DataView } from '@kbn/data-views-plugin/public';
+import type { SavedQuery } from '@kbn/data-plugin/public';
+import type { InfraClientStartDeps } from '../../../../types';
+import { useUnifiedSearchContext } from '../hooks/use_unified_search';
+
+interface Props {
+ dataView: DataView;
+}
+
+export const UnifiedSearchBar = ({ dataView }: Props) => {
+ const {
+ services: { unifiedSearch },
+ } = useKibana();
+ const {
+ unifiedSearchDateRange,
+ unifiedSearchQuery,
+ submitFilterChange,
+ saveQuery,
+ clearSavedQUery,
+ } = useUnifiedSearchContext();
+
+ const { SearchBar } = unifiedSearch.ui;
+
+ const onFilterChange = (filters: Filter[]) => {
+ onQueryChange({ filters });
+ };
+
+ const onQuerySubmit = (payload: { dateRange: TimeRange; query?: Query }) => {
+ onQueryChange({ payload });
+ };
+
+ const onClearSavedQuery = () => {
+ clearSavedQUery();
+ };
+
+ const onQuerySave = (savedQuery: SavedQuery) => {
+ saveQuery(savedQuery);
+ };
+
+ const onQueryChange = ({
+ payload,
+ filters,
+ }: {
+ payload?: { dateRange: TimeRange; query?: Query };
+ filters?: Filter[];
+ }) => {
+ submitFilterChange(payload?.query, payload?.dateRange, filters);
+ };
+
+ return (
+
+ );
+};
diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_data_view.test.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_data_view.test.ts
new file mode 100644
index 0000000000000..2a2bb57b102ff
--- /dev/null
+++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_data_view.test.ts
@@ -0,0 +1,85 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useDataView } from './use_data_view';
+import { renderHook } from '@testing-library/react-hooks';
+import { KibanaReactContextValue, useKibana } from '@kbn/kibana-react-plugin/public';
+import { coreMock, notificationServiceMock } from '@kbn/core/public/mocks';
+import type { DataView } from '@kbn/data-views-plugin/public';
+import { DataViewsServicePublic } from '@kbn/data-views-plugin/public/types';
+import { InfraClientStartDeps } from '../../../../types';
+import { CoreStart } from '@kbn/core/public';
+
+jest.mock('@kbn/i18n');
+jest.mock('@kbn/kibana-react-plugin/public');
+
+let dataViewMock: jest.Mocked;
+const useKibanaMock = useKibana as jest.MockedFunction;
+const notificationMock = notificationServiceMock.createStartContract();
+const prop = { metricAlias: 'test' };
+
+const mockUseKibana = () => {
+ useKibanaMock.mockReturnValue({
+ services: {
+ ...coreMock.createStart(),
+ notifications: notificationMock,
+ dataViews: dataViewMock,
+ } as Partial & Partial,
+ } as unknown as KibanaReactContextValue & Partial>);
+};
+
+const mockDataView = {
+ id: 'mock-id',
+ title: 'mock-title',
+ timeFieldName: 'mock-time-field-name',
+ isPersisted: () => false,
+ getName: () => 'mock-data-view',
+ toSpec: () => ({}),
+} as jest.Mocked;
+
+describe('useHostTable hook', () => {
+ beforeEach(() => {
+ dataViewMock = {
+ createAndSave: jest.fn(),
+ find: jest.fn(),
+ } as Partial as jest.Mocked;
+
+ mockUseKibana();
+ });
+
+ it('should find an existing Data view', async () => {
+ dataViewMock.find.mockReturnValue(Promise.resolve([mockDataView]));
+ const { result, waitForNextUpdate } = renderHook(() => useDataView(prop));
+
+ await waitForNextUpdate();
+ expect(result.current.isDataViewLoading).toEqual(false);
+ expect(result.current.hasFailedLoadingDataView).toEqual(false);
+ expect(result.current.metricsDataView).toEqual(mockDataView);
+ });
+
+ it('should create a new Data view', async () => {
+ dataViewMock.find.mockReturnValue(Promise.resolve([]));
+ dataViewMock.createAndSave.mockReturnValue(Promise.resolve(mockDataView));
+ const { result, waitForNextUpdate } = renderHook(() => useDataView(prop));
+
+ await waitForNextUpdate();
+ expect(result.current.isDataViewLoading).toEqual(false);
+ expect(result.current.hasFailedLoadingDataView).toEqual(false);
+ expect(result.current.metricsDataView).toEqual(mockDataView);
+ });
+
+ it('should display a toast when it fails to load the data view', async () => {
+ dataViewMock.find.mockReturnValue(Promise.reject());
+ const { result, waitForNextUpdate } = renderHook(() => useDataView(prop));
+
+ await waitForNextUpdate();
+ expect(result.current.isDataViewLoading).toEqual(false);
+ expect(result.current.hasFailedLoadingDataView).toEqual(true);
+ expect(result.current.metricsDataView).toBeUndefined();
+ expect(notificationMock.toasts.addDanger).toBeCalledTimes(1);
+ });
+});
diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_data_view.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_data_view.ts
index b60b2aa89db62..f927afa72890c 100644
--- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_data_view.ts
+++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_data_view.ts
@@ -5,7 +5,8 @@
* 2.0.
*/
-import { useCallback, useState, useEffect } from 'react';
+import { i18n } from '@kbn/i18n';
+import { useCallback, useState, useEffect, useMemo } from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import createContainer from 'constate';
import type { DataView } from '@kbn/data-views-plugin/public';
@@ -15,7 +16,7 @@ import { useTrackedPromise } from '../../../../utils/use_tracked_promise';
export const useDataView = ({ metricAlias }: { metricAlias: string }) => {
const [metricsDataView, setMetricsDataView] = useState();
const {
- services: { dataViews },
+ services: { dataViews, notifications },
} = useKibana();
const [createDataViewRequest, createDataView] = useTrackedPromise(
@@ -33,7 +34,7 @@ export const useDataView = ({ metricAlias }: { metricAlias: string }) => {
const [getDataViewRequest, getDataView] = useTrackedPromise(
{
- createPromise: (indexPattern: string): Promise => {
+ createPromise: (_indexPattern: string): Promise => {
return dataViews.find(metricAlias, 1);
},
onResolve: (response: DataView[]) => {
@@ -58,17 +59,36 @@ export const useDataView = ({ metricAlias }: { metricAlias: string }) => {
}
}, [metricAlias, createDataView, getDataView]);
- const hasFailedFetchingDataView = getDataViewRequest.state === 'rejected';
- const hasFailedCreatingDataView = createDataViewRequest.state === 'rejected';
+ const isDataViewLoading = useMemo(
+ () => getDataViewRequest.state === 'pending' || createDataViewRequest.state === 'pending',
+ [getDataViewRequest.state, createDataViewRequest.state]
+ );
+
+ const hasFailedLoadingDataView = useMemo(
+ () => getDataViewRequest.state === 'rejected' || createDataViewRequest.state === 'rejected',
+ [getDataViewRequest.state, createDataViewRequest.state]
+ );
useEffect(() => {
loadDataView();
}, [metricAlias, loadDataView]);
+ useEffect(() => {
+ if (hasFailedLoadingDataView && notifications) {
+ notifications.toasts.addDanger(
+ i18n.translate('xpack.infra.hostsTable.errorOnCreateOrLoadDataview', {
+ defaultMessage:
+ 'There was an error trying to load or create the Data View: {metricAlias}',
+ values: { metricAlias },
+ })
+ );
+ }
+ }, [hasFailedLoadingDataView, notifications, metricAlias]);
+
return {
metricsDataView,
- hasFailedCreatingDataView,
- hasFailedFetchingDataView,
+ isDataViewLoading,
+ hasFailedLoadingDataView,
};
};
diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts
new file mode 100644
index 0000000000000..4b3d4e7a47df6
--- /dev/null
+++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts
@@ -0,0 +1,105 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { useKibana } from '@kbn/kibana-react-plugin/public';
+import createContainer from 'constate';
+import { useCallback, useReducer } from 'react';
+import { buildEsQuery, Filter, Query, TimeRange } from '@kbn/es-query';
+import DateMath from '@kbn/datemath';
+import type { SavedQuery } from '@kbn/data-plugin/public';
+import type { InfraClientStartDeps } from '../../../../types';
+import { useMetricsDataViewContext } from './use_data_view';
+import { useKibanaTimefilterTime } from '../../../../hooks/use_kibana_timefilter_time';
+
+const DEFAULT_FROM_MINUTES_VALUE = 15;
+
+export const useUnifiedSearch = () => {
+ const [, forceUpdate] = useReducer((x: number) => x + 1, 0);
+
+ const { metricsDataView } = useMetricsDataViewContext();
+ const { services } = useKibana();
+ const {
+ data: { query: queryManager },
+ } = services;
+
+ const [getTime, setTime] = useKibanaTimefilterTime({
+ from: `now-${DEFAULT_FROM_MINUTES_VALUE}m`,
+ to: 'now',
+ });
+ const { queryString, filterManager } = queryManager;
+
+ const currentDate = new Date();
+ const fromTS =
+ DateMath.parse(getTime().from)?.valueOf() ??
+ new Date(currentDate.getMinutes() - DEFAULT_FROM_MINUTES_VALUE).getTime();
+ const toTS = DateMath.parse(getTime().to)?.valueOf() ?? currentDate.getTime();
+
+ const currentTimeRange = {
+ from: fromTS,
+ to: toTS,
+ };
+
+ const submitFilterChange = useCallback(
+ (query?: Query, dateRange?: TimeRange, filters?: Filter[]) => {
+ if (filters) {
+ filterManager.setFilters(filters);
+ }
+
+ setTime({
+ ...getTime(),
+ ...dateRange,
+ });
+
+ queryString.setQuery({ ...queryString.getQuery(), ...query });
+ // Unified search holds the all state, we need to force the hook to rerender so that it can return the most recent values
+ // This can be removed once we get the state from the URL
+ forceUpdate();
+ },
+ [filterManager, queryString, getTime, setTime]
+ );
+
+ const saveQuery = useCallback(
+ (newSavedQuery: SavedQuery) => {
+ const savedQueryFilters = newSavedQuery.attributes.filters ?? [];
+ const globalFilters = filterManager.getGlobalFilters();
+ filterManager.setFilters([...savedQueryFilters, ...globalFilters]);
+
+ // Unified search holds the all state, we need to force the hook to rerender so that it can return the most recent values
+ // This can be removed once we get the state from the URL
+ forceUpdate();
+ },
+ [filterManager]
+ );
+
+ const clearSavedQUery = useCallback(() => {
+ filterManager.setFilters(filterManager.getGlobalFilters());
+
+ // Unified search holds the all state, we need to force the hook to rerender so that it can return the most recent values
+ // This can be removed once we get the state from the URL
+ forceUpdate();
+ }, [filterManager]);
+
+ const buildQuery = useCallback(() => {
+ if (!metricsDataView) {
+ return null;
+ }
+ return buildEsQuery(metricsDataView, queryString.getQuery(), filterManager.getFilters());
+ }, [filterManager, metricsDataView, queryString]);
+
+ return {
+ dateRangeTimestamp: currentTimeRange,
+ esQuery: buildQuery(),
+ submitFilterChange,
+ saveQuery,
+ clearSavedQUery,
+ unifiedSearchQuery: queryString.getQuery() as Query,
+ unifiedSearchDateRange: getTime(),
+ unifiedSearchFilters: filterManager.getFilters(),
+ };
+};
+
+export const UnifiedSearch = createContainer(useUnifiedSearch);
+export const [UnifiedSearchProvider, useUnifiedSearchContext] = UnifiedSearch;
diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hosts_content.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/hosts_content.tsx
deleted file mode 100644
index 5ab4a062d7fc9..0000000000000
--- a/x-pack/plugins/infra/public/pages/metrics/hosts/hosts_content.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import type { Query, TimeRange } from '@kbn/es-query';
-import { i18n } from '@kbn/i18n';
-import React, { useState, useCallback } from 'react';
-import { SearchBar } from '@kbn/unified-search-plugin/public';
-import { EuiSpacer } from '@elastic/eui';
-import { NoData } from '../../../components/empty_states';
-import { InfraLoadingPanel } from '../../../components/loading';
-import { useMetricsDataViewContext } from './hooks/use_data_view';
-import { HostsTable } from './components/hosts_table';
-import { useSourceContext } from '../../../containers/metrics_source';
-import { useSnapshot } from '../inventory_view/hooks/use_snaphot';
-import type { SnapshotMetricType } from '../../../../common/inventory_models/types';
-
-export const HostsContent: React.FunctionComponent = () => {
- const { source, sourceId } = useSourceContext();
- const [dateRange, setDateRange] = useState({ from: 'now-15m', to: 'now' });
- const [query, setQuery] = useState({ query: '', language: 'kuery' });
- const { metricsDataView, hasFailedCreatingDataView, hasFailedFetchingDataView } =
- useMetricsDataViewContext();
- // needed to refresh the lens table when filters havent changed
-
- const onQuerySubmit = useCallback(
- (payload: { dateRange: TimeRange; query?: Query }) => {
- setDateRange(payload.dateRange);
- if (payload.query) {
- setQuery(payload.query);
- }
- },
- [setDateRange, setQuery]
- );
-
- const hostMetrics: Array<{ type: SnapshotMetricType }> = [
- { type: 'rx' },
- { type: 'tx' },
- { type: 'memory' },
- { type: 'cpuCores' },
- { type: 'memoryTotal' },
- ];
-
- const { loading, nodes, reload } = useSnapshot(
- '', // use the unified search query, supported type?
- hostMetrics,
- [],
- 'host',
- sourceId,
- 1666710279338, // currentTime. need to add support for TimeRange?
- '',
- '',
- true,
- {
- from: 1666710279338, // dynamic time range needs to be supported
- interval: '1m',
- lookbackSize: 5,
- to: 1666711479338,
- }
- );
-
- const noData = !loading && nodes && nodes.length === 0;
-
- return (
-
- {metricsDataView && !loading ? (
- noData ? (
-
{
- reload();
- }}
- testString="noMetricsDataPrompt"
- />
- ) : (
- <>
-
-
-
- >
- )
- ) : hasFailedCreatingDataView || hasFailedFetchingDataView ? (
-
-
There was an error trying to load or create the Data View:
- {source?.configuration.metricAlias}
-
- ) : (
-
- )}
-
- );
-};
diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/index.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/index.tsx
index a5dfd7f2ddd0f..3321be0af193c 100644
--- a/x-pack/plugins/infra/public/pages/metrics/hosts/index.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/hosts/index.tsx
@@ -9,16 +9,16 @@ import { EuiErrorBoundary } from '@elastic/eui';
import React from 'react';
import { useTrackPageview } from '@kbn/observability-plugin/public';
import { APP_WRAPPER_CLASS } from '@kbn/core/public';
-
import { SourceErrorPage } from '../../../components/source_error_page';
import { SourceLoadingPage } from '../../../components/source_loading_page';
import { useSourceContext } from '../../../containers/metrics_source';
import { useMetricsBreadcrumbs } from '../../../hooks/use_metrics_breadcrumbs';
import { MetricsPageTemplate } from '../page_template';
import { hostsTitle } from '../../../translations';
-import { HostsContent } from './hosts_content';
import { MetricsDataViewProvider } from './hooks/use_data_view';
import { fullHeightContentStyles } from '../../../page_template.styles';
+import { UnifiedSearchProvider } from './hooks/use_unified_search';
+import { HostContainer } from './components/hosts_container';
export const HostsPage = () => {
const {
@@ -56,7 +56,9 @@ export const HostsPage = () => {
}}
>
-
+
+
+
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index 7beb42271c73f..3ebe3a63523d2 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -15595,15 +15595,15 @@
"xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel": "Voir les instructions de configuration",
"xpack.infra.homePage.settingsTabTitle": "Paramètres",
"xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder": "Rechercher des données d'infrastructure… (par exemple host.name:host-1)",
+ "xpack.infra.hostsTable.averageMemoryTotalColumnHeader": "Total de la mémoire (moy.)",
+ "xpack.infra.hostsTable.averageMemoryUsageColumnHeader": "Utilisation de la mémoire (moy.)",
+ "xpack.infra.hostsTable.averageRxColumnHeader": "",
+ "xpack.infra.hostsTable.averageTxColumnHeader": "",
+ "xpack.infra.hostsTable.diskLatencyColumnHeader": "",
"xpack.infra.hostsTable.nameColumnHeader": "Nom",
- "xpack.infra.hostsTable.operatingSystemColumnHeader": "Système d'exploitation",
"xpack.infra.hostsTable.numberOfCpusColumnHeader": "Nombre de processeurs",
- "xpack.infra.hostsTable.diskLatencyColumnHeader": "",
- "xpack.infra.hostsTable.averageTxColumnHeader": "",
- "xpack.infra.hostsTable.averageRxColumnHeader": "",
- "xpack.infra.hostsTable.averageMemoryTotalColumnHeader": "Total de la mémoire (moy.)",
+ "xpack.infra.hostsTable.operatingSystemColumnHeader": "Système d'exploitation",
"xpack.infra.hostsTable.servicesOnHostColumnHeader": "",
- "xpack.infra.hostsTable.averageMemoryUsageColumnHeader": "Utilisation de la mémoire (moy.)",
"xpack.infra.infra.nodeDetails.apmTabLabel": "APM",
"xpack.infra.infra.nodeDetails.createAlertLink": "Créer une règle d'inventaire",
"xpack.infra.infra.nodeDetails.openAsPage": "Ouvrir en tant que page",
@@ -33705,4 +33705,4 @@
"xpack.painlessLab.title": "Painless Lab",
"xpack.painlessLab.walkthroughButtonLabel": "Présentation"
}
-}
+}
\ No newline at end of file
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index fcc2c1c49cdf8..a74277d17862a 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -15580,15 +15580,15 @@
"xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel": "セットアップの手順を表示",
"xpack.infra.homePage.settingsTabTitle": "設定",
"xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder": "インフラストラクチャデータを検索…(例:host.name:host-1)",
- "xpack.infra.hostsTable.nameColumnHeader": "名前",
- "xpack.infra.hostsTable.operatingSystemColumnHeader": "オペレーティングシステム",
- "xpack.infra.hostsTable.numberOfCpusColumnHeader": "CPU数",
- "xpack.infra.hostsTable.diskLatencyColumnHeader": "",
+ "xpack.infra.hostsTable.averageMemoryTotalColumnHeader": "メモリ合計 (平均) ",
+ "xpack.infra.hostsTable.averageMemoryUsageColumnHeader": "メモリー使用状況(平均)",
"xpack.infra.hostsTable.averageTxColumnHeader": "",
"xpack.infra.hostsTable.averageRxColumnHeader": "",
- "xpack.infra.hostsTable.averageMemoryTotalColumnHeader": "メモリ合計 (平均) ",
+ "xpack.infra.hostsTable.diskLatencyColumnHeader": "",
+ "xpack.infra.hostsTable.nameColumnHeader": "名前",
+ "xpack.infra.hostsTable.numberOfCpusColumnHeader": "CPU数",
+ "xpack.infra.hostsTable.operatingSystemColumnHeader": "オペレーティングシステム",
"xpack.infra.hostsTable.servicesOnHostColumnHeader": "",
- "xpack.infra.hostsTable.averageMemoryUsageColumnHeader": "メモリー使用状況(平均)",
"xpack.infra.infra.nodeDetails.apmTabLabel": "APM",
"xpack.infra.infra.nodeDetails.createAlertLink": "インベントリルールの作成",
"xpack.infra.infra.nodeDetails.openAsPage": "ページとして開く",
@@ -33679,4 +33679,4 @@
"xpack.painlessLab.title": "Painless Lab",
"xpack.painlessLab.walkthroughButtonLabel": "実地検証"
}
-}
+}
\ No newline at end of file
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index ff49a2cd73d6c..6127534e13b93 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -15601,15 +15601,15 @@
"xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel": "查看设置说明",
"xpack.infra.homePage.settingsTabTitle": "设置",
"xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder": "搜索基础设施数据……(例如 host.name:host-1)",
+ "xpack.infra.hostsTable.averageMemoryTotalColumnHeader": "内存合计 (平均值)",
+ "xpack.infra.hostsTable.averageMemoryUsageColumnHeader": "内存使用率(平均值)",
+ "xpack.infra.hostsTable.averageRxColumnHeader": "",
+ "xpack.infra.hostsTable.averageTxColumnHeader": "",
+ "xpack.infra.hostsTable.diskLatencyColumnHeader": "",
"xpack.infra.hostsTable.nameColumnHeader": "名称",
- "xpack.infra.hostsTable.operatingSystemColumnHeader": "操作系统",
"xpack.infra.hostsTable.numberOfCpusColumnHeader": "# 个 CPU",
- "xpack.infra.hostsTable.diskLatencyColumnHeader": "",
- "xpack.infra.hostsTable.averageTxColumnHeader": "",
- "xpack.infra.hostsTable.averageRxColumnHeader": "",
- "xpack.infra.hostsTable.averageMemoryTotalColumnHeader": "内存合计 (平均值)",
+ "xpack.infra.hostsTable.operatingSystemColumnHeader": "操作系统",
"xpack.infra.hostsTable.servicesOnHostColumnHeader": "",
- "xpack.infra.hostsTable.averageMemoryUsageColumnHeader": "内存使用率(平均值)",
"xpack.infra.infra.nodeDetails.apmTabLabel": "APM",
"xpack.infra.infra.nodeDetails.createAlertLink": "创建库存规则",
"xpack.infra.infra.nodeDetails.openAsPage": "以页面形式打开",
@@ -33716,4 +33716,4 @@
"xpack.painlessLab.title": "Painless 实验室",
"xpack.painlessLab.walkthroughButtonLabel": "指导"
}
-}
+}
\ No newline at end of file
From 028fa94e2cbe793da9520cf0eaaa28c6a9142b84 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20S=C3=A1nchez?=
Date: Thu, 27 Oct 2022 16:11:30 +0200
Subject: [PATCH 07/28] Adds RBAC API checks for event filters (#144009)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../validators/event_filter_validator.ts | 24 ++++++++++++-------
1 file changed, 16 insertions(+), 8 deletions(-)
diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/event_filter_validator.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/event_filter_validator.ts
index 7759caa20e1f9..2ff4a663560b7 100644
--- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/event_filter_validator.ts
+++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/event_filter_validator.ts
@@ -48,8 +48,16 @@ export class EventFilterValidator extends BaseValidator {
return item.listId === ENDPOINT_EVENT_FILTERS_LIST_ID;
}
+ protected async validateHasWritePrivilege(): Promise {
+ return super.validateHasPrivilege('canWriteEventFilters');
+ }
+
+ protected async validateHasReadPrivilege(): Promise {
+ return super.validateHasPrivilege('canReadEventFilters');
+ }
+
async validatePreCreateItem(item: CreateExceptionListItemOptions) {
- await this.validateCanManageEndpointArtifacts();
+ await this.validateHasWritePrivilege();
await this.validateEventFilterData(item);
// user can always create a global entry so additional checks not needed
@@ -67,7 +75,7 @@ export class EventFilterValidator extends BaseValidator {
): Promise {
const updatedItem = _updatedItem as ExceptionItemLikeOptions;
- await this.validateCanManageEndpointArtifacts();
+ await this.validateHasWritePrivilege();
await this.validateEventFilterData(updatedItem);
try {
@@ -96,27 +104,27 @@ export class EventFilterValidator extends BaseValidator {
}
async validatePreGetOneItem(): Promise {
- await this.validateCanManageEndpointArtifacts();
+ await this.validateHasReadPrivilege();
}
async validatePreSummary(): Promise {
- await this.validateCanManageEndpointArtifacts();
+ await this.validateHasReadPrivilege();
}
async validatePreDeleteItem(): Promise {
- await this.validateCanManageEndpointArtifacts();
+ await this.validateHasWritePrivilege();
}
async validatePreExport(): Promise {
- await this.validateCanManageEndpointArtifacts();
+ await this.validateHasWritePrivilege();
}
async validatePreSingleListFind(): Promise {
- await this.validateCanManageEndpointArtifacts();
+ await this.validateHasReadPrivilege();
}
async validatePreMultiListFind(): Promise {
- await this.validateCanManageEndpointArtifacts();
+ await this.validateHasReadPrivilege();
}
async validatePreImport(): Promise {
From 83e85bd9607ad4506d7b5dc8a13ae0224c70d8db Mon Sep 17 00:00:00 2001
From: Marco Liberati
Date: Thu, 27 Oct 2022 16:16:57 +0200
Subject: [PATCH 08/28] [Lens] Revisit Random sampling UI (#143929)
* :lipstick: Revisit settings ui
* Design suggestions (#18)
Co-authored-by: Michael Marcialis
---
.../datasources/form_based/layer_settings.tsx | 120 +++++++++++++-----
1 file changed, 86 insertions(+), 34 deletions(-)
diff --git a/x-pack/plugins/lens/public/datasources/form_based/layer_settings.tsx b/x-pack/plugins/lens/public/datasources/form_based/layer_settings.tsx
index 7d02ac98f23a4..ec161ef996737 100644
--- a/x-pack/plugins/lens/public/datasources/form_based/layer_settings.tsx
+++ b/x-pack/plugins/lens/public/datasources/form_based/layer_settings.tsx
@@ -5,9 +5,20 @@
* 2.0.
*/
-import { EuiFormRow, EuiRange, EuiBetaBadge } from '@elastic/eui';
+import {
+ EuiFormRow,
+ EuiRange,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiBetaBadge,
+ EuiText,
+ EuiLink,
+ EuiSpacer,
+} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import { css } from '@emotion/react';
import React from 'react';
+import { FormattedMessage } from '@kbn/i18n-react';
import type { DatasourceLayerSettingsProps } from '../../types';
import type { FormBasedPrivateState } from './types';
@@ -22,54 +33,95 @@ export function LayerSettingsPanel({
const currentSamplingIndex = samplingIndex > -1 ? samplingIndex : samplingValue.length - 1;
return (
+
+
+
+
+
+ ),
+ }}
+ />
+
+ >
+ }
label={
<>
{i18n.translate('xpack.lens.xyChart.randomSampling.label', {
- defaultMessage: 'Sampling',
+ defaultMessage: 'Random sampling',
})}{' '}
>
}
>
- {
- setState({
- ...state,
- layers: {
- ...state.layers,
- [layerId]: {
- ...state.layers[layerId],
- sampling: samplingValue[Number(e.currentTarget.value)],
- },
- },
- });
- }}
- showInput={false}
- showRange={false}
- showTicks
- step={1}
- min={0}
- max={samplingValue.length - 1}
- ticks={samplingValue.map((v, i) => ({ label: `${v}`, value: i }))}
- />
+
+
+
+
+
+
+
+ {
+ setState({
+ ...state,
+ layers: {
+ ...state.layers,
+ [layerId]: {
+ ...state.layers[layerId],
+ sampling: samplingValue[Number(e.currentTarget.value)],
+ },
+ },
+ });
+ }}
+ showInput={false}
+ showRange={false}
+ showTicks
+ step={1}
+ min={0}
+ max={samplingValue.length - 1}
+ ticks={samplingValue.map((v, i) => ({ label: `${v * 100}%`, value: i }))}
+ />
+
+
+
+
+
+
+
);
}
From b1fb85bf0846fbbaa36b7587c487f4092a660fa7 Mon Sep 17 00:00:00 2001
From: Liza Katz
Date: Thu, 27 Oct 2022 17:30:51 +0300
Subject: [PATCH 09/28] fix (#144099)
---
.../kbn-apm-config-loader/src/apm_config.ts | 20 +++++++++----------
1 file changed, 9 insertions(+), 11 deletions(-)
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' }
+);
From cb306dfa073216b73ba2718a70442b20dde3c998 Mon Sep 17 00:00:00 2001
From: Uladzislau Lasitsa
Date: Thu, 27 Oct 2022 17:39:00 +0300
Subject: [PATCH 10/28] Added support of saved search (#144095)
---
.../components/visualize_byvalue_editor.tsx | 1 +
.../components/visualize_editor.tsx | 1 +
.../components/visualize_editor_common.tsx | 4 ++++
.../components/visualize_top_nav.tsx | 7 ++++++-
.../utils/get_top_nav_config.tsx | 7 +++++++
.../utils/use/use_linked_search_updates.ts | 20 ++++++++++---------
6 files changed, 30 insertions(+), 10 deletions(-)
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);
From 9ae38803d8ce559db2bd47400ec083a8ff138cd0 Mon Sep 17 00:00:00 2001
From: Yaroslav Kuznietsov
Date: Thu, 27 Oct 2022 18:00:09 +0300
Subject: [PATCH 11/28] [Lens][Agg based Heatmap] Navigate to Lens Agg based
Heatmap. (#143820)
* Added base code for converting heatmap to lens.
* Added navigateToLens to visType.
* Added canNavigateToLens event.
* Fixed type.
* Added basic support of heatmap converting to lens.
* Added visType as arg.
* Added validation according to the
* Fixed heatmap std_dev.
* Fixed failing xy.
* Fixed tests.
* Added config for legend.
* Added support of converting color ranges.
* Fixed palette for default ranges.
* Refactored.
* Added tests for convertToLens.
* Added tests for getConfiguration.
* Fixed problem
* Added basic functional tests for heatmap.
* Added functional test for not convertable case.
* Added tests for not valid config and fixed one with valid.
* Added test for custom ranges.
* Added empty filters if x-axis is not defined.
- Added empty filters if y-axis is defined, but x-axis is not and if no x/y-axis was defined.
- Added/fixed tests.
* Removed unused service.
* Histogram problems fixed.
* Fixed include/exclude regexp.
* Fixed terms.
---
.../heatmap_function.test.ts.snap | 1 +
.../expression_functions/heatmap_function.ts | 1 +
.../common/types/expression_functions.ts | 1 +
.../expression_renderers/heatmap_renderer.tsx | 9 +-
src/plugins/vis_types/heatmap/kibana.json | 39 +-
.../configurations/index.test.ts | 114 +++++
.../convert_to_lens/configurations/index.ts | 54 +++
.../convert_to_lens/configurations/palette.ts | 52 +++
.../public/convert_to_lens/index.test.ts | 166 +++++++
.../heatmap/public/convert_to_lens/index.ts | 97 ++++
.../heatmap/public/convert_to_lens/types.ts | 17 +
.../vis_types/heatmap/public/plugin.ts | 13 +-
.../heatmap/public/sample_vis.test.mocks.ts | 11 +-
.../vis_types/heatmap/public/services.ts | 13 +
.../vis_types/heatmap/public/to_ast.test.ts | 4 +-
.../vis_types/heatmap/public/utils/palette.ts | 9 +-
.../heatmap/public/vis_type/heatmap.tsx | 7 +
src/plugins/vis_types/pie/kibana.json | 39 +-
.../table/public/convert_to_lens/index.ts | 1 +
.../convert_to_lens/lib/buckets/index.test.ts | 20 +-
.../convert_to_lens/lib/buckets/index.ts | 12 +-
.../lib/configurations/index.ts | 2 +-
.../lib/configurations/palette.ts | 25 +-
.../convert_to_lens/lib/convert/formula.ts | 1 +
.../lib/convert/last_value.test.ts | 16 +-
.../convert_to_lens/lib/convert/last_value.ts | 8 +-
.../lib/convert/metric.test.ts | 5 +
.../convert_to_lens/lib/convert/metric.ts | 4 +-
.../lib/convert/parent_pipeline.test.ts | 17 +
.../lib/convert/parent_pipeline.ts | 12 +-
.../lib/convert/percentage_mode.test.ts | 7 +-
.../lib/convert/percentile.test.ts | 27 +-
.../convert_to_lens/lib/convert/percentile.ts | 3 +-
.../lib/convert/percentile_rank.test.ts | 29 +-
.../lib/convert/percentile_rank.ts | 3 +-
.../convert_to_lens/lib/convert/range.test.ts | 1 -
.../convert_to_lens/lib/convert/range.ts | 13 +-
.../lib/convert/sibling_pipeline.test.ts | 13 +-
.../lib/convert/sibling_pipeline.ts | 11 +-
.../lib/convert/std_deviation.test.ts | 44 +-
.../lib/convert/std_deviation.ts | 4 +-
.../lib/convert/supported_metrics.ts | 68 +--
.../convert_to_lens/lib/convert/terms.test.ts | 14 +
.../convert_to_lens/lib/convert/terms.ts | 48 +-
.../convert_to_lens/lib/convert/types.ts | 2 +
.../lib/metrics/formula.test.ts | 29 +-
.../convert_to_lens/lib/metrics/formula.ts | 28 +-
.../lib/metrics/metrics.test.ts | 438 ++++++++++++------
.../convert_to_lens/lib/metrics/metrics.ts | 20 +-
.../lib/metrics/percentage_formula.test.ts | 7 +-
.../lib/metrics/percentage_formula.ts | 3 +-
.../convert_to_lens/types/configurations.ts | 59 ++-
.../common/convert_to_lens/types/params.ts | 2 +-
.../common/convert_to_lens/utils.ts | 12 +-
.../public/convert_to_lens/index.ts | 2 +
.../public/convert_to_lens/schemas.test.ts | 4 +-
.../public/convert_to_lens/schemas.ts | 8 +-
.../public/convert_to_lens/utils.test.ts | 12 +-
.../public/convert_to_lens/utils.ts | 5 +-
.../public/visualizations/heatmap/types.ts | 2 +-
.../visualizations/heatmap/visualization.tsx | 28 +-
.../lens/open_in_lens/agg_based/heatmap.ts | 219 +++++++++
.../apps/lens/open_in_lens/agg_based/index.ts | 1 +
63 files changed, 1573 insertions(+), 363 deletions(-)
create mode 100644 src/plugins/vis_types/heatmap/public/convert_to_lens/configurations/index.test.ts
create mode 100644 src/plugins/vis_types/heatmap/public/convert_to_lens/configurations/index.ts
create mode 100644 src/plugins/vis_types/heatmap/public/convert_to_lens/configurations/palette.ts
create mode 100644 src/plugins/vis_types/heatmap/public/convert_to_lens/index.test.ts
create mode 100644 src/plugins/vis_types/heatmap/public/convert_to_lens/index.ts
create mode 100644 src/plugins/vis_types/heatmap/public/convert_to_lens/types.ts
create mode 100644 src/plugins/vis_types/heatmap/public/services.ts
create mode 100644 x-pack/test/functional/apps/lens/open_in_lens/agg_based/heatmap.ts
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/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