From 95bc7c0e1cef525cf8cf1cd264efe7e1e77d2294 Mon Sep 17 00:00:00 2001
From: Joseph McElroy
Date: Tue, 4 Apr 2023 10:54:50 +0100
Subject: [PATCH 001/112] [Behavioral Analytics] Reinstate Integrations Page +
Sub routes refactor (#154267)
## Highlights:
- Now have sub-routes for collection view page. The base component is
responsible for fetching the analytics collection and displaying an
error state when analytics collection not found / deleted.
- Integration page now present
- remove unneeded section param in favour for the explicit route paths
- removed the old "events" page
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.../add_analytics_collection_logic.ts | 4 +-
.../analytics_collection_events.test.tsx | 71 ---------
.../analytics_collection_events.tsx | 148 ------------------
.../analytics_collection_integrate.test.tsx | 41 +++--
.../analytics_collection_integrate.tsx | 107 -------------
...tion_integrate_javascript_client_embed.tsx | 58 ++++---
..._collection_integrate_javascript_embed.tsx | 53 +++++--
...nalytics_collection_integrate_searchui.tsx | 2 +-
.../analytics_collection_integrate_view.tsx | 134 ++++++++++++++++
.../analytics_collection_overview.test.tsx | 99 ++++++++++++
.../analytics_collection_overview.tsx | 55 +++++++
.../analytics_collection_view.test.tsx | 64 ++------
.../analytics_collection_view.tsx | 108 ++++++-------
.../analytics_collection_card.tsx | 4 +-
.../components/layout/page_template.tsx | 4 +-
.../public/applications/analytics/index.tsx | 13 +-
.../public/applications/analytics/routes.ts | 7 +-
.../engine/engine_api/engine_api.tsx | 5 +-
.../translations/translations/fr-FR.json | 8 -
.../translations/translations/ja-JP.json | 8 -
.../translations/translations/zh-CN.json | 8 -
21 files changed, 473 insertions(+), 528 deletions(-)
delete mode 100644 x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_events.test.tsx
delete mode 100644 x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_events.tsx
delete mode 100644 x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate_view.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_overview.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_overview.tsx
diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/add_analytics_collections/add_analytics_collection_logic.ts b/x-pack/plugins/enterprise_search/public/applications/analytics/components/add_analytics_collections/add_analytics_collection_logic.ts
index e6c7b64904ba1..552f717ff94cc 100644
--- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/add_analytics_collections/add_analytics_collection_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/add_analytics_collections/add_analytics_collection_logic.ts
@@ -24,7 +24,7 @@ import {
AddAnalyticsCollectionApiLogicArgs,
AddAnalyticsCollectionApiLogicResponse,
} from '../../api/add_analytics_collection/add_analytics_collection_api_logic';
-import { COLLECTION_VIEW_PATH } from '../../routes';
+import { COLLECTION_OVERVIEW_PATH } from '../../routes';
const SERVER_ERROR_CODE = 500;
@@ -102,7 +102,7 @@ export const AddAnalyticsCollectionLogic = kea<
})
);
KibanaLogic.values.navigateToUrl(
- generateEncodedPath(COLLECTION_VIEW_PATH, {
+ generateEncodedPath(COLLECTION_OVERVIEW_PATH, {
name,
})
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_events.test.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_events.test.tsx
deleted file mode 100644
index ebe2c67ea8f9f..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_events.test.tsx
+++ /dev/null
@@ -1,71 +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 '../../../__mocks__/shallow_useeffect.mock';
-
-import { setMockValues, setMockActions } from '../../../__mocks__/kea_logic';
-
-import React from 'react';
-
-import { shallow } from 'enzyme';
-
-import { EuiEmptyPrompt } from '@elastic/eui';
-
-import { AnalyticsCollection } from '../../../../../common/types/analytics';
-import { EntSearchLogStream } from '../../../shared/log_stream';
-
-import { AnalyticsCollectionEvents } from './analytics_collection_events';
-
-describe('AnalyticsCollectionEvents', () => {
- const analyticsCollection: AnalyticsCollection = {
- events_datastream: 'logs-elastic_analytics.events-example',
- name: 'example',
- };
-
- const mockActions = {
- analyticsEventsIndexExists: jest.fn(),
- };
-
- beforeEach(() => {
- jest.clearAllMocks();
-
- setMockActions(mockActions);
- });
-
- it('renders', () => {
- setMockValues({
- isPresent: true,
- isLoading: false,
- });
- const expectedQuery = '_index: logs-elastic_analytics.events-example';
-
- const wrapper = shallow( );
- expect(wrapper.find(EntSearchLogStream).prop('query')).toEqual(expectedQuery);
- });
-
- describe('empty state', () => {
- it('renders when analytics events index is not present', () => {
- setMockValues({
- isPresent: false,
- });
-
- const wrapper = shallow( );
-
- expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);
- });
-
- it('renders when analytics events index check is not performed yet', () => {
- setMockValues({
- isLoading: true,
- });
-
- const wrapper = shallow( );
-
- expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);
- });
- });
-});
diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_events.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_events.tsx
deleted file mode 100644
index ad220dbb3efe1..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_events.tsx
+++ /dev/null
@@ -1,148 +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 React, { useEffect } from 'react';
-
-import { useValues, useActions } from 'kea';
-
-import { EuiEmptyPrompt, EuiButton, EuiLink, EuiTitle } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n-react';
-
-import { ENTERPRISE_SEARCH_ANALYTICS_LOGS_SOURCE_ID } from '../../../../../common/constants';
-import { AnalyticsCollection } from '../../../../../common/types/analytics';
-import { docLinks } from '../../../shared/doc_links';
-import { generateEncodedPath } from '../../../shared/encode_path_params';
-import { KibanaLogic } from '../../../shared/kibana';
-
-import { EntSearchLogStream } from '../../../shared/log_stream';
-import { COLLECTION_VIEW_PATH } from '../../routes';
-
-import { AnalyticsEventsIndexExistsLogic } from './analytics_events_index_exists_logic';
-
-interface AnalyticsCollectionEventsProps {
- collection: AnalyticsCollection;
-}
-
-const EVENTS_POLLING_INTERVAL = 30 * 1000;
-
-export const AnalyticsCollectionEvents: React.FC = ({
- collection,
-}) => {
- const { analyticsEventsIndexExists } = useActions(AnalyticsEventsIndexExistsLogic);
- const { isLoading, isPresent } = useValues(AnalyticsEventsIndexExistsLogic);
- const { navigateToUrl } = useValues(KibanaLogic);
-
- useEffect(() => {
- analyticsEventsIndexExists(collection.events_datastream);
-
- const interval = setInterval(() => {
- analyticsEventsIndexExists(collection.events_datastream);
- }, EVENTS_POLLING_INTERVAL);
-
- return () => clearInterval(interval);
- }, []);
-
- return (
- <>
- {(isLoading || !isPresent) && (
-
-
-
- There are no analytics events for {collection.name} yet
- >
- ),
- }}
- />
-
-
- }
- body={i18n.translate(
- 'xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.emptyState.body',
- {
- defaultMessage:
- "Start tracking events by adding the behavioral analytics client to every page of your website or application that you'd like to track",
- }
- )}
- actions={
-
- navigateToUrl(
- generateEncodedPath(COLLECTION_VIEW_PATH, {
- id: collection.name,
- section: 'integrate',
- })
- )
- }
- >
- {i18n.translate(
- 'xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.emptyState.actions',
- {
- defaultMessage: 'View integration instructions',
- }
- )}
-
- }
- footer={
-
- {i18n.translate(
- 'xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.emptyState.footer',
- {
- defaultMessage: 'Visit the behavioral analytics documentation',
- }
- )}
-
- }
- />
- )}
- {!isLoading && isPresent && (
-
- )}
- >
- );
-};
diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate.test.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate.test.tsx
index 53300b2649a08..668f58534ea5e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate.test.tsx
@@ -9,12 +9,17 @@ import '../../../../__mocks__/shallow_useeffect.mock';
import React from 'react';
-import { EuiCodeBlock } from '@elastic/eui';
-import { mountWithIntl } from '@kbn/test-jest-helpers';
+import { shallow } from 'enzyme';
+
+import { EuiCodeBlock, EuiSteps } from '@elastic/eui';
import { AnalyticsCollection } from '../../../../../../common/types/analytics';
-import { AnalyticsCollectionIntegrate } from './analytics_collection_integrate';
+import { AnalyticsCollectionIntegrateView } from './analytics_collection_integrate_view';
+
+jest.mock('../../../../shared/enterprise_search_url', () => ({
+ getEnterpriseSearchUrl: () => 'http://localhost:3002',
+}));
describe('AnalyticsCollectionIntegrate', () => {
const analyticsCollections: AnalyticsCollection = {
@@ -27,23 +32,31 @@ describe('AnalyticsCollectionIntegrate', () => {
});
it('renders', () => {
- const wrapper = mountWithIntl(
-
+ const wrapper = shallow(
+
);
- expect(wrapper.find(EuiCodeBlock)).toHaveLength(3);
+ expect(wrapper.find(EuiSteps).dive().find(EuiCodeBlock)).toHaveLength(3);
wrapper.find('[data-test-subj="searchuiEmbed"]').at(0).simulate('click');
- expect(wrapper.find(EuiCodeBlock)).toHaveLength(3);
+ expect(wrapper.find(EuiSteps).dive().find(EuiCodeBlock)).toHaveLength(3);
wrapper.find('[data-test-subj="javascriptClientEmbed"]').at(0).simulate('click');
- expect(wrapper.find(EuiCodeBlock)).toHaveLength(5);
+ expect(wrapper.find(EuiSteps).dive().find(EuiCodeBlock)).toHaveLength(5);
});
- it('check value of analyticsDNSUrl & webClientSrc', () => {
- const wrapper = mountWithIntl(
-
+ it('check value of config & webClientSrc', () => {
+ const wrapper = shallow(
+
);
- expect(wrapper.find(EuiCodeBlock).at(0).text()).toContain(
- 'data-dsn="/api/analytics/collections/example"'
+ expect(wrapper.find(EuiSteps).dive().find(EuiCodeBlock).at(0).dive().text()).toContain(
+ 'https://cdn.jsdelivr.net/npm/@elastic/behavioral-analytics-browser-tracker@2/dist/umd/index.global.js'
);
- expect(wrapper.find(EuiCodeBlock).at(0).text()).toContain('src="/analytics.js"');
+
+ expect(wrapper.find(EuiSteps).dive().find(EuiCodeBlock).at(1).dive().text())
+ .toMatchInlineSnapshot(`
+ ""
+ `);
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate.tsx
deleted file mode 100644
index dee042584ad8d..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate.tsx
+++ /dev/null
@@ -1,107 +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 React from 'react';
-
-import { EuiPanel, EuiSpacer, EuiSteps, EuiTab, EuiTabs, EuiTitle } from '@elastic/eui';
-
-import { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps';
-import { i18n } from '@kbn/i18n';
-
-import { AnalyticsCollection } from '../../../../../../common/types/analytics';
-import { getEnterpriseSearchUrl } from '../../../../shared/enterprise_search_url';
-
-import { javascriptClientEmbedSteps } from './analytics_collection_integrate_javascript_client_embed';
-import { javascriptEmbedSteps } from './analytics_collection_integrate_javascript_embed';
-import { searchUIEmbedSteps } from './analytics_collection_integrate_searchui';
-
-interface AnalyticsCollectionIntegrateProps {
- collection: AnalyticsCollection;
-}
-
-export type TabKey = 'javascriptEmbed' | 'searchuiEmbed' | 'javascriptClientEmbed';
-
-export const AnalyticsCollectionIntegrate: React.FC = ({
- collection,
-}) => {
- const analyticsDNSUrl = getEnterpriseSearchUrl(`/api/analytics/collections/${collection.name}`);
- const webClientSrc = getEnterpriseSearchUrl('/analytics.js');
-
- const [selectedTab, setSelectedTab] = React.useState('javascriptEmbed');
-
- const tabs: Array<{
- key: TabKey;
- title: string;
- }> = [
- {
- key: 'javascriptEmbed',
- title: i18n.translate(
- 'xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.javascriptEmbed.title',
- {
- defaultMessage: 'Javascript Embed',
- }
- ),
- },
- {
- key: 'javascriptClientEmbed',
- title: i18n.translate(
- 'xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.javascriptClientEmbed.title',
- {
- defaultMessage: 'Javascript Client',
- }
- ),
- },
- {
- key: 'searchuiEmbed',
- title: i18n.translate(
- 'xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchuiEmbed.title',
- {
- defaultMessage: 'Search UI',
- }
- ),
- },
- ];
-
- const steps: Record = {
- javascriptClientEmbed: javascriptClientEmbedSteps(analyticsDNSUrl),
- javascriptEmbed: javascriptEmbedSteps(webClientSrc, analyticsDNSUrl),
- searchuiEmbed: searchUIEmbedSteps(setSelectedTab),
- };
-
- return (
-
-
-
- {i18n.translate(
- 'xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.title',
- {
- defaultMessage: 'Start tracking events',
- }
- )}
-
-
-
-
- {tabs.map((tab) => (
- {
- setSelectedTab(tab.key);
- }}
- isSelected={selectedTab === tab.key}
- data-test-subj={tab.key}
- data-telemetry-id={`entSearch-analytics-integrate-${tab.key}-tab`}
- >
- {tab.title}
-
- ))}
-
-
-
-
- );
-};
diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate_javascript_client_embed.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate_javascript_client_embed.tsx
index 136617db09112..c240d941c74b5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate_javascript_client_embed.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate_javascript_client_embed.tsx
@@ -11,7 +11,9 @@ import { EuiCodeBlock, EuiSpacer, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-export const javascriptClientEmbedSteps = (analyticsDNSUrl: string) => [
+import { AnalyticsConfig } from './analytics_collection_integrate_view';
+
+export const javascriptClientEmbedSteps = (analyticsConfig: AnalyticsConfig) => [
{
title: i18n.translate(
'xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.javascriptClientEmbed.stepOne.title',
@@ -60,7 +62,8 @@ export const javascriptClientEmbedSteps = (analyticsDNSUrl: string) => [
{`import {
createTracker,
trackPageView,
- trackEvent,
+ trackSearch,
+ trackSearchClick
} from "@elastic/behavioral-analytics-javascript-tracker";`}
@@ -82,7 +85,7 @@ export const javascriptClientEmbedSteps = (analyticsDNSUrl: string) => [
'xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.javascriptClientEmbed.stepThree.description',
{
defaultMessage:
- ' Use createTracker method to initialize the tracker with your DSN. You will then be able to use the tracker to send events to Behavioral Analytics.',
+ 'Use createTracker method to initialize the tracker with your Configuration. You will then be able to use the tracker to send events to Behavioral Analytics.',
}
)}
@@ -97,7 +100,9 @@ export const javascriptClientEmbedSteps = (analyticsDNSUrl: string) => [
{`createTracker({
- dsn: "${analyticsDNSUrl}",
+ endpoint: "${analyticsConfig.endpoint}",
+ collectionName: "${analyticsConfig.collectionName}",
+ apiKey: "${analyticsConfig.apiKey}"
});`}
@@ -108,7 +113,7 @@ export const javascriptClientEmbedSteps = (analyticsDNSUrl: string) => [
title: i18n.translate(
'xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.javascriptClientEmbed.stepFour.title',
{
- defaultMessage: 'Dispatch Pageview and behavior events',
+ defaultMessage: 'Dispatch Pageview and search behavior events',
}
),
children: (
@@ -155,28 +160,41 @@ const SearchPage = (props) => {
'xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.javascriptClientEmbed.stepFour.descriptionThree',
{
defaultMessage:
- 'You can also dispatch custom events to Behavioral Analytics by calling the trackEvent method.',
+ 'You can also use trackSearch and trackSearchClick to track what your customers are searching and clicking on in your application.',
}
)}
- {`// track a custom event in React
-import { trackEvent } from '@elastic/behavioral-analytics-javascript-tracker';
+ {`
+import { trackSearch } from '@elastic/behavioral-analytics-javascript-tracker';
+
+const SearchResult = ({ hit }) => {
-const ProductDetailPage = (props) => {
+ const clickHandler = () => {
+ trackSearchClick({
+ document: { id: hit.id, index: "products" },
+ search: {
+ query: "search term",
+ filters: [],
+ page: { current: 1, size: 10 },
+ results: {
+ items: [
+ { id: "123", index: "products" }
+ ],
+ total_results: 10
+ },
+ sort: {
+ name: "relevance",
+ },
+ search_application: "website",
+ }
+ })
+ }
return (
-
-
Product detail page
- {
- trackEvent("click", {
- category: "product",
- action: "add_to_cart",
- label: "product_id",
- value: "123"
- })
- }} value="Add to Basket"/>
-
+
+ {hit.title}
+
)
}`}
diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate_javascript_embed.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate_javascript_embed.tsx
index 425912d8166e6..632a6626404cd 100644
--- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate_javascript_embed.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate_javascript_embed.tsx
@@ -14,7 +14,9 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { docLinks } from '../../../../shared/doc_links';
-export const javascriptEmbedSteps = (webClientSrc: string, analyticsDNSUrl: string) => [
+import { AnalyticsConfig } from './analytics_collection_integrate_view';
+
+export const javascriptEmbedSteps = (webClientSrc: string, analyticsConfig: AnalyticsConfig) => [
{
title: i18n.translate(
'xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.javascriptEmbed.stepOne.title',
@@ -35,7 +37,7 @@ export const javascriptEmbedSteps = (webClientSrc: string, analyticsDNSUrl: stri
)}
- {``}
+ {``}
>
@@ -61,7 +63,11 @@ export const javascriptEmbedSteps = (webClientSrc: string, analyticsDNSUrl: stri
)}
- {''}
+ {``}
>
@@ -69,9 +75,9 @@ export const javascriptEmbedSteps = (webClientSrc: string, analyticsDNSUrl: stri
},
{
title: i18n.translate(
- 'xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchuiEmbed.stepThree.title',
+ 'xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.javascriptEmbed.stepThree.title',
{
- defaultMessage: 'Track individual events',
+ defaultMessage: 'Track search events',
}
),
children: (
@@ -80,7 +86,7 @@ export const javascriptEmbedSteps = (webClientSrc: string, analyticsDNSUrl: stri
- {`window.elasticAnalytics.trackEvent("click", {
- category: "product",
- action: "add_to_cart",
- label: "product_id",
- value: "123"
+ {`window.elasticAnalytics.trackSearch({
+ search: {
+ query: "laptop",
+ filters: [
+ { field: "brand", value: ["apple"] },
+ { field: "price", value: ["1000-2000"] },
+ ],
+ page: {
+ current: 1,
+ size: 10,
+ },
+ results: {
+ items: [
+ {
+ document: {
+ id: "123",
+ index: "products",
+ },
+ page: {
+ url: "http://my-website.com/products/123",
+ },
+ },
+ ],
+ total_results: 100,
+ },
+ sort: {
+ name: "relevance",
+ },
+ search_application: "website",
+ }
});`}
diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate_searchui.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate_searchui.tsx
index 6db73c11cb7ee..c52fbdbff01f5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate_searchui.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate_searchui.tsx
@@ -12,7 +12,7 @@ import { EuiCodeBlock, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
-import { TabKey } from './analytics_collection_integrate';
+import { TabKey } from './analytics_collection_integrate_view';
export const searchUIEmbedSteps = (setSelectedTab: (tab: TabKey) => void) => [
{
diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate_view.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate_view.tsx
new file mode 100644
index 0000000000000..497317712646c
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate_view.tsx
@@ -0,0 +1,134 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { EuiSpacer, EuiSteps, EuiTab, EuiTabs } from '@elastic/eui';
+
+import { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps';
+
+import { i18n } from '@kbn/i18n';
+
+import { AnalyticsCollection } from '../../../../../../common/types/analytics';
+
+import { getEnterpriseSearchUrl } from '../../../../shared/enterprise_search_url';
+
+import { EnterpriseSearchAnalyticsPageTemplate } from '../../layout/page_template';
+
+import { javascriptClientEmbedSteps } from './analytics_collection_integrate_javascript_client_embed';
+import { javascriptEmbedSteps } from './analytics_collection_integrate_javascript_embed';
+import { searchUIEmbedSteps } from './analytics_collection_integrate_searchui';
+
+interface AnalyticsCollectionIntegrateProps {
+ analyticsCollection: AnalyticsCollection;
+}
+
+export type TabKey = 'javascriptEmbed' | 'searchuiEmbed' | 'javascriptClientEmbed';
+
+export interface AnalyticsConfig {
+ apiKey: string;
+ collectionName: string;
+ endpoint: string;
+}
+
+export const AnalyticsCollectionIntegrateView: React.FC = ({
+ analyticsCollection,
+}) => {
+ const [selectedTab, setSelectedTab] = React.useState('javascriptEmbed');
+
+ const analyticsConfig: AnalyticsConfig = {
+ apiKey: '########',
+ collectionName: analyticsCollection?.name,
+ endpoint: getEnterpriseSearchUrl(),
+ };
+ const webClientSrc = `https://cdn.jsdelivr.net/npm/@elastic/behavioral-analytics-browser-tracker@2/dist/umd/index.global.js`;
+
+ const tabs: Array<{
+ key: TabKey;
+ title: string;
+ }> = [
+ {
+ key: 'javascriptEmbed',
+ title: i18n.translate(
+ 'xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.javascriptEmbed.title',
+ {
+ defaultMessage: 'Javascript Embed',
+ }
+ ),
+ },
+ {
+ key: 'javascriptClientEmbed',
+ title: i18n.translate(
+ 'xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.javascriptClientEmbed.title',
+ {
+ defaultMessage: 'Javascript Client',
+ }
+ ),
+ },
+ {
+ key: 'searchuiEmbed',
+ title: i18n.translate(
+ 'xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchuiEmbed.title',
+ {
+ defaultMessage: 'Search UI',
+ }
+ ),
+ },
+ ];
+
+ const steps: Record = {
+ javascriptClientEmbed: javascriptClientEmbedSteps(analyticsConfig),
+ javascriptEmbed: javascriptEmbedSteps(webClientSrc, analyticsConfig),
+ searchuiEmbed: searchUIEmbedSteps(setSelectedTab),
+ };
+
+ return (
+
+ <>
+
+ {tabs.map((tab) => (
+ {
+ setSelectedTab(tab.key);
+ }}
+ isSelected={selectedTab === tab.key}
+ data-test-subj={tab.key}
+ data-telemetry-id={`entSearch-analytics-integrate-${tab.key}-tab`}
+ >
+ {tab.title}
+
+ ))}
+
+
+
+ >
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_overview.test.tsx
new file mode 100644
index 0000000000000..3de4065975705
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_overview.test.tsx
@@ -0,0 +1,99 @@
+/*
+ * 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 '../../../__mocks__/shallow_useeffect.mock';
+
+import { setMockValues, setMockActions } from '../../../__mocks__/kea_logic';
+import { mockUseParams } from '../../../__mocks__/react_router';
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { AnalyticsCollection } from '../../../../../common/types/analytics';
+import { EnterpriseSearchAnalyticsPageTemplate } from '../layout/page_template';
+
+import { AnalyticsCollectionChartWithLens } from './analytics_collection_chart';
+
+import { AnalyticsCollectionOverview } from './analytics_collection_overview';
+
+const mockValues = {
+ analyticsCollection: {
+ events_datastream: 'analytics-events-example',
+ name: 'Analytics-Collection-1',
+ } as AnalyticsCollection,
+ searchSessionId: 'session-id',
+ timeRange: {
+ from: 'now-90d',
+ to: 'now',
+ },
+};
+
+const mockActions = {
+ fetchAnalyticsCollection: jest.fn(),
+ fetchAnalyticsCollectionDataViewId: jest.fn(),
+ setTimeRange: jest.fn(),
+};
+
+describe('AnalyticsOverView', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ mockUseParams.mockReturnValue({ name: '1', section: 'settings' });
+ });
+
+ it('renders with Data', async () => {
+ const wrapper = shallow(
+
+ );
+ expect(wrapper.find(AnalyticsCollectionChartWithLens)).toHaveLength(1);
+ });
+
+ it('sends correct telemetry page name for selected tab', async () => {
+ setMockValues(mockValues);
+ setMockActions(mockActions);
+
+ const wrapper = shallow(
+
+ );
+
+ expect(wrapper.prop('pageViewTelemetry')).toBe('View Analytics Collection - Overview');
+ });
+
+ it('render toolbar in pageHeader rightSideItems ', async () => {
+ setMockValues({ ...mockValues, dataViewId: null });
+ setMockActions(mockActions);
+
+ const wrapper = shallow(
+
+ );
+
+ expect(
+ wrapper?.find(EnterpriseSearchAnalyticsPageTemplate)?.prop('pageHeader')?.rightSideItems
+ ).toHaveLength(1);
+ });
+
+ it('render AnalyticsCollectionChartWithLens with collection', () => {
+ setMockValues(mockValues);
+ setMockActions(mockActions);
+
+ const wrapper = shallow(
+
+ );
+ expect(wrapper?.find(AnalyticsCollectionChartWithLens)).toHaveLength(1);
+ expect(wrapper?.find(AnalyticsCollectionChartWithLens).props()).toEqual({
+ dataViewQuery: 'analytics-events-example',
+ id: 'analytics-collection-chart-Analytics-Collection-1',
+ searchSessionId: 'session-id',
+ setTimeRange: mockActions.setTimeRange,
+ timeRange: {
+ from: 'now-90d',
+ to: 'now',
+ },
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_overview.tsx
new file mode 100644
index 0000000000000..e0df130e9256b
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_overview.tsx
@@ -0,0 +1,55 @@
+/*
+ * 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 { useActions, useValues } from 'kea';
+
+import { i18n } from '@kbn/i18n';
+
+import { AnalyticsCollection } from '../../../../../common/types/analytics';
+
+import { EnterpriseSearchAnalyticsPageTemplate } from '../layout/page_template';
+
+import { AnalyticsCollectionChartWithLens } from './analytics_collection_chart';
+import { AnalyticsCollectionToolbar } from './analytics_collection_toolbar/analytics_collection_toolbar';
+import { AnalyticsCollectionToolbarLogic } from './analytics_collection_toolbar/analytics_collection_toolbar_logic';
+
+interface AnalyticsCollectionOverviewProps {
+ analyticsCollection: AnalyticsCollection;
+}
+
+export const AnalyticsCollectionOverview: React.FC = ({
+ analyticsCollection,
+}) => {
+ const { setTimeRange } = useActions(AnalyticsCollectionToolbarLogic);
+ const { timeRange, searchSessionId } = useValues(AnalyticsCollectionToolbarLogic);
+
+ return (
+ ],
+ }}
+ >
+
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.test.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.test.tsx
index 60261af169928..3aea0bf7c64b6 100644
--- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.test.tsx
@@ -14,13 +14,12 @@ import React from 'react';
import { shallow } from 'enzyme';
+import { EuiEmptyPrompt } from '@elastic/eui';
+
import { AnalyticsCollection } from '../../../../../common/types/analytics';
import { EnterpriseSearchAnalyticsPageTemplate } from '../layout/page_template';
-import { AnalyticsCollectionChartWithLens } from './analytics_collection_chart';
-
-import { AnalyticsCollectionIntegrate } from './analytics_collection_integrate/analytics_collection_integrate';
-import { AnalyticsCollectionSettings } from './analytics_collection_settings';
+import { AnalyticsCollectionIntegrateView } from './analytics_collection_integrate/analytics_collection_integrate_view';
import { AnalyticsCollectionView } from './analytics_collection_view';
@@ -29,11 +28,6 @@ const mockValues = {
events_datastream: 'analytics-events-example',
name: 'Analytics-Collection-1',
} as AnalyticsCollection,
- searchSessionId: 'session-id',
- timeRange: {
- from: 'now-90d',
- to: 'now',
- },
};
const mockActions = {
@@ -46,7 +40,7 @@ describe('AnalyticsView', () => {
beforeEach(() => {
jest.clearAllMocks();
- mockUseParams.mockReturnValue({ name: '1', section: 'settings' });
+ mockUseParams.mockReturnValue({ name: '1' });
});
it('renders when analytics collection is empty on initial query', () => {
@@ -59,54 +53,18 @@ describe('AnalyticsView', () => {
expect(mockActions.fetchAnalyticsCollection).toHaveBeenCalled();
- expect(wrapper.find(AnalyticsCollectionSettings)).toHaveLength(0);
- expect(wrapper.find(AnalyticsCollectionIntegrate)).toHaveLength(0);
- });
-
- it('renders with Data', async () => {
- setMockValues(mockValues);
- setMockActions(mockActions);
-
- shallow( );
-
- expect(mockActions.fetchAnalyticsCollection).toHaveBeenCalled();
- });
-
- it('sends correct telemetry page name for selected tab', async () => {
- setMockValues(mockValues);
- setMockActions(mockActions);
-
- const wrapper = shallow( );
-
- expect(wrapper.prop('pageViewTelemetry')).toBe('View Analytics Collection - settings');
+ expect(wrapper.find(AnalyticsCollectionIntegrateView)).toHaveLength(0);
+ expect(wrapper.find(EnterpriseSearchAnalyticsPageTemplate)).toHaveLength(1);
});
- it('render toolbar in pageHeader rightSideItems ', async () => {
- setMockValues({ ...mockValues, dataViewId: null });
+ it('render deleted state for deleted analytics collection', async () => {
+ setMockValues({ ...mockValues, analyticsCollection: null });
setMockActions(mockActions);
const wrapper = shallow( );
- expect(
- wrapper?.find(EnterpriseSearchAnalyticsPageTemplate)?.prop('pageHeader')?.rightSideItems
- ).toHaveLength(1);
- });
-
- it('render AnalyticsCollectionChartWithLens with collection', () => {
- setMockValues(mockValues);
- setMockActions(mockActions);
-
- const wrapper = shallow( );
- expect(wrapper?.find(AnalyticsCollectionChartWithLens)).toHaveLength(1);
- expect(wrapper?.find(AnalyticsCollectionChartWithLens).props()).toEqual({
- dataViewQuery: 'analytics-events-example',
- id: 'analytics-collection-chart-Analytics-Collection-1',
- searchSessionId: 'session-id',
- setTimeRange: mockActions.setTimeRange,
- timeRange: {
- from: 'now-90d',
- to: 'now',
- },
- });
+ expect(wrapper?.find(EnterpriseSearchAnalyticsPageTemplate).find(EuiEmptyPrompt)).toHaveLength(
+ 1
+ );
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.tsx
index ace77928430a1..b33f509fb5db7 100644
--- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_view.tsx
@@ -7,84 +7,80 @@
import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom';
+import { Switch } from 'react-router-dom';
import { useActions, useValues } from 'kea';
import { EuiEmptyPrompt } from '@elastic/eui';
-
import { i18n } from '@kbn/i18n';
+import { Route } from '@kbn/shared-ux-router';
+import {
+ COLLECTION_EXPLORER_PATH,
+ COLLECTION_INTEGRATE_PATH,
+ COLLECTION_OVERVIEW_PATH,
+} from '../../routes';
import { AddAnalyticsCollection } from '../add_analytics_collections/add_analytics_collection';
import { EnterpriseSearchAnalyticsPageTemplate } from '../layout/page_template';
-import { AnalyticsCollectionChartWithLens } from './analytics_collection_chart';
-import { AnalyticsCollectionToolbar } from './analytics_collection_toolbar/analytics_collection_toolbar';
-import { AnalyticsCollectionToolbarLogic } from './analytics_collection_toolbar/analytics_collection_toolbar_logic';
+import { AnalyticsCollectionIntegrateView } from './analytics_collection_integrate/analytics_collection_integrate_view';
+import { AnalyticsCollectionOverview } from './analytics_collection_overview';
import { FetchAnalyticsCollectionLogic } from './fetch_analytics_collection_logic';
export const AnalyticsCollectionView: React.FC = () => {
const { fetchAnalyticsCollection } = useActions(FetchAnalyticsCollectionLogic);
- const { setTimeRange } = useActions(AnalyticsCollectionToolbarLogic);
const { analyticsCollection, isLoading } = useValues(FetchAnalyticsCollectionLogic);
- const { timeRange, searchSessionId } = useValues(AnalyticsCollectionToolbarLogic);
- const { name, section } = useParams<{ name: string; section: string }>();
+ const { name } = useParams<{ name: string }>();
useEffect(() => {
fetchAnalyticsCollection(name);
}, []);
+ if (analyticsCollection) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
return (
- ],
- }}
- >
- {analyticsCollection ? (
-
- ) : (
-
- {i18n.translate(
- 'xpack.enterpriseSearch.analytics.collections.collectionsView.collectionNotFoundState.headingTitle',
- {
- defaultMessage: 'You may have deleted this analytics collection',
- }
- )}
-
- }
- body={
-
- {i18n.translate(
- 'xpack.enterpriseSearch.analytics.collections.collectionsView.collectionNotFoundState.subHeading',
- {
- defaultMessage:
- 'An analytics collection provides a place to store the analytics events for any given search application you are building. Create a new collection to get started.',
- }
- )}
-
- }
- actions={[ ]}
- />
- )}
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.analytics.collections.collectionsView.collectionNotFoundState.headingTitle',
+ {
+ defaultMessage: 'You may have deleted this analytics collection',
+ }
+ )}
+
+ }
+ body={
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.analytics.collections.collectionsView.collectionNotFoundState.subHeading',
+ {
+ defaultMessage:
+ 'An analytics collection provides a place to store the analytics events for any given search application you are building. Create a new collection to get started.',
+ }
+ )}
+
+ }
+ actions={[ ]}
+ />
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collection_card/analytics_collection_card.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collection_card/analytics_collection_card.tsx
index b7a288f2fca33..031212489213b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collection_card/analytics_collection_card.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_overview/analytics_collection_card/analytics_collection_card.tsx
@@ -35,7 +35,7 @@ import { generateEncodedPath } from '../../../../shared/encode_path_params';
import { KibanaLogic } from '../../../../shared/kibana';
import { withLensData } from '../../../hoc/with_lens_data';
-import { COLLECTION_VIEW_PATH } from '../../../routes';
+import { COLLECTION_OVERVIEW_PATH } from '../../../routes';
import { FilterBy, getFormulaByFilter } from '../../../utils/get_formula_by_filter';
@@ -102,7 +102,7 @@ export const AnalyticsCollectionCard: React.FC<
const cardStyles = AnalyticsCollectionCardStyles(euiTheme);
const status = getChartStatus(secondaryMetric);
const CARD_THEME = getCardTheme(euiTheme)[status];
- const collectionViewUrl = generateEncodedPath(COLLECTION_VIEW_PATH, {
+ const collectionViewUrl = generateEncodedPath(COLLECTION_OVERVIEW_PATH, {
name: collection.name,
});
const handleCardClick = (event: MouseEvent) => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/layout/page_template.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/layout/page_template.tsx
index a6df50dd76895..ea2a8ab6088f1 100644
--- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/layout/page_template.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/layout/page_template.tsx
@@ -17,7 +17,7 @@ import { SendEnterpriseSearchTelemetry } from '../../../shared/telemetry';
import {
COLLECTION_EXPLORER_PATH,
COLLECTION_INTEGRATE_PATH,
- COLLECTION_VIEW_PATH,
+ COLLECTION_OVERVIEW_PATH,
} from '../../routes';
interface EnterpriseSearchAnalyticsPageTemplateProps extends PageTemplateProps {
@@ -41,7 +41,7 @@ export const EnterpriseSearchAnalyticsPageTemplate: React.FC<
integration: generateEncodedPath(COLLECTION_INTEGRATE_PATH, {
name: analyticsName,
}),
- overview: generateEncodedPath(COLLECTION_VIEW_PATH, {
+ overview: generateEncodedPath(COLLECTION_OVERVIEW_PATH, {
name: analyticsName,
}),
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/index.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/index.tsx
index d84e2c33dc7cc..b578ace741182 100644
--- a/x-pack/plugins/enterprise_search/public/applications/analytics/index.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/analytics/index.tsx
@@ -17,12 +17,7 @@ import { VersionMismatchPage } from '../shared/version_mismatch';
import { AnalyticsCollectionView } from './components/analytics_collection_view/analytics_collection_view';
import { AnalyticsOverview } from './components/analytics_overview/analytics_overview';
-import {
- ROOT_PATH,
- COLLECTION_VIEW_PATH,
- COLLECTION_INTEGRATE_PATH,
- COLLECTION_EXPLORER_PATH,
-} from './routes';
+import { ROOT_PATH, COLLECTION_VIEW_PATH } from './routes';
export const Analytics: React.FC = (props) => {
const { enterpriseSearchVersion, kibanaVersion } = props;
@@ -40,13 +35,9 @@ export const Analytics: React.FC = (props) => {
)}
-
+
-
-
-
-
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/routes.ts b/x-pack/plugins/enterprise_search/public/applications/analytics/routes.ts
index 1ae97b9a184b0..7da6b08e13718 100644
--- a/x-pack/plugins/enterprise_search/public/applications/analytics/routes.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/analytics/routes.ts
@@ -7,6 +7,7 @@
export const ROOT_PATH = '/';
export const COLLECTIONS_PATH = '/collections';
-export const COLLECTION_VIEW_PATH = `${COLLECTIONS_PATH}/:name/overview`;
-export const COLLECTION_INTEGRATE_PATH = `${COLLECTIONS_PATH}/:name/integrate`;
-export const COLLECTION_EXPLORER_PATH = `${COLLECTIONS_PATH}/:name/explorer`;
+export const COLLECTION_VIEW_PATH = `${COLLECTIONS_PATH}/:name`;
+export const COLLECTION_OVERVIEW_PATH = `${COLLECTION_VIEW_PATH}/overview`;
+export const COLLECTION_INTEGRATE_PATH = `${COLLECTION_VIEW_PATH}/integrate`;
+export const COLLECTION_EXPLORER_PATH = `${COLLECTION_VIEW_PATH}/explorer`;
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_api/engine_api.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_api/engine_api.tsx
index a439d9aba4950..aad7e84d99084 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_api/engine_api.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_api/engine_api.tsx
@@ -23,7 +23,7 @@ import {
import { i18n } from '@kbn/i18n';
import { ANALYTICS_PLUGIN } from '../../../../../../common/constants';
-import { COLLECTION_VIEW_PATH } from '../../../../analytics/routes';
+import { COLLECTION_INTEGRATE_PATH } from '../../../../analytics/routes';
import { docLinks } from '../../../../shared/doc_links';
import { generateEncodedPath } from '../../../../shared/encode_path_params';
import { getEnterpriseSearchUrl } from '../../../../shared/enterprise_search_url';
@@ -165,9 +165,8 @@ export const EngineAPI: React.FC = () => {
data-telemetry-id="entSearchContent-engines-api-step4-learnHowLink"
onClick={() =>
navigateToUrl(
- generateEncodedPath(`${ANALYTICS_PLUGIN.URL}${COLLECTION_VIEW_PATH}`, {
+ generateEncodedPath(`${ANALYTICS_PLUGIN.URL}${COLLECTION_INTEGRATE_PATH}`, {
id: engineName,
- section: 'integrate',
}),
{ shouldNotCreateHref: true }
)
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index 97dba3a7a5ccf..6b7465d5b8c4a 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -11104,7 +11104,6 @@
"xpack.embeddableEnhanced.actions.panelNotifications.manyDrilldowns": "Le panneau comporte {count} explorations",
"xpack.embeddableEnhanced.actions.panelNotifications.oneDrilldown": "Le panneau comporte 1 recherche",
"xpack.embeddableEnhanced.Drilldowns": "Explorations",
- "xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.emptyState.title": "{title}",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.javascriptEmbed.stepThree.description": "Suivez des événements individuels, tels que les clics, en appelant la méthode trackEvent. {link}",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchui.stepOne.description": "Suivez les instructions pour incorporer Behavioral Analytics dans votre site via {embedLink} ou {clientLink}.",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchui.stepTwo.moreInfoDescription": "Pour en savoir plus sur l'initialisation du suivi et le déclenchement d'événements, consultez {link}.",
@@ -11379,11 +11378,6 @@
"xpack.enterpriseSearch.actionsHeader": "Actions",
"xpack.enterpriseSearch.analytics.collections.collectionsView.collectionNotFoundState.headingTitle": "Vous avez peut-être supprimé cette collection d'analyses",
"xpack.enterpriseSearch.analytics.collections.collectionsView.collectionNotFoundState.subHeading": "Une collection d'analyse permet de stocker les événements d'analyse pour toute application de recherche que vous créez. Créez une nouvelle collection pour commencer.",
- "xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.columns.eventName": "Nom de l'événement",
- "xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.columns.userUuid": "UUID d'utilisateur",
- "xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.emptyState.actions": "Afficher les instructions de l'intégration",
- "xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.emptyState.body": "Commencer à suivre les événements en ajoutant le client d'analyse comportementale à chaque page de votre site web ou de l'application que vous souhaitez suivre",
- "xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.emptyState.footer": "Visiter la documentation relative à l'analyse comportementale",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.javascriptClientEmbed.stepFour.description": "Une fois que vous avez appelé createTracker, vous pouvez utiliser les méthodes de suivi telles que trackPageView pour envoyer les événements vers Behavioral Analytics.",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.javascriptClientEmbed.stepFour.descriptionThree": "Vous pouvez également déployer des événements personnalisés dans Behavioral Analytics en appelant la méthode trackEvent.",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.javascriptClientEmbed.stepFour.descriptionTwo": "Une fois initialisé, vous aurez la possibilité de suivre les vues de page dans votre application.",
@@ -11409,10 +11403,8 @@
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchui.stepTwo.importDescription": "Puis importez le plug-in Behavioral Analytics dans votre application.",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchui.stepTwo.setupDescription": "Enfin, ajoutez le plug-in dans la configuration Search UI. Selon la façon dont vous avez incorporé Behavioral Analytics, vous devrez peut-être transmettre le client. L'exemple ci-dessous montre comment transmettre le client lorsque le client Javascript est utilisé.",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchuiEmbed.stepOne.title": "Incorporer Behavioral Analytics",
- "xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchuiEmbed.stepThree.title": "Suivre les événements individuels",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchuiEmbed.stepTwo.title": "Installer le plug-in Behavioral Analytics Search UI",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchuiEmbed.title": "Search UI",
- "xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.title": "Commencer à suivre les événements",
"xpack.enterpriseSearch.analytics.collections.collectionsView.settingsTab.delete.buttonTitle": "Supprimer cette collection",
"xpack.enterpriseSearch.analytics.collections.collectionsView.settingsTab.delete.headingTitle": "Supprimer cette collection d'analyses",
"xpack.enterpriseSearch.analytics.collections.collectionsView.settingsTab.delete.warning": "Cette action est irréversible",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 041bb04c8111f..0405d4a82c61f 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -11103,7 +11103,6 @@
"xpack.embeddableEnhanced.actions.panelNotifications.manyDrilldowns": "パネルには{count}個のドリルダウンがあります",
"xpack.embeddableEnhanced.actions.panelNotifications.oneDrilldown": "パネルには 1 個のドリルダウンがあります",
"xpack.embeddableEnhanced.Drilldowns": "ドリルダウン",
- "xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.emptyState.title": "{title}",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.javascriptEmbed.stepThree.description": "trackEventメソッドを呼び出し、クリックなどの個別のイベントを追跡します。{link}",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchui.stepOne.description": "指示に従って、{embedLink}または{clientLink}からサイトに行動分析を組み込んでください。",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchui.stepTwo.moreInfoDescription": "トラッカーの初期化およびイベントの発生については、{link}を参照してください。",
@@ -11378,11 +11377,6 @@
"xpack.enterpriseSearch.actionsHeader": "アクション",
"xpack.enterpriseSearch.analytics.collections.collectionsView.collectionNotFoundState.headingTitle": "この分析コレクションを削除した可能性があります",
"xpack.enterpriseSearch.analytics.collections.collectionsView.collectionNotFoundState.subHeading": "分析コレクションには、構築している特定の検索アプリケーションの分析イベントを格納できます。開始するには、新しいコレクションを作成してください。",
- "xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.columns.eventName": "イベント名",
- "xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.columns.userUuid": "ユーザーUUID",
- "xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.emptyState.actions": "統合手順を表示",
- "xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.emptyState.body": "追跡したいWebサイトやアプリケーションの各ページに行動分析クライアントを追加して、イベントの追跡を開始します。",
- "xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.emptyState.footer": "行動分析ドキュメントを表示",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.javascriptClientEmbed.stepFour.description": "createTrackerを呼び出したら、trackPageViewなどのtrackerメソッドを使って、Behavioral Analyticsにイベントを送ることができます。",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.javascriptClientEmbed.stepFour.descriptionThree": "また、trackEventメソッドを呼び出すことで、Behavioral Analyticsにカスタムイベントをディスパッチすることもできます。",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.javascriptClientEmbed.stepFour.descriptionTwo": "初期化すると、アプリケーションのページビューを追跡することができるようになります。",
@@ -11408,10 +11402,8 @@
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchui.stepTwo.importDescription": "Behavioral Analyticsプラグインをアプリにインポートします。",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchui.stepTwo.setupDescription": "最後に、プラグインをSearch UI構成に追加します。Behavioral Analyticsをどのように組み込んだかによって、クライアントを渡す必要がある場合があります。以下の例では、Javascriptクライアントを使用する場合の受け渡し方法を示しています。",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchuiEmbed.stepOne.title": "Behavioral Analyticsを組み込み",
- "xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchuiEmbed.stepThree.title": "個別のイベントを追跡",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchuiEmbed.stepTwo.title": "Search UI行動分析プラグインをインストール",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchuiEmbed.title": "Search UI",
- "xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.title": "追跡イベントの開始",
"xpack.enterpriseSearch.analytics.collections.collectionsView.settingsTab.delete.buttonTitle": "このコレクションを削除",
"xpack.enterpriseSearch.analytics.collections.collectionsView.settingsTab.delete.headingTitle": "この分析コレクションを削除",
"xpack.enterpriseSearch.analytics.collections.collectionsView.settingsTab.delete.warning": "この操作は元に戻すことができません",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 1b9b344ab9986..8f62ac80cf764 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -11104,7 +11104,6 @@
"xpack.embeddableEnhanced.actions.panelNotifications.manyDrilldowns": "面板有 {count} 个向下钻取",
"xpack.embeddableEnhanced.actions.panelNotifications.oneDrilldown": "面板有 1 个向下钻取",
"xpack.embeddableEnhanced.Drilldowns": "向下钻取",
- "xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.emptyState.title": "{title}",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.javascriptEmbed.stepThree.description": "通过调用 trackEvent 方法跟踪单个事件,如点击。{link}",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchui.stepOne.description": "按照说明通过 {embedLink} 或 {clientLink} 将行为分析嵌入到您的站点中。",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchui.stepTwo.moreInfoDescription": "请参阅 {link} 了解有关初始化跟踪器和触发事件的更多信息。",
@@ -11379,11 +11378,6 @@
"xpack.enterpriseSearch.actionsHeader": "操作",
"xpack.enterpriseSearch.analytics.collections.collectionsView.collectionNotFoundState.headingTitle": "您可能已删除此分析集合",
"xpack.enterpriseSearch.analytics.collections.collectionsView.collectionNotFoundState.subHeading": "分析集合为您正在构建的任何给定搜索应用程序提供了一个用于存储分析事件的位置。创建新集合以开始。",
- "xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.columns.eventName": "事件名称",
- "xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.columns.userUuid": "用户 UUID",
- "xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.emptyState.actions": "查看集成说明",
- "xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.emptyState.body": "通过将行为分析客户端添加到您要跟踪的每个网站页面或应用程序来启动事件跟踪",
- "xpack.enterpriseSearch.analytics.collections.collectionsView.eventsTab.emptyState.footer": "访问行为分析文档",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.javascriptClientEmbed.stepFour.description": "调用 createTracker 后,可以使用跟踪器方法(如 trackPageView)将事件发送到行为分析。",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.javascriptClientEmbed.stepFour.descriptionThree": "还可以通过调用 trackEvent 方法来向行为分析分派定制事件。",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.javascriptClientEmbed.stepFour.descriptionTwo": "完成初始化后,您将能够跟踪您应用程序中的页面视图。",
@@ -11409,10 +11403,8 @@
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchui.stepTwo.importDescription": "然后将行为分析插件导入到您的应用。",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchui.stepTwo.setupDescription": "最后,将插件添加到搜索 UI 配置。根据您嵌入行为分析的方式,您可能需要传入客户端。以下示例说明如何在使用 Javascript 客户端时传入客户端。",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchuiEmbed.stepOne.title": "嵌入行为分析",
- "xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchuiEmbed.stepThree.title": "跟踪单个事件",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchuiEmbed.stepTwo.title": "安装搜索 UI 行为分析插件",
"xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.searchuiEmbed.title": "搜索 UI",
- "xpack.enterpriseSearch.analytics.collections.collectionsView.integrateTab.title": "开始跟踪事件",
"xpack.enterpriseSearch.analytics.collections.collectionsView.settingsTab.delete.buttonTitle": "删除此集合",
"xpack.enterpriseSearch.analytics.collections.collectionsView.settingsTab.delete.headingTitle": "删除此分析集合",
"xpack.enterpriseSearch.analytics.collections.collectionsView.settingsTab.delete.warning": "此操作不可逆",
From bd48d13e171cd1ead17a72bf34cb3eff17311ee6 Mon Sep 17 00:00:00 2001
From: Marco Liberati
Date: Tue, 4 Apr 2023 12:22:49 +0200
Subject: [PATCH 002/112] [Lens] Embeddable overrides feature (#153204)
## Summary
Fixes #149220
Fixes https://github.com/elastic/kibana/issues/148845
This PR introduces the concept of `Embeddable` overrides within the Lens
Embeddable: overrides are a small subset of configuration options that
can overwrite Lens original Elastic Chart rendering configuration.
This feature will only apply at the Embeddable level and is ignored
within the Lens editor context.
The readme has been updated with some documentation on the topic.
### Playground changes
This PR also contains a refactor/enhancement of the playground example
app to showcase the `attributes` and `overrides` for most Lens charts.
The UI has been redesigned with 3 main dropdowns with some explanation
of the different switch controls:
For each override setting an example code snippet is shown on hover:
The second menu badge shows when overrides are enabled:
Different chart types have different options available:
The datatable and metric visualization have no overrides for now.
### Difference with #152842
The two feature work in a similar space, but they are substantially
different from their use cases.
The `overrides` feature is something to use in 2 scenarios:
* small styling/tuning configuration of the final chart via
Lens-unsupported Elastic Chart props
* For instance having `integersOnly` ticks on a XY axis, or value labels
outside only for a pie chart
* Selectively turning off specific event handlers on the component
* For instance to completely remove any complex logic from a legend item
(i.e. filter popup)
The `preventDefault` feature is useful instead when the user wants to
keep all the handlers at chart level, but selectively disabled some
Kibana-wide event from bubble. For instance clicking on a bar or pie
slice should trigger the `edit` event but the consumer's custom handler
should be the only one to be executed, without bubbling up to the
`unifiedSearch` registered triggers.
### Checklist
Delete any items that are not applicable to this PR.
- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
### Risk Matrix
Delete this section if it is not applicable to this PR.
Before closing this PR, invite QA, stakeholders, and other developers to
identify risks that should be tested prior to the change/feature
release.
When forming the risk matrix, consider some of the following examples
and how they may potentially impact the change:
| Risk | Probability | Severity | Mitigation/Notes |
|---------------------------|-------------|----------|-------------------------|
| Multiple Spaces—unexpected behavior in non-default Kibana Space.
| Low | High | Integration tests will verify that all features are still
supported in non-default Kibana Space and when user switches between
spaces. |
| Multiple nodes—Elasticsearch polling might have race conditions
when multiple Kibana nodes are polling for the same tasks. | High | Low
| Tasks are idempotent, so executing them multiple times will not result
in logical error, but will degrade performance. To test for this case we
add plenty of unit tests around this logic and document manual testing
procedure. |
| Code should gracefully handle cases when feature X or plugin Y are
disabled. | Medium | High | Unit tests will verify that any feature flag
or plugin combination still results in our service operational. |
| [See more potential risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) |
### For maintainers
- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Stratoula Kalafateli
Co-authored-by: Drew Tate
---
src/plugins/chart_expressions/common/index.ts | 3 +-
src/plugins/chart_expressions/common/types.ts | 28 +
.../chart_expressions/common/utils.test.ts | 33 +
src/plugins/chart_expressions/common/utils.ts | 28 +-
.../__snapshots__/gauge_function.test.ts.snap | 14 +
.../gauge_function.test.ts | 20 +
.../expression_functions/gauge_function.ts | 3 +-
.../expression_gauge/common/index.ts | 1 +
.../common/types/expression_functions.ts | 6 +
.../common/types/expression_renderers.ts | 4 +-
.../components/gauge_component.test.tsx | 17 +-
.../public/components/gauge_component.tsx | 14 +-
.../heatmap_function.test.ts.snap | 1 +
.../heatmap_function.test.ts | 25 +-
.../expression_functions/heatmap_function.ts | 9 +-
.../common/types/expression_functions.ts | 3 +-
.../components/heatmap_component.test.tsx | 15 +
.../public/components/heatmap_component.tsx | 17 +-
.../metric_vis_function.test.ts | 48 ++
.../metric_vis_function.ts | 3 +-
.../common/types/expression_functions.ts | 3 +-
.../public/components/metric_vis.test.tsx | 25 +
.../public/components/metric_vis.tsx | 16 +-
.../metric_vis_renderer.tsx | 3 +-
.../mosaic_vis_function.test.ts.snap | 1 +
.../pie_vis_function.test.ts.snap | 2 +
.../treemap_vis_function.test.ts.snap | 1 +
.../waffle_vis_function.test.ts.snap | 1 +
.../mosaic_vis_function.test.ts | 21 +-
.../mosaic_vis_function.ts | 7 +-
.../pie_vis_function.test.ts | 21 +-
.../expression_functions/pie_vis_function.ts | 8 +-
.../treemap_vis_function.test.ts | 21 +-
.../treemap_vis_function.ts | 7 +-
.../waffle_vis_function.test.ts | 21 +-
.../waffle_vis_function.ts | 7 +-
.../expression_partition_vis/common/index.ts | 1 +
.../common/types/expression_functions.ts | 32 +-
.../common/types/expression_renderers.ts | 16 +-
.../mosaic_vis_renderer.stories.tsx | 8 +-
.../__stories__/pie_vis_renderer.stories.tsx | 8 +-
.../public/__stories__/shared/config.ts | 4 +-
.../public/__stories__/shared/data.ts | 4 +-
.../treemap_vis_renderer.stories.tsx | 8 +-
.../waffle_vis_renderer.stories.tsx | 8 +-
.../partition_vis_component.test.tsx.snap | 6 +
.../partition_vis_component.test.tsx | 15 +
.../components/partition_vis_component.tsx | 32 +-
.../partition_vis_renderer.tsx | 7 +-
.../expression_functions/layered_xy_vis_fn.ts | 3 +-
.../expression_functions/xy_vis.test.ts | 49 ++
.../common/expression_functions/xy_vis_fn.ts | 3 +-
.../expression_xy/common/index.ts | 1 +
.../common/types/expression_functions.ts | 15 +-
.../common/types/expression_renderers.ts | 4 +-
.../__snapshots__/xy_chart.test.tsx.snap | 250 ++++----
.../public/components/xy_chart.test.tsx | 82 ++-
.../public/components/xy_chart.tsx | 67 +-
src/plugins/charts/common/index.ts | 2 +-
src/plugins/charts/common/static/index.ts | 1 +
.../charts/common/static/overrides/index.ts | 9 +
.../common/static/overrides/settings.ts | 45 ++
.../static/styles/multilayer_timeaxis.ts | 2 +-
.../testing_embedded_lens/public/app.tsx | 167 ++---
.../testing_embedded_lens/public/controls.tsx | 597 ++++++++++++++++++
x-pack/plugins/lens/common/types.ts | 4 +
.../public/embeddable/embeddable.test.tsx | 65 ++
.../lens/public/embeddable/embeddable.tsx | 33 +-
.../embeddable/embeddable_component.tsx | 29 +-
x-pack/plugins/lens/public/index.ts | 1 +
.../public/visualizations/gauge/constants.ts | 2 +-
.../public/visualizations/metric/types.ts | 37 ++
x-pack/plugins/lens/readme.md | 21 +-
73 files changed, 1793 insertions(+), 302 deletions(-)
create mode 100644 src/plugins/chart_expressions/common/types.ts
create mode 100644 src/plugins/chart_expressions/common/utils.test.ts
create mode 100644 src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.test.ts
create mode 100644 src/plugins/charts/common/static/overrides/index.ts
create mode 100644 src/plugins/charts/common/static/overrides/settings.ts
create mode 100644 x-pack/examples/testing_embedded_lens/public/controls.tsx
create mode 100644 x-pack/plugins/lens/public/visualizations/metric/types.ts
diff --git a/src/plugins/chart_expressions/common/index.ts b/src/plugins/chart_expressions/common/index.ts
index 0277ad87b7a71..4373260657909 100644
--- a/src/plugins/chart_expressions/common/index.ts
+++ b/src/plugins/chart_expressions/common/index.ts
@@ -6,4 +6,5 @@
* Side Public License, v 1.
*/
-export { extractContainerType, extractVisualizationType } from './utils';
+export { extractContainerType, extractVisualizationType, getOverridesFor } from './utils';
+export type { Simplify, MakeOverridesSerializable } from './types';
diff --git a/src/plugins/chart_expressions/common/types.ts b/src/plugins/chart_expressions/common/types.ts
new file mode 100644
index 0000000000000..acdd5909f1aec
--- /dev/null
+++ b/src/plugins/chart_expressions/common/types.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+
+export type Simplify = { [KeyType in keyof T]: T[KeyType] } & {};
+
+// Overrides should not expose Functions, React nodes and children props
+// So filter out any type which is not serializable
+export type MakeOverridesSerializable = {
+ [KeyType in keyof T]: NonNullable extends Function
+ ? // cannot use boolean here as it would be challenging to distinguish
+ // between a "native" boolean props and a disabled callback
+ // so use a specific keyword
+ 'ignore'
+ : // be careful here to not filter out string/number types
+ NonNullable extends React.ReactChildren | React.ReactElement
+ ? never
+ : // make it recursive
+ NonNullable extends object
+ ? MakeOverridesSerializable
+ : NonNullable;
+};
diff --git a/src/plugins/chart_expressions/common/utils.test.ts b/src/plugins/chart_expressions/common/utils.test.ts
new file mode 100644
index 0000000000000..2ed71e9a17b92
--- /dev/null
+++ b/src/plugins/chart_expressions/common/utils.test.ts
@@ -0,0 +1,33 @@
+/*
+ * 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 { getOverridesFor } from './utils';
+
+describe('Overrides utilities', () => {
+ describe('getOverridesFor', () => {
+ it('should return an empty object for undefined values', () => {
+ expect(getOverridesFor(undefined, 'settings')).toEqual({});
+ // @ts-expect-error
+ expect(getOverridesFor({}, 'settings')).toEqual({});
+ // @ts-expect-error
+ expect(getOverridesFor({ otherOverride: {} }, 'settings')).toEqual({});
+ });
+
+ it('should return only the component specific overrides', () => {
+ expect(
+ getOverridesFor({ otherOverride: { a: 15 }, settings: { b: 10 } }, 'settings')
+ ).toEqual({ b: 10 });
+ });
+
+ it('should swap any "ignore" value into undefined value', () => {
+ expect(
+ getOverridesFor({ otherOverride: { a: 15 }, settings: { b: 10, c: 'ignore' } }, 'settings')
+ ).toEqual({ b: 10, c: undefined });
+ });
+ });
+});
diff --git a/src/plugins/chart_expressions/common/utils.ts b/src/plugins/chart_expressions/common/utils.ts
index d3b9e64c827d2..2966532c44117 100644
--- a/src/plugins/chart_expressions/common/utils.ts
+++ b/src/plugins/chart_expressions/common/utils.ts
@@ -5,7 +5,6 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-
import type { KibanaExecutionContext } from '@kbn/core-execution-context-common';
export const extractContainerType = (context?: KibanaExecutionContext): string | undefined => {
@@ -33,3 +32,30 @@ export const extractVisualizationType = (context?: KibanaExecutionContext): stri
return recursiveGet(context)?.type;
}
};
+
+/**
+ * Get an override specification and returns a props object to use directly with the Component
+ * @param overrides Overrides object
+ * @param componentName name of the Component to look for (i.e. "settings", "axisX")
+ * @returns an props object to use directly with the component
+ */
+export function getOverridesFor<
+ // Component props
+ P extends Record,
+ // Overrides
+ O extends Record,
+ // Overrides Component names
+ K extends keyof O
+>(overrides: O | undefined, componentName: K) {
+ if (!overrides || !overrides[componentName]) {
+ return {};
+ }
+ return Object.fromEntries(
+ Object.entries(overrides[componentName]).map(([key, value]) => {
+ if (value === 'ignore') {
+ return [key, undefined];
+ }
+ return [key, value];
+ })
+ );
+}
diff --git a/src/plugins/chart_expressions/expression_gauge/common/expression_functions/__snapshots__/gauge_function.test.ts.snap b/src/plugins/chart_expressions/expression_gauge/common/expression_functions/__snapshots__/gauge_function.test.ts.snap
index 3a1f13de9c525..93339463a7715 100644
--- a/src/plugins/chart_expressions/expression_gauge/common/expression_functions/__snapshots__/gauge_function.test.ts.snap
+++ b/src/plugins/chart_expressions/expression_gauge/common/expression_functions/__snapshots__/gauge_function.test.ts.snap
@@ -78,6 +78,7 @@ Object {
],
"type": "datatable",
},
+ "overrides": undefined,
},
}
`;
@@ -131,6 +132,7 @@ Object {
],
"type": "datatable",
},
+ "overrides": undefined,
},
}
`;
@@ -182,6 +184,7 @@ Object {
],
"type": "datatable",
},
+ "overrides": undefined,
},
}
`;
@@ -233,6 +236,7 @@ Object {
],
"type": "datatable",
},
+ "overrides": undefined,
},
}
`;
@@ -284,6 +288,7 @@ Object {
],
"type": "datatable",
},
+ "overrides": undefined,
},
}
`;
@@ -337,6 +342,7 @@ Object {
],
"type": "datatable",
},
+ "overrides": undefined,
},
}
`;
@@ -390,6 +396,7 @@ Object {
],
"type": "datatable",
},
+ "overrides": undefined,
},
}
`;
@@ -441,6 +448,7 @@ Object {
],
"type": "datatable",
},
+ "overrides": undefined,
},
}
`;
@@ -492,6 +500,7 @@ Object {
],
"type": "datatable",
},
+ "overrides": undefined,
},
}
`;
@@ -543,6 +552,7 @@ Object {
],
"type": "datatable",
},
+ "overrides": undefined,
},
}
`;
@@ -594,6 +604,7 @@ Object {
],
"type": "datatable",
},
+ "overrides": undefined,
},
}
`;
@@ -645,6 +656,7 @@ Object {
],
"type": "datatable",
},
+ "overrides": undefined,
},
}
`;
@@ -696,6 +708,7 @@ Object {
],
"type": "datatable",
},
+ "overrides": undefined,
},
}
`;
@@ -747,6 +760,7 @@ Object {
],
"type": "datatable",
},
+ "overrides": undefined,
},
}
`;
diff --git a/src/plugins/chart_expressions/expression_gauge/common/expression_functions/gauge_function.test.ts b/src/plugins/chart_expressions/expression_gauge/common/expression_functions/gauge_function.test.ts
index 078990d237dbf..b6a966a7cd858 100644
--- a/src/plugins/chart_expressions/expression_gauge/common/expression_functions/gauge_function.test.ts
+++ b/src/plugins/chart_expressions/expression_gauge/common/expression_functions/gauge_function.test.ts
@@ -11,6 +11,7 @@ import { GaugeArguments, GaugeShapes } from '..';
import { functionWrapper } from '@kbn/expressions-plugin/common/expression_functions/specs/tests/utils';
import { Datatable } from '@kbn/expressions-plugin/common/expression_types/specs';
import {
+ EXPRESSION_GAUGE_NAME,
GaugeCentralMajorModes,
GaugeColorModes,
GaugeLabelMajorModes,
@@ -110,4 +111,23 @@ describe('interpreter/functions#gauge', () => {
expect(loggedTable!).toMatchSnapshot();
});
+
+ it('should pass over overrides from variables', async () => {
+ const overrides = {
+ settings: {
+ onBrushEnd: 'ignore',
+ },
+ };
+ const handlers = {
+ variables: { overrides },
+ getExecutionContext: jest.fn(),
+ } as unknown as ExecutionContext;
+ const result = await fn(context, args, handlers);
+
+ expect(result).toEqual({
+ type: 'render',
+ as: EXPRESSION_GAUGE_NAME,
+ value: expect.objectContaining({ overrides }),
+ });
+ });
});
diff --git a/src/plugins/chart_expressions/expression_gauge/common/expression_functions/gauge_function.ts b/src/plugins/chart_expressions/expression_gauge/common/expression_functions/gauge_function.ts
index 72919e7c9b414..0346cef6b4ef2 100644
--- a/src/plugins/chart_expressions/expression_gauge/common/expression_functions/gauge_function.ts
+++ b/src/plugins/chart_expressions/expression_gauge/common/expression_functions/gauge_function.ts
@@ -8,7 +8,7 @@
import { i18n } from '@kbn/i18n';
import { prepareLogTable, validateAccessor } from '@kbn/visualizations-plugin/common/utils';
-import { GaugeExpressionFunctionDefinition } from '../types';
+import { GaugeExpressionFunctionDefinition, GaugeRenderProps } from '../types';
import {
EXPRESSION_GAUGE_NAME,
GaugeCentralMajorModes,
@@ -232,6 +232,7 @@ export const gaugeFunction = (): GaugeExpressionFunctionDefinition => ({
handlers.getExecutionContext?.()?.description,
},
canNavigateToLens: Boolean(handlers?.variables?.canNavigateToLens),
+ overrides: handlers.variables?.overrides as GaugeRenderProps['overrides'],
},
};
},
diff --git a/src/plugins/chart_expressions/expression_gauge/common/index.ts b/src/plugins/chart_expressions/expression_gauge/common/index.ts
index 24d4dc3c8d997..a773ae1d27bb4 100755
--- a/src/plugins/chart_expressions/expression_gauge/common/index.ts
+++ b/src/plugins/chart_expressions/expression_gauge/common/index.ts
@@ -10,6 +10,7 @@ export const PLUGIN_ID = 'expressionGauge';
export const PLUGIN_NAME = 'expressionGauge';
export type {
+ AllowedGaugeOverrides,
GaugeExpressionFunctionDefinition,
GaugeExpressionProps,
FormatFactory,
diff --git a/src/plugins/chart_expressions/expression_gauge/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_gauge/common/types/expression_functions.ts
index a7a89a876d699..b354c176f7e0c 100644
--- a/src/plugins/chart_expressions/expression_gauge/common/types/expression_functions.ts
+++ b/src/plugins/chart_expressions/expression_gauge/common/types/expression_functions.ts
@@ -15,6 +15,8 @@ import {
} from '@kbn/expressions-plugin/common';
import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common';
import { CustomPaletteState } from '@kbn/charts-plugin/common';
+import type { MakeOverridesSerializable, Simplify } from '@kbn/chart-expressions-common/types';
+import type { GoalProps } from '@elastic/charts';
import {
EXPRESSION_GAUGE_NAME,
GAUGE_FUNCTION_RENDERER_NAME,
@@ -84,3 +86,7 @@ export interface Accessors {
metric?: string;
goal?: string;
}
+
+export type AllowedGaugeOverrides = Partial<
+ Record<'gauge', Simplify>>
+>;
diff --git a/src/plugins/chart_expressions/expression_gauge/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_gauge/common/types/expression_renderers.ts
index 6219134892ab1..918c02948c373 100644
--- a/src/plugins/chart_expressions/expression_gauge/common/types/expression_renderers.ts
+++ b/src/plugins/chart_expressions/expression_gauge/common/types/expression_renderers.ts
@@ -10,7 +10,8 @@ import type { PaletteRegistry } from '@kbn/coloring';
import type { PersistedState } from '@kbn/visualizations-plugin/public';
import type { ChartsPluginSetup } from '@kbn/charts-plugin/public';
import type { IFieldFormat, SerializedFieldFormat } from '@kbn/field-formats-plugin/common';
-import type { GaugeExpressionProps } from './expression_functions';
+import type { AllowedSettingsOverrides } from '@kbn/charts-plugin/common';
+import type { AllowedGaugeOverrides, GaugeExpressionProps } from './expression_functions';
export type FormatFactory = (mapping?: SerializedFieldFormat) => IFieldFormat;
@@ -20,4 +21,5 @@ export type GaugeRenderProps = GaugeExpressionProps & {
paletteService: PaletteRegistry;
renderComplete: () => void;
uiState: PersistedState;
+ overrides?: AllowedGaugeOverrides & AllowedSettingsOverrides;
};
diff --git a/src/plugins/chart_expressions/expression_gauge/public/components/gauge_component.test.tsx b/src/plugins/chart_expressions/expression_gauge/public/components/gauge_component.test.tsx
index 640a8e17b47c7..da8985d0427af 100644
--- a/src/plugins/chart_expressions/expression_gauge/public/components/gauge_component.test.tsx
+++ b/src/plugins/chart_expressions/expression_gauge/public/components/gauge_component.test.tsx
@@ -21,7 +21,7 @@ import {
GaugeColorModes,
} from '../../common';
import GaugeComponent from './gauge_component';
-import { Chart, Goal } from '@elastic/charts';
+import { Chart, Goal, Settings } from '@elastic/charts';
jest.mock('@elastic/charts', () => {
const original = jest.requireActual('@elastic/charts');
@@ -405,4 +405,19 @@ describe('GaugeComponent', function () {
expect(goal.prop('bands')).toEqual([0, 2, 6, 8, 10]);
});
});
+
+ describe('overrides', () => {
+ it('should apply overrides to the settings component', () => {
+ const component = shallowWithIntl(
+
+ );
+
+ const settingsComponent = component.find(Settings);
+ expect(settingsComponent.prop('onBrushEnd')).toBeUndefined();
+ expect(settingsComponent.prop('ariaUseDefaultSummary')).toEqual(true);
+ });
+ });
});
diff --git a/src/plugins/chart_expressions/expression_gauge/public/components/gauge_component.tsx b/src/plugins/chart_expressions/expression_gauge/public/components/gauge_component.tsx
index 489b44e44babb..c20f5d089a889 100644
--- a/src/plugins/chart_expressions/expression_gauge/public/components/gauge_component.tsx
+++ b/src/plugins/chart_expressions/expression_gauge/public/components/gauge_component.tsx
@@ -12,6 +12,7 @@ import type { PaletteOutput } from '@kbn/coloring';
import { FieldFormat } from '@kbn/field-formats-plugin/common';
import type { CustomPaletteState } from '@kbn/charts-plugin/public';
import { EmptyPlaceholder } from '@kbn/charts-plugin/public';
+import { getOverridesFor } from '@kbn/chart-expressions-common';
import { isVisDimension } from '@kbn/visualizations-plugin/common/utils';
import {
GaugeRenderProps,
@@ -167,7 +168,16 @@ function getTicks(
}
export const GaugeComponent: FC = memo(
- ({ data, args, uiState, formatFactory, paletteService, chartsThemeService, renderComplete }) => {
+ ({
+ data,
+ args,
+ uiState,
+ formatFactory,
+ paletteService,
+ chartsThemeService,
+ renderComplete,
+ overrides,
+ }) => {
const {
shape: gaugeType,
palette,
@@ -360,6 +370,7 @@ export const GaugeComponent: FC = memo(
ariaLabel={args.ariaLabel}
ariaUseDefaultSummary={!args.ariaLabel}
onRenderChange={onRenderChange}
+ {...getOverridesFor(overrides, 'settings')}
/>
= memo(
labelMinor={labelMinor ? `${labelMinor}${minorExtraSpaces}` : ''}
{...extraTitles}
{...goalConfig}
+ {...getOverridesFor(overrides, 'gauge')}
/>
{commonLabel && {commonLabel}
}
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 1b644ef0a4938..c661fe65a434d 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
@@ -102,6 +102,7 @@ Object {
],
"type": "datatable",
},
+ "overrides": undefined,
"syncCursor": true,
"syncTooltips": false,
},
diff --git a/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_function.test.ts b/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_function.test.ts
index b3506cfa3578f..24eabf62245be 100644
--- a/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_function.test.ts
+++ b/src/plugins/chart_expressions/expression_heatmap/common/expression_functions/heatmap_function.test.ts
@@ -10,7 +10,11 @@ import { heatmapFunction } from './heatmap_function';
import type { HeatmapArguments } from '..';
import { functionWrapper } from '@kbn/expressions-plugin/common/expression_functions/specs/tests/utils';
import { Datatable } from '@kbn/expressions-plugin/common/expression_types/specs';
-import { EXPRESSION_HEATMAP_GRID_NAME, EXPRESSION_HEATMAP_LEGEND_NAME } from '../constants';
+import {
+ EXPRESSION_HEATMAP_GRID_NAME,
+ EXPRESSION_HEATMAP_LEGEND_NAME,
+ EXPRESSION_HEATMAP_NAME,
+} from '../constants';
import { ExecutionContext } from '@kbn/expressions-plugin/common';
describe('interpreter/functions#heatmap', () => {
@@ -80,4 +84,23 @@ describe('interpreter/functions#heatmap', () => {
expect(loggedTable!).toMatchSnapshot();
});
+
+ it('should pass over overrides from variables', async () => {
+ const overrides = {
+ settings: {
+ onBrushEnd: 'ignore',
+ },
+ };
+ const handlers = {
+ variables: { overrides },
+ getExecutionContext: jest.fn(),
+ } as unknown as ExecutionContext;
+ const result = await fn(context, args, handlers);
+
+ expect(result).toEqual({
+ type: 'render',
+ as: EXPRESSION_HEATMAP_NAME,
+ value: expect.objectContaining({ overrides }),
+ });
+ });
});
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 f0c309de19236..2334aa342ad4f 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
@@ -14,7 +14,7 @@ import {
Dimension,
validateAccessor,
} from '@kbn/visualizations-plugin/common/utils';
-import { HeatmapExpressionFunctionDefinition } from '../types';
+import type { HeatmapExpressionFunctionDefinition, HeatmapExpressionProps } from '../types';
import {
EXPRESSION_HEATMAP_NAME,
EXPRESSION_HEATMAP_GRID_NAME,
@@ -230,9 +230,10 @@ export const heatmapFunction = (): HeatmapExpressionFunctionDefinition => ({
(handlers.variables?.embeddableTitle as string) ??
handlers.getExecutionContext?.()?.description,
},
- syncTooltips: handlers?.isSyncTooltipsEnabled?.() ?? false,
- syncCursor: handlers?.isSyncCursorEnabled?.() ?? true,
- canNavigateToLens: Boolean(handlers?.variables?.canNavigateToLens),
+ syncTooltips: handlers.isSyncTooltipsEnabled?.() ?? false,
+ syncCursor: handlers.isSyncCursorEnabled?.() ?? true,
+ canNavigateToLens: Boolean(handlers.variables?.canNavigateToLens),
+ overrides: handlers.variables?.overrides as HeatmapExpressionProps['overrides'],
},
};
},
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 2537d4c7f5105..80cea368cf21f 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
@@ -14,7 +14,7 @@ import {
} from '@kbn/expressions-plugin/common';
import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common';
-import { CustomPaletteState } from '@kbn/charts-plugin/common';
+import { AllowedSettingsOverrides, CustomPaletteState } from '@kbn/charts-plugin/common';
import type { LegendSize } from '@kbn/visualizations-plugin/public';
import {
EXPRESSION_HEATMAP_NAME,
@@ -95,6 +95,7 @@ export interface HeatmapExpressionProps {
syncTooltips: boolean;
syncCursor: boolean;
canNavigateToLens?: boolean;
+ overrides?: AllowedSettingsOverrides;
}
export interface HeatmapRender {
diff --git a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.test.tsx b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.test.tsx
index 4720e9025d63d..43d8d61dea133 100644
--- a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.test.tsx
+++ b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.test.tsx
@@ -428,4 +428,19 @@ describe('HeatmapComponent', function () {
expect(component.find(Settings).first().prop('onElementClick')).toBeUndefined();
expect(component.find(Settings).first().prop('onBrushEnd')).toBeUndefined();
});
+
+ describe('overrides', () => {
+ it('should apply overrides to the settings component', () => {
+ const component = shallowWithIntl(
+
+ );
+
+ const settingsComponent = component.find(Settings);
+ expect(settingsComponent.prop('onBrushEnd')).toBeUndefined();
+ expect(settingsComponent.prop('ariaUseDefaultSummary')).toEqual(true);
+ });
+ });
});
diff --git a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx
index 51ede8e7f59ba..a9b4a2d227b27 100644
--- a/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx
+++ b/src/plugins/chart_expressions/expression_heatmap/public/components/heatmap_component.tsx
@@ -22,6 +22,7 @@ import {
ESFixedIntervalUnit,
ESCalendarIntervalUnit,
PartialTheme,
+ SettingsProps,
} from '@elastic/charts';
import type { CustomPaletteState } from '@kbn/charts-plugin/public';
import { search } from '@kbn/data-plugin/public';
@@ -36,6 +37,7 @@ import {
} from '@kbn/visualizations-plugin/common/constants';
import { DatatableColumn } from '@kbn/expressions-plugin/public';
import { IconChartHeatmap } from '@kbn/chart-icons';
+import { getOverridesFor } from '@kbn/chart-expressions-common';
import type { HeatmapRenderProps, FilterEvent, BrushEvent } from '../../common';
import {
applyPaletteParams,
@@ -148,6 +150,7 @@ export const HeatmapComponent: FC = memo(
syncTooltips,
syncCursor,
renderComplete,
+ overrides,
}) => {
const chartRef = useRef(null);
const chartTheme = chartsThemeService.useChartsTheme();
@@ -498,6 +501,11 @@ export const HeatmapComponent: FC = memo(
};
});
+ const { theme: settingsThemeOverrides = {}, ...settingsOverrides } = getOverridesFor(
+ overrides,
+ 'settings'
+ ) as Partial;
+
const themeOverrides: PartialTheme = {
legend: {
labelOptions: {
@@ -591,7 +599,13 @@ export const HeatmapComponent: FC = memo(
legendColorPicker={uiState ? LegendColorPickerWrapper : undefined}
debugState={window._echDebugStateFlag ?? false}
tooltip={tooltip}
- theme={[themeOverrides, chartTheme]}
+ theme={[
+ themeOverrides,
+ chartTheme,
+ ...(Array.isArray(settingsThemeOverrides)
+ ? settingsThemeOverrides
+ : [settingsThemeOverrides]),
+ ]}
baseTheme={chartBaseTheme}
xDomain={{
min:
@@ -606,6 +620,7 @@ export const HeatmapComponent: FC = memo(
onBrushEnd={interactive ? (onBrushEnd as BrushEndListener) : undefined}
ariaLabel={args.ariaLabel}
ariaUseDefaultSummary={!args.ariaLabel}
+ {...settingsOverrides}
/>
{
+ const fn = functionWrapper(metricVisFunction());
+ const context: Datatable = {
+ type: 'datatable',
+ rows: [{ 'col-0-1': 0 }],
+ columns: [{ id: 'col-0-1', name: 'Count', meta: { type: 'number' } }],
+ };
+ const args: MetricArguments = {
+ metric: 'col-0-1',
+ progressDirection: 'horizontal',
+ maxCols: 1,
+ inspectorTableId: 'random-id',
+ };
+
+ it('should pass over overrides from variables', async () => {
+ const overrides = {
+ settings: {
+ onBrushEnd: 'ignore',
+ },
+ };
+ const handlers = {
+ variables: { overrides },
+ getExecutionContext: jest.fn(),
+ } as unknown as ExecutionContext;
+ const result = await fn(context, args, handlers);
+
+ expect(result).toEqual({
+ type: 'render',
+ as: EXPRESSION_METRIC_NAME,
+ value: expect.objectContaining({ overrides }),
+ });
+ });
+});
diff --git a/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts b/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts
index 04a1284c1cf34..c5be73ab0b73c 100644
--- a/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts
+++ b/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts
@@ -14,7 +14,7 @@ import {
validateAccessor,
} from '@kbn/visualizations-plugin/common/utils';
import { LayoutDirection } from '@elastic/charts';
-import { visType } from '../types';
+import { MetricVisRenderConfig, visType } from '../types';
import { MetricVisExpressionFunctionDefinition } from '../types';
import { EXPRESSION_METRIC_NAME, EXPRESSION_METRIC_TRENDLINE_NAME } from '../constants';
@@ -194,6 +194,7 @@ export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({
breakdownBy: args.breakdownBy,
},
},
+ overrides: handlers.variables?.overrides as MetricVisRenderConfig['overrides'],
},
};
},
diff --git a/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts
index 9aa67b0df2ee5..2440ef597c0bd 100644
--- a/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts
+++ b/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts
@@ -14,7 +14,7 @@ import {
ExpressionValueRender,
} from '@kbn/expressions-plugin/common';
import { ExpressionValueVisDimension, prepareLogTable } from '@kbn/visualizations-plugin/common';
-import { CustomPaletteState } from '@kbn/charts-plugin/common';
+import type { AllowedSettingsOverrides, CustomPaletteState } from '@kbn/charts-plugin/common';
import { VisParams, visType } from './expression_renderers';
import { EXPRESSION_METRIC_NAME, EXPRESSION_METRIC_TRENDLINE_NAME } from '../constants';
@@ -40,6 +40,7 @@ export interface MetricVisRenderConfig {
visType: typeof visType;
visData: Datatable;
visConfig: Pick;
+ overrides?: AllowedSettingsOverrides;
}
export type MetricVisExpressionFunctionDefinition = ExpressionFunctionDefinition<
diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx
index 5b2d260dbfae5..d10d1e39f3544 100644
--- a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx
+++ b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx
@@ -1471,4 +1471,29 @@ describe('MetricVisComponent', function () {
expect(secondary).toBe('1.12K%');
});
});
+
+ describe('overrides', () => {
+ it('should apply overrides to the settings component', () => {
+ const component = shallow(
+
+ );
+
+ const settingsComponent = component.find(Settings);
+ expect(settingsComponent.prop('onBrushEnd')).toBeUndefined();
+ expect(settingsComponent.prop('ariaUseDefaultSummary')).toEqual(true);
+ });
+ });
});
diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx
index a3ec33b0e8023..6bac88177bf50 100644
--- a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx
+++ b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx
@@ -20,6 +20,7 @@ import {
Settings,
MetricWTrend,
MetricWNumber,
+ SettingsProps,
} from '@elastic/charts';
import { getColumnByAccessor, getFormatByAccessor } from '@kbn/visualizations-plugin/common/utils';
import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common';
@@ -36,6 +37,8 @@ import { CUSTOM_PALETTE } from '@kbn/coloring';
import { css } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';
import { useResizeObserver, useEuiScrollBar } from '@elastic/eui';
+import { AllowedSettingsOverrides } from '@kbn/charts-plugin/common';
+import { getOverridesFor } from '@kbn/chart-expressions-common';
import { DEFAULT_TRENDLINE_NAME } from '../../common/constants';
import { VisParams } from '../../common';
import {
@@ -177,6 +180,7 @@ export interface MetricVisComponentProps {
fireEvent: IInterpreterRenderHandlers['event'];
renderMode: RenderMode;
filterable: boolean;
+ overrides?: AllowedSettingsOverrides;
}
export const MetricVis = ({
@@ -186,6 +190,7 @@ export const MetricVis = ({
fireEvent,
renderMode,
filterable,
+ overrides,
}: MetricVisComponentProps) => {
const primaryMetricColumn = getColumnByAccessor(config.dimensions.metric, data.columns)!;
const formatPrimaryMetric = getMetricFormatter(config.dimensions.metric, data.columns);
@@ -331,6 +336,11 @@ export const MetricVis = ({
);
}, [grid.length, minHeight, scrollDimensions.height]);
+ const { theme: settingsThemeOverrides = {}, ...settingsOverrides } = getOverridesFor(
+ overrides,
+ 'settings'
+ ) as Partial;
+
return (
diff --git a/src/plugins/chart_expressions/expression_metric/public/expression_renderers/metric_vis_renderer.tsx b/src/plugins/chart_expressions/expression_metric/public/expression_renderers/metric_vis_renderer.tsx
index 677fd8db23928..9841e65d5ed20 100644
--- a/src/plugins/chart_expressions/expression_metric/public/expression_renderers/metric_vis_renderer.tsx
+++ b/src/plugins/chart_expressions/expression_metric/public/expression_renderers/metric_vis_renderer.tsx
@@ -55,7 +55,7 @@ export const getMetricVisRenderer = (
name: EXPRESSION_METRIC_NAME,
displayName: 'metric visualization',
reuseDomNode: true,
- render: async (domNode, { visData, visConfig }, handlers) => {
+ render: async (domNode, { visData, visConfig, overrides }, handlers) => {
const { core, plugins } = deps.getStartDeps();
handlers.onDestroy(() => {
@@ -103,6 +103,7 @@ export const getMetricVisRenderer = (
fireEvent={handlers.event}
renderMode={handlers.getRenderMode()}
filterable={filterable}
+ overrides={overrides}
/>
,
diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/mosaic_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/mosaic_vis_function.test.ts.snap
index f8b999c2bb764..604368d7ab130 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/mosaic_vis_function.test.ts.snap
+++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/mosaic_vis_function.test.ts.snap
@@ -45,6 +45,7 @@ Object {
"as": "partitionVis",
"type": "render",
"value": Object {
+ "overrides": undefined,
"params": Object {
"listenOnChange": true,
},
diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap
index 9e71fcec0c8fa..293f86c6bf9ec 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap
+++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/pie_vis_function.test.ts.snap
@@ -27,6 +27,7 @@ Object {
"type": "render",
"value": Object {
"canNavigateToLens": false,
+ "overrides": undefined,
"params": Object {
"listenOnChange": true,
},
@@ -168,6 +169,7 @@ Object {
"type": "render",
"value": Object {
"canNavigateToLens": false,
+ "overrides": undefined,
"params": Object {
"listenOnChange": true,
},
diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/treemap_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/treemap_vis_function.test.ts.snap
index 891b217df37f0..f6817eca439cf 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/treemap_vis_function.test.ts.snap
+++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/treemap_vis_function.test.ts.snap
@@ -45,6 +45,7 @@ Object {
"as": "partitionVis",
"type": "render",
"value": Object {
+ "overrides": undefined,
"params": Object {
"listenOnChange": true,
},
diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap
index 50400b3839b57..7c74291190a2d 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap
+++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/__snapshots__/waffle_vis_function.test.ts.snap
@@ -37,6 +37,7 @@ Object {
"as": "partitionVis",
"type": "render",
"value": Object {
+ "overrides": undefined,
"params": Object {
"listenOnChange": true,
},
diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.test.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.test.ts
index fd2951a2f1fb6..c10912c1e56a4 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.test.ts
+++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.test.ts
@@ -16,7 +16,7 @@ import {
import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common';
import { Datatable } from '@kbn/expressions-plugin/common/expression_types/specs';
import { mosaicVisFunction } from './mosaic_vis_function';
-import { PARTITION_LABELS_VALUE } from '../constants';
+import { PARTITION_LABELS_VALUE, PARTITION_VIS_RENDERER_NAME } from '../constants';
import { ExecutionContext } from '@kbn/expressions-plugin/common';
describe('interpreter/functions#mosaicVis', () => {
@@ -147,4 +147,23 @@ describe('interpreter/functions#mosaicVis', () => {
expect(loggedTable!).toMatchSnapshot();
});
+
+ it('should pass over overrides from variables', async () => {
+ const overrides = {
+ settings: {
+ onBrushEnd: 'ignore',
+ },
+ };
+ const handlers = {
+ variables: { overrides },
+ getExecutionContext: jest.fn(),
+ } as unknown as ExecutionContext;
+ const result = await fn(context, visConfig, handlers);
+
+ expect(result).toEqual({
+ type: 'render',
+ as: PARTITION_VIS_RENDERER_NAME,
+ value: expect.objectContaining({ overrides }),
+ });
+ });
});
diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts
index 75f2aa3c17dc1..fc863cf73c68c 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts
+++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/mosaic_vis_function.ts
@@ -9,7 +9,11 @@
import { Position } from '@elastic/charts';
import { prepareLogTable, validateAccessor } from '@kbn/visualizations-plugin/common/utils';
import { DEFAULT_LEGEND_SIZE, LegendSize } from '@kbn/visualizations-plugin/common/constants';
-import { LegendDisplay, PartitionVisParams } from '../types/expression_renderers';
+import {
+ LegendDisplay,
+ type PartitionChartProps,
+ type PartitionVisParams,
+} from '../types/expression_renderers';
import { ChartTypes, MosaicVisExpressionFunctionDefinition } from '../types';
import {
PARTITION_LABELS_FUNCTION,
@@ -172,6 +176,7 @@ export const mosaicVisFunction = (): MosaicVisExpressionFunctionDefinition => ({
params: {
listenOnChange: true,
},
+ overrides: handlers.variables?.overrides as PartitionChartProps['overrides'],
},
};
},
diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.test.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.test.ts
index dc975e9a92758..4575a01237096 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.test.ts
+++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.test.ts
@@ -17,7 +17,7 @@ import {
import { ExpressionValueVisDimension, LegendSize } from '@kbn/visualizations-plugin/common';
import { Datatable } from '@kbn/expressions-plugin/common/expression_types/specs';
import { pieVisFunction } from './pie_vis_function';
-import { PARTITION_LABELS_VALUE } from '../constants';
+import { PARTITION_LABELS_VALUE, PARTITION_VIS_RENDERER_NAME } from '../constants';
import { ExecutionContext } from '@kbn/expressions-plugin/common';
describe('interpreter/functions#pieVis', () => {
@@ -144,4 +144,23 @@ describe('interpreter/functions#pieVis', () => {
expect(loggedTable!).toMatchSnapshot();
});
+
+ it('should pass over overrides from variables', async () => {
+ const overrides = {
+ settings: {
+ onBrushEnd: 'ignore',
+ },
+ };
+ const handlers = {
+ variables: { overrides },
+ getExecutionContext: jest.fn(),
+ } as unknown as ExecutionContext;
+ const result = await fn(context, { ...visConfig, isDonut: false }, handlers);
+
+ expect(result).toEqual({
+ type: 'render',
+ as: PARTITION_VIS_RENDERER_NAME,
+ value: expect.objectContaining({ overrides }),
+ });
+ });
});
diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts
index 4bf2ead1b9c52..0cf6522456c62 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts
+++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/pie_vis_function.ts
@@ -9,7 +9,12 @@
import { Position } from '@elastic/charts';
import { prepareLogTable, validateAccessor } from '@kbn/visualizations-plugin/common/utils';
import { DEFAULT_LEGEND_SIZE, LegendSize } from '@kbn/visualizations-plugin/common/constants';
-import { EmptySizeRatios, LegendDisplay, PartitionVisParams } from '../types/expression_renderers';
+import {
+ EmptySizeRatios,
+ LegendDisplay,
+ type PartitionChartProps,
+ type PartitionVisParams,
+} from '../types/expression_renderers';
import { ChartTypes, PieVisExpressionFunctionDefinition } from '../types';
import {
PARTITION_LABELS_FUNCTION,
@@ -199,6 +204,7 @@ export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({
params: {
listenOnChange: true,
},
+ overrides: handlers.variables?.overrides as PartitionChartProps['overrides'],
},
};
},
diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.test.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.test.ts
index edc8ec8b99100..4c6cffd6a9fb8 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.test.ts
+++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.test.ts
@@ -16,7 +16,7 @@ import {
import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common';
import { Datatable } from '@kbn/expressions-plugin/common/expression_types/specs';
import { treemapVisFunction } from './treemap_vis_function';
-import { PARTITION_LABELS_VALUE } from '../constants';
+import { PARTITION_LABELS_VALUE, PARTITION_VIS_RENDERER_NAME } from '../constants';
import { ExecutionContext } from '@kbn/expressions-plugin/common';
describe('interpreter/functions#treemapVis', () => {
@@ -150,4 +150,23 @@ describe('interpreter/functions#treemapVis', () => {
expect(loggedTable!).toMatchSnapshot();
});
+
+ it('should pass over overrides from variables', async () => {
+ const overrides = {
+ settings: {
+ onBrushEnd: 'ignore',
+ },
+ };
+ const handlers = {
+ variables: { overrides },
+ getExecutionContext: jest.fn(),
+ } as unknown as ExecutionContext;
+ const result = await fn(context, visConfig, handlers);
+
+ expect(result).toEqual({
+ type: 'render',
+ as: PARTITION_VIS_RENDERER_NAME,
+ value: expect.objectContaining({ overrides }),
+ });
+ });
});
diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts
index d5f91b1f0e1d3..2a5d0a6af7a8a 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts
+++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/treemap_vis_function.ts
@@ -9,7 +9,11 @@
import { Position } from '@elastic/charts';
import { prepareLogTable, validateAccessor } from '@kbn/visualizations-plugin/common/utils';
import { DEFAULT_LEGEND_SIZE, LegendSize } from '@kbn/visualizations-plugin/common/constants';
-import { LegendDisplay, PartitionVisParams } from '../types/expression_renderers';
+import {
+ LegendDisplay,
+ type PartitionChartProps,
+ type PartitionVisParams,
+} from '../types/expression_renderers';
import { ChartTypes, TreemapVisExpressionFunctionDefinition } from '../types';
import {
PARTITION_LABELS_FUNCTION,
@@ -178,6 +182,7 @@ export const treemapVisFunction = (): TreemapVisExpressionFunctionDefinition =>
params: {
listenOnChange: true,
},
+ overrides: handlers.variables?.overrides as PartitionChartProps['overrides'],
},
};
},
diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.test.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.test.ts
index 606ff2c9b84c2..d84df1d4d0fc3 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.test.ts
+++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.test.ts
@@ -16,7 +16,7 @@ import {
import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common';
import { Datatable } from '@kbn/expressions-plugin/common/expression_types/specs';
import { waffleVisFunction } from './waffle_vis_function';
-import { PARTITION_LABELS_VALUE } from '../constants';
+import { PARTITION_LABELS_VALUE, PARTITION_VIS_RENDERER_NAME } from '../constants';
import { ExecutionContext } from '@kbn/expressions-plugin/common';
describe('interpreter/functions#waffleVis', () => {
@@ -121,4 +121,23 @@ describe('interpreter/functions#waffleVis', () => {
expect(loggedTable!).toMatchSnapshot();
});
+
+ it('should pass over overrides from variables', async () => {
+ const overrides = {
+ settings: {
+ onBrushEnd: 'ignore',
+ },
+ };
+ const handlers = {
+ variables: { overrides },
+ getExecutionContext: jest.fn(),
+ } as unknown as ExecutionContext;
+ const result = await fn(context, visConfig, handlers);
+
+ expect(result).toEqual({
+ type: 'render',
+ as: PARTITION_VIS_RENDERER_NAME,
+ value: expect.objectContaining({ overrides }),
+ });
+ });
});
diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts
index 1568454b86eb2..e4176cf6015c1 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts
+++ b/src/plugins/chart_expressions/expression_partition_vis/common/expression_functions/waffle_vis_function.ts
@@ -9,7 +9,11 @@
import { Position } from '@elastic/charts';
import { prepareLogTable, validateAccessor } from '@kbn/visualizations-plugin/common/utils';
import { DEFAULT_LEGEND_SIZE, LegendSize } from '@kbn/visualizations-plugin/common/constants';
-import { LegendDisplay, PartitionVisParams } from '../types/expression_renderers';
+import {
+ LegendDisplay,
+ type PartitionChartProps,
+ type PartitionVisParams,
+} from '../types/expression_renderers';
import { ChartTypes, WaffleVisExpressionFunctionDefinition } from '../types';
import {
PARTITION_LABELS_FUNCTION,
@@ -173,6 +177,7 @@ export const waffleVisFunction = (): WaffleVisExpressionFunctionDefinition => ({
params: {
listenOnChange: true,
},
+ overrides: handlers.variables?.overrides as PartitionChartProps['overrides'],
},
};
},
diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/index.ts b/src/plugins/chart_expressions/expression_partition_vis/common/index.ts
index d51838b334a09..d46f5a9b26b9d 100755
--- a/src/plugins/chart_expressions/expression_partition_vis/common/index.ts
+++ b/src/plugins/chart_expressions/expression_partition_vis/common/index.ts
@@ -26,6 +26,7 @@ export {
} from './expression_functions';
export type {
+ AllowedPartitionOverrides,
ExpressionValuePartitionLabels,
PieVisExpressionFunctionDefinition,
TreemapVisExpressionFunctionDefinition,
diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_functions.ts
index f5f2f0ef7f3cd..41e172c11829c 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_functions.ts
+++ b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_functions.ts
@@ -6,6 +6,8 @@
* Side Public License, v 1.
*/
+import type { PartitionProps } from '@elastic/charts';
+import type { MakeOverridesSerializable, Simplify } from '@kbn/chart-expressions-common/types';
import {
ExpressionFunctionDefinition,
Datatable,
@@ -21,13 +23,13 @@ import {
PARTITION_LABELS_FUNCTION,
} from '../constants';
import {
- RenderValue,
- PieVisConfig,
+ type PartitionChartProps,
+ type PieVisConfig,
LabelPositions,
ValueFormats,
- TreemapVisConfig,
- MosaicVisConfig,
- WaffleVisConfig,
+ type TreemapVisConfig,
+ type MosaicVisConfig,
+ type WaffleVisConfig,
} from './expression_renderers';
export interface PartitionLabelsArguments {
@@ -63,28 +65,28 @@ export type PieVisExpressionFunctionDefinition = ExpressionFunctionDefinition<
typeof PIE_VIS_EXPRESSION_NAME,
Datatable,
PieVisConfig,
- ExpressionValueRender
+ ExpressionValueRender
>;
export type TreemapVisExpressionFunctionDefinition = ExpressionFunctionDefinition<
typeof TREEMAP_VIS_EXPRESSION_NAME,
Datatable,
TreemapVisConfig,
- ExpressionValueRender
+ ExpressionValueRender
>;
export type MosaicVisExpressionFunctionDefinition = ExpressionFunctionDefinition<
typeof MOSAIC_VIS_EXPRESSION_NAME,
Datatable,
MosaicVisConfig,
- ExpressionValueRender
+ ExpressionValueRender
>;
export type WaffleVisExpressionFunctionDefinition = ExpressionFunctionDefinition<
typeof WAFFLE_VIS_EXPRESSION_NAME,
Datatable,
WaffleVisConfig,
- ExpressionValueRender
+ ExpressionValueRender
>;
export enum ChartTypes {
@@ -101,3 +103,15 @@ export type PartitionLabelsExpressionFunctionDefinition = ExpressionFunctionDefi
PartitionLabelsArguments,
ExpressionValuePartitionLabels
>;
+
+export type AllowedPartitionOverrides = Partial<
+ Record<
+ 'partition',
+ Simplify<
+ Omit<
+ MakeOverridesSerializable,
+ 'id' | 'data' | 'valueAccessor' | 'valueFormatter' | 'layers' | 'layout'
+ >
+ >
+ >
+>;
diff --git a/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts
index b5c9ad985dd49..c2c4ebe72fd20 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts
+++ b/src/plugins/chart_expressions/expression_partition_vis/common/types/expression_renderers.ts
@@ -7,12 +7,17 @@
*/
import { Position } from '@elastic/charts';
+import type { AllowedSettingsOverrides } from '@kbn/charts-plugin/common';
import type { PaletteOutput } from '@kbn/coloring';
-import { Datatable, DatatableColumn } from '@kbn/expressions-plugin/common';
-import { SerializedFieldFormat } from '@kbn/field-formats-plugin/common';
-import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common';
+import type { Datatable, DatatableColumn } from '@kbn/expressions-plugin/common';
+import type { SerializedFieldFormat } from '@kbn/field-formats-plugin/common';
+import type { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common';
import type { LegendSize } from '@kbn/visualizations-plugin/public';
-import { ChartTypes, ExpressionValuePartitionLabels } from './expression_functions';
+import {
+ type AllowedPartitionOverrides,
+ ChartTypes,
+ type ExpressionValuePartitionLabels,
+} from './expression_functions';
export enum EmptySizeRatios {
SMALL = 0.3,
@@ -107,12 +112,13 @@ export interface WaffleVisConfig extends Omit {
showValuesInLegend: boolean;
}
-export interface RenderValue {
+export interface PartitionChartProps {
visData: Datatable;
visType: ChartTypes;
visConfig: PartitionVisParams;
syncColors: boolean;
canNavigateToLens?: boolean;
+ overrides?: AllowedPartitionOverrides & AllowedSettingsOverrides;
}
export enum LabelPositions {
diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/mosaic_vis_renderer.stories.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/mosaic_vis_renderer.stories.tsx
index 2ab55c1c2c9cb..f43d7d8840b87 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/mosaic_vis_renderer.stories.tsx
+++ b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/mosaic_vis_renderer.stories.tsx
@@ -10,7 +10,7 @@ import React, { FC } from 'react';
import { ComponentStory } from '@storybook/react';
import { Render } from '@kbn/presentation-util-plugin/public/__stories__';
import { getPartitionVisRenderer } from '../expression_renderers';
-import { ChartTypes, RenderValue } from '../../common/types';
+import { ChartTypes, PartitionChartProps } from '../../common/types';
import { getStartDeps } from '../__mocks__';
import { mosaicArgTypes, treemapMosaicConfig, data } from './shared';
@@ -22,9 +22,9 @@ const containerSize = {
const PartitionVisRenderer = () => getPartitionVisRenderer({ getStartDeps });
type Props = {
- visType: RenderValue['visType'];
- syncColors: RenderValue['syncColors'];
-} & RenderValue['visConfig'];
+ visType: PartitionChartProps['visType'];
+ syncColors: PartitionChartProps['syncColors'];
+} & PartitionChartProps['visConfig'];
const PartitionVis: ComponentStory> = ({
visType,
diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/pie_vis_renderer.stories.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/pie_vis_renderer.stories.tsx
index 0a58ec048c179..e896c3b382928 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/pie_vis_renderer.stories.tsx
+++ b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/pie_vis_renderer.stories.tsx
@@ -10,7 +10,7 @@ import React, { FC } from 'react';
import { ComponentStory } from '@storybook/react';
import { Render } from '@kbn/presentation-util-plugin/public/__stories__';
import { getPartitionVisRenderer } from '../expression_renderers';
-import { ChartTypes, RenderValue } from '../../common/types';
+import { ChartTypes, PartitionChartProps } from '../../common/types';
import { getStartDeps } from '../__mocks__';
import { pieDonutArgTypes, pieConfig, data } from './shared';
@@ -22,9 +22,9 @@ const containerSize = {
const PartitionVisRenderer = () => getPartitionVisRenderer({ getStartDeps });
type Props = {
- visType: RenderValue['visType'];
- syncColors: RenderValue['syncColors'];
-} & RenderValue['visConfig'];
+ visType: PartitionChartProps['visType'];
+ syncColors: PartitionChartProps['syncColors'];
+} & PartitionChartProps['visConfig'];
const PartitionVis: ComponentStory> = ({
visType,
diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/config.ts b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/config.ts
index 544e5ea0ce593..c1c8098838624 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/config.ts
+++ b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/config.ts
@@ -9,12 +9,12 @@ import { Position } from '@elastic/charts';
import {
LabelPositions,
LegendDisplay,
- RenderValue,
+ PartitionChartProps,
PartitionVisParams,
ValueFormats,
} from '../../../common/types';
-export const config: RenderValue['visConfig'] = {
+export const config: PartitionChartProps['visConfig'] = {
addTooltip: true,
legendDisplay: LegendDisplay.HIDE,
metricsToLabels: { percent_uptime: 'percent_uptime' },
diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/data.ts b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/data.ts
index e02f090b5f7fa..33b337e652f92 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/data.ts
+++ b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/shared/data.ts
@@ -6,9 +6,9 @@
* Side Public License, v 1.
*/
-import { RenderValue } from '../../../common/types';
+import { PartitionChartProps } from '../../../common/types';
-export const data: RenderValue['visData'] = {
+export const data: PartitionChartProps['visData'] = {
type: 'datatable',
columns: [
{
diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/treemap_vis_renderer.stories.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/treemap_vis_renderer.stories.tsx
index bb6ff6de87feb..6b9248292c69b 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/treemap_vis_renderer.stories.tsx
+++ b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/treemap_vis_renderer.stories.tsx
@@ -10,7 +10,7 @@ import React, { FC } from 'react';
import { ComponentStory } from '@storybook/react';
import { Render } from '@kbn/presentation-util-plugin/public/__stories__';
import { getPartitionVisRenderer } from '../expression_renderers';
-import { ChartTypes, RenderValue } from '../../common/types';
+import { ChartTypes, PartitionChartProps } from '../../common/types';
import { getStartDeps } from '../__mocks__';
import { treemapArgTypes, treemapMosaicConfig, data } from './shared';
@@ -22,9 +22,9 @@ const containerSize = {
const PartitionVisRenderer = () => getPartitionVisRenderer({ getStartDeps });
type Props = {
- visType: RenderValue['visType'];
- syncColors: RenderValue['syncColors'];
-} & RenderValue['visConfig'];
+ visType: PartitionChartProps['visType'];
+ syncColors: PartitionChartProps['syncColors'];
+} & PartitionChartProps['visConfig'];
const PartitionVis: ComponentStory> = ({
visType,
diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/waffle_vis_renderer.stories.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/waffle_vis_renderer.stories.tsx
index 08db3a58d67a7..d2c472018cf78 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/waffle_vis_renderer.stories.tsx
+++ b/src/plugins/chart_expressions/expression_partition_vis/public/__stories__/waffle_vis_renderer.stories.tsx
@@ -10,7 +10,7 @@ import React, { FC } from 'react';
import { ComponentStory } from '@storybook/react';
import { Render } from '@kbn/presentation-util-plugin/public/__stories__';
import { getPartitionVisRenderer } from '../expression_renderers';
-import { ChartTypes, RenderValue } from '../../common/types';
+import { ChartTypes, PartitionChartProps } from '../../common/types';
import { getStartDeps } from '../__mocks__';
import { waffleArgTypes, waffleConfig, data } from './shared';
@@ -22,9 +22,9 @@ const containerSize = {
const PartitionVisRenderer = () => getPartitionVisRenderer({ getStartDeps });
type Props = {
- visType: RenderValue['visType'];
- syncColors: RenderValue['syncColors'];
-} & RenderValue['visConfig'];
+ visType: PartitionChartProps['visType'];
+ syncColors: PartitionChartProps['syncColors'];
+} & PartitionChartProps['visConfig'];
const PartitionVis: ComponentStory> = ({
visType,
diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap b/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap
index 1a566571b4d6c..94512bd8b43b7 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap
+++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap
@@ -280,6 +280,7 @@ exports[`PartitionVisComponent should render correct structure for donut 1`] = `
},
},
},
+ Object {},
]
}
tooltip={
@@ -666,6 +667,7 @@ exports[`PartitionVisComponent should render correct structure for mosaic 1`] =
},
},
},
+ Object {},
]
}
tooltip={
@@ -1115,6 +1117,7 @@ exports[`PartitionVisComponent should render correct structure for multi-metric
},
},
},
+ Object {},
]
}
tooltip={
@@ -1564,6 +1567,7 @@ exports[`PartitionVisComponent should render correct structure for pie 1`] = `
},
},
},
+ Object {},
]
}
tooltip={
@@ -1950,6 +1954,7 @@ exports[`PartitionVisComponent should render correct structure for treemap 1`] =
},
},
},
+ Object {},
]
}
tooltip={
@@ -2295,6 +2300,7 @@ exports[`PartitionVisComponent should render correct structure for waffle 1`] =
},
},
},
+ Object {},
]
}
tooltip={
diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.test.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.test.tsx
index a16f1c2ad77ab..5eb48cfab6cd5 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.test.tsx
+++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.test.tsx
@@ -329,4 +329,19 @@ describe('PartitionVisComponent', function () {
"Pie chart can't render with negative values."
);
});
+
+ describe('overrides', () => {
+ it('should apply overrides to the settings component', () => {
+ const component = shallow(
+
+ );
+
+ const settingsComponent = component.find(Settings);
+ expect(settingsComponent.prop('onBrushEnd')).toBeUndefined();
+ expect(settingsComponent.prop('ariaUseDefaultSummary')).toEqual(true);
+ });
+ });
});
diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx
index 843d6075ac60d..251caa0b4a88a 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx
+++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx
@@ -18,6 +18,7 @@ import {
TooltipType,
SeriesIdentifier,
PartitionElementEvent,
+ SettingsProps,
} from '@elastic/charts';
import { useEuiTheme } from '@elastic/eui';
import type { PaletteRegistry } from '@kbn/coloring';
@@ -34,13 +35,15 @@ import {
IInterpreterRenderHandlers,
} from '@kbn/expressions-plugin/public';
import type { FieldFormat } from '@kbn/field-formats-plugin/common';
+import { getOverridesFor } from '@kbn/chart-expressions-common';
import { consolidateMetricColumns } from '../../common/utils';
import { DEFAULT_PERCENT_DECIMALS } from '../../common/constants';
import {
- PartitionVisParams,
- BucketColumns,
+ type BucketColumns,
ValueFormats,
- PieContainerDimensions,
+ type PieContainerDimensions,
+ type PartitionChartProps,
+ type PartitionVisParams,
} from '../../common/types/expression_renderers';
import {
LegendColorPickerWrapper,
@@ -66,7 +69,6 @@ import {
partitionVisContainerStyle,
partitionVisContainerWithToggleStyleFactory,
} from './partition_vis_component.styles';
-import { ChartTypes } from '../../common/types';
import { filterOutConfig } from '../utils/filter_out_config';
import { ColumnCellValueActions, FilterEvent, StartDeps } from '../types';
@@ -78,10 +80,11 @@ declare global {
_echDebugStateFlag?: boolean;
}
}
-export interface PartitionVisComponentProps {
+export type PartitionVisComponentProps = Omit<
+ PartitionChartProps,
+ 'navigateToLens' | 'visConfig'
+> & {
visParams: PartitionVisParams;
- visData: Datatable;
- visType: ChartTypes;
uiState: PersistedState;
fireEvent: IInterpreterRenderHandlers['event'];
renderComplete: IInterpreterRenderHandlers['done'];
@@ -89,9 +92,8 @@ export interface PartitionVisComponentProps {
chartsThemeService: ChartsPluginSetup['theme'];
palettesRegistry: PaletteRegistry;
services: Pick;
- syncColors: boolean;
columnCellValueActions: ColumnCellValueActions;
-}
+};
const PartitionVisComponent = (props: PartitionVisComponentProps) => {
const {
@@ -102,6 +104,7 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => {
services,
syncColors,
interactive,
+ overrides,
} = props;
const visParams = useMemo(() => filterOutConfig(visType, preVisParams), [preVisParams, visType]);
const chartTheme = props.chartsThemeService.useChartsTheme();
@@ -354,6 +357,11 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => {
return 1;
}, [visData.rows, metricColumn]);
+ const { theme: settingsThemeOverrides = {}, ...settingsOverrides } = getOverridesFor(
+ overrides,
+ 'settings'
+ ) as Partial;
+
const themeOverrides = useMemo(
() => getPartitionTheme(visType, visParams, chartTheme, containerDimensions, rescaleFactor),
[visType, visParams, chartTheme, containerDimensions, rescaleFactor]
@@ -489,11 +497,16 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => {
},
},
},
+
+ ...(Array.isArray(settingsThemeOverrides)
+ ? settingsThemeOverrides
+ : [settingsThemeOverrides]),
]}
baseTheme={chartBaseTheme}
onRenderChange={onRenderChange}
ariaLabel={props.visParams.ariaLabel}
ariaUseDefaultSummary={!props.visParams.ariaLabel}
+ {...settingsOverrides}
/>
{
}
layers={layers}
topGroove={!visParams.labels.show ? 0 : undefined}
+ {...getOverridesFor(overrides, 'partition')}
/>
diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/expression_renderers/partition_vis_renderer.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/expression_renderers/partition_vis_renderer.tsx
index 19bd89a893cec..056ba6b7136ce 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/public/expression_renderers/partition_vis_renderer.tsx
+++ b/src/plugins/chart_expressions/expression_partition_vis/public/expression_renderers/partition_vis_renderer.tsx
@@ -25,7 +25,7 @@ import { extractContainerType, extractVisualizationType } from '@kbn/chart-expre
import { VisTypePieDependencies } from '../plugin';
import { PARTITION_VIS_RENDERER_NAME } from '../../common/constants';
import { CellValueAction, GetCompatibleCellValueActions } from '../types';
-import { ChartTypes, PartitionVisParams, RenderValue } from '../../common/types';
+import { ChartTypes, type PartitionVisParams, type PartitionChartProps } from '../../common/types';
export const strings = {
getDisplayName: () =>
@@ -73,14 +73,14 @@ export const getColumnCellValueActions = async (
export const getPartitionVisRenderer: (
deps: VisTypePieDependencies
-) => ExpressionRenderDefinition = ({ getStartDeps }) => ({
+) => ExpressionRenderDefinition = ({ getStartDeps }) => ({
name: PARTITION_VIS_RENDERER_NAME,
displayName: strings.getDisplayName(),
help: strings.getHelpDescription(),
reuseDomNode: true,
render: async (
domNode,
- { visConfig, visData, visType, syncColors, canNavigateToLens },
+ { visConfig, visData, visType, syncColors, canNavigateToLens, overrides },
handlers
) => {
const { core, plugins } = getStartDeps();
@@ -127,6 +127,7 @@ export const getPartitionVisRenderer: (
services={{ data: plugins.data, fieldFormats: plugins.fieldFormats }}
syncColors={syncColors}
columnCellValueActions={columnCellValueActions}
+ overrides={overrides}
/>
diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts
index 305c6c2ee6496..cf1325f09bf22 100644
--- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts
+++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/layered_xy_vis_fn.ts
@@ -7,7 +7,7 @@
*/
import { XY_VIS_RENDERER } from '../constants';
-import { LayeredXyVisFn } from '../types';
+import { LayeredXyVisFn, type XYRender } from '../types';
import { logDatatables, logDatatable } from '../utils';
import {
validateMarkSizeRatioLimits,
@@ -65,6 +65,7 @@ export const layeredXyVisFn: LayeredXyVisFn['fn'] = async (data, args, handlers)
syncColors: handlers?.isSyncColorsEnabled?.() ?? false,
syncTooltips: handlers?.isSyncTooltipsEnabled?.() ?? false,
syncCursor: handlers?.isSyncCursorEnabled?.() ?? true,
+ overrides: handlers.variables?.overrides as XYRender['value']['overrides'],
},
};
};
diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts
index 90710b945c763..9a71ec92d7a51 100644
--- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts
+++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts
@@ -355,4 +355,53 @@ describe('xyVis', () => {
},
});
});
+
+ test('should pass over overrides from variables', async () => {
+ const { data, args } = sampleArgs();
+ const { layers, ...rest } = args;
+ const { layerId, layerType, table, type, ...restLayerArgs } = sampleLayer;
+ const overrides = {
+ settings: {
+ onBrushEnd: 'ignore',
+ },
+ axisX: {
+ showOverlappingTicks: true,
+ },
+ };
+ const context = {
+ ...createMockExecutionContext(),
+ variables: {
+ overrides,
+ },
+ };
+ const result = await xyVisFunction.fn(
+ data,
+ { ...rest, ...restLayerArgs, referenceLines: [] },
+ context
+ );
+
+ expect(result).toEqual({
+ type: 'render',
+ as: XY_VIS,
+ value: {
+ args: {
+ ...rest,
+ layers: [
+ {
+ layerType,
+ table: data,
+ layerId: 'dataLayers-0',
+ type,
+ ...restLayerArgs,
+ },
+ ],
+ },
+ canNavigateToLens: false,
+ syncColors: false,
+ syncTooltips: false,
+ syncCursor: true,
+ overrides,
+ },
+ });
+ });
});
diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts
index ac867401dbe24..94d788106acb3 100644
--- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts
+++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts
@@ -11,7 +11,7 @@ import type { Datatable } from '@kbn/expressions-plugin/common';
import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common/expression_functions';
import { LayerTypes, XY_VIS_RENDERER, DATA_LAYER } from '../constants';
import { appendLayerIds, getAccessors, getShowLines, normalizeTable } from '../helpers';
-import { DataLayerConfigResult, XYLayerConfig, XyVisFn, XYArgs } from '../types';
+import type { DataLayerConfigResult, XYLayerConfig, XyVisFn, XYArgs, XYRender } from '../types';
import {
hasAreaLayer,
hasBarLayer,
@@ -137,6 +137,7 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => {
syncColors: handlers?.isSyncColorsEnabled?.() ?? false,
syncTooltips: handlers?.isSyncTooltipsEnabled?.() ?? false,
syncCursor: handlers?.isSyncCursorEnabled?.() ?? true,
+ overrides: handlers.variables?.overrides as XYRender['value']['overrides'],
},
};
};
diff --git a/src/plugins/chart_expressions/expression_xy/common/index.ts b/src/plugins/chart_expressions/expression_xy/common/index.ts
index 76c73d766b6cc..7e57b8b47a4aa 100755
--- a/src/plugins/chart_expressions/expression_xy/common/index.ts
+++ b/src/plugins/chart_expressions/expression_xy/common/index.ts
@@ -12,6 +12,7 @@ export const PLUGIN_NAME = 'expressionXy';
export { LayerTypes } from './constants';
export type {
+ AllowedXYOverrides,
XYArgs,
EndValue,
XYRender,
diff --git a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts
index 7d5df1a7394bb..0a2c00ed4f17f 100644
--- a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts
+++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts
@@ -6,14 +6,15 @@
* Side Public License, v 1.
*/
-import { HorizontalAlignment, Position, VerticalAlignment } from '@elastic/charts';
-import { $Values } from '@kbn/utility-types';
+import { type AxisProps, HorizontalAlignment, Position, VerticalAlignment } from '@elastic/charts';
+import type { $Values } from '@kbn/utility-types';
import type { PaletteOutput } from '@kbn/coloring';
-import { Datatable, ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common';
+import type { Datatable, ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common';
import { LegendSize } from '@kbn/visualizations-plugin/common';
import { EventAnnotationOutput } from '@kbn/event-annotation-plugin/common';
import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common';
+import { MakeOverridesSerializable, Simplify } from '@kbn/chart-expressions-common/types';
import {
AxisExtentModes,
FillStyles,
@@ -497,3 +498,11 @@ export type ExtendedAnnotationLayerFn = ExpressionFunctionDefinition<
ExtendedAnnotationLayerArgs,
ExtendedAnnotationLayerConfigResult
>;
+
+export type AllowedXYOverrides = Partial<
+ Record<
+ 'axisX' | 'axisLeft' | 'axisRight',
+ // id and groupId should not be overridden
+ Simplify, 'id' | 'groupId'>>
+ >
+>;
diff --git a/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts
index 0b65347192106..4da58cf64ae66 100644
--- a/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts
+++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_renderers.ts
@@ -7,12 +7,13 @@
*/
import { CustomAnnotationTooltip } from '@elastic/charts';
+import { AllowedSettingsOverrides } from '@kbn/charts-plugin/common';
import {
AvailableAnnotationIcon,
ManualPointEventAnnotationArgs,
} from '@kbn/event-annotation-plugin/common';
import { XY_VIS_RENDERER } from '../constants';
-import { XYProps } from './expression_functions';
+import type { AllowedXYOverrides, XYProps } from './expression_functions';
export interface XYChartProps {
args: XYProps;
@@ -20,6 +21,7 @@ export interface XYChartProps {
syncCursor: boolean;
syncColors: boolean;
canNavigateToLens?: boolean;
+ overrides?: AllowedXYOverrides & AllowedSettingsOverrides;
}
export interface XYRender {
diff --git a/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap
index 2bcd8f89b9f60..fb8faf123fa66 100644
--- a/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap
+++ b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap
@@ -607,19 +607,22 @@ exports[`XYChart component it renders area 1`] = `
showLegend={false}
showLegendExtra={false}
theme={
- Object {
- "background": Object {
- "color": undefined,
- },
- "barSeriesStyle": Object {},
- "chartMargins": Object {},
- "legend": Object {
- "labelOptions": Object {
- "maxLines": 0,
+ Array [
+ Object {
+ "background": Object {
+ "color": undefined,
+ },
+ "barSeriesStyle": Object {},
+ "chartMargins": Object {},
+ "legend": Object {
+ "labelOptions": Object {
+ "maxLines": 0,
+ },
},
+ "markSizeRatio": undefined,
},
- "markSizeRatio": undefined,
- }
+ Object {},
+ ]
}
/>
{
);
expect(component.find(Settings).at(0).prop('theme')).toEqual(
- expect.objectContaining(markSizeRatioArg)
+ expect.arrayContaining([expect.objectContaining(markSizeRatioArg)])
);
});
@@ -3468,4 +3468,84 @@ describe('XYChart component', () => {
expect(headerFormatter).not.toBeUndefined();
});
});
+
+ describe('overrides', () => {
+ it('should work for settings component', () => {
+ const { args } = sampleArgs();
+
+ const component = shallow(
+
+ );
+
+ const settingsComponent = component.find(Settings);
+ expect(settingsComponent.prop('onBrushEnd')).toBeUndefined();
+ expect(settingsComponent.prop('ariaUseDefaultSummary')).toEqual(true);
+ });
+
+ it('should work for all axes components', () => {
+ const args = createArgsWithLayers();
+ const layer = args.layers[0] as DataLayerConfig;
+
+ const component = shallow(
+
+ );
+
+ const axes = component.find(Axis);
+ expect(axes).toHaveLength(3);
+ if (Array.isArray(axes)) {
+ for (const axis of axes) {
+ expect(axis.prop('showOverlappingTicks').toEqual(true));
+ }
+ }
+ });
+ });
});
diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx
index 2c0327d7c4491..8d903a233fee7 100644
--- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx
+++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx
@@ -31,6 +31,7 @@ import {
Tooltip,
XYChartSeriesIdentifier,
TooltipValue,
+ SettingsProps,
} from '@elastic/charts';
import { partition } from 'lodash';
import { IconType } from '@elastic/eui';
@@ -51,6 +52,7 @@ import {
LegendSizeToPixels,
} from '@kbn/visualizations-plugin/common/constants';
import { PersistedState } from '@kbn/visualizations-plugin/public';
+import { getOverridesFor } from '@kbn/chart-expressions-common';
import type {
FilterEvent,
BrushEvent,
@@ -226,6 +228,7 @@ export function XYChart({
renderComplete,
uiState,
timeFormat,
+ overrides,
}: XYChartRenderProps) {
const {
legend,
@@ -792,6 +795,11 @@ export function XYChart({
// enable the tooltip actions only if there is at least one splitAccessor to the dataLayer
const hasTooltipActions = dataLayers.some((dataLayer) => dataLayer.splitAccessors) && interactive;
+ const { theme: settingsThemeOverrides = {}, ...settingsOverrides } = getOverridesFor(
+ overrides,
+ 'settings'
+ ) as Partial;
+
return (
{showLegend !== undefined && uiState && (
@@ -886,31 +894,36 @@ export function XYChart({
showLegend={showLegend}
legendPosition={legend?.isInside ? legendInsideParams : legend.position}
legendSize={LegendSizeToPixels[legend.legendSize ?? DEFAULT_LEGEND_SIZE]}
- theme={{
- ...chartTheme,
- barSeriesStyle: {
- ...chartTheme.barSeriesStyle,
- ...valueLabelsStyling,
- },
- background: {
- color: undefined, // removes background for embeddables
- },
- legend: {
- labelOptions: { maxLines: legend.shouldTruncate ? legend?.maxLines ?? 1 : 0 },
+ theme={[
+ {
+ ...chartTheme,
+ barSeriesStyle: {
+ ...chartTheme.barSeriesStyle,
+ ...valueLabelsStyling,
+ },
+ background: {
+ color: undefined, // removes background for embeddables
+ },
+ legend: {
+ labelOptions: { maxLines: legend.shouldTruncate ? legend?.maxLines ?? 1 : 0 },
+ },
+ // if not title or labels are shown for axes, add some padding if required by reference line markers
+ chartMargins: {
+ ...chartTheme.chartPaddings,
+ ...computeChartMargins(
+ linesPaddings,
+ { ...tickLabelsVisibilitySettings, x: xAxisConfig?.showLabels },
+ { ...axisTitlesVisibilitySettings, x: xAxisConfig?.showTitle },
+ yAxesMap,
+ shouldRotate
+ ),
+ },
+ markSizeRatio: args.markSizeRatio,
},
- // if not title or labels are shown for axes, add some padding if required by reference line markers
- chartMargins: {
- ...chartTheme.chartPaddings,
- ...computeChartMargins(
- linesPaddings,
- { ...tickLabelsVisibilitySettings, x: xAxisConfig?.showLabels },
- { ...axisTitlesVisibilitySettings, x: xAxisConfig?.showTitle },
- yAxesMap,
- shouldRotate
- ),
- },
- markSizeRatio: args.markSizeRatio,
- }}
+ ...(Array.isArray(settingsThemeOverrides)
+ ? settingsThemeOverrides
+ : [settingsThemeOverrides]),
+ ]}
baseTheme={chartBaseTheme}
allowBrushingLastHistogramBin={isTimeViz}
rotation={shouldRotate ? 90 : 0}
@@ -940,6 +953,7 @@ export function XYChart({
}
: undefined
}
+ {...settingsOverrides}
/>
{isSplitChart && splitTable && (
);
})}
diff --git a/src/plugins/charts/common/index.ts b/src/plugins/charts/common/index.ts
index 4d7b4df6028ee..9947305c50757 100644
--- a/src/plugins/charts/common/index.ts
+++ b/src/plugins/charts/common/index.ts
@@ -18,7 +18,7 @@ export type {
export { palette, systemPalette } from './expressions/palette';
export { paletteIds, defaultCustomColors } from './constants';
-export type { ColorSchema, RawColorSchema, ColorMap } from './static';
+export type { AllowedSettingsOverrides, ColorSchema, RawColorSchema, ColorMap } from './static';
export {
ColorSchemas,
vislibColorMaps,
diff --git a/src/plugins/charts/common/static/index.ts b/src/plugins/charts/common/static/index.ts
index 6e1b3c5e5ebea..7f281c7d2c342 100644
--- a/src/plugins/charts/common/static/index.ts
+++ b/src/plugins/charts/common/static/index.ts
@@ -18,3 +18,4 @@ export {
export { ColorMode, LabelRotation, defaultCountLabel } from './components';
export * from './styles';
+export type { AllowedSettingsOverrides } from './overrides';
diff --git a/src/plugins/charts/common/static/overrides/index.ts b/src/plugins/charts/common/static/overrides/index.ts
new file mode 100644
index 0000000000000..578af2f9edf3f
--- /dev/null
+++ b/src/plugins/charts/common/static/overrides/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export * from './settings';
diff --git a/src/plugins/charts/common/static/overrides/settings.ts b/src/plugins/charts/common/static/overrides/settings.ts
new file mode 100644
index 0000000000000..8fa9c9a2087c3
--- /dev/null
+++ b/src/plugins/charts/common/static/overrides/settings.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 { SettingsProps } from '@elastic/charts';
+
+type Simplify
= { [KeyType in keyof T]: T[KeyType] } & {};
+
+// Overrides should not expose Functions, React nodes and children props
+// So filter out any type which is not serializable
+export type MakeOverridesSerializable = {
+ [KeyType in keyof T]: NonNullable extends Function
+ ? // cannot use boolean here as it would be challenging to distinguish
+ // between a "native" boolean props and a disabled callback
+ // so use a specific keyword
+ 'ignore'
+ : // be careful here to not filter out string/number types
+ NonNullable extends React.ReactChildren | React.ReactElement
+ ? never
+ : // make it recursive
+ NonNullable extends object
+ ? MakeOverridesSerializable
+ : NonNullable;
+};
+
+export type AllowedSettingsOverrides = Partial<
+ Record<
+ 'settings',
+ Simplify<
+ MakeOverridesSerializable<
+ Omit<
+ SettingsProps,
+ | 'onRenderChange'
+ | 'onPointerUpdate'
+ | 'orderOrdinalBinsBy'
+ | 'baseTheme'
+ | 'legendColorPicker'
+ >
+ >
+ >
+ >
+>;
diff --git a/src/plugins/charts/common/static/styles/multilayer_timeaxis.ts b/src/plugins/charts/common/static/styles/multilayer_timeaxis.ts
index 02a5533f53fca..478cc7b52a73a 100644
--- a/src/plugins/charts/common/static/styles/multilayer_timeaxis.ts
+++ b/src/plugins/charts/common/static/styles/multilayer_timeaxis.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import { Position, RecursivePartial, AxisStyle } from '@elastic/charts';
+import { Position, type RecursivePartial, type AxisStyle } from '@elastic/charts';
export const MULTILAYER_TIME_AXIS_STYLE: RecursivePartial = {
tickLabel: {
diff --git a/x-pack/examples/testing_embedded_lens/public/app.tsx b/x-pack/examples/testing_embedded_lens/public/app.tsx
index 0f747135d3e99..4bf702d681054 100644
--- a/x-pack/examples/testing_embedded_lens/public/app.tsx
+++ b/x-pack/examples/testing_embedded_lens/public/app.tsx
@@ -36,10 +36,18 @@ import type {
RangeIndexPatternColumn,
PieVisualizationState,
MedianIndexPatternColumn,
+ MetricVisualizationState,
} from '@kbn/lens-plugin/public';
import type { ActionExecutionContext } from '@kbn/ui-actions-plugin/public';
import { CodeEditor, HJsonLang, KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import type { StartDependencies } from './plugin';
+import {
+ AllOverrides,
+ AttributesMenu,
+ LensAttributesByType,
+ OverridesMenu,
+ PanelMenu,
+} from './controls';
type RequiredType = 'date' | 'string' | 'number';
type FieldsMap = Record;
@@ -78,6 +86,13 @@ function getColumnFor(type: RequiredType, fieldName: string, isBucketed: boolean
maxBars: 'auto',
format: undefined,
parentFormat: undefined,
+ ranges: [
+ {
+ from: 0,
+ to: 1000,
+ label: '',
+ },
+ ],
},
} as RangeIndexPatternColumn;
}
@@ -162,12 +177,12 @@ function getBaseAttributes(
// Generate a Lens state based on some app-specific input parameters.
// `TypedLensByValueInput` can be used for type-safety - it uses the same interfaces as Lens-internal code.
-function getLensAttributes(
+function getLensAttributesXY(
defaultIndexPattern: DataView,
fields: FieldsMap,
- chartType: 'bar_stacked' | 'line' | 'area',
+ chartType: XYState['preferredSeriesType'],
color: string
-): TypedLensByValueInput['attributes'] {
+): LensAttributesByType<'lnsXY'> {
const baseAttributes = getBaseAttributes(defaultIndexPattern, fields);
const xyConfig: XYState = {
@@ -203,7 +218,7 @@ function getLensAttributes(
function getLensAttributesHeatmap(
defaultIndexPattern: DataView,
fields: FieldsMap
-): TypedLensByValueInput['attributes'] {
+): LensAttributesByType<'lnsHeatmap'> {
const initialType = getInitialType(defaultIndexPattern);
const dataLayer = getDataLayer(initialType, fields[initialType]);
const heatmapDataLayer = {
@@ -252,7 +267,7 @@ function getLensAttributesHeatmap(
function getLensAttributesDatatable(
defaultIndexPattern: DataView,
fields: FieldsMap
-): TypedLensByValueInput['attributes'] {
+): LensAttributesByType<'lnsDatatable'> {
const initialType = getInitialType(defaultIndexPattern);
const baseAttributes = getBaseAttributes(defaultIndexPattern, fields, initialType);
@@ -274,8 +289,9 @@ function getLensAttributesDatatable(
function getLensAttributesGauge(
defaultIndexPattern: DataView,
- fields: FieldsMap
-): TypedLensByValueInput['attributes'] {
+ fields: FieldsMap,
+ shape: GaugeVisualizationState['shape'] = 'horizontalBullet'
+): LensAttributesByType<'lnsGauge'> {
const dataLayer = getDataLayer('number', fields.number, false);
const gaugeDataLayer = {
columnOrder: ['col1'],
@@ -288,7 +304,7 @@ function getLensAttributesGauge(
const gaugeConfig: GaugeVisualizationState = {
layerId: 'layer1',
layerType: 'data',
- shape: 'horizontalBullet',
+ shape,
ticksPosition: 'auto',
labelMajorMode: 'auto',
metricAccessor: 'col1',
@@ -306,7 +322,7 @@ function getLensAttributesGauge(
function getLensAttributesPartition(
defaultIndexPattern: DataView,
fields: FieldsMap
-): TypedLensByValueInput['attributes'] {
+): LensAttributesByType<'lnsPie'> {
const baseAttributes = getBaseAttributes(defaultIndexPattern, fields, 'number');
const pieConfig: PieVisualizationState = {
layers: [
@@ -317,7 +333,7 @@ function getLensAttributesPartition(
layerType: 'data',
numberDisplay: 'percent',
categoryDisplay: 'default',
- legendDisplay: 'default',
+ legendDisplay: 'show',
},
],
shape: 'pie',
@@ -332,6 +348,30 @@ function getLensAttributesPartition(
};
}
+function getLensAttributesMetric(
+ defaultIndexPattern: DataView,
+ fields: FieldsMap,
+ color: string
+): LensAttributesByType<'lnsMetric'> {
+ const dataLayer = getDataLayer('string', fields.number, true);
+ const baseAttributes = getBaseAttributes(defaultIndexPattern, fields, 'number', dataLayer);
+ const metricConfig: MetricVisualizationState = {
+ layerId: 'layer1',
+ layerType: 'data',
+ metricAccessor: 'col2',
+ color,
+ breakdownByAccessor: 'col1',
+ };
+ return {
+ ...baseAttributes,
+ visualizationType: 'lnsMetric',
+ state: {
+ ...baseAttributes.state,
+ visualization: metricConfig,
+ },
+ };
+}
+
function getFieldsByType(dataView: DataView) {
const aggregatableFields = dataView.fields.filter((f) => f.aggregatable);
const fields: Partial = {
@@ -350,10 +390,6 @@ function getFieldsByType(dataView: DataView) {
return fields as FieldsMap;
}
-function isXYChart(attributes: TypedLensByValueInput['attributes']) {
- return attributes.visualizationType === 'lnsXY';
-}
-
function checkAndParseSO(newSO: string) {
try {
return JSON.parse(newSO) as TypedLensByValueInput['attributes'];
@@ -394,23 +430,29 @@ export const App = (props: {
to: 'now',
});
+ const initialColor = '#D6BF57';
+
const defaultCharts = [
{
id: 'bar_stacked',
- attributes: getLensAttributes(props.defaultDataView, fields, 'bar_stacked', 'green'),
+ attributes: getLensAttributesXY(props.defaultDataView, fields, 'bar_stacked', initialColor),
},
{
id: 'line',
- attributes: getLensAttributes(props.defaultDataView, fields, 'line', 'green'),
+ attributes: getLensAttributesXY(props.defaultDataView, fields, 'line', initialColor),
},
{
id: 'area',
- attributes: getLensAttributes(props.defaultDataView, fields, 'area', 'green'),
+ attributes: getLensAttributesXY(props.defaultDataView, fields, 'area', initialColor),
},
{ id: 'pie', attributes: getLensAttributesPartition(props.defaultDataView, fields) },
{ id: 'table', attributes: getLensAttributesDatatable(props.defaultDataView, fields) },
{ id: 'heatmap', attributes: getLensAttributesHeatmap(props.defaultDataView, fields) },
{ id: 'gauge', attributes: getLensAttributesGauge(props.defaultDataView, fields) },
+ {
+ id: 'metric',
+ attributes: getLensAttributesMetric(props.defaultDataView, fields, initialColor),
+ },
];
// eslint-disable-next-line react-hooks/exhaustive-deps
const charts = useMemo(() => [...defaultCharts, ...loadedCharts], [loadedCharts]);
@@ -429,11 +471,13 @@ export const App = (props: {
const newAttributes = JSON.stringify(newChart.attributes, null, 2);
currentSO.current = newAttributes;
saveValidSO(newAttributes);
+ // clear the overrides
+ setOverrides(undefined);
},
[charts]
);
- const currentAttributes = useMemo(() => {
+ const currentAttributes: TypedLensByValueInput['attributes'] = useMemo(() => {
try {
return JSON.parse(currentSO.current);
} catch (e) {
@@ -442,10 +486,11 @@ export const App = (props: {
}, [currentValid, currentSO]);
const isDisabled = !currentAttributes;
- const isColorDisabled = isDisabled || !isXYChart(currentAttributes);
useDebounce(() => setErrorDebounced(hasParsingError), 500, [hasParsingError]);
+ const [overrides, setOverrides] = useState();
+
return (
@@ -475,29 +520,28 @@ export const App = (props: {
- {
- const newColor = `rgb(${[1, 2, 3].map(() =>
- Math.floor(Math.random() * 256)
- )})`;
- const newAttributes = JSON.stringify(
- getLensAttributes(
- props.defaultDataView,
- fields,
- currentAttributes.state.visualization.preferredSeriesType,
- newColor
- ),
- null,
- 2
- );
- currentSO.current = newAttributes;
- saveValidSO(newAttributes);
- }}
- isDisabled={isColorDisabled}
- >
- Change color
-
+
+
+
+
+
+
+
- Edit in Lens (new tab)
-
-
-
- {
- toggleTriggers((prevState) => !prevState);
- }}
- >
- {enableTriggers ? 'Disable triggers' : 'Enable triggers'}
-
-
-
- {
- setEnableExtraAction((prevState) => !prevState);
- }}
- >
- {enableExtraAction ? 'Disable extra action' : 'Enable extra action'}
-
-
-
- {
- setEnableDefaultAction((prevState) => !prevState);
- }}
- >
- {enableDefaultAction ? 'Disable default action' : 'Enable default action'}
+ Open in Lens (new tab)
@@ -602,6 +610,7 @@ export const App = (props: {
style={{ height: 500 }}
timeRange={time}
attributes={currentAttributes}
+ overrides={overrides}
onLoad={(val) => {
setIsLoading(val);
}}
diff --git a/x-pack/examples/testing_embedded_lens/public/controls.tsx b/x-pack/examples/testing_embedded_lens/public/controls.tsx
new file mode 100644
index 0000000000000..19d2ab257666f
--- /dev/null
+++ b/x-pack/examples/testing_embedded_lens/public/controls.tsx
@@ -0,0 +1,597 @@
+/*
+ * 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, { useState } from 'react';
+import { isEqual } from 'lodash';
+import {
+ EuiButton,
+ EuiText,
+ EuiSpacer,
+ EuiColorPicker,
+ EuiFormRow,
+ EuiPopover,
+ useColorPickerState,
+ EuiSwitch,
+ EuiNotificationBadge,
+ EuiCodeBlock,
+ EuiIcon,
+ EuiToolTip,
+ EuiPopoverTitle,
+} from '@elastic/eui';
+import type { TypedLensByValueInput } from '@kbn/lens-plugin/public';
+
+export type LensAttributesByType = Extract<
+ TypedLensByValueInput['attributes'],
+ { visualizationType: VizType }
+>;
+
+function isXYChart(
+ attributes: TypedLensByValueInput['attributes']
+): attributes is LensAttributesByType<'lnsXY'> {
+ return attributes.visualizationType === 'lnsXY';
+}
+
+function isPieChart(
+ attributes: TypedLensByValueInput['attributes']
+): attributes is LensAttributesByType<'lnsPie'> {
+ return attributes.visualizationType === 'lnsPie';
+}
+
+function isHeatmapChart(
+ attributes: TypedLensByValueInput['attributes']
+): attributes is LensAttributesByType<'lnsHeatmap'> {
+ return attributes.visualizationType === 'lnsHeatmap';
+}
+
+function isDatatable(
+ attributes: TypedLensByValueInput['attributes']
+): attributes is LensAttributesByType<'lnsDatatable'> {
+ return attributes.visualizationType === 'lnsDatatable';
+}
+
+function isGaugeChart(
+ attributes: TypedLensByValueInput['attributes']
+): attributes is LensAttributesByType<'lnsGauge'> {
+ return attributes.visualizationType === 'lnsGauge';
+}
+
+function isMetricChart(
+ attributes: TypedLensByValueInput['attributes']
+): attributes is LensAttributesByType<'lnsMetric'> {
+ return attributes.visualizationType === 'lnsMetric';
+}
+
+function isSupportedChart(attributes: TypedLensByValueInput['attributes']) {
+ return (
+ isXYChart(attributes) ||
+ isPieChart(attributes) ||
+ isHeatmapChart(attributes) ||
+ isGaugeChart(attributes) ||
+ isMetricChart(attributes)
+ );
+}
+
+function mergeOverrides(
+ currentOverrides: AllOverrides | undefined,
+ newOverrides: AllOverrides | undefined,
+ defaultOverrides: AllOverrides
+): AllOverrides | undefined {
+ if (currentOverrides == null || isEqual(currentOverrides, defaultOverrides)) {
+ return newOverrides;
+ }
+ if (newOverrides == null) {
+ return Object.fromEntries(
+ Object.entries(currentOverrides)
+ .map(([key, value]) => {
+ if (!(key in defaultOverrides)) {
+ return [key, value];
+ }
+ // @ts-expect-error
+ if (isEqual(currentOverrides[key], defaultOverrides[key])) {
+ return [];
+ }
+ const newObject: Partial = {};
+ // @ts-expect-error
+ for (const [innerKey, innerValue] of Object.entries(currentOverrides[key])) {
+ // @ts-expect-error
+ if (!(innerKey in defaultOverrides[key])) {
+ // @ts-expect-error
+ newObject[innerKey] = innerValue;
+ }
+ }
+ return [key, newObject];
+ })
+ .filter((arr) => arr.length)
+ );
+ }
+ return {
+ ...currentOverrides,
+ ...newOverrides,
+ };
+}
+
+export function OverrideSwitch({
+ rowLabel,
+ controlLabel,
+ value,
+ override,
+ setOverrideValue,
+ helpText,
+}: {
+ rowLabel: string;
+ controlLabel: string;
+ helpText?: string;
+ value: AllOverrides | undefined;
+ override: AllOverrides;
+ setOverrideValue: (v: AllOverrides | undefined) => void;
+}) {
+ // check if value contains an object with the same structure as the default override
+ const rootKey = Object.keys(override)[0] as keyof AllOverrides;
+ const overridePath = [
+ rootKey,
+ Object.keys(override[rootKey] || {})[0] as keyof AllOverrides[keyof AllOverrides],
+ ];
+ const hasOverrideEnabled = Boolean(
+ value && overridePath[0] in value && overridePath[1] in value[overridePath[0]]!
+ );
+ return (
+ }
+ position="right"
+ >
+
+ {rowLabel}
+
+
+ }
+ helpText={helpText}
+ display="columnCompressedSwitch"
+ hasChildLabel={false}
+ >
+ {
+ const finalOverrides = mergeOverrides(
+ value,
+ hasOverrideEnabled ? undefined : override,
+ override
+ );
+ setOverrideValue(finalOverrides);
+ }}
+ compressed
+ />
+
+ );
+}
+
+function CodeExample({ propName, code }: { propName: string; code: string }) {
+ return (
+
+ {`
+
+ `}
+
+ );
+}
+
+export function AttributesMenu({
+ currentAttributes,
+ currentSO,
+ saveValidSO,
+}: {
+ currentAttributes: TypedLensByValueInput['attributes'];
+ currentSO: React.MutableRefObject;
+ saveValidSO: (attr: string) => void;
+}) {
+ const [attributesPopoverOpen, setAttributesPopoverOpen] = useState(false);
+ const [color, setColor, errors] = useColorPickerState('#D6BF57');
+
+ return (
+ setAttributesPopoverOpen(!attributesPopoverOpen)}
+ iconType="arrowDown"
+ iconSide="right"
+ color="primary"
+ isDisabled={!isSupportedChart(currentAttributes)}
+ >
+ Lens Attributes
+
+ }
+ isOpen={attributesPopoverOpen}
+ closePopover={() => setAttributesPopoverOpen(false)}
+ >
+
+ {isXYChart(currentAttributes) ? (
+
+ {
+ setColor(newColor, output);
+ // for sake of semplicity of this example change it locally and then shallow copy it
+ const dataLayer = currentAttributes.state.visualization.layers[0];
+ if ('yConfig' in dataLayer && dataLayer.yConfig) {
+ dataLayer.yConfig[0].color = newColor;
+ // this will make a string copy of it
+ const newAttributes = JSON.stringify(currentAttributes, null, 2);
+ currentSO.current = newAttributes;
+ saveValidSO(newAttributes);
+ }
+ }}
+ color={color}
+ isInvalid={!!errors}
+ />
+
+ ) : null}
+ {isMetricChart(currentAttributes) ? (
+
+ {
+ setColor(newColor, output);
+ // for sake of semplicity of this example change it locally and then shallow copy it
+ currentAttributes.state.visualization.color = newColor;
+ // this will make a string copy of it
+ const newAttributes = JSON.stringify(currentAttributes, null, 2);
+ currentSO.current = newAttributes;
+ saveValidSO(newAttributes);
+ }}
+ color={color}
+ isInvalid={!!errors}
+ />
+
+ ) : null}
+ {isPieChart(currentAttributes) ? (
+
+ {
+ currentAttributes.state.visualization.layers[0].numberDisplay =
+ currentAttributes.state.visualization.layers[0].numberDisplay === 'percent'
+ ? 'value'
+ : 'percent';
+ // this will make a string copy of it
+ const newAttributes = JSON.stringify(currentAttributes, null, 2);
+ currentSO.current = newAttributes;
+ saveValidSO(newAttributes);
+ }}
+ compressed
+ />
+
+ ) : null}
+ {isHeatmapChart(currentAttributes) ? (
+
+ {
+ currentAttributes.state.visualization.percentageMode =
+ !currentAttributes.state.visualization.percentageMode;
+ // this will make a string copy of it
+ const newAttributes = JSON.stringify(currentAttributes, null, 2);
+ currentSO.current = newAttributes;
+ saveValidSO(newAttributes);
+ }}
+ compressed
+ />
+
+ ) : null}
+ {isGaugeChart(currentAttributes) ? (
+
+ {
+ currentAttributes.state.visualization.ticksPosition =
+ currentAttributes.state.visualization.ticksPosition === 'hidden'
+ ? 'auto'
+ : 'hidden';
+ // this will make a string copy of it
+ const newAttributes = JSON.stringify(currentAttributes, null, 2);
+ currentSO.current = newAttributes;
+ saveValidSO(newAttributes);
+ }}
+ compressed
+ />
+
+ ) : null}
+
+
+ );
+}
+
+type XYOverride = Record<'axisX' | 'axisLeft' | 'axisRight', { hide: boolean }>;
+type PieOverride = Record<'partition', { fillOutside: boolean }>;
+type GaugeOverride = Record<'gauge', { subtype: 'goal'; angleStart: number; angleEnd: number }>;
+type SettingsOverride = Record<
+ 'settings',
+ | { onBrushEnd: 'ignore' }
+ | {
+ theme: {
+ heatmap: { xAxisLabel: { visible: boolean }; yAxisLabel: { visible: boolean } };
+ };
+ }
+ | {
+ theme: {
+ metric: { border: string };
+ };
+ }
+>;
+
+export type AllOverrides = Partial;
+
+export function OverridesMenu({
+ currentAttributes,
+ overrides,
+ setOverrides,
+}: {
+ currentAttributes: TypedLensByValueInput['attributes'];
+ overrides: AllOverrides | undefined;
+ setOverrides: (overrides: AllOverrides | undefined) => void;
+}) {
+ const [overridesPopoverOpen, setOverridesPopoverOpen] = useState(false);
+ const hasOverridesEnabled = Boolean(overrides) && !isDatatable(currentAttributes);
+ return (
+ setOverridesPopoverOpen(!overridesPopoverOpen)}
+ iconType="arrowDown"
+ iconSide="right"
+ isDisabled={!isSupportedChart(currentAttributes)}
+ >
+ Overrides{' '}
+
+ {hasOverridesEnabled ? 'ON' : 'OFF'}
+
+
+ }
+ isOpen={overridesPopoverOpen}
+ closePopover={() => setOverridesPopoverOpen(false)}
+ >
+
+
Overrides
+
+
+ Overrides are local to the Embeddable and forgotten when the visualization is open in
+ the Editor. They should be used carefully for specific tweaks within the integration.
+
+
+ There are mainly 2 use cases for overrides:
+
+ Specific styling/tuning feature missing in Lens
+ Disable specific chart behaviour
+
+
+ Here's some examples:
+
+
+ {isXYChart(currentAttributes) ? (
+
+ ) : null}
+ {isHeatmapChart(currentAttributes) ? (
+
+ ) : null}
+ {isPieChart(currentAttributes) ? (
+
+ ) : null}
+ {isXYChart(currentAttributes) ? (
+
+ ) : null}
+ {isGaugeChart(currentAttributes) ? (
+
+ ) : null}
+ {isMetricChart(currentAttributes) ? (
+
+ ) : null}
+
+
+ );
+}
+
+export function PanelMenu({
+ enableTriggers,
+ toggleTriggers,
+ enableDefaultAction,
+ setEnableDefaultAction,
+ enableExtraAction,
+ setEnableExtraAction,
+}: {
+ enableTriggers: boolean;
+ enableDefaultAction: boolean;
+ enableExtraAction: boolean;
+ toggleTriggers: (v: boolean) => void;
+ setEnableDefaultAction: (v: boolean) => void;
+ setEnableExtraAction: (v: boolean) => void;
+}) {
+ const [panelPopoverOpen, setPanelPopoverOpen] = useState(false);
+ return (
+ setPanelPopoverOpen(!panelPopoverOpen)}
+ iconType="arrowDown"
+ iconSide="right"
+ >
+ Embeddable settings
+
+ }
+ isOpen={panelPopoverOpen}
+ closePopover={() => setPanelPopoverOpen(false)}
+ >
+
+
Embeddable settings
+
+
+ It is possible to control and customize how the Embeddables is shown, disabling the
+ interactivity of the chart or filtering out default actions.
+
+
+
+
+ {
+ toggleTriggers(!enableTriggers);
+ }}
+ compressed
+ />
+
+
+ {
+ setEnableDefaultAction(!enableDefaultAction);
+ }}
+ compressed
+ />
+
+
+
It is also possible to pass custom actions to the panel:
+
+
'save',
+ async isCompatible(
+ context: ActionExecutionContext
+ ): Promise {
+ return true;
+ },
+ execute: async (context: ActionExecutionContext) => {
+ alert('I am an extra action');
+ return;
+ },
+ getDisplayName: () =>
+ 'Extra action',
+ }
+ ]`}
+ />
+ }
+ position="right"
+ >
+
+ Show custom action
+
+
+ }
+ display="columnCompressedSwitch"
+ helpText="Pass a consumer defined action to show in the panel context menu."
+ >
+ {
+ setEnableExtraAction(!enableExtraAction);
+ }}
+ compressed
+ />
+
+
+
+ );
+}
diff --git a/x-pack/plugins/lens/common/types.ts b/x-pack/plugins/lens/common/types.ts
index 2d8212d8c7d07..3c6830ba50b0d 100644
--- a/x-pack/plugins/lens/common/types.ts
+++ b/x-pack/plugins/lens/common/types.ts
@@ -17,6 +17,10 @@ import { layerTypes } from './layer_types';
import { CollapseFunction } from './expressions';
export type { OriginalColumn } from './expressions/map_to_columns';
+export type { AllowedPartitionOverrides } from '@kbn/expression-partition-vis-plugin/common';
+export type { AllowedSettingsOverrides } from '@kbn/charts-plugin/common';
+export type { AllowedGaugeOverrides } from '@kbn/expression-gauge-plugin/common';
+export type { AllowedXYOverrides } from '@kbn/expression-xy-plugin/common';
export type FormatFactory = (mapping?: SerializedFieldFormat) => IFieldFormat;
diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx
index 0af0f151fc08a..787c6766592ab 100644
--- a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx
+++ b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx
@@ -1677,4 +1677,69 @@ describe('embeddable', () => {
expect(test.initializeSavedVis).toHaveBeenCalledTimes(2);
expect(test.expressionRenderer).toHaveBeenCalledTimes(2);
});
+
+ it('should pass over the overrides as variables', async () => {
+ const embeddable = new Embeddable(
+ {
+ timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter,
+ attributeService,
+ data: dataMock,
+ expressionRenderer,
+ coreStart: {} as CoreStart,
+ basePath,
+ dataViews: {} as DataViewsContract,
+ capabilities: {
+ canSaveDashboards: true,
+ canSaveVisualizations: true,
+ discover: {},
+ navLinks: {},
+ },
+ inspector: inspectorPluginMock.createStartContract(),
+ getTrigger,
+ theme: themeServiceMock.createStartContract(),
+ visualizationMap: defaultVisualizationMap,
+ datasourceMap: defaultDatasourceMap,
+ injectFilterReferences: jest.fn(mockInjectFilterReferences),
+ documentToExpression: () =>
+ Promise.resolve({
+ ast: {
+ type: 'expression',
+ chain: [
+ { type: 'function', function: 'my', arguments: {} },
+ { type: 'function', function: 'expression', arguments: {} },
+ ],
+ },
+ indexPatterns: {},
+ indexPatternRefs: [],
+ }),
+ uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
+ },
+ {
+ timeRange: {
+ from: 'now-15m',
+ to: 'now',
+ },
+ overrides: {
+ settings: {
+ onBrushEnd: 'ignore',
+ },
+ },
+ } as LensEmbeddableInput
+ );
+ embeddable.render(mountpoint);
+
+ // wait one tick to give embeddable time to initialize
+ await new Promise((resolve) => setTimeout(resolve, 0));
+
+ expect(expressionRenderer).toHaveBeenCalledTimes(1);
+ expect(expressionRenderer.mock.calls[0][0]!.variables).toEqual(
+ expect.objectContaining({
+ overrides: {
+ settings: {
+ onBrushEnd: 'ignore',
+ },
+ },
+ })
+ );
+ });
});
diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx
index 21aae0b8a4d44..6ea166df5bb99 100644
--- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx
+++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx
@@ -102,6 +102,12 @@ import {
UserMessagesDisplayLocationId,
} from '../types';
+import type {
+ AllowedPartitionOverrides,
+ AllowedSettingsOverrides,
+ AllowedGaugeOverrides,
+ AllowedXYOverrides,
+} from '../../common/types';
import { getEditPath, DOC_TYPE } from '../../common/constants';
import { LensAttributeService } from '../lens_attribute_service';
import type { TableInspectorAdapter } from '../editor_frame_service/types';
@@ -150,6 +156,18 @@ interface LensBaseEmbeddableInput extends EmbeddableInput {
export type LensByValueInput = {
attributes: LensSavedObjectAttributes;
+ /**
+ * Overrides can tweak the style of the final embeddable and are executed at the end of the Lens rendering pipeline.
+ * Each visualization type offers various type of overrides, per component (i.e. 'setting', 'axisX', 'partition', etc...)
+ *
+ * While it is not possible to pass function/callback/handlers to the renderer, it is possible to overwrite
+ * the current behaviour by passing the "ignore" string to the override prop (i.e. onBrushEnd: "ignore" to stop brushing)
+ */
+ overrides?:
+ | AllowedSettingsOverrides
+ | AllowedXYOverrides
+ | AllowedPartitionOverrides
+ | AllowedGaugeOverrides;
} & LensBaseEmbeddableInput;
export type LensByReferenceInput = SavedObjectEmbeddableInput & LensBaseEmbeddableInput;
@@ -469,8 +487,18 @@ export class Embeddable
const attributesOrSavedObjectId$ = input$.pipe(
distinctUntilChanged((a, b) =>
fastIsEqual(
- ['attributes' in a && a.attributes, 'savedObjectId' in a && a.savedObjectId],
- ['attributes' in b && b.attributes, 'savedObjectId' in b && b.savedObjectId]
+ [
+ 'attributes' in a && a.attributes,
+ 'savedObjectId' in a && a.savedObjectId,
+ 'overrides' in a && a.overrides,
+ 'disableTriggers' in a && a.disableTriggers,
+ ],
+ [
+ 'attributes' in b && b.attributes,
+ 'savedObjectId' in b && b.savedObjectId,
+ 'overrides' in b && b.overrides,
+ 'disableTriggers' in b && b.disableTriggers,
+ ]
)
),
skip(1),
@@ -875,6 +903,7 @@ export class Embeddable
variables={{
embeddableTitle: this.getTitle(),
...(input.palette ? { theme: { palette: input.palette } } : {}),
+ ...('overrides' in input ? { overrides: input.overrides } : {}),
}}
searchSessionId={this.getInput().searchSessionId}
handleEvent={this.handleEvent}
diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx b/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx
index c53f9f5543ff7..943e87c9c00c2 100644
--- a/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx
+++ b/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx
@@ -24,9 +24,16 @@ import type { LensByReferenceInput, LensByValueInput } from './embeddable';
import type { Document } from '../persistence';
import type { FormBasedPersistedState } from '../datasources/form_based/types';
import type { XYState } from '../visualizations/xy/types';
-import type { PieVisualizationState, LegacyMetricState } from '../../common/types';
+import type {
+ PieVisualizationState,
+ LegacyMetricState,
+ AllowedGaugeOverrides,
+ AllowedPartitionOverrides,
+ AllowedSettingsOverrides,
+ AllowedXYOverrides,
+} from '../../common/types';
import type { DatatableVisualizationState } from '../visualizations/datatable/visualization';
-import type { MetricVisualizationState } from '../visualizations/metric/visualization';
+import type { MetricVisualizationState } from '../visualizations/metric/types';
import type { HeatmapVisualizationState } from '../visualizations/heatmap/types';
import type { GaugeVisualizationState } from '../visualizations/gauge/constants';
@@ -47,16 +54,28 @@ type LensAttributes = Omit<
* Type-safe variant of by value embeddable input for Lens.
* This can be used to hardcode certain Lens chart configurations within another app.
*/
-export type TypedLensByValueInput = Omit & {
+export type TypedLensByValueInput = Omit & {
attributes:
| LensAttributes<'lnsXY', XYState>
| LensAttributes<'lnsPie', PieVisualizationState>
+ | LensAttributes<'lnsHeatmap', HeatmapVisualizationState>
+ | LensAttributes<'lnsGauge', GaugeVisualizationState>
| LensAttributes<'lnsDatatable', DatatableVisualizationState>
| LensAttributes<'lnsLegacyMetric', LegacyMetricState>
| LensAttributes<'lnsMetric', MetricVisualizationState>
- | LensAttributes<'lnsHeatmap', HeatmapVisualizationState>
- | LensAttributes<'lnsGauge', GaugeVisualizationState>
| LensAttributes;
+
+ /**
+ * Overrides can tweak the style of the final embeddable and are executed at the end of the Lens rendering pipeline.
+ * XY charts offer an override of the Settings ('settings') and Axis ('axisX', 'axisLeft', 'axisRight') components.
+ * While it is not possible to pass function/callback/handlers to the renderer, it is possible to stop them by passing the
+ * "ignore" string as override value (i.e. onBrushEnd: "ignore")
+ */
+ overrides?:
+ | AllowedSettingsOverrides
+ | AllowedXYOverrides
+ | AllowedPartitionOverrides
+ | AllowedGaugeOverrides;
};
export type EmbeddableComponentProps = (TypedLensByValueInput | LensByReferenceInput) & {
diff --git a/x-pack/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts
index c240623e706c5..65d5ca12df094 100644
--- a/x-pack/plugins/lens/public/index.ts
+++ b/x-pack/plugins/lens/public/index.ts
@@ -44,6 +44,7 @@ export type {
export type { DatatableVisualizationState } from './visualizations/datatable/visualization';
export type { HeatmapVisualizationState } from './visualizations/heatmap/types';
export type { GaugeVisualizationState } from './visualizations/gauge/constants';
+export type { MetricVisualizationState } from './visualizations/metric/types';
export type {
FormBasedPersistedState,
PersistedIndexPatternLayer,
diff --git a/x-pack/plugins/lens/public/visualizations/gauge/constants.ts b/x-pack/plugins/lens/public/visualizations/gauge/constants.ts
index 0a8e11f82ba08..bd7dc76d05c5a 100644
--- a/x-pack/plugins/lens/public/visualizations/gauge/constants.ts
+++ b/x-pack/plugins/lens/public/visualizations/gauge/constants.ts
@@ -6,7 +6,7 @@
*/
import type { GaugeState as GaugeStateOriginal } from '@kbn/expression-gauge-plugin/common';
-import { LayerType } from '../../../common/types';
+import type { LayerType } from '../../../common/types';
export const LENS_GAUGE_ID = 'lnsGauge';
diff --git a/x-pack/plugins/lens/public/visualizations/metric/types.ts b/x-pack/plugins/lens/public/visualizations/metric/types.ts
new file mode 100644
index 0000000000000..d25a4b1b33396
--- /dev/null
+++ b/x-pack/plugins/lens/public/visualizations/metric/types.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { LayoutDirection } from '@elastic/charts';
+import type { PaletteOutput, CustomPaletteParams } from '@kbn/coloring';
+import type { CollapseFunction } from '@kbn/visualizations-plugin/common';
+import type { LayerType } from '../../../common/types';
+
+export interface MetricVisualizationState {
+ layerId: string;
+ layerType: LayerType;
+ metricAccessor?: string;
+ secondaryMetricAccessor?: string;
+ maxAccessor?: string;
+ breakdownByAccessor?: string;
+ // the dimensions can optionally be single numbers
+ // computed by collapsing all rows
+ collapseFn?: CollapseFunction;
+ subtitle?: string;
+ secondaryPrefix?: string;
+ progressDirection?: LayoutDirection;
+ showBar?: boolean;
+ color?: string;
+ palette?: PaletteOutput;
+ maxCols?: number;
+
+ trendlineLayerId?: string;
+ trendlineLayerType?: LayerType;
+ trendlineTimeAccessor?: string;
+ trendlineMetricAccessor?: string;
+ trendlineSecondaryMetricAccessor?: string;
+ trendlineBreakdownByAccessor?: string;
+}
diff --git a/x-pack/plugins/lens/readme.md b/x-pack/plugins/lens/readme.md
index b01f19e4ee8ce..9fec3f154fbf3 100644
--- a/x-pack/plugins/lens/readme.md
+++ b/x-pack/plugins/lens/readme.md
@@ -25,11 +25,13 @@ When adding visualizations to a solution page, there are multiple ways to approa
Pros:
* No need to manage searches and rendering logic on your own
* "Open in Lens" comes for free
+ * Simple extended visualization options - if Lens can't do it, there's also a limited set of overrides to customize the final result
Cons:
* Each panel does its own data fetching and rendering (can lead to performance problems for high number of embeddables on a single page, e.g. more than 20)
* Limited data processing options - if the Lens UI doesn't support it, it can't be used
- * Limited visualization options - if Lens can't do it, it's not possible
+
+
* #### **Using custom data fetching and rendering**
In case the disadvantages of using the Lens embeddable heavily affect your use case, it sometimes makes sense to roll your own data fetching and rendering by using the underlying APIs of search service and `elastic-charts` directly. This allows a high degree of flexibility when it comes to data processing, efficiently querying data for multiple charts in a single query and adjusting small details in how charts are rendered. However, do not choose these option lightly as maintenance as well as initial development effort will most likely be much higher than by using the Lens embeddable directly. In this case, almost always an "Open in Lens" button can still be offered to the user to drill down and further explore the data by generating a Lens configuration which is similar to the displayed visualization given the possibilities of Lens. Keep in mind that for the "Open in Lens" flow, the most important property isn't perfect fidelity of the chart but retaining the mental context of the user when switching so they don't have to start over. It's also possible to mix this approach with Lens embeddables on a single page. **Note**: In this situation, please let the Visualizations team know what features you are missing / why you chose not to use Lens.
@@ -182,6 +184,23 @@ The Lens embeddable is handling both data fetching and rendering - all the user
/>
```
+## Overrides
+
+The Lens embeddable offers a way to extends the current set of visualization feature provided within the Lens editor, via the `overrides` property, which enables the consumer to override some visualization configurations in the embeddable instance.
+
+```tsx
+
+```
+
+The each override is component-specific and it inherits the prop from its `elastic-charts` definition directly. Callback/handlers are not supported as functions, but the special value `"ignore"` can be provided in order to disable them in the embeddable rendering.
+**Note**: overrides are only applied to the local embeddable instance and will disappear when the visualization is open in the Lens editor.
+
# Lens Development
The following sections are concerned with developing the Lens plugin itself.
From 73068f1e875729951a6f57ce98509cae159ce4e4 Mon Sep 17 00:00:00 2001
From: Zacqary Adam Xeper
Date: Tue, 4 Apr 2023 12:30:25 +0200
Subject: [PATCH 003/112] [RAM] Fix broken custom snooze recurrences with
monthly frequency (#154251)
## Summary
Fixes #153579
The custom recurrence scheduler improperly expected `byweekday` to never
be `undefined`, which caused it to throw an error when setting custom
recurrences like "Every 2 months on the 4th day of the month."
Recurrences like "Every 2 months on the 1st Tuesday" were working fine
as these had a defined `byweekday`.
---------
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../components/rule_snooze/panel/base_snooze_panel.tsx | 1 +
.../recurrence_scheduler/custom_recurrence_scheduler.tsx | 2 +-
.../components/rule_snooze/recurrence_scheduler/helpers.tsx | 4 ++--
3 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_snooze/panel/base_snooze_panel.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_snooze/panel/base_snooze_panel.tsx
index 437c75aa3ab2a..a6761ec4de882 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_snooze/panel/base_snooze_panel.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_snooze/panel/base_snooze_panel.tsx
@@ -213,6 +213,7 @@ export const BaseSnoozePanel: React.FunctionComponent = ({
style={{
paddingLeft: '9px',
paddingRight: '9px',
+ height: '36px',
// Replicate euiPanel--accent vs euiPanel--subdued
// Applying these classNames by themselves doesn't work due to a CSS-in-JS issue with EuiPanel
color: isActive ? '#a8376a' : euiTheme.colors.subduedText,
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_snooze/recurrence_scheduler/custom_recurrence_scheduler.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_snooze/recurrence_scheduler/custom_recurrence_scheduler.tsx
index b7568cfdc99db..2136525c028b6 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_snooze/recurrence_scheduler/custom_recurrence_scheduler.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_snooze/recurrence_scheduler/custom_recurrence_scheduler.tsx
@@ -53,7 +53,7 @@ export const CustomRecurrenceScheduler: React.FC
getInitialByweekday(initialState.byweekday, startDate)
);
const [monthlyRecurDay, setMonthlyRecurDay] = useState(
- initialState.freq === RRuleFrequency.MONTHLY && initialState.byweekday.length > 0
+ initialState.freq === RRuleFrequency.MONTHLY && (initialState.byweekday?.length ?? 0) > 0
? 'weekday'
: 'day'
);
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_snooze/recurrence_scheduler/helpers.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_snooze/recurrence_scheduler/helpers.tsx
index 6e549fdc5a032..ca72691a9839f 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_snooze/recurrence_scheduler/helpers.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_snooze/recurrence_scheduler/helpers.tsx
@@ -16,7 +16,7 @@ import { i18nFreqSummary, i18nNthWeekdayShort } from './translations';
export interface CustomFrequencyState {
freq: RRuleFrequency;
interval: number;
- byweekday: string[];
+ byweekday?: string[];
bymonthday: number[];
bymonth: number[];
}
@@ -37,7 +37,7 @@ export const getInitialByweekday = (
(result, n) => ({
...result,
[n]:
- initialStateByweekday?.length > 0
+ initialStateByweekday && initialStateByweekday.length > 0
? initialStateByweekday
// Sanitize nth day strings, e.g. +2MO, -1FR, into just days of the week
.map((w) => w.replace(/[0-9+\-]/g, ''))
From 606eb9cd61ef69e63e6d5769cc372c45dfa72492 Mon Sep 17 00:00:00 2001
From: Marco Liberati
Date: Tue, 4 Apr 2023 12:42:13 +0200
Subject: [PATCH 004/112] [Lens] Enabling Random Sampling (#151749)
## Summary
This PR is a design implementation to improve the Random sampling
feedback for the user.
Some work has been done to extract locally the `SamplingSlider`
component, that will be eventually moved into a separate package outside
of Lens.
In terms of design, to start, the Layer setting now inherits the same
`Data/Appearance` design from the dimension editor:
Next, on the dataView picker of the layer panel the random sampling
information is shown when enabled:
when transitioning to `Maximum`/`Minimum` operation during editing the
sampling is disabled and a info toast is shown:
The toast will show only when transitioning to such operations. If the
user goes from `Maximum`/`Minimum` to other supported operations then
the toast flag is reset, therefore going again into `Maximum` will
trigger a new toast.
Transitioning from a quick function `Maximum` into a formula `max(...)`
will not trigger a toast as the flag is persisted within the same
"editing session".
If the user configured a random sampling setting but then picked a
`Maximum` operation the Layer setting becomes disabled:
and last the embeddable view with the new visualization modifiers view
on the bottom-left:
Previous PoC design
This PR works as a PoC for random sampling with the current state of the
design.
The UI is still a bit rough and not final.
when transitioning to `Maximum`/`Minimum` operation:
The toast will show only when transitioning to such operations. If the
user goes from `Maximum`/`Minimum` to other supported operations then
the toast flag is reset, therefore going again into `Maximum` will
trigger a new toast.
Transitioning from a quick function `Maximum` into a formula `max(...)`
will not trigger a toast as the flag is persisted within the same
"editing session".
If the user configured a random sampling setting but then picked a
`Maximum` operation the Layer setting becomes disabled:
At dashboard level the random sampling is notified via an icon on the
bottom left:
Hovering the icon will show a detailed tooltip:
At the dashboard level a new `i` icon is displayed when a random
sampling feature is enabled in the panel and a popup with more details
is shown on hover:
### Checklist
Delete any items that are not applicable to this PR.
- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
### Risk Matrix
Delete this section if it is not applicable to this PR.
Before closing this PR, invite QA, stakeholders, and other developers to
identify risks that should be tested prior to the change/feature
release.
When forming the risk matrix, consider some of the following examples
and how they may potentially impact the change:
| Risk | Probability | Severity | Mitigation/Notes |
|---------------------------|-------------|----------|-------------------------|
| Multiple Spaces—unexpected behavior in non-default Kibana Space.
| Low | High | Integration tests will verify that all features are still
supported in non-default Kibana Space and when user switches between
spaces. |
| Multiple nodes—Elasticsearch polling might have race conditions
when multiple Kibana nodes are polling for the same tasks. | High | Low
| Tasks are idempotent, so executing them multiple times will not result
in logical error, but will degrade performance. To test for this case we
add plenty of unit tests around this logic and document manual testing
procedure. |
| Code should gracefully handle cases when feature X or plugin Y are
disabled. | Medium | High | Unit tests will verify that any feature flag
or plugin combination still results in our service operational. |
| [See more potential risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) |
### For maintainers
- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
---------
Co-authored-by: Stratoula Kalafateli
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Michael Marcialis
---
.../get_application_user_messages.tsx | 20 +-
.../dimension_panel/dimension_editor.tsx | 82 +++++--
.../dimension_panel/dimension_panel.test.tsx | 2 +
.../dimension_panel/dimension_panel.tsx | 8 +-
.../datasources/form_based/form_based.test.ts | 176 ++++++++++++--
.../datasources/form_based/form_based.tsx | 8 +-
.../datasources/form_based/info_badges.tsx | 61 +++++
.../datasources/form_based/layer_settings.tsx | 230 ++++++++++++------
.../datasources/form_based/layerpanel.tsx | 3 +
.../definitions/calculations/counter_rate.tsx | 5 +
.../operations/definitions/index.ts | 6 +
.../operations/definitions/metrics.tsx | 7 +-
.../datasources/form_based/to_expression.ts | 3 +-
.../public/datasources/form_based/utils.tsx | 81 +++++-
.../editor_frame/config_panel/layer_panel.tsx | 9 +-
.../workspace_panel/message_list.tsx | 5 +-
.../lens/public/embeddable/embeddable.tsx | 24 +-
.../embeddable/embeddable_info_badges.scss | 5 +
.../embeddable/embeddable_info_badges.tsx | 90 +++++++
x-pack/plugins/lens/public/mocks/index.ts | 4 +-
.../dataview_picker/dataview_picker.tsx | 129 ++++++++--
.../dataview_picker/sampling_icon.tsx | 56 +++++
.../dataview_picker/toolbar_button.scss | 61 +++++
.../dataview_picker/toolbar_button.tsx | 87 +++++++
x-pack/plugins/lens/public/types.ts | 3 +-
.../lens/public/visualization_container.scss | 6 +
.../public/visualizations/xy/info_badges.tsx | 52 ++++
.../visualizations/xy/visualization.test.ts | 56 ++++-
.../visualizations/xy/visualization.tsx | 203 +++++++++-------
.../translations/translations/fr-FR.json | 6 -
.../translations/translations/ja-JP.json | 6 -
.../translations/translations/zh-CN.json | 6 -
.../apps/lens/group1/layer_actions.ts | 144 ++++++++++-
33 files changed, 1364 insertions(+), 280 deletions(-)
create mode 100644 x-pack/plugins/lens/public/datasources/form_based/info_badges.tsx
create mode 100644 x-pack/plugins/lens/public/embeddable/embeddable_info_badges.scss
create mode 100644 x-pack/plugins/lens/public/embeddable/embeddable_info_badges.tsx
create mode 100644 x-pack/plugins/lens/public/shared_components/dataview_picker/sampling_icon.tsx
create mode 100644 x-pack/plugins/lens/public/shared_components/dataview_picker/toolbar_button.scss
create mode 100644 x-pack/plugins/lens/public/shared_components/dataview_picker/toolbar_button.tsx
create mode 100644 x-pack/plugins/lens/public/visualizations/xy/info_badges.tsx
diff --git a/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.tsx b/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.tsx
index b56736eabe7ac..4a323a4d654b8 100644
--- a/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/get_application_user_messages.tsx
@@ -198,11 +198,7 @@ export const filterAndSortUserMessages = (
return false;
}
- if (location.id === 'dimensionButton' && location.dimensionId !== dimensionId) {
- return false;
- }
-
- return true;
+ return !(location.id === 'dimensionButton' && location.dimensionId !== dimensionId);
});
if (!hasMatch) {
@@ -221,11 +217,17 @@ export const filterAndSortUserMessages = (
};
function bySeverity(a: UserMessage, b: UserMessage) {
- if (a.severity === 'warning' && b.severity === 'error') {
+ if (a.severity === b.severity) {
+ return 0;
+ }
+ if (a.severity === 'error') {
+ return -1;
+ }
+ if (b.severity === 'error') {
return 1;
- } else if (a.severity === 'error' && b.severity === 'warning') {
+ }
+ if (a.severity === 'warning') {
return -1;
- } else {
- return 0;
}
+ return 1;
}
diff --git a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_editor.tsx
index f8d58b460e4b3..4f7842ee125c4 100644
--- a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_editor.tsx
+++ b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_editor.tsx
@@ -44,7 +44,7 @@ import {
} from '../operations';
import { mergeLayer } from '../state_helpers';
import { getReferencedField, hasField } from '../pure_utils';
-import { fieldIsInvalid } from '../utils';
+import { fieldIsInvalid, getSamplingValue, isSamplingValueEnabled } from '../utils';
import { BucketNestingEditor } from './bucket_nesting_editor';
import type { FormBasedLayer } from '../types';
import { FormatSelector } from './format_selector';
@@ -126,6 +126,10 @@ export function DimensionEditor(props: DimensionEditorProps) {
const [temporaryState, setTemporaryState] = useState('none');
const [isHelpOpen, setIsHelpOpen] = useState(false);
+ // If a layer has sampling disabled, assume the toast has already fired in the past
+ const [hasRandomSamplingToastFired, setSamplingToastAsFired] = useState(
+ !isSamplingValueEnabled(state.layers[layerId])
+ );
const onHelpClick = () => setIsHelpOpen((prevIsHelpOpen) => !prevIsHelpOpen);
const closeHelp = () => setIsHelpOpen(false);
@@ -139,6 +143,28 @@ export function DimensionEditor(props: DimensionEditorProps) {
[layerId, setState]
);
+ const fireOrResetRandomSamplingToast = useCallback(
+ (newLayer: FormBasedLayer) => {
+ // if prev and current sampling state is different, show a toast to the user
+ if (isSamplingValueEnabled(state.layers[layerId]) && !isSamplingValueEnabled(newLayer)) {
+ if (newLayer.sampling != null && newLayer.sampling < 1) {
+ props.notifications.toasts.add({
+ title: i18n.translate('xpack.lens.uiInfo.samplingDisabledTitle', {
+ defaultMessage: 'Layer sampling changed to 100%',
+ }),
+ text: i18n.translate('xpack.lens.uiInfo.samplingDisabledMessage', {
+ defaultMessage:
+ 'The use of a maximum or minimum function on a layer requires all documents to be sampled in order to function properly.',
+ }),
+ });
+ }
+ }
+ // reset the flag if the user switches to another supported operation
+ setSamplingToastAsFired(!hasRandomSamplingToastFired);
+ },
+ [hasRandomSamplingToastFired, layerId, props.notifications.toasts, state.layers]
+ );
+
const setStateWrapper = useCallback(
(
setter:
@@ -177,10 +203,14 @@ export function DimensionEditor(props: DimensionEditorProps) {
} else {
outputLayer = typeof setter === 'function' ? setter(prevState.layers[layerId]) : setter;
}
+ const newLayer = adjustColumnReferencesForChangedColumn(outputLayer, columnId);
+ // Fire an info toast (eventually) on layer update
+ fireOrResetRandomSamplingToast(newLayer);
+
return mergeLayer({
state: prevState,
layerId,
- newLayer: adjustColumnReferencesForChangedColumn(outputLayer, columnId),
+ newLayer,
});
},
{
@@ -189,7 +219,7 @@ export function DimensionEditor(props: DimensionEditorProps) {
}
);
},
- [columnId, layerId, setState, state.layers]
+ [columnId, fireOrResetRandomSamplingToast, layerId, setState, state.layers]
);
const setIsCloseable = (isCloseable: boolean) => {
@@ -337,6 +367,9 @@ export function DimensionEditor(props: DimensionEditorProps) {
state.layers[layerId],
layerType
),
+ compatibleWithSampling:
+ getSamplingValue(state.layers[layerId]) === 1 ||
+ (definition.getUnsupportedSettings?.()?.sampling ?? true),
};
});
@@ -350,7 +383,7 @@ export function DimensionEditor(props: DimensionEditorProps) {
(selectedColumn?.operationType != null && isQuickFunction(selectedColumn?.operationType));
const sideNavItems: EuiListGroupItemProps[] = operationsWithCompatibility.map(
- ({ operationType, compatibleWithCurrentField, disabledStatus }) => {
+ ({ operationType, compatibleWithCurrentField, disabledStatus, compatibleWithSampling }) => {
const isActive = Boolean(
incompleteOperation === operationType ||
(!incompleteOperation && selectedColumn && selectedColumn.operationType === operationType)
@@ -417,6 +450,26 @@ export function DimensionEditor(props: DimensionEditorProps) {
)}
);
+ } else if (!compatibleWithSampling) {
+ label = (
+
+
+ {label}
+
+ {shouldDisplayDots && (
+
+
+
+ )}
+
+ );
}
return {
@@ -741,16 +794,16 @@ export function DimensionEditor(props: DimensionEditorProps) {
);
}}
onChooseFunction={(operationType: string, field?: IndexPatternField) => {
- updateLayer(
- insertOrReplaceColumn({
- layer,
- columnId: referenceId,
- op: operationType,
- indexPattern: currentIndexPattern,
- field,
- visualizationGroups: dimensionGroups,
- })
- );
+ const newLayer = insertOrReplaceColumn({
+ layer,
+ columnId: referenceId,
+ op: operationType,
+ indexPattern: currentIndexPattern,
+ field,
+ visualizationGroups: dimensionGroups,
+ });
+ fireOrResetRandomSamplingToast(newLayer);
+ updateLayer(newLayer);
}}
onChooseField={(choice: FieldChoiceWithOperationType) => {
updateLayer(
@@ -784,6 +837,7 @@ export function DimensionEditor(props: DimensionEditorProps) {
} else {
newLayer = setter;
}
+ fireOrResetRandomSamplingToast(newLayer);
return updateLayer(adjustColumnReferencesForChangedColumn(newLayer, referenceId));
}}
validation={validation}
diff --git a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.test.tsx
index 1646250d75bf5..a4fd8135f00be 100644
--- a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.test.tsx
+++ b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.test.tsx
@@ -30,6 +30,7 @@ import {
SavedObjectsClientContract,
HttpSetup,
CoreStart,
+ NotificationsStart,
} from '@kbn/core/public';
import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
import { useExistingFieldsReader } from '@kbn/unified-field-list-plugin/public/hooks/use_existing_fields';
@@ -232,6 +233,7 @@ describe('FormBasedDimensionEditor', () => {
fieldFormats: fieldFormatsServiceMock.createStartContract(),
unifiedSearch: unifiedSearchPluginMock.createStartContract(),
dataViews: dataViewPluginMocks.createStartContract(),
+ notifications: {} as NotificationsStart,
data: {
fieldFormats: {
getType: jest.fn().mockReturnValue({
diff --git a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.tsx b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.tsx
index c023d3045c331..dd3a66e26f036 100644
--- a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.tsx
+++ b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.tsx
@@ -6,7 +6,12 @@
*/
import React, { memo } from 'react';
-import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from '@kbn/core/public';
+import type {
+ IUiSettingsClient,
+ SavedObjectsClientContract,
+ HttpSetup,
+ NotificationsStart,
+} from '@kbn/core/public';
import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
@@ -38,6 +43,7 @@ export type FormBasedDimensionEditorProps =
dataViews: DataViewsPublicPluginStart;
uniqueLabel: string;
dateRange: DateRange;
+ notifications: NotificationsStart;
};
export const FormBasedDimensionEditorComponent = function FormBasedDimensionPanel(
diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts b/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts
index a65d29104360a..52d600a82d6ae 100644
--- a/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts
+++ b/x-pack/plugins/lens/public/datasources/form_based/form_based.test.ts
@@ -48,7 +48,7 @@ import {
} from './operations';
import { createMockedFullReference } from './operations/mocks';
import { cloneDeep } from 'lodash';
-import { DatatableColumn } from '@kbn/expressions-plugin/common';
+import { Datatable, DatatableColumn } from '@kbn/expressions-plugin/common';
import { createMockFramePublicAPI } from '../../mocks';
import { filterAndSortUserMessages } from '../../app_plugin/get_application_user_messages';
@@ -194,11 +194,17 @@ describe('IndexPattern Data Source', () => {
let FormBasedDatasource: Datasource;
beforeEach(() => {
+ const data = dataPluginMock.createStartContract();
+ data.query.timefilter.timefilter.getAbsoluteTime = jest.fn(() => ({
+ from: '',
+ to: '',
+ }));
+
FormBasedDatasource = getFormBasedDatasource({
unifiedSearch: unifiedSearchPluginMock.createStartContract(),
storage: {} as IStorageWrapper,
core: coreMock.createStart(),
- data: dataPluginMock.createStartContract(),
+ data,
dataViews: dataViewPluginMocks.createStartContract(),
fieldFormats: fieldFormatsServiceMock.createStartContract(),
charts: chartPluginMock.createSetupContract(),
@@ -3013,6 +3019,22 @@ describe('IndexPattern Data Source', () => {
});
describe('#getUserMessages', () => {
+ function createMockFrameDatasourceAPI({
+ activeData,
+ dataViews,
+ }: Partial> & {
+ dataViews?: Partial;
+ }): FrameDatasourceAPI {
+ return {
+ ...createMockFramePublicAPI({
+ activeData,
+ dataViews,
+ }),
+ query: { query: '', language: 'kuery' },
+ filters: [],
+ };
+ }
+
describe('error messages', () => {
it('should generate error messages for a single layer', () => {
(getErrorMessages as jest.Mock).mockClear();
@@ -3029,7 +3051,7 @@ describe('IndexPattern Data Source', () => {
};
expect(
FormBasedDatasource.getUserMessages(state, {
- frame: { dataViews: { indexPatterns } } as unknown as FrameDatasourceAPI,
+ frame: createMockFrameDatasourceAPI({ dataViews: { indexPatterns } }),
setState: () => {},
})
).toMatchInlineSnapshot(`
@@ -3081,7 +3103,7 @@ describe('IndexPattern Data Source', () => {
};
expect(
FormBasedDatasource.getUserMessages(state, {
- frame: { dataViews: { indexPatterns } } as unknown as FrameDatasourceAPI,
+ frame: createMockFrameDatasourceAPI({ dataViews: { indexPatterns } }),
setState: () => {},
})
).toMatchInlineSnapshot(`
@@ -3170,7 +3192,7 @@ describe('IndexPattern Data Source', () => {
(getErrorMessages as jest.Mock).mockReturnValueOnce([]);
const messages = FormBasedDatasource.getUserMessages(state, {
- frame: { dataViews: { indexPatterns } } as unknown as FrameDatasourceAPI,
+ frame: createMockFrameDatasourceAPI({ dataViews: { indexPatterns } }),
setState: () => {},
});
@@ -3208,7 +3230,7 @@ describe('IndexPattern Data Source', () => {
] as ReturnType);
const messages = FormBasedDatasource.getUserMessages(state, {
- frame: { dataViews: { indexPatterns } } as unknown as FrameDatasourceAPI,
+ frame: createMockFrameDatasourceAPI({ dataViews: { indexPatterns } }),
setState: () => {},
});
@@ -3238,7 +3260,7 @@ describe('IndexPattern Data Source', () => {
describe('warning messages', () => {
let state: FormBasedPrivateState;
- let framePublicAPI: FramePublicAPI;
+ let framePublicAPI: FrameDatasourceAPI;
beforeEach(() => {
(getErrorMessages as jest.Mock).mockReturnValueOnce([]);
@@ -3320,7 +3342,7 @@ describe('IndexPattern Data Source', () => {
currentIndexPatternId: '1',
};
- framePublicAPI = {
+ framePublicAPI = createMockFrameDatasourceAPI({
activeData: {
first: {
type: 'datatable',
@@ -3355,14 +3377,9 @@ describe('IndexPattern Data Source', () => {
},
},
dataViews: {
- ...createMockFramePublicAPI().dataViews,
indexPatterns: expectedIndexPatterns,
- indexPatternRefs: Object.values(expectedIndexPatterns).map(({ id, title }) => ({
- id,
- title,
- })),
},
- } as unknown as FramePublicAPI;
+ });
});
const extractTranslationIdsFromWarnings = (warnings: UserMessage[]) => {
@@ -3378,7 +3395,7 @@ describe('IndexPattern Data Source', () => {
it('should return mismatched time shifts', () => {
const warnings = FormBasedDatasource.getUserMessages!(state, {
- frame: framePublicAPI as FrameDatasourceAPI,
+ frame: framePublicAPI,
setState: () => {},
});
@@ -3394,7 +3411,7 @@ describe('IndexPattern Data Source', () => {
framePublicAPI.activeData!.first.columns[1].meta.sourceParams!.hasPrecisionError = true;
const warnings = FormBasedDatasource.getUserMessages!(state, {
- frame: framePublicAPI as FrameDatasourceAPI,
+ frame: framePublicAPI,
setState: () => {},
});
@@ -3407,6 +3424,133 @@ describe('IndexPattern Data Source', () => {
`);
});
});
+
+ describe('info messages', () => {
+ function createLayer(
+ index: number = 0,
+ sampling?: number
+ ): FormBasedPrivateState['layers'][number] {
+ return {
+ sampling,
+ indexPatternId: '1',
+ columnOrder: [`col-${index}-1`, `col-${index}-2`],
+ columns: {
+ [`col-${index}-1`]: {
+ operationType: 'date_histogram',
+ params: {
+ interval: '12h',
+ },
+ label: '',
+ dataType: 'date',
+ isBucketed: true,
+ sourceField: 'timestamp',
+ } as DateHistogramIndexPatternColumn,
+ [`col-${index}-2`]: {
+ operationType: 'count',
+ label: '',
+ dataType: 'number',
+ isBucketed: false,
+ sourceField: 'records',
+ },
+ },
+ };
+ }
+
+ function createDatatableForLayer(index: number): Datatable {
+ return {
+ type: 'datatable' as const,
+ rows: [],
+ columns: [
+ {
+ id: `col-${index}-1`,
+ name: `col-${index}-1`,
+ meta: {
+ type: 'date',
+ source: 'esaggs',
+ sourceParams: {
+ type: 'date_histogram',
+ params: {
+ used_interval: '12h',
+ },
+ },
+ },
+ },
+ {
+ id: `col-${index}-2`,
+ name: `col-${index}-2`,
+ meta: {
+ type: 'number',
+ },
+ },
+ ],
+ };
+ }
+
+ beforeEach(() => {
+ (getErrorMessages as jest.Mock).mockReturnValueOnce([]);
+ });
+
+ it.each`
+ sampling | infoMessages
+ ${undefined} | ${0}
+ ${1} | ${0}
+ ${0.1} | ${1}
+ `(
+ 'should return $infoMessages info messages when sampling is set to $sampling',
+ ({ sampling, infoMessages }) => {
+ const messages = FormBasedDatasource.getUserMessages!(
+ {
+ layers: {
+ first: createLayer(0, sampling),
+ },
+ currentIndexPatternId: '1',
+ },
+ {
+ frame: createMockFrameDatasourceAPI({
+ activeData: {
+ first: createDatatableForLayer(0),
+ },
+ dataViews: {
+ indexPatterns: expectedIndexPatterns,
+ },
+ }),
+ setState: () => {},
+ visualizationInfo: { layers: [] },
+ }
+ );
+ expect(messages.filter(({ severity }) => severity === 'info')).toHaveLength(infoMessages);
+ }
+ );
+
+ it('should return a single info message for multiple layers with sampling < 100%', () => {
+ const state: FormBasedPrivateState = {
+ layers: {
+ first: createLayer(0, 0.1),
+ second: createLayer(1, 0.001),
+ },
+ currentIndexPatternId: '1',
+ };
+ const messages = FormBasedDatasource.getUserMessages!(state, {
+ frame: createMockFrameDatasourceAPI({
+ activeData: {
+ first: createDatatableForLayer(0),
+ second: createDatatableForLayer(1),
+ },
+ dataViews: {
+ indexPatterns: expectedIndexPatterns,
+ },
+ }),
+ setState: () => {},
+ visualizationInfo: { layers: [] },
+ });
+ const infoMessages = messages.filter(({ severity }) => severity === 'info');
+ expect(infoMessages).toHaveLength(1);
+ const [info] = infoMessages;
+ if (isFragment(info.longMessage)) {
+ expect(info.longMessage.props.layers).toHaveLength(2);
+ }
+ });
+ });
});
describe('#updateStateOnCloseDimension', () => {
diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx b/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx
index 103175bf329f1..4152212e51fea 100644
--- a/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx
+++ b/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx
@@ -69,6 +69,7 @@ import {
getVisualDefaultsForLayer,
isColumnInvalid,
cloneLayer,
+ getNotifiableFeatures,
} from './utils';
import { isDraggedDataViewField } from '../../utils';
import { hasField, normalizeOperationDataType } from './pure_utils';
@@ -585,6 +586,7 @@ export function getFormBasedDatasource({
unifiedSearch={unifiedSearch}
dataViews={dataViews}
uniqueLabel={columnLabelMap[props.columnId]}
+ notifications={core.notifications}
{...props}
/>
@@ -818,7 +820,7 @@ export function getFormBasedDatasource({
getDatasourceSuggestionsForVisualizeField,
getDatasourceSuggestionsForVisualizeCharts,
- getUserMessages(state, { frame: frameDatasourceAPI, setState }) {
+ getUserMessages(state, { frame: frameDatasourceAPI, setState, visualizationInfo }) {
if (!state) {
return [];
}
@@ -872,7 +874,9 @@ export function getFormBasedDatasource({
),
];
- return [...layerErrorMessages, ...dimensionErrorMessages, ...warningMessages];
+ const infoMessages = getNotifiableFeatures(state, frameDatasourceAPI, visualizationInfo);
+
+ return layerErrorMessages.concat(dimensionErrorMessages, warningMessages, infoMessages);
},
getSearchWarningMessages: (state, warning, request, response) => {
diff --git a/x-pack/plugins/lens/public/datasources/form_based/info_badges.tsx b/x-pack/plugins/lens/public/datasources/form_based/info_badges.tsx
new file mode 100644
index 0000000000000..c17339945161b
--- /dev/null
+++ b/x-pack/plugins/lens/public/datasources/form_based/info_badges.tsx
@@ -0,0 +1,61 @@
+/*
+ * 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 { EuiFlexGroup, EuiFlexItem, EuiText, useEuiTheme } from '@elastic/eui';
+import { css } from '@emotion/react';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { FormBasedLayer } from '../..';
+import { FramePublicAPI, VisualizationInfo } from '../../types';
+import { getSamplingValue } from './utils';
+
+export function ReducedSamplingSectionEntries({
+ layers,
+ visualizationInfo,
+ dataViews,
+}: {
+ layers: Array<[string, FormBasedLayer]>;
+ visualizationInfo: VisualizationInfo;
+ dataViews: FramePublicAPI['dataViews'];
+}) {
+ const { euiTheme } = useEuiTheme();
+ return (
+ <>
+ {layers.map(([id, layer], layerIndex) => {
+ const dataView = dataViews.indexPatterns[layer.indexPatternId];
+ const layerTitle =
+ visualizationInfo.layers.find(({ layerId }) => layerId === id)?.label ||
+ i18n.translate('xpack.lens.indexPattern.samplingPerLayer.fallbackLayerName', {
+ defaultMessage: 'Data layer',
+ });
+ return (
+
+
+
+ {layerTitle}
+
+
+ {`${Number(getSamplingValue(layer)) * 100}%`}
+
+
+
+ );
+ })}
+ >
+ );
+}
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 566d381ba9a4c..38da5475e22e1 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
@@ -14,6 +14,8 @@ import {
EuiText,
EuiLink,
EuiSpacer,
+ useEuiTheme,
+ EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
@@ -21,107 +23,187 @@ import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import type { DatasourceLayerSettingsProps } from '../../types';
import type { FormBasedPrivateState } from './types';
+import { isSamplingValueEnabled } from './utils';
+import { TooltipWrapper } from '../../shared_components';
-const samplingValue = [0.0001, 0.001, 0.01, 0.1, 1];
-
-export function LayerSettingsPanel({
- state,
- setState,
- layerId,
-}: DatasourceLayerSettingsProps) {
- const samplingIndex = samplingValue.findIndex((v) => v === state.layers[layerId].sampling);
- const currentSamplingIndex = samplingIndex > -1 ? samplingIndex : samplingValue.length - 1;
+const samplingValues = [0.00001, 0.0001, 0.001, 0.01, 0.1, 1];
+interface SamplingSliderProps {
+ values: number[];
+ currentValue: number | undefined;
+ disabled: boolean;
+ disabledReason: string;
+ onChange: (value: number) => void;
+ 'data-test-subj'?: string;
+}
+/**
+ * Stub for a shared component
+ */
+function SamplingSlider({
+ values,
+ currentValue,
+ disabled,
+ disabledReason,
+ onChange,
+ 'data-test-subj': dataTestSubj,
+}: SamplingSliderProps) {
+ const { euiTheme } = useEuiTheme();
+ const samplingIndex = values.findIndex((v) => v === currentValue);
+ const currentSamplingIndex = samplingIndex > -1 ? samplingIndex : values.length - 1;
return (
-
-
-
-
-
-
- ),
- }}
- />
-
- >
- }
- label={
- <>
- {i18n.translate('xpack.lens.xyChart.randomSampling.label', {
- defaultMessage: 'Random sampling',
- })}{' '}
-
- >
- }
+
-
+
{
- setState({
- ...state,
- layers: {
- ...state.layers,
- [layerId]: {
- ...state.layers[layerId],
- sampling: samplingValue[Number(e.currentTarget.value)],
- },
- },
- });
+ onChange(values[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 }))}
+ max={values.length - 1}
+ ticks={values.map((v, i) => ({
+ label: `${v * 100}%`.slice(Number.isInteger(v * 100) ? 0 : 1),
+ value: i,
+ }))}
/>
-
+
-
+
+ );
+}
+
+export function LayerSettingsPanel({
+ state,
+ setState,
+ layerId,
+}: DatasourceLayerSettingsProps) {
+ const { euiTheme } = useEuiTheme();
+ const isSamplingValueDisabled = !isSamplingValueEnabled(state.layers[layerId]);
+ const currentValue = isSamplingValueDisabled
+ ? samplingValues[samplingValues.length - 1]
+ : state.layers[layerId].sampling;
+ return (
+
+
+
+ {i18n.translate('xpack.lens.indexPattern.layerSettings.headingData', {
+ defaultMessage: 'Data',
+ })}
+
+
+
+
+
+
+
+
+ ),
+ }}
+ />
+
+ >
+ }
+ label={
+ <>
+ {i18n.translate('xpack.lens.indexPattern.randomSampling.label', {
+ defaultMessage: 'Sampling',
+ })}{' '}
+
+
+
+ >
+ }
+ >
+ {
+ setState({
+ ...state,
+ layers: {
+ ...state.layers,
+ [layerId]: {
+ ...state.layers[layerId],
+ sampling: newSamplingValue,
+ },
+ },
+ });
+ }}
+ />
+
+
);
}
diff --git a/x-pack/plugins/lens/public/datasources/form_based/layerpanel.tsx b/x-pack/plugins/lens/public/datasources/form_based/layerpanel.tsx
index 349139cd41b27..1de4d0844245f 100644
--- a/x-pack/plugins/lens/public/datasources/form_based/layerpanel.tsx
+++ b/x-pack/plugins/lens/public/datasources/form_based/layerpanel.tsx
@@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n';
import { DatasourceLayerPanelProps } from '../../types';
import { FormBasedPrivateState } from './types';
import { ChangeIndexPattern } from '../../shared_components/dataview_picker/dataview_picker';
+import { getSamplingValue } from './utils';
export interface FormBasedLayerPanelProps extends DatasourceLayerPanelProps {
state: FormBasedPrivateState;
@@ -36,6 +37,7 @@ export function LayerPanel({
isAdhoc: !isPersisted,
};
});
+
return (
meta.dataType === 'number' && !meta.isBucketed,
},
],
+ // return false for quick function as the built-in reference will use max
+ // in formula this check won't be used and the check is performed on the formula AST tree traversal independently
+ getUnsupportedSettings: () => ({
+ sampling: false,
+ }),
getPossibleOperation: (indexPattern) => {
if (hasDateField(indexPattern)) {
return {
diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/index.ts b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/index.ts
index 5e73b53bcf8d9..f050604cfc0d0 100644
--- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/index.ts
+++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/index.ts
@@ -229,6 +229,8 @@ export interface HelpProps {
export type TimeScalingMode = 'disabled' | 'mandatory' | 'optional';
+export type LayerSettingsFeatures = Record<'sampling', boolean>;
+
export interface AdvancedOption {
dataTestSubj: string;
inlineElement: React.ReactElement | null;
@@ -434,6 +436,10 @@ interface BaseOperationDefinitionProps<
* Boolean flag whether the data section extra element passed in from the visualization is handled by the param editor of the operation or whether the datasource general logic should be used.
*/
handleDataSectionExtra?: boolean;
+ /**
+ * When present returns a dictionary of unsupported layer settings
+ */
+ getUnsupportedSettings?: () => LayerSettingsFeatures;
}
interface BaseBuildColumnArgs {
diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/metrics.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/metrics.tsx
index b672d50232172..deb0c19dc4837 100644
--- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/metrics.tsx
+++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/metrics.tsx
@@ -10,7 +10,7 @@ import React from 'react';
import { EuiSwitch, EuiText } from '@elastic/eui';
import { euiThemeVars } from '@kbn/ui-theme';
import { buildExpressionFunction } from '@kbn/expressions-plugin/public';
-import { OperationDefinition, ParamEditorProps } from '.';
+import { LayerSettingsFeatures, OperationDefinition, ParamEditorProps } from '.';
import {
getFormatFromPreviousColumn,
getInvalidFieldMessage,
@@ -64,6 +64,7 @@ function buildMetricOperation>({
aggConfigParams,
documentationDescription,
quickFunctionDocumentation,
+ unsupportedSettings,
}: {
type: T['operationType'];
displayName: string;
@@ -76,6 +77,7 @@ function buildMetricOperation>({
aggConfigParams?: Record;
documentationDescription?: string;
quickFunctionDocumentation?: string;
+ unsupportedSettings?: LayerSettingsFeatures;
}) {
const labelLookup = (name: string, column?: BaseIndexPatternColumn) => {
const label = ofName(name);
@@ -98,6 +100,7 @@ function buildMetricOperation>({
description,
input: 'field',
timeScalingMode: optionalTimeScaling ? 'optional' : undefined,
+ getUnsupportedSettings: () => unsupportedSettings,
getPossibleOperationForField: ({
aggregationRestrictions,
aggregatable,
@@ -281,6 +284,7 @@ export const minOperation = buildMetricOperation({
}
),
supportsDate: true,
+ unsupportedSettings: { sampling: false },
});
export const maxOperation = buildMetricOperation({
@@ -304,6 +308,7 @@ export const maxOperation = buildMetricOperation({
}
),
supportsDate: true,
+ unsupportedSettings: { sampling: false },
});
export const averageOperation = buildMetricOperation({
diff --git a/x-pack/plugins/lens/public/datasources/form_based/to_expression.ts b/x-pack/plugins/lens/public/datasources/form_based/to_expression.ts
index 9ec654c47868f..8781e63de1cbe 100644
--- a/x-pack/plugins/lens/public/datasources/form_based/to_expression.ts
+++ b/x-pack/plugins/lens/public/datasources/form_based/to_expression.ts
@@ -31,6 +31,7 @@ import { isColumnFormatted, isColumnOfType } from './operations/definitions/help
import type { IndexPattern, IndexPatternMap } from '../../types';
import { dedupeAggs } from './dedupe_aggs';
import { resolveTimeShift } from './time_shift_utils';
+import { getSamplingValue } from './utils';
export type OriginalColumn = { id: string } & GenericIndexPatternColumn;
@@ -415,7 +416,7 @@ function getExpressionForLayer(
metricsAtAllLevels: false,
partialRows: false,
timeFields: allDateHistogramFields,
- probability: layer.sampling || 1,
+ probability: getSamplingValue(layer),
samplerSeed: seedrandom(searchSessionId).int32(),
}).toAst(),
{
diff --git a/x-pack/plugins/lens/public/datasources/form_based/utils.tsx b/x-pack/plugins/lens/public/datasources/form_based/utils.tsx
index 5837d77c2a92d..d14cf42e3c31b 100644
--- a/x-pack/plugins/lens/public/datasources/form_based/utils.tsx
+++ b/x-pack/plugins/lens/public/datasources/form_based/utils.tsx
@@ -26,7 +26,13 @@ import {
import { estypes } from '@elastic/elasticsearch';
import type { DateRange } from '../../../common/types';
-import type { FramePublicAPI, IndexPattern, StateSetter, UserMessage } from '../../types';
+import type {
+ FramePublicAPI,
+ IndexPattern,
+ StateSetter,
+ UserMessage,
+ VisualizationInfo,
+} from '../../types';
import { renewIDs } from '../../utils';
import type { FormBasedLayer, FormBasedPersistedState, FormBasedPrivateState } from './types';
import type { ReferenceBasedIndexPatternColumn } from './operations/definitions/column_types';
@@ -41,6 +47,8 @@ import {
RangeIndexPatternColumn,
FormulaIndexPatternColumn,
DateHistogramIndexPatternColumn,
+ MaxIndexPatternColumn,
+ MinIndexPatternColumn,
} from './operations';
import { getInvalidFieldMessage, isColumnOfType } from './operations/definitions/helpers';
@@ -51,6 +59,43 @@ import { supportsRarityRanking } from './operations/definitions/terms';
import { DEFAULT_MAX_DOC_COUNT } from './operations/definitions/terms/constants';
import { getOriginalId } from '../../../common/expressions/datatable/transpose_helpers';
import { isQueryValid } from '../../shared_components';
+import { ReducedSamplingSectionEntries } from './info_badges';
+
+function isMinOrMaxColumn(
+ column?: GenericIndexPatternColumn
+): column is MaxIndexPatternColumn | MinIndexPatternColumn {
+ if (!column) {
+ return false;
+ }
+ return (
+ isColumnOfType('max', column) ||
+ isColumnOfType('min', column)
+ );
+}
+
+function isReferenceColumn(
+ column: GenericIndexPatternColumn
+): column is ReferenceBasedIndexPatternColumn {
+ return 'references' in column;
+}
+
+export function isSamplingValueEnabled(layer: FormBasedLayer) {
+ // Do not use columnOrder here as it needs to check also inside formulas columns
+ return !Object.values(layer.columns).some(
+ (column) =>
+ isMinOrMaxColumn(column) ||
+ (isReferenceColumn(column) && isMinOrMaxColumn(layer.columns[column.references[0]]))
+ );
+}
+
+/**
+ * Centralized logic to get the actual random sampling value for a layer
+ * @param layer
+ * @returns
+ */
+export function getSamplingValue(layer: FormBasedLayer) {
+ return isSamplingValueEnabled(layer) ? layer.sampling ?? 1 : 1;
+}
export function isColumnInvalid(
layer: FormBasedLayer,
@@ -449,6 +494,40 @@ export function getVisualDefaultsForLayer(layer: FormBasedLayer) {
);
}
+export function getNotifiableFeatures(
+ state: FormBasedPrivateState,
+ frame: FramePublicAPI,
+ visualizationInfo?: VisualizationInfo
+): UserMessage[] {
+ if (!visualizationInfo) {
+ return [];
+ }
+ const layersWithCustomSamplingValues = Object.entries(state.layers).filter(
+ ([, layer]) => getSamplingValue(layer) !== 1
+ );
+ if (!layersWithCustomSamplingValues.length) {
+ return [];
+ }
+ return [
+ {
+ uniqueId: 'random_sampling_info',
+ severity: 'info',
+ fixableInEditor: false,
+ shortMessage: i18n.translate('xpack.lens.indexPattern.samplingPerLayer', {
+ defaultMessage: 'Layers with reduced sampling',
+ }),
+ longMessage: (
+
+ ),
+ displayLocations: [{ id: 'embeddableBadge' }],
+ },
+ ];
+}
+
/**
* Some utilities to extract queries/filters from specific column types
*/
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
index 3046df112dc4f..17df0a527c145 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
@@ -51,9 +51,6 @@ import { onDropForVisualization, shouldRemoveSource } from './buttons/drop_targe
import { getSharedActions } from './layer_actions/layer_actions';
import { FlyoutContainer } from './flyout_container';
-// hide the random sampling settings from the UI
-const DISPLAY_RANDOM_SAMPLING_SETTINGS = false;
-
const initialActiveDimensionState = {
isNew: false,
};
@@ -350,7 +347,7 @@ export function LayerPanel(
frame: props.framePublicAPI,
}) &&
activeVisualization.renderLayerSettings) ||
- (layerDatasource?.renderLayerSettings && DISPLAY_RANDOM_SAMPLING_SETTINGS)
+ layerDatasource?.renderLayerSettings
),
openLayerSettings: () => setPanelSettingsOpen(true),
onCloneLayer,
@@ -684,8 +681,8 @@ export function LayerPanel(
}}
>
-
- {layerDatasource?.renderLayerSettings && DISPLAY_RANDOM_SAMPLING_SETTINGS && (
+
+ {layerDatasource?.renderLayerSettings && (
<>
{
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
@@ -87,7 +85,7 @@ export const MessageList = ({
>
{errorCount > 0 && (
<>
-
+
{errorCount}
>
)}
@@ -95,7 +93,6 @@ export const MessageList = ({
<>
;
@@ -347,13 +347,15 @@ const EmbeddableMessagesPopover = ({ messages }: { messages: UserMessage[] }) =>
const { euiTheme } = useEuiTheme();
const xsFontSize = useEuiFontSize('xs').fontSize;
+ if (!messages.length) {
+ return null;
+ }
+
return (
* {
@@ -630,6 +632,9 @@ export class Embeddable
...(this.activeDatasource?.getUserMessages(this.activeDatasourceState, {
setState: () => {},
frame: frameDatasourceAPI,
+ visualizationInfo: this.activeVisualization?.getVisualizationInfo?.(
+ this.activeVisualizationState
+ ),
}) ?? []),
...(this.activeVisualization?.getUserMessages?.(this.activeVisualizationState, {
frame: frameDatasourceAPI,
@@ -981,11 +986,16 @@ export class Embeddable
*/
private renderBadgeMessages = () => {
const messages = this.getUserMessages('embeddableBadge');
+ const [warningOrErrorMessages, infoMessages] = partition(
+ messages,
+ ({ severity }) => severity !== 'info'
+ );
- if (messages.length && this.badgeDomNode) {
+ if (this.badgeDomNode) {
render(
-
+
+
,
this.badgeDomNode
);
diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_info_badges.scss b/x-pack/plugins/lens/public/embeddable/embeddable_info_badges.scss
new file mode 100644
index 0000000000000..d7f5840e4ff17
--- /dev/null
+++ b/x-pack/plugins/lens/public/embeddable/embeddable_info_badges.scss
@@ -0,0 +1,5 @@
+
+.lnsEmbeddablePanelFeatureList {
+ @include euiYScroll;
+ max-height: $euiSize * 20;
+}
diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_info_badges.tsx b/x-pack/plugins/lens/public/embeddable/embeddable_info_badges.tsx
new file mode 100644
index 0000000000000..d47892b0b3aa0
--- /dev/null
+++ b/x-pack/plugins/lens/public/embeddable/embeddable_info_badges.tsx
@@ -0,0 +1,90 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import {
+ EuiPopover,
+ EuiToolTip,
+ EuiHorizontalRule,
+ EuiTitle,
+ useEuiTheme,
+ EuiButtonEmpty,
+ useEuiFontSize,
+} from '@elastic/eui';
+import { css } from '@emotion/react';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { useState } from 'react';
+import type { UserMessage } from '../types';
+import './embeddable_info_badges.scss';
+
+export const EmbeddableFeatureBadge = ({ messages }: { messages: UserMessage[] }) => {
+ const { euiTheme } = useEuiTheme();
+ const xsFontSize = useEuiFontSize('xs').fontSize;
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+ const onButtonClick = () => setIsPopoverOpen((isOpen) => !isOpen);
+ const closePopover = () => setIsPopoverOpen(false);
+ if (!messages.length) {
+ return null;
+ }
+ const iconTitle = i18n.translate('xpack.lens.embeddable.featureBadge.iconDescription', {
+ defaultMessage: `{count} visualization {count, plural, one {modifier} other {modifiers}}`,
+ values: {
+ count: messages.length,
+ },
+ });
+ return (
+
+
+ {messages.length}
+
+
+ }
+ isOpen={isPopoverOpen}
+ closePopover={closePopover}
+ >
+
+ {messages.map(({ shortMessage, longMessage }, index) => (
+
0 ? 0 : euiTheme.size.base} ${euiTheme.size.base}
+ ${index > 0 ? euiTheme.size.s : 0};
+ `}
+ >
+ {index ? : null}
+
+ {shortMessage}
+
+
+
+ ))}
+
+
+ );
+};
diff --git a/x-pack/plugins/lens/public/mocks/index.ts b/x-pack/plugins/lens/public/mocks/index.ts
index 4cfdfbad661af..5cd62b5427cb4 100644
--- a/x-pack/plugins/lens/public/mocks/index.ts
+++ b/x-pack/plugins/lens/public/mocks/index.ts
@@ -35,7 +35,9 @@ export const createMockFramePublicAPI = ({
dateRange,
dataViews,
activeData,
-}: Partial = {}): FrameMock => ({
+}: Partial> & {
+ dataViews?: Partial;
+} = {}): FrameMock => ({
datasourceLayers: datasourceLayers ?? {},
dateRange: dateRange ?? {
fromDate: '2022-03-17T08:25:00.000Z',
diff --git a/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx b/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx
index 35c0215a35c53..6467cbcb58494 100644
--- a/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx
+++ b/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx
@@ -7,17 +7,110 @@
import { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
-import { EuiPopover, EuiPopoverTitle, EuiSelectableProps } from '@elastic/eui';
-import { ToolbarButton, ToolbarButtonProps } from '@kbn/kibana-react-plugin/public';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiPopover,
+ EuiPopoverTitle,
+ EuiSelectableProps,
+ EuiTextColor,
+ EuiToolTip,
+ useEuiTheme,
+} from '@elastic/eui';
import { DataViewsList } from '@kbn/unified-search-plugin/public';
-import { IndexPatternRef } from '../../types';
+import { css } from '@emotion/react';
+import { type IndexPatternRef } from '../../types';
+import { type ToolbarButtonProps, ToolbarButton } from './toolbar_button';
+import { RandomSamplingIcon } from './sampling_icon';
export type ChangeIndexPatternTriggerProps = ToolbarButtonProps & {
label: string;
title?: string;
isDisabled?: boolean;
+ samplingValue?: number;
};
+function TriggerButton({
+ label,
+ title,
+ togglePopover,
+ isMissingCurrent,
+ samplingValue,
+ ...rest
+}: ChangeIndexPatternTriggerProps &
+ ToolbarButtonProps & {
+ togglePopover: () => void;
+ isMissingCurrent?: boolean;
+ }) {
+ const { euiTheme } = useEuiTheme();
+ // be careful to only add color with a value, otherwise it will fallbacks to "primary"
+ const colorProp = isMissingCurrent
+ ? {
+ color: 'danger' as const,
+ }
+ : {};
+ const content =
+ samplingValue != null && samplingValue !== 1 ? (
+
+
+ {label}
+
+
+
+
+
+
+
+
+
+ {samplingValue * 100}%
+
+
+
+
+
+
+ ) : (
+ label
+ );
+ return (
+ togglePopover()}
+ fullWidth
+ {...colorProp}
+ {...rest}
+ textProps={{ style: { width: '100%' } }}
+ >
+ {content}
+
+ );
+}
+
export function ChangeIndexPattern({
indexPatternRefs,
isMissingCurrent,
@@ -35,33 +128,17 @@ export function ChangeIndexPattern({
}) {
const [isPopoverOpen, setPopoverIsOpen] = useState(false);
- // be careful to only add color with a value, otherwise it will fallbacks to "primary"
- const colorProp = isMissingCurrent
- ? {
- color: 'danger' as const,
- }
- : {};
-
- const createTrigger = function () {
- const { label, title, ...rest } = trigger;
- return (
- setPopoverIsOpen(!isPopoverOpen)}
- fullWidth
- {...colorProp}
- {...rest}
- >
- {label}
-
- );
- };
-
return (
<>
setPopoverIsOpen(!isPopoverOpen)}
+ />
+ }
panelProps={{
['data-test-subj']: 'lnsChangeIndexPatternPopover',
}}
diff --git a/x-pack/plugins/lens/public/shared_components/dataview_picker/sampling_icon.tsx b/x-pack/plugins/lens/public/shared_components/dataview_picker/sampling_icon.tsx
new file mode 100644
index 0000000000000..8241c1d86a5fc
--- /dev/null
+++ b/x-pack/plugins/lens/public/shared_components/dataview_picker/sampling_icon.tsx
@@ -0,0 +1,56 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React from 'react';
+
+interface CustomProps {
+ title?: string;
+ titleId?: string;
+}
+
+export function RandomSamplingIcon({
+ title,
+ titleId,
+ ...props
+}: React.SVGProps & CustomProps) {
+ return (
+
+ {title ? {title} : null}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/x-pack/plugins/lens/public/shared_components/dataview_picker/toolbar_button.scss b/x-pack/plugins/lens/public/shared_components/dataview_picker/toolbar_button.scss
new file mode 100644
index 0000000000000..cbf6d85349446
--- /dev/null
+++ b/x-pack/plugins/lens/public/shared_components/dataview_picker/toolbar_button.scss
@@ -0,0 +1,61 @@
+.kbnToolbarButton {
+ line-height: $euiButtonHeight; // Keeps alignment of text and chart icon
+
+ // Override background color for non-disabled buttons
+ &:not(:disabled) {
+ background-color: $euiColorEmptyShade;
+ }
+
+ // todo: once issue https://github.com/elastic/eui/issues/4730 is merged, this code might be safe to remove
+ // Some toolbar buttons are just icons, but EuiButton comes with margin and min-width that need to be removed
+ min-width: 0;
+ border-width: $euiBorderWidthThin;
+ border-style: solid;
+ border-color: $euiBorderColor; // Lighten the border color for all states
+
+ .kbnToolbarButton__text > svg {
+ margin-top: -1px; // Just some weird alignment issue when icon is the child not the `iconType`
+ }
+
+ .kbnToolbarButton__text:empty {
+ margin: 0;
+ }
+
+ // Toolbar buttons don't look good with centered text when fullWidth
+ &[class*='fullWidth'] {
+ text-align: left;
+
+ .kbnToolbarButton__content {
+ justify-content: space-between;
+ }
+ }
+}
+
+.kbnToolbarButton--groupLeft {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.kbnToolbarButton--groupCenter {
+ border-radius: 0;
+ border-left: none;
+}
+
+.kbnToolbarButton--groupRight {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ border-left: none;
+}
+
+.kbnToolbarButton--bold {
+ font-weight: $euiFontWeightBold;
+}
+
+.kbnToolbarButton--normal {
+ font-weight: $euiFontWeightRegular;
+}
+
+.kbnToolbarButton--s {
+ box-shadow: none !important; // sass-lint:disable-line no-important
+ font-size: $euiFontSizeS;
+}
diff --git a/x-pack/plugins/lens/public/shared_components/dataview_picker/toolbar_button.tsx b/x-pack/plugins/lens/public/shared_components/dataview_picker/toolbar_button.tsx
new file mode 100644
index 0000000000000..55b5d1ce060fe
--- /dev/null
+++ b/x-pack/plugins/lens/public/shared_components/dataview_picker/toolbar_button.tsx
@@ -0,0 +1,87 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import './toolbar_button.scss';
+import React from 'react';
+import classNames from 'classnames';
+import { EuiButton, PropsOf, EuiButtonProps } from '@elastic/eui';
+
+const groupPositionToClassMap = {
+ none: null,
+ left: 'kbnToolbarButton--groupLeft',
+ center: 'kbnToolbarButton--groupCenter',
+ right: 'kbnToolbarButton--groupRight',
+};
+
+type ButtonPositions = keyof typeof groupPositionToClassMap;
+export const POSITIONS = Object.keys(groupPositionToClassMap) as ButtonPositions[];
+
+type Weights = 'normal' | 'bold';
+export const WEIGHTS = ['normal', 'bold'] as Weights[];
+
+export const TOOLBAR_BUTTON_SIZES: Array = ['s', 'm'];
+
+export type ToolbarButtonProps = PropsOf & {
+ /**
+ * Determines prominence
+ */
+ fontWeight?: Weights;
+ /**
+ * Smaller buttons also remove extra shadow for less prominence
+ */
+ size?: EuiButtonProps['size'];
+ /**
+ * Determines if the button will have a down arrow or not
+ */
+ hasArrow?: boolean;
+ /**
+ * Adjusts the borders for groupings
+ */
+ groupPosition?: ButtonPositions;
+ dataTestSubj?: string;
+ textProps?: EuiButtonProps['textProps'];
+};
+
+export const ToolbarButton: React.FunctionComponent = ({
+ children,
+ className,
+ fontWeight = 'normal',
+ size = 'm',
+ hasArrow = true,
+ groupPosition = 'none',
+ dataTestSubj = '',
+ textProps,
+ ...rest
+}) => {
+ const classes = classNames(
+ 'kbnToolbarButton',
+ groupPositionToClassMap[groupPosition],
+ [`kbnToolbarButton--${fontWeight}`, `kbnToolbarButton--${size}`],
+ className
+ );
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts
index c7deaeac283e9..4b3c69e8ec825 100644
--- a/x-pack/plugins/lens/public/types.ts
+++ b/x-pack/plugins/lens/public/types.ts
@@ -282,7 +282,7 @@ export type UserMessagesDisplayLocationId = UserMessageDisplayLocation['id'];
export interface UserMessage {
uniqueId?: string;
- severity: 'error' | 'warning';
+ severity: 'error' | 'warning' | 'info';
shortMessage: string;
longMessage: React.ReactNode | string;
fixableInEditor: boolean;
@@ -475,6 +475,7 @@ export interface Datasource {
deps: {
frame: FrameDatasourceAPI;
setState: StateSetter;
+ visualizationInfo?: VisualizationInfo;
}
) => UserMessage[];
diff --git a/x-pack/plugins/lens/public/visualization_container.scss b/x-pack/plugins/lens/public/visualization_container.scss
index ea4a8bdbce994..cdadb22feb634 100644
--- a/x-pack/plugins/lens/public/visualization_container.scss
+++ b/x-pack/plugins/lens/public/visualization_container.scss
@@ -25,4 +25,10 @@
align-items: center;
justify-content: center;
overflow: auto;
+}
+
+// Make the visualization modifiers icon appear only on panel hover
+.embPanel__content:hover .lnsEmbeddablePanelFeatureList_button {
+ color: $euiTextColor;
+ transition: color $euiAnimSpeedSlow;
}
\ No newline at end of file
diff --git a/x-pack/plugins/lens/public/visualizations/xy/info_badges.tsx b/x-pack/plugins/lens/public/visualizations/xy/info_badges.tsx
new file mode 100644
index 0000000000000..d3c0ac1653ad5
--- /dev/null
+++ b/x-pack/plugins/lens/public/visualizations/xy/info_badges.tsx
@@ -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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiFlexGroup, EuiFlexItem, EuiText, useEuiTheme } from '@elastic/eui';
+import { css } from '@emotion/react';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { FramePublicAPI, VisualizationInfo } from '../../types';
+import { XYAnnotationLayerConfig } from './types';
+
+export function IgnoredGlobalFiltersEntries({
+ layers,
+ visualizationInfo,
+ dataViews,
+}: {
+ layers: XYAnnotationLayerConfig[];
+ visualizationInfo: VisualizationInfo;
+ dataViews: FramePublicAPI['dataViews'];
+}) {
+ const { euiTheme } = useEuiTheme();
+ return (
+ <>
+ {layers.map((layer, layerIndex) => {
+ const dataView = dataViews.indexPatterns[layer.indexPatternId];
+ const layerTitle =
+ visualizationInfo.layers.find(({ layerId, label }) => layerId === layer.layerId)?.label ||
+ i18n.translate('xpack.lens.xyChart.layerAnnotationsLabel', {
+ defaultMessage: 'Annotations',
+ });
+ return (
+
+
+
+ {layerTitle}
+
+
+
+ );
+ })}
+ >
+ );
+}
diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts
index 9e7f5fcdb2bc2..159014b043aec 100644
--- a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts
+++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts
@@ -40,6 +40,7 @@ import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks
import { KEEP_GLOBAL_FILTERS_ACTION_ID } from './annotations/actions';
import { layerTypes, Visualization } from '../..';
+const DATE_HISTORGRAM_COLUMN_ID = 'date_histogram_column';
const exampleAnnotation: EventAnnotationConfig = {
id: 'an1',
type: 'manual',
@@ -2623,8 +2624,6 @@ describe('xy_visualization', () => {
});
describe('Annotation layers', () => {
- const DATE_HISTORGRAM_COLUMN_ID = 'date_histogram_column';
-
function createStateWithAnnotationProps(annotation: Partial) {
return {
layers: [
@@ -2693,7 +2692,7 @@ describe('xy_visualization', () => {
layerType: layerTypes.ANNOTATIONS,
indexPatternId: 'indexPattern1',
annotations: [exampleAnnotation],
- ignoreGlobalFilters: true,
+ ignoreGlobalFilters: false,
},
],
};
@@ -2879,6 +2878,57 @@ describe('xy_visualization', () => {
`);
});
});
+
+ describe('info', () => {
+ function getFrameMock() {
+ const datasourceMock = createMockDatasource('testDatasource');
+ datasourceMock.publicAPIMock.getOperationForColumnId.mockImplementation((id) =>
+ id === DATE_HISTORGRAM_COLUMN_ID
+ ? ({
+ label: DATE_HISTORGRAM_COLUMN_ID,
+ dataType: 'date',
+ scale: 'interval',
+ } as OperationDescriptor)
+ : ({
+ dataType: 'number',
+ label: 'MyOperation',
+ } as OperationDescriptor)
+ );
+
+ return createMockFramePublicAPI({
+ datasourceLayers: { first: datasourceMock.publicAPIMock },
+ dataViews: createMockDataViewsState({
+ indexPatterns: { first: createMockedIndexPattern() },
+ }),
+ });
+ }
+
+ it('should return an info message if annotation layer is ignoring the global filters', () => {
+ const initialState = exampleState();
+ const state: State = {
+ ...initialState,
+ layers: [
+ ...initialState.layers,
+ {
+ layerId: 'annotation',
+ layerType: layerTypes.ANNOTATIONS,
+ annotations: [exampleAnnotation2],
+ ignoreGlobalFilters: true,
+ indexPatternId: 'myIndexPattern',
+ },
+ ],
+ };
+ expect(xyVisualization.getUserMessages!(state, { frame: getFrameMock() })).toContainEqual(
+ expect.objectContaining({
+ displayLocations: [{ id: 'embeddableBadge' }],
+ fixableInEditor: false,
+ severity: 'info',
+ shortMessage: 'Layers ignoring global filters',
+ uniqueId: 'ignoring-global-filters-layers',
+ })
+ );
+ });
+ });
});
describe('#getUniqueLabels', () => {
diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx
index 44c19e5cd4467..274b82e5a1cf4 100644
--- a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx
+++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx
@@ -108,6 +108,7 @@ import {
IGNORE_GLOBAL_FILTERS_ACTION_ID,
KEEP_GLOBAL_FILTERS_ACTION_ID,
} from './annotations/actions';
+import { IgnoredGlobalFiltersEntries } from './info_badges';
const XY_ID = 'lnsXY';
export const getXyVisualization = ({
@@ -876,7 +877,9 @@ export const getXyVisualization = ({
);
}
- return [...errors, ...warnings];
+ const info = getNotifiableFeatures(state, frame.dataViews);
+
+ return errors.concat(warnings, info);
},
getUniqueLabels(state) {
@@ -919,88 +922,7 @@ export const getXyVisualization = ({
return suggestion;
},
- getVisualizationInfo(state: XYState) {
- const isHorizontal = isHorizontalChart(state.layers);
- const visualizationLayersInfo = state.layers.map((layer) => {
- const dimensions = [];
- let chartType: SeriesType | undefined;
- let icon;
- let label;
- if (isDataLayer(layer)) {
- chartType = layer.seriesType;
- const layerVisType = visualizationTypes.find((visType) => visType.id === chartType);
- icon = layerVisType?.icon;
- label = layerVisType?.fullLabel || layerVisType?.label;
- if (layer.xAccessor) {
- dimensions.push({
- name: getAxisName('x', { isHorizontal }),
- id: layer.xAccessor,
- dimensionType: 'x',
- });
- }
- if (layer.accessors && layer.accessors.length) {
- layer.accessors.forEach((accessor) => {
- dimensions.push({
- name: getAxisName('y', { isHorizontal }),
- id: accessor,
- dimensionType: 'y',
- });
- });
- }
- if (layer.splitAccessor) {
- dimensions.push({
- name: i18n.translate('xpack.lens.xyChart.splitSeries', {
- defaultMessage: 'Breakdown',
- }),
- dimensionType: 'breakdown',
- id: layer.splitAccessor,
- });
- }
- }
- if (isReferenceLayer(layer) && layer.accessors && layer.accessors.length) {
- layer.accessors.forEach((accessor) => {
- dimensions.push({
- name: i18n.translate('xpack.lens.xyChart.layerReferenceLine', {
- defaultMessage: 'Reference line',
- }),
- dimensionType: 'reference_line',
- id: accessor,
- });
- });
- label = i18n.translate('xpack.lens.xyChart.layerReferenceLineLabel', {
- defaultMessage: 'Reference lines',
- });
- icon = IconChartBarReferenceLine;
- }
- if (isAnnotationsLayer(layer) && layer.annotations && layer.annotations.length) {
- layer.annotations.forEach((annotation) => {
- dimensions.push({
- name: i18n.translate('xpack.lens.xyChart.layerAnnotation', {
- defaultMessage: 'Annotation',
- }),
- dimensionType: 'annotation',
- id: annotation.id,
- });
- });
- label = i18n.translate('xpack.lens.xyChart.layerAnnotationsLabel', {
- defaultMessage: 'Annotations',
- });
- icon = IconChartBarAnnotations;
- }
-
- return {
- layerId: layer.layerId,
- layerType: layer.layerType,
- chartType,
- icon,
- label,
- dimensions,
- };
- });
- return {
- layers: visualizationLayersInfo,
- };
- },
+ getVisualizationInfo,
});
const getMappedAccessors = ({
@@ -1040,3 +962,118 @@ const getMappedAccessors = ({
}
return mappedAccessors;
};
+
+function getVisualizationInfo(state: XYState) {
+ const isHorizontal = isHorizontalChart(state.layers);
+ const visualizationLayersInfo = state.layers.map((layer) => {
+ const dimensions = [];
+ let chartType: SeriesType | undefined;
+ let icon;
+ let label;
+ if (isDataLayer(layer)) {
+ chartType = layer.seriesType;
+ const layerVisType = visualizationTypes.find((visType) => visType.id === chartType);
+ icon = layerVisType?.icon;
+ label = layerVisType?.fullLabel || layerVisType?.label;
+ if (layer.xAccessor) {
+ dimensions.push({
+ name: getAxisName('x', { isHorizontal }),
+ id: layer.xAccessor,
+ dimensionType: 'x',
+ });
+ }
+ if (layer.accessors && layer.accessors.length) {
+ layer.accessors.forEach((accessor) => {
+ dimensions.push({
+ name: getAxisName('y', { isHorizontal }),
+ id: accessor,
+ dimensionType: 'y',
+ });
+ });
+ }
+ if (layer.splitAccessor) {
+ dimensions.push({
+ name: i18n.translate('xpack.lens.xyChart.splitSeries', {
+ defaultMessage: 'Breakdown',
+ }),
+ dimensionType: 'breakdown',
+ id: layer.splitAccessor,
+ });
+ }
+ }
+ if (isReferenceLayer(layer) && layer.accessors && layer.accessors.length) {
+ layer.accessors.forEach((accessor) => {
+ dimensions.push({
+ name: i18n.translate('xpack.lens.xyChart.layerReferenceLine', {
+ defaultMessage: 'Reference line',
+ }),
+ dimensionType: 'reference_line',
+ id: accessor,
+ });
+ });
+ label = i18n.translate('xpack.lens.xyChart.layerReferenceLineLabel', {
+ defaultMessage: 'Reference lines',
+ });
+ icon = IconChartBarReferenceLine;
+ }
+ if (isAnnotationsLayer(layer) && layer.annotations && layer.annotations.length) {
+ layer.annotations.forEach((annotation) => {
+ dimensions.push({
+ name: i18n.translate('xpack.lens.xyChart.layerAnnotation', {
+ defaultMessage: 'Annotation',
+ }),
+ dimensionType: 'annotation',
+ id: annotation.id,
+ });
+ });
+ label = i18n.translate('xpack.lens.xyChart.layerAnnotationsLabel', {
+ defaultMessage: 'Annotations',
+ });
+ icon = IconChartBarAnnotations;
+ }
+
+ return {
+ layerId: layer.layerId,
+ layerType: layer.layerType,
+ chartType,
+ icon,
+ label,
+ dimensions,
+ };
+ });
+ return {
+ layers: visualizationLayersInfo,
+ };
+}
+
+function getNotifiableFeatures(
+ state: XYState,
+ dataViews: FramePublicAPI['dataViews']
+): UserMessage[] {
+ const annotationsWithIgnoreFlag = getAnnotationsLayers(state.layers).filter(
+ (layer) => layer.ignoreGlobalFilters
+ );
+ if (!annotationsWithIgnoreFlag.length) {
+ return [];
+ }
+ const visualizationInfo = getVisualizationInfo(state);
+
+ return [
+ {
+ uniqueId: 'ignoring-global-filters-layers',
+ severity: 'info',
+ fixableInEditor: false,
+ shortMessage: i18n.translate('xpack.lens.xyChart.layerAnnotationsIgnoreTitle', {
+ defaultMessage: 'Layers ignoring global filters',
+ }),
+ longMessage: (
+
+ ),
+ displayLocations: [{ id: 'embeddableBadge' }],
+ },
+ ];
+}
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index 6b7465d5b8c4a..a65243339088c 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -19199,7 +19199,6 @@
"xpack.lens.xyChart.annotationError.textFieldNotFound": "Champ de texte {textField} introuvable dans la vue de données {dataView}",
"xpack.lens.xyChart.annotationError.timeFieldNotFound": "Champ temporel {timeField} introuvable dans la vue de données {dataView}",
"xpack.lens.xyChart.annotationError.tooltipFieldNotFound": "{missingFields, plural, one {Champ d'infobulle introuvable} other {Champs d'infobulle introuvables}} {missingTooltipFields} dans la vue de données {dataView}",
- "xpack.lens.xyChart.randomSampling.help": "Des pourcentages d'échantillonnage plus faibles augmentent la vitesse, mais diminuent la précision. Une bonne pratique consiste à utiliser un échantillonnage plus faible uniquement pour les ensembles de données volumineux. {link}",
"xpack.lens.xySuggestions.dateSuggestion": "{yTitle} sur {xTitle}",
"xpack.lens.xySuggestions.nonDateSuggestion": "{yTitle} de {xTitle}",
"xpack.lens.xyVisualization.arrayValues": "{label} contient des valeurs de tableau. Le rendu de votre visualisation peut ne pas se présenter comme attendu.",
@@ -19939,7 +19938,6 @@
"xpack.lens.primaryMetric.headingLabel": "Valeur",
"xpack.lens.primaryMetric.label": "Indicateur principal",
"xpack.lens.queryInput.appName": "Lens",
- "xpack.lens.randomSampling.experimentalLabel": "Version d'évaluation technique",
"xpack.lens.reporting.shareContextMenu.csvReportsButtonLabel": "Téléchargement CSV",
"xpack.lens.resetLayerAriaLabel": "Effacer le calque",
"xpack.lens.saveDuplicateRejectedDescription": "La confirmation d'enregistrement avec un doublon de titre a été rejetée.",
@@ -20151,10 +20149,6 @@
"xpack.lens.xyChart.missingValuesStyle": "Afficher sous la forme d’une ligne pointillée",
"xpack.lens.xyChart.nestUnderRoot": "Ensemble de données entier",
"xpack.lens.xyChart.placement": "Placement",
- "xpack.lens.xyChart.randomSampling.accuracyLabel": "Précision",
- "xpack.lens.xyChart.randomSampling.label": "Échantillonnage aléatoire",
- "xpack.lens.xyChart.randomSampling.learnMore": "Afficher la documentation",
- "xpack.lens.xyChart.randomSampling.speedLabel": "Rapidité",
"xpack.lens.xyChart.rightAxisDisabledHelpText": "Ce paramètre s'applique uniquement lorsque l'axe de droite est activé.",
"xpack.lens.xyChart.rightAxisLabel": "Axe de droite",
"xpack.lens.xyChart.scaleLinear": "Linéaire",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 0405d4a82c61f..53fb91ae6d566 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -19198,7 +19198,6 @@
"xpack.lens.xyChart.annotationError.textFieldNotFound": "テキストフィールド{textField}がデータビュー{dataView}で見つかりません",
"xpack.lens.xyChart.annotationError.timeFieldNotFound": "時刻フィールド{timeField}がデータビュー{dataView}で見つかりません",
"xpack.lens.xyChart.annotationError.tooltipFieldNotFound": "フィールド{missingFields, plural, other {フィールド}}{missingTooltipFields}がデータビュー{dataView}で見つかりません",
- "xpack.lens.xyChart.randomSampling.help": "サンプリング割合が低いと、速度が上がりますが、精度が低下します。ベストプラクティスとして、大きいデータセットの場合にのみ低サンプリングを使用してください。{link}",
"xpack.lens.xySuggestions.dateSuggestion": "{xTitle} の {yTitle}",
"xpack.lens.xySuggestions.nonDateSuggestion": "{yTitle} / {xTitle}",
"xpack.lens.xyVisualization.arrayValues": "{label}には配列値が含まれます。可視化が想定通りに表示されない場合があります。",
@@ -19939,7 +19938,6 @@
"xpack.lens.primaryMetric.headingLabel": "値",
"xpack.lens.primaryMetric.label": "主メトリック",
"xpack.lens.queryInput.appName": "レンズ",
- "xpack.lens.randomSampling.experimentalLabel": "テクニカルプレビュー",
"xpack.lens.reporting.shareContextMenu.csvReportsButtonLabel": "CSVダウンロード",
"xpack.lens.resetLayerAriaLabel": "レイヤーをクリア",
"xpack.lens.saveDuplicateRejectedDescription": "重複ファイルの保存確認が拒否されました",
@@ -20151,10 +20149,6 @@
"xpack.lens.xyChart.missingValuesStyle": "点線として表示",
"xpack.lens.xyChart.nestUnderRoot": "データセット全体",
"xpack.lens.xyChart.placement": "配置",
- "xpack.lens.xyChart.randomSampling.accuracyLabel": "精度",
- "xpack.lens.xyChart.randomSampling.label": "無作為抽出",
- "xpack.lens.xyChart.randomSampling.learnMore": "ドキュメンテーションを表示",
- "xpack.lens.xyChart.randomSampling.speedLabel": "スピード",
"xpack.lens.xyChart.rightAxisDisabledHelpText": "この設定は、右の軸が有効であるときにのみ適用されます。",
"xpack.lens.xyChart.rightAxisLabel": "右の軸",
"xpack.lens.xyChart.scaleLinear": "線形",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 8f62ac80cf764..b7b7503d9a62a 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -19199,7 +19199,6 @@
"xpack.lens.xyChart.annotationError.textFieldNotFound": "在数据视图 {dataView} 中未找到文本字段 {textField}",
"xpack.lens.xyChart.annotationError.timeFieldNotFound": "在数据视图 {dataView} 中未找到时间字段 {timeField}",
"xpack.lens.xyChart.annotationError.tooltipFieldNotFound": "在数据视图 {dataView} 中未找到工具提示{missingFields, plural, other {字段}} {missingTooltipFields}",
- "xpack.lens.xyChart.randomSampling.help": "较低采样百分比会提高速度,但会降低准确性。作为最佳做法,请仅将较低采样用于大型数据库。{link}",
"xpack.lens.xySuggestions.dateSuggestion": "{yTitle} / {xTitle}",
"xpack.lens.xySuggestions.nonDateSuggestion": "{xTitle} 的 {yTitle}",
"xpack.lens.xyVisualization.arrayValues": "{label} 包含数组值。您的可视化可能无法正常渲染。",
@@ -19940,7 +19939,6 @@
"xpack.lens.primaryMetric.headingLabel": "值",
"xpack.lens.primaryMetric.label": "主要指标",
"xpack.lens.queryInput.appName": "Lens",
- "xpack.lens.randomSampling.experimentalLabel": "技术预览",
"xpack.lens.reporting.shareContextMenu.csvReportsButtonLabel": "CSV 下载",
"xpack.lens.resetLayerAriaLabel": "清除图层",
"xpack.lens.saveDuplicateRejectedDescription": "已拒绝使用重复标题保存确认",
@@ -20152,10 +20150,6 @@
"xpack.lens.xyChart.missingValuesStyle": "显示为虚线",
"xpack.lens.xyChart.nestUnderRoot": "整个数据集",
"xpack.lens.xyChart.placement": "位置",
- "xpack.lens.xyChart.randomSampling.accuracyLabel": "准确性",
- "xpack.lens.xyChart.randomSampling.label": "随机采样",
- "xpack.lens.xyChart.randomSampling.learnMore": "查看文档",
- "xpack.lens.xyChart.randomSampling.speedLabel": "速度",
"xpack.lens.xyChart.rightAxisDisabledHelpText": "此设置仅在启用右轴时应用。",
"xpack.lens.xyChart.rightAxisLabel": "右轴",
"xpack.lens.xyChart.scaleLinear": "线性",
diff --git a/x-pack/test/functional/apps/lens/group1/layer_actions.ts b/x-pack/test/functional/apps/lens/group1/layer_actions.ts
index 6143129aaa841..f3104a7a81c7a 100644
--- a/x-pack/test/functional/apps/lens/group1/layer_actions.ts
+++ b/x-pack/test/functional/apps/lens/group1/layer_actions.ts
@@ -13,13 +13,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const find = getService('find');
const testSubjects = getService('testSubjects');
- // skip random sampling FTs until we figure out next steps
- describe.skip('lens layer actions tests', () => {
+ describe('lens layer actions tests', () => {
it('should allow creation of lens xy chart', async () => {
await PageObjects.visualize.navigateToNewVisualization();
await PageObjects.visualize.clickVisType('lens');
await PageObjects.lens.goToTimeRange();
+ // check that no sampling info is shown in the dataView picker
+ expect(await testSubjects.exists('lnsChangeIndexPatternSamplingInfo')).to.be(false);
+
await PageObjects.lens.openLayerContextMenu();
// should be 3 actions available
@@ -28,18 +30,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
).to.eql(3);
});
- it('should open layer settings for a data layer', async () => {
+ it('should open layer settings for a data layer and set a sampling rate', async () => {
// click on open layer settings
await testSubjects.click('lnsLayerSettings');
// random sampling available
await testSubjects.existOrFail('lns-indexPattern-random-sampling-row');
// tweak the value
- await PageObjects.lens.dragRangeInput('lns-indexPattern-random-sampling', 2, 'left');
+ await PageObjects.lens.dragRangeInput('lns-indexPattern-random-sampling-slider', 2, 'left');
- expect(await PageObjects.lens.getRangeInputValue('lns-indexPattern-random-sampling')).to.eql(
- 2 // 0.01
+ expect(
+ await PageObjects.lens.getRangeInputValue('lns-indexPattern-random-sampling-slider')
+ ).to.eql(
+ 3 // 1%
);
await testSubjects.click('lns-indexPattern-dimensionContainerBack');
+
+ // now check that the dataView picker has the sampling info
+ await testSubjects.existOrFail('lnsChangeIndexPatternSamplingInfo');
+ expect(await testSubjects.getVisibleText('lnsChangeIndexPatternSamplingInfo')).to.be('1%');
});
it('should add an annotation layer and settings shoud not be available', async () => {
@@ -56,13 +64,54 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
field: 'bytes',
});
// add annotation layer
- await testSubjects.click('lnsLayerAddButton');
- await testSubjects.click(`lnsLayerAddButton-annotations`);
+ await PageObjects.lens.createLayer('annotations');
await PageObjects.lens.openLayerContextMenu(1);
+ await testSubjects.existOrFail('lnsXY_annotationLayer_keepFilters');
// layer settings not available
await testSubjects.missingOrFail('lnsLayerSettings');
});
+ it('should add a new visualization layer and disable the sampling if max operation is chosen', async () => {
+ await PageObjects.lens.createLayer('data');
+
+ await PageObjects.lens.openLayerContextMenu(2);
+ // click on open layer settings
+ await testSubjects.click('lnsLayerSettings');
+ // tweak the value
+ await PageObjects.lens.dragRangeInput('lns-indexPattern-random-sampling-slider', 2, 'left');
+ await testSubjects.click('lns-indexPattern-dimensionContainerBack');
+ // check the sampling is shown
+ await testSubjects.existOrFail('lns-layerPanel-2 > lnsChangeIndexPatternSamplingInfo');
+ await PageObjects.lens.configureDimension({
+ dimension: 'lns-layerPanel-2 > lnsXY_xDimensionPanel > lns-empty-dimension',
+ operation: 'date_histogram',
+ field: '@timestamp',
+ });
+
+ // now configure a max operation
+ await PageObjects.lens.configureDimension({
+ dimension: 'lns-layerPanel-2 > lnsXY_yDimensionPanel > lns-empty-dimension',
+ operation: 'max',
+ field: 'bytes',
+ keepOpen: true, // keep it open as the toast will cover the close button anyway
+ });
+
+ // close the toast about disabling sampling
+ // note: this has also the side effect to close the dimension editor
+ await testSubjects.click('toastCloseButton');
+
+ // check that sampling info is hidden as disabled now the dataView picker
+ await testSubjects.missingOrFail('lns-layerPanel-2 > lnsChangeIndexPatternSamplingInfo');
+ // open the layer settings and check that the slider is disabled
+ await PageObjects.lens.openLayerContextMenu(2);
+ // click on open layer settings
+ await testSubjects.click('lnsLayerSettings');
+ expect(
+ await testSubjects.getAttribute('lns-indexPattern-random-sampling-slider', 'disabled')
+ ).to.be('true');
+ await testSubjects.click('lns-indexPattern-dimensionContainerBack');
+ });
+
it('should switch to pie chart and have layer settings available', async () => {
await PageObjects.lens.switchToVisualization('pie');
await PageObjects.lens.openLayerContextMenu();
@@ -70,8 +119,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
// open the panel
await testSubjects.click('lnsLayerSettings');
// check the sampling value
- expect(await PageObjects.lens.getRangeInputValue('lns-indexPattern-random-sampling')).to.eql(
- 2 // 0.01
+ expect(
+ await PageObjects.lens.getRangeInputValue('lns-indexPattern-random-sampling-slider')
+ ).to.eql(
+ 3 // 1%
);
await testSubjects.click('lns-indexPattern-dimensionContainerBack');
});
@@ -83,10 +134,79 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
// open the panel
await testSubjects.click('lnsLayerSettings');
// check the sampling value
- expect(await PageObjects.lens.getRangeInputValue('lns-indexPattern-random-sampling')).to.eql(
- 2 // 0.01
+ expect(
+ await PageObjects.lens.getRangeInputValue('lns-indexPattern-random-sampling-slider')
+ ).to.eql(
+ 3 // 1%
);
await testSubjects.click('lns-indexPattern-dimensionContainerBack');
});
+
+ it('should show visualization modifiers for layer settings when embedded in a dashboard', async () => {
+ await PageObjects.visualize.navigateToNewVisualization();
+ await PageObjects.visualize.clickVisType('lens');
+ await PageObjects.lens.goToTimeRange();
+ // click on open layer settings
+ await PageObjects.lens.openLayerContextMenu();
+ await testSubjects.click('lnsLayerSettings');
+ // tweak the value
+ await PageObjects.lens.dragRangeInput('lns-indexPattern-random-sampling-slider', 2, 'left');
+ await testSubjects.click('lns-indexPattern-dimensionContainerBack');
+
+ await PageObjects.lens.configureDimension({
+ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
+ operation: 'date_histogram',
+ field: '@timestamp',
+ });
+
+ await PageObjects.lens.configureDimension({
+ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
+ operation: 'average',
+ field: 'bytes',
+ });
+
+ // add another layer with a different sampling rate
+ await PageObjects.lens.createLayer('data');
+
+ await PageObjects.lens.openLayerContextMenu(1);
+ // click on open layer settings
+ await testSubjects.click('lnsLayerSettings');
+ // tweak the value
+ await PageObjects.lens.dragRangeInput('lns-indexPattern-random-sampling-slider', 3, 'left');
+ await testSubjects.click('lns-indexPattern-dimensionContainerBack');
+
+ await PageObjects.lens.configureDimension({
+ dimension: 'lns-layerPanel-1 > lnsXY_xDimensionPanel > lns-empty-dimension',
+ operation: 'date_histogram',
+ field: '@timestamp',
+ });
+
+ await PageObjects.lens.configureDimension({
+ dimension: 'lns-layerPanel-1 > lnsXY_yDimensionPanel > lns-empty-dimension',
+ operation: 'average',
+ field: 'bytes',
+ });
+
+ // add annotation layer
+ // by default annotations ignore global filters
+ await PageObjects.lens.createLayer('annotations');
+
+ await PageObjects.lens.save('sampledVisualization', false, true, false, 'new');
+
+ // now check for the bottom-left badge
+ await testSubjects.existOrFail('lns-feature-badges-trigger');
+
+ // click on the badge and check the popover
+ await testSubjects.click('lns-feature-badges-trigger');
+ expect(
+ (await testSubjects.getVisibleText('lns-feature-badges-reducedSampling-0')).split('\n')
+ ).to.contain('1%');
+ expect(
+ (await testSubjects.getVisibleText('lns-feature-badges-reducedSampling-1')).split('\n')
+ ).to.contain('0.1%');
+ expect(
+ (await testSubjects.getVisibleText('lns-feature-badges-ignoreGlobalFilters-0')).split('\n')
+ ).to.contain('Annotations');
+ });
});
}
From 48ded93bc72cd5ae23a0a0eeee975a110f31e900 Mon Sep 17 00:00:00 2001
From: Andrea Mazzucchelli <60042277+andrmaz@users.noreply.github.com>
Date: Tue, 4 Apr 2023 13:02:39 +0200
Subject: [PATCH 005/112] [Fleet] Add a tooltip for ES outputs limitation
(#154300)
## Summary
Closes #144361
![image](https://user-images.githubusercontent.com/60042277/229639120-25fde88c-47ab-4163-878c-291196cdc5be.png)
### Checklist
- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
### For maintainers
- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
---
.../components/edit_output_flyout/index.tsx | 42 ++++++++++++++-----
.../translations/translations/fr-FR.json | 1 +
.../translations/translations/ja-JP.json | 1 +
.../translations/translations/zh-CN.json | 1 +
4 files changed, 34 insertions(+), 11 deletions(-)
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx
index 8eaaaa62ab161..f7535ffe5b588 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx
@@ -145,18 +145,38 @@ export const EditOutputFlyout: React.FunctionComponent =
/>
}
>
-
+
+ {isESOutput && (
+ <>
+
+
+ >
)}
- />
+ >
{showLogstashNeedEncryptedSavedObjectCallout && (
<>
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index a65243339088c..5bfe3d51872f4 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -15371,6 +15371,7 @@
"xpack.fleet.settings.editOutputFlyout.editTitle": "Modifier la sortie",
"xpack.fleet.settings.editOutputFlyout.esHostsInputLabel": "Hôtes",
"xpack.fleet.settings.editOutputFlyout.esHostsInputPlaceholder": "Indiquer l’URL de l’hôte",
+ "xpack.fleet.settings.editOutputFlyout.esOutputTypeCallout": "Ce type de sortie ne prend actuellement pas en charge la connectivité à un cluster Elasticsearch distant.",
"xpack.fleet.settings.editOutputFlyout.loadBalancingDescription": "Une fois activés, les agents équilibreront la charge sur tous les hôtes définis pour cette sortie. Cela augmentera le nombre de connexions ouvertes par l'agent.",
"xpack.fleet.settings.editOutputFlyout.loadBalancingSwitchLabel": "Équilibrage des charges",
"xpack.fleet.settings.editOutputFlyout.logstashHostsInputLabel": "Hôtes Logstash",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 53fb91ae6d566..c521bce59c554 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -15370,6 +15370,7 @@
"xpack.fleet.settings.editOutputFlyout.editTitle": "出力を編集",
"xpack.fleet.settings.editOutputFlyout.esHostsInputLabel": "ホスト",
"xpack.fleet.settings.editOutputFlyout.esHostsInputPlaceholder": "ホストURLを指定",
+ "xpack.fleet.settings.editOutputFlyout.esOutputTypeCallout": "この出力タイプは現在、リモートElasticsearchクラスタへの接続をサポートしていません。",
"xpack.fleet.settings.editOutputFlyout.loadBalancingDescription": "有効にすると、エージェントは、この出力に対して定義されたすべてのホストで負荷を分散します。これにより、エージェントによって開かれる接続数が増えます。",
"xpack.fleet.settings.editOutputFlyout.loadBalancingSwitchLabel": "ロードバランシング",
"xpack.fleet.settings.editOutputFlyout.logstashHostsInputLabel": "Logstashホスト",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index b7b7503d9a62a..0f1e4bfbc2a8d 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -15371,6 +15371,7 @@
"xpack.fleet.settings.editOutputFlyout.editTitle": "编辑输出",
"xpack.fleet.settings.editOutputFlyout.esHostsInputLabel": "主机",
"xpack.fleet.settings.editOutputFlyout.esHostsInputPlaceholder": "指定主机 URL",
+ "xpack.fleet.settings.editOutputFlyout.esOutputTypeCallout": "该输出类型目前不支持与远程Elasticsearch集群的连接。",
"xpack.fleet.settings.editOutputFlyout.loadBalancingDescription": "启用后,代理将在为此输出定义的所有主机之间执行负载均衡。这会增加被代理打开的连接的数量。",
"xpack.fleet.settings.editOutputFlyout.loadBalancingSwitchLabel": "负载均衡",
"xpack.fleet.settings.editOutputFlyout.logstashHostsInputLabel": "Logstash 主机",
From d718066d75f86557ef71a9bfa1bc30b0cfff35fa Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Tue, 4 Apr 2023 14:06:24 +0300
Subject: [PATCH 006/112] [Lens] Improvements on the suggestions api (#153931)
## Summary
This is a follow up PR from my PR that enables Lens suggestions in
Discover. Specifically:
- it moves the filtering from unified_histogram to lens api
- it solves a bug that exists atm. If I have a query that it doesn't
return a valid chart (e.g. select from index) the chart
is rendered wrongly (we should render nothing at this case). For this
reason we add the incomplete flag in each visualization suggest which is
set to true every time my chart is incomplete (missing required
dimensions)
---
.../layout/hooks/use_lens_suggestions.ts | 28 ++----
.../lens/public/lens_suggestions_api.test.ts | 97 +++++++++++++++++++
.../lens/public/lens_suggestions_api.ts | 12 ++-
x-pack/plugins/lens/public/plugin.ts | 9 +-
x-pack/plugins/lens/public/types.ts | 6 ++
.../visualizations/gauge/suggestions.test.ts | 3 +
.../visualizations/gauge/suggestions.ts | 1 +
.../heatmap/suggestions.test.ts | 7 ++
.../visualizations/heatmap/suggestions.ts | 1 +
.../partition/suggestions.test.ts | 3 +
.../visualizations/partition/suggestions.ts | 1 +
.../visualizations/xy/xy_suggestions.test.ts | 19 ++++
.../visualizations/xy/xy_suggestions.ts | 5 +-
13 files changed, 166 insertions(+), 26 deletions(-)
diff --git a/src/plugins/unified_histogram/public/layout/hooks/use_lens_suggestions.ts b/src/plugins/unified_histogram/public/layout/hooks/use_lens_suggestions.ts
index 2884118872a2d..c94767eb67842 100644
--- a/src/plugins/unified_histogram/public/layout/hooks/use_lens_suggestions.ts
+++ b/src/plugins/unified_histogram/public/layout/hooks/use_lens_suggestions.ts
@@ -36,21 +36,15 @@ export const useLensSuggestions = ({
contextualFields: columns,
query: query && isOfAggregateQueryType(query) ? query : undefined,
};
- const lensSuggestions = isPlainRecord ? lensSuggestionsApi(context, dataView) : undefined;
- const firstSuggestion = lensSuggestions?.length ? lensSuggestions[0] : undefined;
- const restSuggestions = lensSuggestions?.filter((sug) => {
- return !sug.hide && sug.visualizationId !== 'lnsLegacyMetric';
- });
- const firstSuggestionExists = restSuggestions?.find(
- (sug) => sug.title === firstSuggestion?.title
- );
- if (firstSuggestion && !firstSuggestionExists) {
- restSuggestions?.push(firstSuggestion);
- }
- return { firstSuggestion, restSuggestions };
+ const allSuggestions = isPlainRecord
+ ? lensSuggestionsApi(context, dataView, ['lnsDatatable']) ?? []
+ : [];
+ const [firstSuggestion] = allSuggestions;
+
+ return { firstSuggestion, allSuggestions };
}, [columns, dataView, isPlainRecord, lensSuggestionsApi, query]);
- const [allSuggestions, setAllSuggestions] = useState(suggestions.restSuggestions);
+ const [allSuggestions, setAllSuggestions] = useState(suggestions.allSuggestions);
const currentSuggestion = originalSuggestion ?? suggestions.firstSuggestion;
const suggestionDeps = useRef(getSuggestionDeps({ dataView, query, columns }));
@@ -58,7 +52,7 @@ export const useLensSuggestions = ({
const newSuggestionsDeps = getSuggestionDeps({ dataView, query, columns });
if (!isEqual(suggestionDeps.current, newSuggestionsDeps)) {
- setAllSuggestions(suggestions.restSuggestions);
+ setAllSuggestions(suggestions.allSuggestions);
onSuggestionChange?.(suggestions.firstSuggestion);
suggestionDeps.current = newSuggestionsDeps;
@@ -69,15 +63,13 @@ export const useLensSuggestions = ({
onSuggestionChange,
query,
suggestions.firstSuggestion,
- suggestions.restSuggestions,
+ suggestions.allSuggestions,
]);
return {
allSuggestions,
currentSuggestion,
- suggestionUnsupported:
- isPlainRecord &&
- (!currentSuggestion || currentSuggestion?.visualizationId === 'lnsDatatable'),
+ suggestionUnsupported: isPlainRecord && !currentSuggestion,
};
};
diff --git a/x-pack/plugins/lens/public/lens_suggestions_api.test.ts b/x-pack/plugins/lens/public/lens_suggestions_api.test.ts
index 7e5f8a7b1b50f..4f2155a42a8f1 100644
--- a/x-pack/plugins/lens/public/lens_suggestions_api.test.ts
+++ b/x-pack/plugins/lens/public/lens_suggestions_api.test.ts
@@ -102,4 +102,101 @@ describe('suggestionsApi', () => {
expect(datasourceMap.textBased.getDatasourceSuggestionsFromCurrentState).toHaveBeenCalled();
expect(suggestions?.length).toEqual(1);
});
+
+ test('filters out legacy metric and incomplete suggestions', async () => {
+ const dataView = { id: 'index1' } as unknown as DataView;
+ const visualizationMap = {
+ testVis: {
+ ...mockVis,
+ getSuggestions: () => [
+ {
+ score: 0.2,
+ title: 'Test',
+ state: {},
+ previewIcon: 'empty',
+ visualizationId: 'lnsLegacyMetric',
+ },
+ {
+ score: 0.8,
+ title: 'Test2',
+ state: {},
+ previewIcon: 'empty',
+ },
+ {
+ score: 0.8,
+ title: 'Test2',
+ state: {},
+ previewIcon: 'empty',
+ incomplete: true,
+ },
+ ],
+ },
+ };
+ datasourceMap.textBased.getDatasourceSuggestionsForVisualizeField.mockReturnValue([
+ generateSuggestion(),
+ ]);
+ const context = {
+ dataViewSpec: {
+ id: 'index1',
+ title: 'index1',
+ name: 'DataView',
+ },
+ fieldName: '',
+ contextualFields: ['field1', 'field2'],
+ query: {
+ sql: 'SELECT field1, field2 FROM "index1"',
+ },
+ };
+ const suggestions = suggestionsApi({ context, dataView, datasourceMap, visualizationMap });
+ expect(datasourceMap.textBased.getDatasourceSuggestionsFromCurrentState).toHaveBeenCalled();
+ expect(suggestions?.length).toEqual(1);
+ });
+
+ test('filters out the suggestion if exists on excludedVisualizations', async () => {
+ const dataView = { id: 'index1' } as unknown as DataView;
+ const visualizationMap = {
+ testVis: {
+ ...mockVis,
+ getSuggestions: () => [
+ {
+ score: 0.2,
+ title: 'Test',
+ state: {},
+ previewIcon: 'empty',
+ visualizationId: 'lnsXY',
+ },
+ {
+ score: 0.8,
+ title: 'Test2',
+ state: {},
+ previewIcon: 'empty',
+ },
+ ],
+ },
+ };
+ datasourceMap.textBased.getDatasourceSuggestionsForVisualizeField.mockReturnValue([
+ generateSuggestion(),
+ ]);
+ const context = {
+ dataViewSpec: {
+ id: 'index1',
+ title: 'index1',
+ name: 'DataView',
+ },
+ fieldName: '',
+ contextualFields: ['field1', 'field2'],
+ query: {
+ sql: 'SELECT field1, field2 FROM "index1"',
+ },
+ };
+ const suggestions = suggestionsApi({
+ context,
+ dataView,
+ datasourceMap,
+ visualizationMap,
+ excludedVisualizations: ['lnsXY'],
+ });
+ expect(datasourceMap.textBased.getDatasourceSuggestionsFromCurrentState).toHaveBeenCalled();
+ expect(suggestions?.length).toEqual(1);
+ });
});
diff --git a/x-pack/plugins/lens/public/lens_suggestions_api.ts b/x-pack/plugins/lens/public/lens_suggestions_api.ts
index 0e5901d861b1b..d9414db7ff346 100644
--- a/x-pack/plugins/lens/public/lens_suggestions_api.ts
+++ b/x-pack/plugins/lens/public/lens_suggestions_api.ts
@@ -15,6 +15,7 @@ interface SuggestionsApi {
dataView: DataView;
visualizationMap?: VisualizationMap;
datasourceMap?: DatasourceMap;
+ excludedVisualizations?: string[];
}
export const suggestionsApi = ({
@@ -22,6 +23,7 @@ export const suggestionsApi = ({
dataView,
datasourceMap,
visualizationMap,
+ excludedVisualizations,
}: SuggestionsApi) => {
if (!datasourceMap || !visualizationMap || !dataView.id) return undefined;
const datasourceStates = {
@@ -63,7 +65,13 @@ export const suggestionsApi = ({
});
if (!suggestions.length) return [];
const activeVisualization = suggestions[0];
- // compute the rest suggestions depending on the active one
+ if (
+ activeVisualization.incomplete ||
+ excludedVisualizations?.includes(activeVisualization.visualizationId)
+ ) {
+ return [];
+ }
+ // compute the rest suggestions depending on the active one and filter out the lnsLegacyMetric
const newSuggestions = getSuggestions({
datasourceMap,
datasourceStates: {
@@ -76,7 +84,7 @@ export const suggestionsApi = ({
activeVisualization: visualizationMap[activeVisualization.visualizationId],
visualizationState: activeVisualization.visualizationState,
dataViews,
- });
+ }).filter((sug) => !sug.hide && sug.visualizationId !== 'lnsLegacyMetric');
return [activeVisualization, ...newSuggestions];
};
diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts
index 1eb2389591015..0ed438adefc1e 100644
--- a/x-pack/plugins/lens/public/plugin.ts
+++ b/x-pack/plugins/lens/public/plugin.ts
@@ -241,7 +241,8 @@ export interface LensPublicStart {
export type LensSuggestionsApi = (
context: VisualizeFieldContext | VisualizeEditorContext,
- dataViews: DataView
+ dataViews: DataView,
+ excludedVisualizations?: string[]
) => Suggestion[] | undefined;
export class LensPlugin {
@@ -607,15 +608,13 @@ export class LensPlugin {
return {
formula: createFormulaPublicApi(),
chartInfo: createChartInfoApi(startDependencies.dataViews, this.editorFrameService),
- suggestions: (
- context: VisualizeFieldContext | VisualizeEditorContext,
- dataView: DataView
- ) => {
+ suggestions: (context, dataView, excludedVisualizations) => {
return suggestionsApi({
datasourceMap,
visualizationMap,
context,
dataView,
+ excludedVisualizations,
});
},
};
diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts
index 4b3c69e8ec825..edb2240d28ebb 100644
--- a/x-pack/plugins/lens/public/types.ts
+++ b/x-pack/plugins/lens/public/types.ts
@@ -874,6 +874,8 @@ export interface Suggestion {
previewExpression?: Ast | string;
previewIcon: IconType;
hide?: boolean;
+ // flag to indicate if the visualization is incomplete
+ incomplete?: boolean;
changeType: TableChangeType;
keptLayerIds: string[];
}
@@ -926,6 +928,10 @@ export interface VisualizationSuggestion {
* directly.
*/
hide?: boolean;
+ /**
+ * Flag indicating whether this suggestion is incomplete
+ */
+ incomplete?: boolean;
/**
* Descriptive title of the suggestion. Should be as short as possible. This title is shown if
* the suggestion is advertised to the user and will also show either the `previewExpression` or
diff --git a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.test.ts b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.test.ts
index 485289cda75a7..6e5bb64fa2d3e 100644
--- a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.test.ts
+++ b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.test.ts
@@ -158,11 +158,13 @@ describe('shows suggestions', () => {
},
title: 'Gauge',
hide: true,
+ incomplete: true,
previewIcon: IconChartHorizontalBullet,
score: 0.5,
},
{
hide: true,
+ incomplete: true,
previewIcon: IconChartVerticalBullet,
title: 'Gauge',
score: 0.5,
@@ -208,6 +210,7 @@ describe('shows suggestions', () => {
previewIcon: IconChartVerticalBullet,
title: 'Gauge',
hide: false, // shows suggestion when current is gauge
+ incomplete: false,
score: 0.5,
},
]);
diff --git a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts
index fc8e5b1895796..c07f006b932d5 100644
--- a/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts
+++ b/x-pack/plugins/lens/public/visualizations/gauge/suggestions.ts
@@ -69,6 +69,7 @@ export const getSuggestions: Visualization['getSuggesti
shape === GaugeShapes.VERTICAL_BULLET ? IconChartVerticalBullet : IconChartHorizontalBullet,
score: 0.5,
hide: !isGauge || state?.metricAccessor === undefined, // only display for gauges for beta
+ incomplete: state?.metricAccessor === undefined,
};
const suggestions = isGauge
diff --git a/x-pack/plugins/lens/public/visualizations/heatmap/suggestions.test.ts b/x-pack/plugins/lens/public/visualizations/heatmap/suggestions.test.ts
index 5913d61ebb3cb..ff631ee605a59 100644
--- a/x-pack/plugins/lens/public/visualizations/heatmap/suggestions.test.ts
+++ b/x-pack/plugins/lens/public/visualizations/heatmap/suggestions.test.ts
@@ -299,6 +299,7 @@ describe('heatmap suggestions', () => {
},
title: 'Heat map',
hide: true,
+ incomplete: false,
previewIcon: IconChartHeatmap,
score: 0.3,
},
@@ -352,6 +353,7 @@ describe('heatmap suggestions', () => {
},
title: 'Heat map',
hide: true,
+ incomplete: true,
previewIcon: IconChartHeatmap,
score: 0,
},
@@ -405,6 +407,7 @@ describe('heatmap suggestions', () => {
},
title: 'Heat map',
hide: true,
+ incomplete: true,
previewIcon: IconChartHeatmap,
score: 0.3,
},
@@ -469,6 +472,7 @@ describe('heatmap suggestions', () => {
},
title: 'Heat map',
hide: true,
+ incomplete: false,
previewIcon: IconChartHeatmap,
score: 0.3,
},
@@ -535,6 +539,7 @@ describe('heatmap suggestions', () => {
},
title: 'Heat map',
hide: false,
+ incomplete: false,
previewIcon: IconChartHeatmap,
score: 0.6,
},
@@ -609,6 +614,7 @@ describe('heatmap suggestions', () => {
},
title: 'Heat map',
hide: false,
+ incomplete: false,
previewIcon: IconChartHeatmap,
score: 0.3,
},
@@ -683,6 +689,7 @@ describe('heatmap suggestions', () => {
},
title: 'Heat map',
hide: false,
+ incomplete: false,
previewIcon: IconChartHeatmap,
score: 0.9,
},
diff --git a/x-pack/plugins/lens/public/visualizations/heatmap/suggestions.ts b/x-pack/plugins/lens/public/visualizations/heatmap/suggestions.ts
index 21e276215acd1..ccb9c1014c25b 100644
--- a/x-pack/plugins/lens/public/visualizations/heatmap/suggestions.ts
+++ b/x-pack/plugins/lens/public/visualizations/heatmap/suggestions.ts
@@ -130,6 +130,7 @@ export const getSuggestions: Visualization['getSugges
hide,
previewIcon: IconChartHeatmap,
score: Number(score.toFixed(1)),
+ incomplete: isSingleBucketDimension || isOnlyMetricDimension,
},
];
};
diff --git a/x-pack/plugins/lens/public/visualizations/partition/suggestions.test.ts b/x-pack/plugins/lens/public/visualizations/partition/suggestions.test.ts
index eb793a0402e54..84b0e7cfbbf9f 100644
--- a/x-pack/plugins/lens/public/visualizations/partition/suggestions.test.ts
+++ b/x-pack/plugins/lens/public/visualizations/partition/suggestions.test.ts
@@ -455,6 +455,7 @@ describe('suggestions', () => {
});
expect(currentSuggestions).toHaveLength(5);
expect(currentSuggestions.every((s) => s.hide)).toEqual(true);
+ expect(currentSuggestions.every((s) => s.incomplete)).toEqual(true);
});
it('should suggest a donut chart as initial state when only one bucket', () => {
@@ -1039,6 +1040,7 @@ describe('suggestions', () => {
Array [
Object {
"hide": false,
+ "incomplete": false,
"previewIcon": [Function],
"score": 0.61,
"state": Object {
@@ -1148,6 +1150,7 @@ describe('suggestions', () => {
Array [
Object {
"hide": false,
+ "incomplete": false,
"previewIcon": [Function],
"score": 0.46,
"state": Object {
diff --git a/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts b/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts
index 5458630b7568f..4cb87847b25b2 100644
--- a/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts
+++ b/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts
@@ -330,5 +330,6 @@ export function suggestions({
.map((suggestion) => ({
...suggestion,
hide: shouldHideSuggestion || incompleteConfiguration || suggestion.hide,
+ incomplete: incompleteConfiguration,
}));
}
diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.test.ts b/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.test.ts
index 7f9bc9f67ab3f..6dd7887b59ad1 100644
--- a/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.test.ts
+++ b/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.test.ts
@@ -149,6 +149,25 @@ describe('xy_suggestions', () => {
);
});
+ test('marks incomplete as true when no metric is provided', () => {
+ expect(
+ (
+ [
+ {
+ isMultiRow: true,
+ columns: [strCol('foo')],
+ layerId: 'first',
+ changeType: 'unchanged',
+ },
+ ] as TableSuggestion[]
+ ).map((table) => {
+ const suggestions = getSuggestions({ table, keptLayerIds: [] });
+ expect(suggestions.every((suggestion) => suggestion.incomplete)).toEqual(true);
+ expect(suggestions).toHaveLength(10);
+ })
+ );
+ });
+
test('rejects the configuration when metric isStaticValue', () => {
(generateId as jest.Mock).mockReturnValueOnce('aaa');
const suggestions = getSuggestions({
diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.ts b/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.ts
index 9e2f882e10ae9..c243420c49498 100644
--- a/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.ts
+++ b/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.ts
@@ -573,6 +573,8 @@ function buildSuggestion({
existingLayer && Object.keys(existingLayer).length ? keptLayers : [...keptLayers, newLayer],
};
+ const isIncomplete = yValues.length === 0;
+
return {
title,
score: getScore(yValues, splitBy, changeType),
@@ -583,10 +585,11 @@ function buildSuggestion({
// Don't advertise removing dimensions
(currentState && changeType === 'reduced') ||
// Don't advertise charts without y axis
- yValues.length === 0 ||
+ isIncomplete ||
// Don't advertise charts without at least one split
(!xValue && !splitBy)),
state,
+ incomplete: isIncomplete,
previewIcon: getIconForSeries(seriesType),
};
}
From cb3a42a88094eba1b1798044da67af215c2cf0de Mon Sep 17 00:00:00 2001
From: claracruz
Date: Tue, 4 Apr 2023 12:15:42 +0100
Subject: [PATCH 007/112] =?UTF-8?q?Filter=20guided=20onboarding=20solution?=
=?UTF-8?q?s=20based=20on=20cloud=20discovery=20questions=E2=80=A6=20(#153?=
=?UTF-8?q?367)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Related to #148911
This PR updates the guided onboarding landing page to filter solutions
based on user selected use case in cloud discovery questions. The value
will be passed as querystring parameter `?cloudDiscoveryUseCase=[value]`
from Cloud UI.
---------
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../components/landing_page/guide_filters.tsx | 28 +++++--
.../getting_started.test.tsx.snap | 74 -------------------
.../getting_started.test.tsx | 46 +++++++++++-
.../guided_onboarding/getting_started.tsx | 31 +++++++-
4 files changed, 93 insertions(+), 86 deletions(-)
delete mode 100644 src/plugins/home/public/application/components/guided_onboarding/__snapshots__/getting_started.test.tsx.snap
diff --git a/packages/kbn-guided-onboarding/src/components/landing_page/guide_filters.tsx b/packages/kbn-guided-onboarding/src/components/landing_page/guide_filters.tsx
index 7dd1641a0ee4d..aeb448ef2a5bf 100644
--- a/packages/kbn-guided-onboarding/src/components/landing_page/guide_filters.tsx
+++ b/packages/kbn-guided-onboarding/src/components/landing_page/guide_filters.tsx
@@ -10,6 +10,7 @@ import React from 'react';
import { EuiButton, EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { FormattedMessage } from '@kbn/i18n-react';
+import { ApplicationStart } from '@kbn/core-application-browser';
import { GuideCardSolutions } from './guide_cards';
const filterButtonCss = css`
@@ -27,22 +28,34 @@ const filterButtonCss = css`
}
`;
export type GuideFilterValues = GuideCardSolutions | 'all';
-interface GuideFiltersProps {
+export interface GuideFiltersProps {
activeFilter: GuideFilterValues;
setActiveFilter: React.Dispatch>;
+ application: ApplicationStart;
}
-export const GuideFilters = ({ activeFilter, setActiveFilter }: GuideFiltersProps) => {
+export const GuideFilters = ({ activeFilter, setActiveFilter, application }: GuideFiltersProps) => {
const { euiTheme } = useEuiTheme();
const activeFilterFill = css`
background: ${euiTheme.colors.darkestShade};
color: ${euiTheme.colors.lightestShade};
`;
+ const setQuerystringParams = ({ useCase }: { useCase: string }) => {
+ application.navigateToApp('home', { path: `#/getting_started?useCase=${useCase}` });
+ };
+ const onSelectFilter = (e: React.BaseSyntheticEvent) => {
+ const {
+ currentTarget: { dataset },
+ } = e;
+ setQuerystringParams({ useCase: dataset.filterId });
+ setActiveFilter(dataset.filterId);
+ };
return (
setActiveFilter('all')}
+ onClick={onSelectFilter}
+ data-filter-id="all"
color="text"
css={[filterButtonCss, activeFilter === 'all' && activeFilterFill]}
>
@@ -54,7 +67,8 @@ export const GuideFilters = ({ activeFilter, setActiveFilter }: GuideFiltersProp
setActiveFilter('search')}
+ onClick={onSelectFilter}
+ data-filter-id="search"
color="text"
css={[filterButtonCss, activeFilter === 'search' && activeFilterFill]}
>
@@ -66,7 +80,8 @@ export const GuideFilters = ({ activeFilter, setActiveFilter }: GuideFiltersProp
setActiveFilter('observability')}
+ onClick={onSelectFilter}
+ data-filter-id="observability"
color="text"
css={[filterButtonCss, activeFilter === 'observability' && activeFilterFill]}
>
@@ -78,7 +93,8 @@ export const GuideFilters = ({ activeFilter, setActiveFilter }: GuideFiltersProp
setActiveFilter('security')}
+ onClick={onSelectFilter}
+ data-filter-id="security"
color="text"
css={[filterButtonCss, activeFilter === 'security' && activeFilterFill]}
>
diff --git a/src/plugins/home/public/application/components/guided_onboarding/__snapshots__/getting_started.test.tsx.snap b/src/plugins/home/public/application/components/guided_onboarding/__snapshots__/getting_started.test.tsx.snap
deleted file mode 100644
index 291d0173a0bbf..0000000000000
--- a/src/plugins/home/public/application/components/guided_onboarding/__snapshots__/getting_started.test.tsx.snap
+++ /dev/null
@@ -1,74 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`getting started should render getting started component 1`] = `
-<_KibanaPageTemplate
- grow={true}
- panelled={false}
->
- <_EuiPageSection
- alignment="center"
- css={
- Object {
- "map": undefined,
- "name": "hdi05h",
- "next": undefined,
- "styles": "
- padding: calc(16px*3) calc(16px*4);
- ",
- "toString": [Function],
- }
- }
- data-test-subj="onboarding--landing-page"
- >
-
-
- What would you like to do first?
-
-
-
-
-
- Select an option and we'll help you get started.
-
-
-
-
-
-
-
-
-
-
- I’d like to do something else.
-
-
-
-
-`;
diff --git a/src/plugins/home/public/application/components/guided_onboarding/getting_started.test.tsx b/src/plugins/home/public/application/components/guided_onboarding/getting_started.test.tsx
index a575ed1d0d82a..ecf12e597efce 100644
--- a/src/plugins/home/public/application/components/guided_onboarding/getting_started.test.tsx
+++ b/src/plugins/home/public/application/components/guided_onboarding/getting_started.test.tsx
@@ -9,13 +9,16 @@
import React from 'react';
import { shallow } from 'enzyme';
import { act } from 'react-dom/test-utils';
-import { findTestSubject, registerTestBed, TestBed } from '@kbn/test-jest-helpers';
+import { findTestSubject, registerTestBed, TestBed, mountWithIntl } from '@kbn/test-jest-helpers';
+import { MemoryRouter } from 'react-router-dom';
import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
import { chromeServiceMock, applicationServiceMock, httpServiceMock } from '@kbn/core/public/mocks';
import { ApiService } from '@kbn/guided-onboarding-plugin/public/services/api.service';
import { GettingStarted } from './getting_started';
import { KEY_ENABLE_WELCOME } from '../home';
+import { GuideFiltersProps } from '@kbn/guided-onboarding/src/components/landing_page/guide_filters';
+import { ReactWrapper } from '@kbn/test-jest-helpers/src/testbed/types';
const mockCloud = cloudMock.createSetup();
const mockChrome = chromeServiceMock.createStartContract();
@@ -48,9 +51,13 @@ describe('getting started', () => {
});
test('should render getting started component', async () => {
- const component = await shallow( );
+ const component = await shallow(
+
+
+
+ );
- expect(component).toMatchSnapshot();
+ expect(component.find('GettingStarted').exists()).toBe(true);
});
test('displays loading indicator', async () => {
@@ -99,4 +106,37 @@ describe('getting started', () => {
expect(localStorage.getItem(KEY_ENABLE_WELCOME)).toBe('false');
});
+
+ test('should set default guide filter value if querystring parameter does NOT exist', async () => {
+ let component: ReactWrapper;
+
+ await act(async () => {
+ component = mountWithIntl(
+
+
+
+ );
+ });
+
+ const guideFilters = component!.find('[data-test-subj="onboarding--guideFilters"]');
+ expect((guideFilters.props() as GuideFiltersProps).activeFilter).toBe('all');
+ });
+
+ test('should auto-select guide filter value based on querystring parameter', async () => {
+ const cloudDiscoveryUseCase = 'observability';
+ let component: ReactWrapper;
+
+ await act(async () => {
+ component = mountWithIntl(
+
+
+
+ );
+ });
+
+ const guideFilters = component!.find('[data-test-subj="onboarding--guideFilters"]');
+ expect((guideFilters.props() as GuideFiltersProps).activeFilter).toBe(cloudDiscoveryUseCase);
+ });
});
diff --git a/src/plugins/home/public/application/components/guided_onboarding/getting_started.tsx b/src/plugins/home/public/application/components/guided_onboarding/getting_started.tsx
index 2b89ad75affbf..e7e24fda9676a 100644
--- a/src/plugins/home/public/application/components/guided_onboarding/getting_started.tsx
+++ b/src/plugins/home/public/application/components/guided_onboarding/getting_started.tsx
@@ -7,6 +7,7 @@
*/
import React, { useCallback, useEffect, useState } from 'react';
+import { parse } from 'query-string';
import {
EuiButton,
EuiLink,
@@ -19,7 +20,7 @@ import {
} from '@elastic/eui';
import { css } from '@emotion/react';
-import { useHistory } from 'react-router-dom';
+import { useHistory, useLocation } from 'react-router-dom';
import { METRIC_TYPE } from '@kbn/analytics';
import { i18n } from '@kbn/i18n';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
@@ -48,7 +49,26 @@ export const GettingStarted = () => {
const [guidesState, setGuidesState] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
- const [filter, setFilter] = useState('all');
+ const { search } = useLocation();
+ const query = parse(search);
+
+ const isTypeOfGuideFilterValue = (useCase: string | string[] | null) => {
+ const filterValues: string[] = ['search', 'observability', 'security', 'all']; // list of GuideFilterValues types
+
+ if (!useCase) {
+ return false;
+ }
+
+ if (useCase instanceof Array) {
+ return filterValues.includes(useCase[0]);
+ }
+
+ return filterValues.includes(useCase);
+ };
+
+ const [filter, setFilter] = useState(
+ isTypeOfGuideFilterValue(query.useCase) ? (query.useCase as GuideFilterValues) : 'all'
+ );
const history = useHistory();
useEffect(() => {
@@ -196,7 +216,12 @@ export const GettingStarted = () => {
-
+
Date: Tue, 4 Apr 2023 13:25:51 +0200
Subject: [PATCH 008/112] Use components from Exploratory View app in
Synthetics (#154056)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../common/step_field_trend/step_field_trend.tsx | 4 ++--
.../monitor_details/monitor_errors/failed_tests.tsx | 6 +++---
.../monitor_errors/failed_tests_count.tsx | 6 +++---
.../monitor_summary/availability_panel.tsx | 4 ++--
.../monitor_summary/availability_sparklines.tsx | 5 +++--
.../monitor_summary/duration_panel.tsx | 4 ++--
.../monitor_summary/duration_sparklines.tsx | 5 +++--
.../monitor_summary/duration_trend.tsx | 6 +++---
.../monitor_summary/monitor_alerts.tsx | 8 +++++---
.../monitor_summary/monitor_complete_count.tsx | 8 ++++----
.../monitor_summary/monitor_complete_sparklines.tsx | 6 +++---
.../monitor_summary/monitor_error_sparklines.tsx | 6 +++---
.../monitor_summary/monitor_errors_count.tsx | 8 ++++----
.../monitor_summary/monitor_total_runs_count.tsx | 8 ++++----
.../monitor_summary/step_duration_panel.tsx | 8 ++++----
.../common/monitor_filters/filter_button.tsx | 2 +-
.../management/monitor_stats/monitor_test_runs.tsx | 8 ++++----
.../monitor_stats/monitor_test_runs_sparkline.tsx | 6 +++---
.../overview/overview/monitor_detail_flyout.tsx | 5 +++--
.../overview/overview/overview_alerts.tsx | 5 +++--
.../overview_errors/overview_errors_count.tsx | 8 ++++----
.../overview_errors/overview_errors_sparklines.tsx | 6 +++---
.../step_details_page/network_timings_breakdown.tsx | 8 ++++----
.../waterfall_marker/waterfall_marker_test_helper.tsx | 2 +-
.../public/apps/synthetics/synthetics_app.tsx | 1 +
.../apps/synthetics/utils/testing/rtl_helpers.tsx | 5 +++++
.../public/legacy_uptime/app/uptime_app.tsx | 1 +
.../components/waterfall_marker_test_helper.tsx | 2 +-
.../monitor_expressions/filters_expression_select.tsx | 2 +-
.../overview/filter_group/filter_group.test.tsx | 2 +-
.../components/overview/filter_group/filter_group.tsx | 2 +-
.../components/synthetics/check_steps/stderr_logs.tsx | 7 +++++--
.../synthetics/check_steps/step_field_trend.tsx | 11 +++++------
.../public/legacy_uptime/lib/helper/rtl_helpers.tsx | 5 +++++
x-pack/plugins/synthetics/public/plugin.ts | 6 +++++-
35 files changed, 105 insertions(+), 81 deletions(-)
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/step_field_trend/step_field_trend.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/step_field_trend/step_field_trend.tsx
index 3f214a9e6e8ac..7b687929b78a5 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/step_field_trend/step_field_trend.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/step_field_trend/step_field_trend.tsx
@@ -36,9 +36,9 @@ export function StepFieldTrend({
field: string;
step: JourneyStep;
}) {
- const { observability } = useSyntheticsStartPlugins();
+ const { exploratoryView } = useSyntheticsStartPlugins();
- const EmbeddableExpView = observability!.ExploratoryViewEmbeddable;
+ const EmbeddableExpView = exploratoryView!.ExploratoryViewEmbeddable;
const basePath = useKibana().services.http?.basePath?.get();
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/failed_tests.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/failed_tests.tsx
index 4f91aba3da04f..9b27d72a20ca0 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/failed_tests.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/failed_tests.tsx
@@ -22,9 +22,9 @@ export const MonitorFailedTests = ({
time: { to: string; from: string };
allowBrushing?: boolean;
}) => {
- const { observability } = useKibana().services;
-
- const { ExploratoryViewEmbeddable } = observability;
+ const {
+ exploratoryView: { ExploratoryViewEmbeddable },
+ } = useKibana().services;
const monitorId = useMonitorQueryId();
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/failed_tests_count.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/failed_tests_count.tsx
index 99ef360b886b3..e525630b8b3d8 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/failed_tests_count.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_errors/failed_tests_count.tsx
@@ -13,9 +13,9 @@ import { ClientPluginsStart } from '../../../../../plugin';
import { useMonitorQueryId } from '../hooks/use_monitor_query_id';
export const FailedTestsCount = ({ from, to, id }: { to: string; from: string; id: string }) => {
- const { observability } = useKibana().services;
-
- const { ExploratoryViewEmbeddable } = observability;
+ const {
+ exploratoryView: { ExploratoryViewEmbeddable },
+ } = useKibana().services;
const monitorId = useMonitorQueryId();
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/availability_panel.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/availability_panel.tsx
index abb5fe617d80e..413012edb658d 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/availability_panel.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/availability_panel.tsx
@@ -7,7 +7,7 @@
import React from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
-import { ReportTypes } from '@kbn/observability-plugin/public';
+import { ReportTypes } from '@kbn/exploratory-view-plugin/public';
import { i18n } from '@kbn/i18n';
import { ClientPluginsStart } from '../../../../../plugin';
import { useMonitorQueryId } from '../hooks/use_monitor_query_id';
@@ -22,7 +22,7 @@ interface AvailabilityPanelprops {
export const AvailabilityPanel = (props: AvailabilityPanelprops) => {
const {
services: {
- observability: { ExploratoryViewEmbeddable },
+ exploratoryView: { ExploratoryViewEmbeddable },
},
} = useKibana();
const selectedLocation = useSelectedLocation();
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/availability_sparklines.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/availability_sparklines.tsx
index 4c28fff035d22..da6881dee4328 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/availability_sparklines.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/availability_sparklines.tsx
@@ -7,7 +7,8 @@
import React from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
-import { ReportTypes, useTheme } from '@kbn/observability-plugin/public';
+import { useTheme } from '@kbn/observability-plugin/public';
+import { ReportTypes } from '@kbn/exploratory-view-plugin/public';
import { AVAILABILITY_LABEL } from './availability_panel';
import { ClientPluginsStart } from '../../../../../plugin';
import { useMonitorQueryId } from '../hooks/use_monitor_query_id';
@@ -22,7 +23,7 @@ interface AvailabilitySparklinesProps {
export const AvailabilitySparklines = (props: AvailabilitySparklinesProps) => {
const {
services: {
- observability: { ExploratoryViewEmbeddable },
+ exploratoryView: { ExploratoryViewEmbeddable },
},
} = useKibana();
const monitorId = useMonitorQueryId();
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/duration_panel.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/duration_panel.tsx
index 0f859701b6fc8..85ab6773033be 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/duration_panel.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/duration_panel.tsx
@@ -7,7 +7,7 @@
import React from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
-import { ReportTypes } from '@kbn/observability-plugin/public';
+import { ReportTypes } from '@kbn/exploratory-view-plugin/public';
import { i18n } from '@kbn/i18n';
import { ClientPluginsStart } from '../../../../../plugin';
import { useMonitorQueryId } from '../hooks/use_monitor_query_id';
@@ -22,7 +22,7 @@ interface DurationPanelProps {
export const DurationPanel = (props: DurationPanelProps) => {
const {
services: {
- observability: { ExploratoryViewEmbeddable },
+ exploratoryView: { ExploratoryViewEmbeddable },
},
} = useKibana();
const selectedLocation = useSelectedLocation();
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/duration_sparklines.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/duration_sparklines.tsx
index ca4468763a8f6..1c1370d4da3ab 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/duration_sparklines.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/duration_sparklines.tsx
@@ -7,7 +7,8 @@
import React from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
-import { ReportTypes, useTheme } from '@kbn/observability-plugin/public';
+import { ReportTypes } from '@kbn/exploratory-view-plugin/public';
+import { useTheme } from '@kbn/observability-plugin/public';
import { AVG_DURATION_LABEL } from './duration_panel';
import { useMonitorQueryId } from '../hooks/use_monitor_query_id';
import { ClientPluginsStart } from '../../../../../plugin';
@@ -22,7 +23,7 @@ interface DurationSparklinesProps {
export const DurationSparklines = (props: DurationSparklinesProps) => {
const {
services: {
- observability: { ExploratoryViewEmbeddable },
+ exploratoryView: { ExploratoryViewEmbeddable },
},
} = useKibana();
const monitorId = useMonitorQueryId();
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/duration_trend.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/duration_trend.tsx
index b534a7d13d508..b25f29a2a3b83 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/duration_trend.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/duration_trend.tsx
@@ -18,9 +18,9 @@ interface MonitorDurationTrendProps {
}
export const MonitorDurationTrend = (props: MonitorDurationTrendProps) => {
- const { observability } = useKibana().services;
-
- const { ExploratoryViewEmbeddable } = observability;
+ const {
+ exploratoryView: { ExploratoryViewEmbeddable },
+ } = useKibana().services;
const monitorId = useMonitorQueryId();
const selectedLocation = useSelectedLocation();
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_alerts.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_alerts.tsx
index 25a150eab71f5..2ddad63a561e0 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_alerts.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_alerts.tsx
@@ -15,7 +15,8 @@ import {
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { RECORDS_FIELD, useTheme } from '@kbn/observability-plugin/public';
+import { RECORDS_FIELD } from '@kbn/exploratory-view-plugin/public';
+import { useTheme } from '@kbn/observability-plugin/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useSelectedLocation } from '../hooks/use_selected_location';
import { useMonitorQueryId } from '../hooks/use_monitor_query_id';
@@ -31,8 +32,9 @@ export const MonitorAlerts = ({
from: string;
dateLabel: string;
}) => {
- const { observability } = useKibana().services;
- const { ExploratoryViewEmbeddable } = observability;
+ const {
+ exploratoryView: { ExploratoryViewEmbeddable },
+ } = useKibana().services;
const theme = useTheme();
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_complete_count.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_complete_count.tsx
index 8e8d1a0f6600a..5bff2b1a219a2 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_complete_count.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_complete_count.tsx
@@ -7,7 +7,7 @@
import { useKibana } from '@kbn/kibana-react-plugin/public';
import React from 'react';
-import { ReportTypes } from '@kbn/observability-plugin/public';
+import { ReportTypes } from '@kbn/exploratory-view-plugin/public';
import { i18n } from '@kbn/i18n';
import { ClientPluginsStart } from '../../../../../plugin';
import { useMonitorQueryId } from '../hooks/use_monitor_query_id';
@@ -19,9 +19,9 @@ interface MonitorCompleteCountProps {
}
export const MonitorCompleteCount = (props: MonitorCompleteCountProps) => {
- const { observability } = useKibana().services;
-
- const { ExploratoryViewEmbeddable } = observability;
+ const {
+ exploratoryView: { ExploratoryViewEmbeddable },
+ } = useKibana().services;
const monitorId = useMonitorQueryId();
const selectedLocation = useSelectedLocation();
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_complete_sparklines.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_complete_sparklines.tsx
index 6b92a6e2b937e..0027c77f03c9b 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_complete_sparklines.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_complete_sparklines.tsx
@@ -18,9 +18,9 @@ interface Props {
to: string;
}
export const MonitorCompleteSparklines = (props: Props) => {
- const { observability } = useKibana().services;
-
- const { ExploratoryViewEmbeddable } = observability;
+ const {
+ exploratoryView: { ExploratoryViewEmbeddable },
+ } = useKibana().services;
const monitorId = useMonitorQueryId();
const selectedLocation = useSelectedLocation();
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_error_sparklines.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_error_sparklines.tsx
index 8643f3ef4d009..f7a0d5b316af1 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_error_sparklines.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_error_sparklines.tsx
@@ -19,9 +19,9 @@ interface Props {
id: string;
}
export const MonitorErrorSparklines = ({ from, to, monitorId, id }: Props) => {
- const { observability } = useKibana().services;
-
- const { ExploratoryViewEmbeddable } = observability;
+ const {
+ exploratoryView: { ExploratoryViewEmbeddable },
+ } = useKibana().services;
const { euiTheme } = useEuiTheme();
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_errors_count.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_errors_count.tsx
index 31a311d3199bd..f8d7bc3c2f0ac 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_errors_count.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_errors_count.tsx
@@ -7,7 +7,7 @@
import { useKibana } from '@kbn/kibana-react-plugin/public';
import React, { useMemo } from 'react';
-import { ReportTypes } from '@kbn/observability-plugin/public';
+import { ReportTypes } from '@kbn/exploratory-view-plugin/public';
import { i18n } from '@kbn/i18n';
import { ClientPluginsStart } from '../../../../../plugin';
import { useSelectedLocation } from '../hooks/use_selected_location';
@@ -20,9 +20,9 @@ interface MonitorErrorsCountProps {
}
export const MonitorErrorsCount = ({ monitorId, from, to, id }: MonitorErrorsCountProps) => {
- const { observability } = useKibana().services;
-
- const { ExploratoryViewEmbeddable } = observability;
+ const {
+ exploratoryView: { ExploratoryViewEmbeddable },
+ } = useKibana().services;
const selectedLocation = useSelectedLocation();
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_total_runs_count.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_total_runs_count.tsx
index ef34498c92ab9..6e4e9f0c3d70d 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_total_runs_count.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/monitor_total_runs_count.tsx
@@ -7,7 +7,7 @@
import { useKibana } from '@kbn/kibana-react-plugin/public';
import React from 'react';
-import { ReportTypes } from '@kbn/observability-plugin/public';
+import { ReportTypes } from '@kbn/exploratory-view-plugin/public';
import { i18n } from '@kbn/i18n';
import { ClientPluginsStart } from '../../../../../plugin';
import { useMonitorQueryId } from '../hooks/use_monitor_query_id';
@@ -19,9 +19,9 @@ interface MonitorTotalRunsCountProps {
}
export const MonitorTotalRunsCount = (props: MonitorTotalRunsCountProps) => {
- const { observability } = useKibana().services;
-
- const { ExploratoryViewEmbeddable } = observability;
+ const {
+ exploratoryView: { ExploratoryViewEmbeddable },
+ } = useKibana().services;
const monitorId = useMonitorQueryId();
const selectedLocation = useSelectedLocation();
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/step_duration_panel.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/step_duration_panel.tsx
index 35f12afcd8b40..a8bd0d9a23b6c 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/step_duration_panel.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_details/monitor_summary/step_duration_panel.tsx
@@ -8,7 +8,7 @@
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText, EuiTitle } from '@elastic/eui';
import { useKibana } from '@kbn/kibana-react-plugin/public';
-import { ReportTypes } from '@kbn/observability-plugin/public';
+import { ReportTypes } from '@kbn/exploratory-view-plugin/public';
import { i18n } from '@kbn/i18n';
import { Position } from '@elastic/charts/dist/utils/common';
@@ -25,11 +25,11 @@ export const StepDurationPanel = ({
legendPosition?: Position;
doBreakdown?: boolean;
}) => {
- const { observability } = useKibana().services;
+ const {
+ exploratoryView: { ExploratoryViewEmbeddable },
+ } = useKibana().services;
const time = useAbsoluteDate({ from: 'now-24h/h', to: 'now' });
- const { ExploratoryViewEmbeddable } = observability;
-
const { monitor } = useSelectedMonitor();
const monitorId = useMonitorQueryId();
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/monitor_filters/filter_button.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/monitor_filters/filter_button.tsx
index 65cc56a473cd1..bd96ede9a18c0 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/monitor_filters/filter_button.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/common/monitor_filters/filter_button.tsx
@@ -6,7 +6,7 @@
*/
import React, { useState } from 'react';
-import { FieldValueSelection } from '@kbn/observability-plugin/public';
+import { FieldValueSelection } from '@kbn/exploratory-view-plugin/public';
import {
getSyntheticsFilterDisplayValues,
SyntheticsMonitorFilterItem,
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_stats/monitor_test_runs.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_stats/monitor_test_runs.tsx
index 9fbb1efceeb2d..6c77b05ae652f 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_stats/monitor_test_runs.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_stats/monitor_test_runs.tsx
@@ -9,18 +9,18 @@ import React from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useTheme } from '@kbn/observability-plugin/public';
-import { ReportTypes } from '@kbn/observability-plugin/public';
+import { ReportTypes } from '@kbn/exploratory-view-plugin/public';
import { useRefreshedRange } from '../../../../hooks';
import { ClientPluginsStart } from '../../../../../../plugin';
import * as labels from '../labels';
export const MonitorTestRunsCount = ({ monitorIds }: { monitorIds: string[] }) => {
- const { observability } = useKibana().services;
+ const {
+ exploratoryView: { ExploratoryViewEmbeddable },
+ } = useKibana().services;
const theme = useTheme();
- const { ExploratoryViewEmbeddable } = observability;
-
const { from, to } = useRefreshedRange(30, 'days');
return (
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_stats/monitor_test_runs_sparkline.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_stats/monitor_test_runs_sparkline.tsx
index 8791e8e1ce6a8..520ac5b7f93ae 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_stats/monitor_test_runs_sparkline.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_stats/monitor_test_runs_sparkline.tsx
@@ -15,9 +15,9 @@ import { ClientPluginsStart } from '../../../../../../plugin';
import * as labels from '../labels';
export const MonitorTestRunsSparkline = ({ monitorIds }: { monitorIds: string[] }) => {
- const { observability } = useKibana().services;
-
- const { ExploratoryViewEmbeddable } = observability;
+ const {
+ exploratoryView: { ExploratoryViewEmbeddable },
+ } = useKibana().services;
const theme = useTheme();
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/monitor_detail_flyout.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/monitor_detail_flyout.tsx
index 3d57ef4af626f..9b8ea0086260f 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/monitor_detail_flyout.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/monitor_detail_flyout.tsx
@@ -98,8 +98,9 @@ function DetailFlyoutDurationChart({
>) {
const theme = useTheme();
- const { observability } = useKibana().services;
- const { ExploratoryViewEmbeddable } = observability;
+ const {
+ exploratoryView: { ExploratoryViewEmbeddable },
+ } = useKibana().services;
return (
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_alerts.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_alerts.tsx
index 77a29f735a40b..68fe35fefeb62 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_alerts.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_alerts.tsx
@@ -26,8 +26,9 @@ import { ClientPluginsStart } from '../../../../../../plugin';
export const OverviewAlerts = () => {
const { from, to } = useRefreshedRange(12, 'hours');
- const { observability } = useKibana().services;
- const { ExploratoryViewEmbeddable } = observability;
+ const {
+ exploratoryView: { ExploratoryViewEmbeddable },
+ } = useKibana().services;
const theme = useTheme();
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_errors/overview_errors_count.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_errors/overview_errors_count.tsx
index 582e6f1988076..aaebf3e4bb041 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_errors/overview_errors_count.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_errors/overview_errors_count.tsx
@@ -7,7 +7,7 @@
import { useKibana } from '@kbn/kibana-react-plugin/public';
import React, { useMemo } from 'react';
-import { ReportTypes } from '@kbn/observability-plugin/public';
+import { ReportTypes } from '@kbn/exploratory-view-plugin/public';
import { ERRORS_LABEL } from '../../../../monitor_details/monitor_summary/monitor_errors_count';
import { ClientPluginsStart } from '../../../../../../../plugin';
@@ -25,9 +25,9 @@ export const OverviewErrorsCount = ({
to,
locations,
}: MonitorErrorsCountProps) => {
- const { observability } = useKibana().services;
-
- const { ExploratoryViewEmbeddable } = observability;
+ const {
+ exploratoryView: { ExploratoryViewEmbeddable },
+ } = useKibana().services;
const time = useMemo(() => ({ from, to }), [from, to]);
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_errors/overview_errors_sparklines.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_errors/overview_errors_sparklines.tsx
index 1a68495f63b3e..b97e0eef8bbb5 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_errors/overview_errors_sparklines.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_errors/overview_errors_sparklines.tsx
@@ -18,9 +18,9 @@ interface Props {
locations?: string[];
}
export const OverviewErrorsSparklines = ({ from, to, monitorIds, locations }: Props) => {
- const { observability } = useKibana().services;
-
- const { ExploratoryViewEmbeddable } = observability;
+ const {
+ exploratoryView: { ExploratoryViewEmbeddable },
+ } = useKibana().services;
const { euiTheme } = useEuiTheme();
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/network_timings_breakdown.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/network_timings_breakdown.tsx
index cd9a89c7e5391..97c8564982cab 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/network_timings_breakdown.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/network_timings_breakdown.tsx
@@ -7,7 +7,7 @@
import React from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useParams } from 'react-router-dom';
-import { ReportTypes } from '@kbn/observability-plugin/public';
+import { ReportTypes } from '@kbn/exploratory-view-plugin/public';
import { EuiSpacer, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ClientPluginsStart } from '../../../../plugin';
@@ -15,9 +15,9 @@ import { useSelectedLocation } from '../monitor_details/hooks/use_selected_locat
import { LoadingState } from '../monitors_page/overview/overview/monitor_detail_flyout';
export const NetworkTimingsBreakdown = ({ monitorId }: { monitorId: string }) => {
- const { observability } = useKibana().services;
-
- const ExploratoryViewEmbeddable = observability.ExploratoryViewEmbeddable;
+ const {
+ exploratoryView: { ExploratoryViewEmbeddable },
+ } = useKibana().services;
const { stepIndex } = useParams<{ checkGroupId: string; stepIndex: string }>();
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_waterfall_chart/waterfall/waterfall_marker/waterfall_marker_test_helper.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_waterfall_chart/waterfall/waterfall_marker/waterfall_marker_test_helper.tsx
index 65df8f8e5a69f..b30cd29a0f065 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_waterfall_chart/waterfall/waterfall_marker/waterfall_marker_test_helper.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/step_details_page/step_waterfall_chart/waterfall/waterfall_marker/waterfall_marker_test_helper.tsx
@@ -43,7 +43,7 @@ export const TestWrapper = ({
),
},
}}
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx
index 5a01d61f9b78c..1b67c882869c7 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx
@@ -91,6 +91,7 @@ const Application = (props: SyntheticsAppProps) => {
inspector: startPlugins.inspector,
triggersActionsUi: startPlugins.triggersActionsUi,
observability: startPlugins.observability,
+ exploratoryView: startPlugins.exploratoryView,
cases: startPlugins.cases,
spaces: startPlugins.spaces,
fleet: startPlugins.fleet,
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/rtl_helpers.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/rtl_helpers.tsx
index c1c9d6072a404..ad79d6f293a45 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/rtl_helpers.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/rtl_helpers.tsx
@@ -141,6 +141,10 @@ export const mockCore: () => Partial = () => {
// @ts-ignore
PageTemplate: EuiPageTemplate,
},
+ },
+ exploratoryView: {
+ createExploratoryViewUrl: jest.fn(),
+ getAppDataView: jest.fn(),
ExploratoryViewEmbeddable: () => Embeddable exploratory view
,
},
};
@@ -164,6 +168,7 @@ export function MockKibanaProvider({
{children}
diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_app.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_app.tsx
index dc8c5918ec31a..df6f287f8ab03 100644
--- a/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_app.tsx
+++ b/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_app.tsx
@@ -124,6 +124,7 @@ const Application = (props: UptimeAppProps) => {
inspector: startPlugins.inspector,
triggersActionsUi: startPlugins.triggersActionsUi,
observability: startPlugins.observability,
+ exploratoryView: startPlugins.exploratoryView,
cases: startPlugins.cases,
}}
>
diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/synthetics/waterfall/components/waterfall_marker_test_helper.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/synthetics/waterfall/components/waterfall_marker_test_helper.tsx
index 691a74361d074..cc2022afaeb6d 100644
--- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/synthetics/waterfall/components/waterfall_marker_test_helper.tsx
+++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/synthetics/waterfall/components/waterfall_marker_test_helper.tsx
@@ -43,7 +43,7 @@ export const TestWrapper = ({
),
},
}}
diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/alerts/monitor_expressions/filters_expression_select.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/alerts/monitor_expressions/filters_expression_select.tsx
index 12b28352a503e..69b9c70993839 100644
--- a/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/alerts/monitor_expressions/filters_expression_select.tsx
+++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/alerts/monitor_expressions/filters_expression_select.tsx
@@ -7,7 +7,7 @@
import React, { useState } from 'react';
import { EuiButtonIcon, EuiExpression, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
-import { FieldValueSuggestions } from '@kbn/observability-plugin/public';
+import { FieldValueSuggestions } from '@kbn/exploratory-view-plugin/public';
import { filterLabels } from '../../filter_group/translations';
import { alertFilterLabels, filterAriaLabels } from './translations';
import { useUptimeDataView } from '../../../../contexts/uptime_data_view_context';
diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/filter_group/filter_group.test.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/filter_group/filter_group.test.tsx
index 706697357dab2..fd777f61ece53 100644
--- a/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/filter_group/filter_group.test.tsx
+++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/filter_group/filter_group.test.tsx
@@ -9,7 +9,7 @@ import React from 'react';
import { fireEvent, waitFor } from '@testing-library/react';
import { render } from '../../../lib/helper/rtl_helpers';
import { FilterGroup } from './filter_group';
-import * as Hooks from '@kbn/observability-plugin/public/hooks/use_values_list';
+import * as Hooks from '@kbn/exploratory-view-plugin/public/hooks/use_values_list';
describe('FilterGroup', () => {
it.each([
diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/filter_group/filter_group.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/filter_group/filter_group.tsx
index 5f10d29d4814f..5e90b1b7439d5 100644
--- a/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/filter_group/filter_group.tsx
+++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/filter_group/filter_group.tsx
@@ -9,7 +9,7 @@ import React, { useCallback, useState } from 'react';
import { EuiFilterGroup } from '@elastic/eui';
import styled from 'styled-components';
import { capitalize } from 'lodash';
-import { FieldValueSuggestions, useInspectorContext } from '@kbn/observability-plugin/public';
+import { FieldValueSuggestions, useInspectorContext } from '@kbn/exploratory-view-plugin/public';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import { useFilterUpdate } from '../../../hooks/use_filter_update';
import { useSelectedFilters } from '../../../hooks/use_selected_filters';
diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/synthetics/check_steps/stderr_logs.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/synthetics/check_steps/stderr_logs.tsx
index 145c1135c9356..c62e959380367 100644
--- a/x-pack/plugins/synthetics/public/legacy_uptime/components/synthetics/check_steps/stderr_logs.tsx
+++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/synthetics/check_steps/stderr_logs.tsx
@@ -67,13 +67,16 @@ export const StdErrorLogs = ({
const { items, loading } = useStdErrorLogs({ monitorId, checkGroup });
- const { discover, observability } = useKibana().services;
+ const { discover, exploratoryView } = useKibana().services;
const { settings } = useSelector(selectDynamicSettings);
const { data: discoverLink } = useFetcher(async () => {
if (settings?.heartbeatIndices) {
- const dataView = await observability.getAppDataView('synthetics', settings?.heartbeatIndices);
+ const dataView = await exploratoryView.getAppDataView(
+ 'synthetics',
+ settings?.heartbeatIndices
+ );
return discover.locator?.getUrl({
query: { language: 'kuery', query: `monitor.check_group: ${checkGroup}` },
indexPatternId: dataView?.id,
diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/synthetics/check_steps/step_field_trend.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/synthetics/check_steps/step_field_trend.tsx
index e40b24f53d096..533a3a4ca8d4c 100644
--- a/x-pack/plugins/synthetics/public/legacy_uptime/components/synthetics/check_steps/step_field_trend.tsx
+++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/synthetics/check_steps/step_field_trend.tsx
@@ -37,12 +37,11 @@ export function StepFieldTrend({
field: string;
step: JourneyStep;
}) {
- const { observability } = useUptimeStartPlugins();
+ const { exploratoryView } = useUptimeStartPlugins();
+ const ExploratoryViewEmbeddable = exploratoryView?.ExploratoryViewEmbeddable;
const indexSettings = useSelector(selectDynamicSettings);
- const EmbeddableExpView = observability!.ExploratoryViewEmbeddable;
-
const basePath = useKibana().services.http?.basePath?.get();
const allSeries: AllSeries = [
@@ -68,9 +67,9 @@ export function StepFieldTrend({
basePath
);
- return (
+ return ExploratoryViewEmbeddable ? (
-
- );
+ ) : null;
}
export const EXPLORE_LABEL = i18n.translate('xpack.synthetics.synthetics.markers.explore', {
diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/rtl_helpers.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/rtl_helpers.tsx
index a2288ff031e1d..6908ebb4677e3 100644
--- a/x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/rtl_helpers.tsx
+++ b/x-pack/plugins/synthetics/public/legacy_uptime/lib/helper/rtl_helpers.tsx
@@ -139,6 +139,10 @@ export const mockCore: () => Partial = () => {
// @ts-ignore
PageTemplate: EuiPageTemplate,
},
+ },
+ exploratoryView: {
+ createExploratoryViewUrl: jest.fn(),
+ getAppDataView: jest.fn(),
ExploratoryViewEmbeddable: () => Embeddable exploratory view
,
},
unifiedSearch: unifiedSearchPluginMock.createStartContract(),
@@ -163,6 +167,7 @@ export function MockKibanaProvider({
{children}
diff --git a/x-pack/plugins/synthetics/public/plugin.ts b/x-pack/plugins/synthetics/public/plugin.ts
index 5cd6943ea922a..5394a0ee6a0cd 100644
--- a/x-pack/plugins/synthetics/public/plugin.ts
+++ b/x-pack/plugins/synthetics/public/plugin.ts
@@ -20,7 +20,10 @@ import { DiscoverStart } from '@kbn/discover-plugin/public';
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/public';
import type { HomePublicPluginSetup } from '@kbn/home-plugin/public';
-import type { ExploratoryViewPublicSetup } from '@kbn/exploratory-view-plugin/public';
+import type {
+ ExploratoryViewPublicSetup,
+ ExploratoryViewPublicStart,
+} from '@kbn/exploratory-view-plugin/public';
import { EmbeddableStart } from '@kbn/embeddable-plugin/public';
import {
TriggersAndActionsUIPublicPluginSetup,
@@ -77,6 +80,7 @@ export interface ClientPluginsStart {
discover: DiscoverStart;
inspector: InspectorPluginStart;
embeddable: EmbeddableStart;
+ exploratoryView: ExploratoryViewPublicStart;
observability: ObservabilityPublicStart;
share: SharePluginStart;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
From 3754d6588053f10cd527ddaeb9d55204a769b93c Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Tue, 4 Apr 2023 14:41:51 +0300
Subject: [PATCH 009/112] [Lens] Supports icon in the new metric (#154210)
## Summary
Closes https://github.com/elastic/kibana/issues/129229
Adds support for icon on the new metric.
Also it moves the IconSelect dropdown to the shared components (from XY)
as it now reused from both visualizations.
### Checklist
- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
---
.../expression_metric/common/constants.ts | 21 ++++
.../metric_vis_function.ts | 7 ++
.../expression_metric/common/index.ts | 1 +
.../common/types/expression_functions.ts | 10 +-
.../common/types/expression_renderers.ts | 1 +
.../public/components/metric_vis.test.tsx | 21 ++++
.../public/components/metric_vis.tsx | 8 +-
.../icon_select}/icon_select.tsx | 0
.../lens/public/shared_components/index.ts | 1 +
.../metric/dimension_editor.test.tsx | 1 +
.../metric/dimension_editor.tsx | 20 +++
.../public/visualizations/metric/icon_set.ts | 118 ++++++++++++++++++
.../visualizations/metric/to_expression.ts | 1 +
.../visualizations/metric/toolbar.test.tsx | 1 +
.../metric/visualization.test.ts | 8 ++
.../visualizations/metric/visualization.tsx | 1 +
.../public/visualizations/xy/to_expression.ts | 2 +-
.../annotations_config_panel/icon_set.ts | 2 +-
.../reference_line_config_panel/icon_set.ts | 2 +-
.../shared/marker_decoration_settings.tsx | 6 +-
20 files changed, 226 insertions(+), 6 deletions(-)
rename x-pack/plugins/lens/public/{visualizations/xy/xy_config_panel/shared => shared_components/icon_select}/icon_select.tsx (100%)
create mode 100644 x-pack/plugins/lens/public/visualizations/metric/icon_set.ts
diff --git a/src/plugins/chart_expressions/expression_metric/common/constants.ts b/src/plugins/chart_expressions/expression_metric/common/constants.ts
index 7e81bc1dddbda..39ac8eebaecdb 100644
--- a/src/plugins/chart_expressions/expression_metric/common/constants.ts
+++ b/src/plugins/chart_expressions/expression_metric/common/constants.ts
@@ -15,3 +15,24 @@ export const LabelPosition = {
BOTTOM: 'bottom',
TOP: 'top',
} as const;
+
+export const AvailableMetricIcons = {
+ EMPTY: 'empty',
+ SORTUP: 'sortUp',
+ SORTDOWN: 'sortDown',
+ COMPUTE: 'compute',
+ ASTERISK: 'asterisk',
+ ALERT: 'alert',
+ BELL: 'bell',
+ BOLT: 'bolt',
+ BUG: 'bug',
+ EDITOR_COMMENT: 'editorComment',
+ FLAG: 'flag',
+ HEART: 'heart',
+ MAP_MARKER: 'mapMarker',
+ PIN: 'pin',
+ STAR_EMPTY: 'starEmpty',
+ TAG: 'tag',
+ GLOBE: 'globe',
+ TEMPERATURE: 'temperature',
+} as const;
diff --git a/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts b/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts
index c5be73ab0b73c..d75bed1f00c34 100644
--- a/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts
+++ b/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts
@@ -84,6 +84,12 @@ export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({
defaultMessage: 'Provides a static visualization color. Overridden by palette.',
}),
},
+ icon: {
+ types: ['string'],
+ help: i18n.translate('expressionMetricVis.function.icon.help', {
+ defaultMessage: 'Provides a static visualization icon.',
+ }),
+ },
palette: {
types: ['palette'],
help: i18n.translate('expressionMetricVis.function.palette.help', {
@@ -181,6 +187,7 @@ export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({
subtitle: args.subtitle,
secondaryPrefix: args.secondaryPrefix,
color: args.color,
+ icon: args.icon,
palette: args.palette?.params,
progressDirection: args.progressDirection,
maxCols: args.maxCols,
diff --git a/src/plugins/chart_expressions/expression_metric/common/index.ts b/src/plugins/chart_expressions/expression_metric/common/index.ts
index ae8f3b9fae7a2..d15b491f41873 100755
--- a/src/plugins/chart_expressions/expression_metric/common/index.ts
+++ b/src/plugins/chart_expressions/expression_metric/common/index.ts
@@ -19,6 +19,7 @@ export type {
MetricVisParam,
VisParams,
MetricOptions,
+ AvailableMetricIcon,
} from './types';
export { metricVisFunction } from './expression_functions';
diff --git a/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts
index 2440ef597c0bd..f03dab2e81435 100644
--- a/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts
+++ b/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts
@@ -8,6 +8,7 @@
import type { PaletteOutput } from '@kbn/coloring';
import { LayoutDirection, MetricWTrend } from '@elastic/charts';
+import { $Values } from '@kbn/utility-types';
import {
Datatable,
ExpressionFunctionDefinition,
@@ -16,7 +17,13 @@ import {
import { ExpressionValueVisDimension, prepareLogTable } from '@kbn/visualizations-plugin/common';
import type { AllowedSettingsOverrides, CustomPaletteState } from '@kbn/charts-plugin/common';
import { VisParams, visType } from './expression_renderers';
-import { EXPRESSION_METRIC_NAME, EXPRESSION_METRIC_TRENDLINE_NAME } from '../constants';
+import {
+ EXPRESSION_METRIC_NAME,
+ EXPRESSION_METRIC_TRENDLINE_NAME,
+ AvailableMetricIcons,
+} from '../constants';
+
+export type AvailableMetricIcon = $Values;
export interface MetricArguments {
metric: ExpressionValueVisDimension | string;
@@ -28,6 +35,7 @@ export interface MetricArguments {
secondaryPrefix?: string;
progressDirection: LayoutDirection;
color?: string;
+ icon?: string;
palette?: PaletteOutput;
maxCols: number;
minTiles?: number;
diff --git a/src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts
index 48b4b4ce0f524..bbebb06bc8e7c 100644
--- a/src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts
+++ b/src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts
@@ -24,6 +24,7 @@ export interface MetricVisParam {
subtitle?: string;
secondaryPrefix?: string;
color?: string;
+ icon?: string;
palette?: CustomPaletteState;
progressDirection: LayoutDirection;
maxCols: number;
diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx
index d10d1e39f3544..c4b130aa3e507 100644
--- a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx
+++ b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx
@@ -232,6 +232,7 @@ describe('MetricVisComponent', function () {
metric: {
progressDirection: 'vertical',
maxCols: 5,
+ icon: 'empty',
},
dimensions: {
metric: basePriceColumnId,
@@ -252,6 +253,7 @@ describe('MetricVisComponent', function () {
Object {
"color": "#f5f7fa",
"extra": ,
+ "icon": [Function],
"subtitle": undefined,
"title": "Median products.base_price",
"value": 28.984375,
@@ -314,6 +316,7 @@ describe('MetricVisComponent', function () {
secondary prefix
13.63
,
+ "icon": [Function],
"subtitle": "subtitle",
"title": "Median products.base_price",
"value": 28.984375,
@@ -360,6 +363,7 @@ describe('MetricVisComponent', function () {
"color": "#f5f7fa",
"domainMax": 28.984375,
"extra": ,
+ "icon": [Function],
"progressBarDirection": "vertical",
"subtitle": undefined,
"title": "Median products.base_price",
@@ -435,6 +439,7 @@ describe('MetricVisComponent', function () {
Object {
"color": "#f5f7fa",
"extra": ,
+ "icon": undefined,
"subtitle": "Median products.base_price",
"title": "Friday",
"value": 28.984375,
@@ -443,6 +448,7 @@ describe('MetricVisComponent', function () {
Object {
"color": "#f5f7fa",
"extra": ,
+ "icon": undefined,
"subtitle": "Median products.base_price",
"title": "Wednesday",
"value": 28.984375,
@@ -451,6 +457,7 @@ describe('MetricVisComponent', function () {
Object {
"color": "#f5f7fa",
"extra": ,
+ "icon": undefined,
"subtitle": "Median products.base_price",
"title": "Saturday",
"value": 25.984375,
@@ -459,6 +466,7 @@ describe('MetricVisComponent', function () {
Object {
"color": "#f5f7fa",
"extra": ,
+ "icon": undefined,
"subtitle": "Median products.base_price",
"title": "Sunday",
"value": 25.784375,
@@ -467,6 +475,7 @@ describe('MetricVisComponent', function () {
Object {
"color": "#f5f7fa",
"extra": ,
+ "icon": undefined,
"subtitle": "Median products.base_price",
"title": "Thursday",
"value": 25.348011363636363,
@@ -595,6 +604,7 @@ describe('MetricVisComponent', function () {
Object {
"color": "#f5f7fa",
"extra": ,
+ "icon": undefined,
"subtitle": "Median products.base_price",
"title": "Friday",
"value": 28.984375,
@@ -603,6 +613,7 @@ describe('MetricVisComponent', function () {
Object {
"color": "#f5f7fa",
"extra": ,
+ "icon": undefined,
"subtitle": "Median products.base_price",
"title": "Wednesday",
"value": 28.984375,
@@ -611,6 +622,7 @@ describe('MetricVisComponent', function () {
Object {
"color": "#f5f7fa",
"extra": ,
+ "icon": undefined,
"subtitle": "Median products.base_price",
"title": "Saturday",
"value": 25.984375,
@@ -619,6 +631,7 @@ describe('MetricVisComponent', function () {
Object {
"color": "#f5f7fa",
"extra": ,
+ "icon": undefined,
"subtitle": "Median products.base_price",
"title": "Sunday",
"value": 25.784375,
@@ -627,6 +640,7 @@ describe('MetricVisComponent', function () {
Object {
"color": "#f5f7fa",
"extra": ,
+ "icon": undefined,
"subtitle": "Median products.base_price",
"title": "Thursday",
"value": 25.348011363636363,
@@ -637,6 +651,7 @@ describe('MetricVisComponent', function () {
Object {
"color": "#f5f7fa",
"extra": ,
+ "icon": undefined,
"subtitle": "Median products.base_price",
"title": "Other",
"value": 24.984375,
@@ -678,6 +693,7 @@ describe('MetricVisComponent', function () {
"color": "#f5f7fa",
"domainMax": 28.984375,
"extra": ,
+ "icon": undefined,
"progressBarDirection": "vertical",
"subtitle": "Median products.base_price",
"title": "Friday",
@@ -688,6 +704,7 @@ describe('MetricVisComponent', function () {
"color": "#f5f7fa",
"domainMax": 28.984375,
"extra": ,
+ "icon": undefined,
"progressBarDirection": "vertical",
"subtitle": "Median products.base_price",
"title": "Wednesday",
@@ -698,6 +715,7 @@ describe('MetricVisComponent', function () {
"color": "#f5f7fa",
"domainMax": 25.984375,
"extra": ,
+ "icon": undefined,
"progressBarDirection": "vertical",
"subtitle": "Median products.base_price",
"title": "Saturday",
@@ -708,6 +726,7 @@ describe('MetricVisComponent', function () {
"color": "#f5f7fa",
"domainMax": 25.784375,
"extra": ,
+ "icon": undefined,
"progressBarDirection": "vertical",
"subtitle": "Median products.base_price",
"title": "Sunday",
@@ -718,6 +737,7 @@ describe('MetricVisComponent', function () {
"color": "#f5f7fa",
"domainMax": 25.348011363636363,
"extra": ,
+ "icon": undefined,
"progressBarDirection": "vertical",
"subtitle": "Median products.base_price",
"title": "Thursday",
@@ -730,6 +750,7 @@ describe('MetricVisComponent', function () {
"color": "#f5f7fa",
"domainMax": 24.984375,
"extra": ,
+ "icon": undefined,
"progressBarDirection": "vertical",
"subtitle": "Median products.base_price",
"title": "Other",
diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx
index 6bac88177bf50..d20cccb46617f 100644
--- a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx
+++ b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx
@@ -36,7 +36,7 @@ import type { FieldFormatConvertFunction } from '@kbn/field-formats-plugin/commo
import { CUSTOM_PALETTE } from '@kbn/coloring';
import { css } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';
-import { useResizeObserver, useEuiScrollBar } from '@elastic/eui';
+import { useResizeObserver, useEuiScrollBar, EuiIcon } from '@elastic/eui';
import { AllowedSettingsOverrides } from '@kbn/charts-plugin/common';
import { getOverridesFor } from '@kbn/chart-expressions-common';
import { DEFAULT_TRENDLINE_NAME } from '../../common/constants';
@@ -173,6 +173,11 @@ const buildFilterEvent = (rowIdx: number, columnIdx: number, table: Datatable) =
};
};
+const getIcon =
+ (type: string) =>
+ ({ width, height, color }: { width: number; height: number; color: string }) =>
+ ;
+
export interface MetricVisComponentProps {
data: Datatable;
config: Pick;
@@ -229,6 +234,7 @@ export const MetricVis = ({
valueFormatter: formatPrimaryMetric,
title,
subtitle,
+ icon: config.metric?.icon ? getIcon(config.metric?.icon) : undefined,
extra: (
{secondaryPrefix}
diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/shared/icon_select.tsx b/x-pack/plugins/lens/public/shared_components/icon_select/icon_select.tsx
similarity index 100%
rename from x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/shared/icon_select.tsx
rename to x-pack/plugins/lens/public/shared_components/icon_select/icon_select.tsx
diff --git a/x-pack/plugins/lens/public/shared_components/index.ts b/x-pack/plugins/lens/public/shared_components/index.ts
index 1b2b9e52a1eee..95000c5c4248b 100644
--- a/x-pack/plugins/lens/public/shared_components/index.ts
+++ b/x-pack/plugins/lens/public/shared_components/index.ts
@@ -41,3 +41,4 @@ export { DimensionEditorSection } from './dimension_section';
export { FilterQueryInput } from './filter_query_input';
export * from './static_header';
export * from './vis_label';
+export { IconSelect } from './icon_select/icon_select';
diff --git a/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.test.tsx b/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.test.tsx
index b45eef4ec379e..346b89666dc22 100644
--- a/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.test.tsx
+++ b/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.test.tsx
@@ -74,6 +74,7 @@ describe('dimension editor', () => {
maxCols: 5,
color: 'static-color',
palette,
+ icon: 'tag',
showBar: true,
trendlineLayerId: 'second',
trendlineLayerType: 'metricTrendline',
diff --git a/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx
index 6571e1bb2c7d6..a1180e0055eb2 100644
--- a/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx
+++ b/x-pack/plugins/lens/public/visualizations/metric/dimension_editor.tsx
@@ -38,6 +38,7 @@ import {
applyPaletteParams,
PalettePanelContainer,
useDebouncedValue,
+ IconSelect,
} from '../../shared_components';
import type { VisualizationDimensionEditorProps } from '../../types';
import { defaultNumberPaletteParams, defaultPercentagePaletteParams } from './palette_config';
@@ -49,6 +50,7 @@ import {
} from './visualization';
import { CollapseSetting } from '../../shared_components/collapse_setting';
import { DebouncedInput } from '../../shared_components/debounced_input';
+import { iconsSet } from './icon_set';
export type SupportingVisType = 'none' | 'bar' | 'trendline';
@@ -381,6 +383,24 @@ function PrimaryMetricEditor(props: SubProps) {
)}
+
+ {
+ setState({
+ ...state,
+ icon: newIcon,
+ });
+ }}
+ />
+
>
);
}
diff --git a/x-pack/plugins/lens/public/visualizations/metric/icon_set.ts b/x-pack/plugins/lens/public/visualizations/metric/icon_set.ts
new file mode 100644
index 0000000000000..c21e6809dd0b4
--- /dev/null
+++ b/x-pack/plugins/lens/public/visualizations/metric/icon_set.ts
@@ -0,0 +1,118 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { AvailableMetricIcon } from '@kbn/expression-metric-vis-plugin/common';
+import { IconSet } from '../../shared_components/icon_select/icon_select';
+
+export const iconsSet: IconSet = [
+ {
+ value: 'empty',
+ label: i18n.translate('xpack.lens.metric.iconSelect.noIconLabel', {
+ defaultMessage: 'None',
+ }),
+ },
+ {
+ value: 'sortUp',
+ label: i18n.translate('xpack.lens.metric.iconSelect.sortUpLabel', {
+ defaultMessage: 'Sort up',
+ }),
+ },
+ {
+ value: 'sortDown',
+ label: i18n.translate('xpack.lens.metric.iconSelect.sortDownLabel', {
+ defaultMessage: 'Sort down',
+ }),
+ },
+ {
+ value: 'compute',
+ label: i18n.translate('xpack.lens.metric.iconSelect.computeLabel', {
+ defaultMessage: 'Compute',
+ }),
+ },
+ {
+ value: 'globe',
+ label: i18n.translate('xpack.lens.metric.iconSelect.globeLabel', {
+ defaultMessage: 'Globe',
+ }),
+ },
+ {
+ value: 'temperature',
+ label: i18n.translate('xpack.lens.metric.iconSelect.temperatureLabel', {
+ defaultMessage: 'Temperature',
+ }),
+ },
+ {
+ value: 'asterisk',
+ label: i18n.translate('xpack.lens.metric.iconSelect.asteriskIconLabel', {
+ defaultMessage: 'Asterisk',
+ }),
+ },
+ {
+ value: 'alert',
+ label: i18n.translate('xpack.lens.metric.iconSelect.alertIconLabel', {
+ defaultMessage: 'Alert',
+ }),
+ },
+ {
+ value: 'bell',
+ label: i18n.translate('xpack.lens.metric.iconSelect.bellIconLabel', {
+ defaultMessage: 'Bell',
+ }),
+ },
+ {
+ value: 'bolt',
+ label: i18n.translate('xpack.lens.metric.iconSelect.boltIconLabel', {
+ defaultMessage: 'Bolt',
+ }),
+ },
+ {
+ value: 'bug',
+ label: i18n.translate('xpack.lens.metric.iconSelect.bugIconLabel', {
+ defaultMessage: 'Bug',
+ }),
+ },
+
+ {
+ value: 'editorComment',
+ label: i18n.translate('xpack.lens.metric.iconSelect.commentIconLabel', {
+ defaultMessage: 'Comment',
+ }),
+ },
+ {
+ value: 'flag',
+ label: i18n.translate('xpack.lens.metric.iconSelect.flagIconLabel', {
+ defaultMessage: 'Flag',
+ }),
+ },
+ {
+ value: 'heart',
+ label: i18n.translate('xpack.lens.metric.iconSelect.heartLabel', { defaultMessage: 'Heart' }),
+ },
+ {
+ value: 'mapMarker',
+ label: i18n.translate('xpack.lens.metric.iconSelect.mapMarkerLabel', {
+ defaultMessage: 'Map Marker',
+ }),
+ },
+ {
+ value: 'pin',
+ label: i18n.translate('xpack.lens.metric.iconSelect.mapPinLabel', {
+ defaultMessage: 'Map Pin',
+ }),
+ },
+ {
+ value: 'starEmpty',
+ label: i18n.translate('xpack.lens.metric.iconSelect.starLabel', { defaultMessage: 'Star' }),
+ },
+ {
+ value: 'tag',
+ label: i18n.translate('xpack.lens.metric.iconSelect.tagIconLabel', {
+ defaultMessage: 'Tag',
+ }),
+ },
+];
diff --git a/x-pack/plugins/lens/public/visualizations/metric/to_expression.ts b/x-pack/plugins/lens/public/visualizations/metric/to_expression.ts
index 1326f5369cbd2..b2367bb7c1fc8 100644
--- a/x-pack/plugins/lens/public/visualizations/metric/to_expression.ts
+++ b/x-pack/plugins/lens/public/visualizations/metric/to_expression.ts
@@ -143,6 +143,7 @@ export const toExpression = (
subtitle: state.subtitle ?? undefined,
progressDirection: state.progressDirection as LayoutDirection,
color: state.color || getDefaultColor(state),
+ icon: state.icon,
palette: state.palette?.params
? [
paletteService
diff --git a/x-pack/plugins/lens/public/visualizations/metric/toolbar.test.tsx b/x-pack/plugins/lens/public/visualizations/metric/toolbar.test.tsx
index d40519037547b..81b2be0d66020 100644
--- a/x-pack/plugins/lens/public/visualizations/metric/toolbar.test.tsx
+++ b/x-pack/plugins/lens/public/visualizations/metric/toolbar.test.tsx
@@ -46,6 +46,7 @@ describe('metric toolbar', () => {
progressDirection: 'vertical',
maxCols: 5,
color: 'static-color',
+ icon: 'compute',
palette,
showBar: true,
trendlineLayerId: 'second',
diff --git a/x-pack/plugins/lens/public/visualizations/metric/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/metric/visualization.test.ts
index 32f6ecf38ab8f..61bea94f43ed1 100644
--- a/x-pack/plugins/lens/public/visualizations/metric/visualization.test.ts
+++ b/x-pack/plugins/lens/public/visualizations/metric/visualization.test.ts
@@ -68,6 +68,7 @@ describe('metric visualization', () => {
breakdownByAccessor: 'breakdown-col-id',
collapseFn: 'sum',
subtitle: 'subtitle',
+ icon: 'empty',
secondaryPrefix: 'extra-text',
progressDirection: 'vertical',
maxCols: 5,
@@ -303,6 +304,9 @@ describe('metric visualization', () => {
"color": Array [
"static-color",
],
+ "icon": Array [
+ "empty",
+ ],
"inspectorTableId": Array [
"first",
],
@@ -364,6 +368,9 @@ describe('metric visualization', () => {
"color": Array [
"static-color",
],
+ "icon": Array [
+ "empty",
+ ],
"inspectorTableId": Array [
"first",
],
@@ -746,6 +753,7 @@ describe('metric visualization', () => {
it('clears a layer', () => {
expect(visualization.clearLayer(fullState, 'some-id', 'indexPattern1')).toMatchInlineSnapshot(`
Object {
+ "icon": "empty",
"layerId": "first",
"layerType": "data",
}
diff --git a/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx b/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx
index a210b04a6e07c..b44f783cb83ef 100644
--- a/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx
+++ b/x-pack/plugins/lens/public/visualizations/metric/visualization.tsx
@@ -61,6 +61,7 @@ export interface MetricVisualizationState {
progressDirection?: LayoutDirection;
showBar?: boolean;
color?: string;
+ icon?: string;
palette?: PaletteOutput;
maxCols?: number;
diff --git a/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts b/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts
index 4e33e35371246..85bd512fa4480 100644
--- a/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts
+++ b/x-pack/plugins/lens/public/visualizations/xy/to_expression.ts
@@ -49,7 +49,7 @@ import type {
} from './types';
import type { OperationMetadata, DatasourcePublicAPI, DatasourceLayers } from '../../types';
import { getColumnToLabelMap } from './state_helpers';
-import { hasIcon } from './xy_config_panel/shared/icon_select';
+import { hasIcon } from '../../shared_components/icon_select/icon_select';
import { defaultReferenceLineColor } from './color_assignment';
import { getDefaultVisualValuesForLayer } from '../../shared_components/datasource_default_values';
import {
diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/icon_set.ts b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/icon_set.ts
index 45019dd204491..436bb84b92e7b 100644
--- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/icon_set.ts
+++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/icon_set.ts
@@ -8,7 +8,7 @@
import { i18n } from '@kbn/i18n';
import { AvailableAnnotationIcon } from '@kbn/event-annotation-plugin/common';
import { IconTriangle, IconCircle } from '@kbn/chart-icons';
-import { IconSet } from '../shared/icon_select';
+import { IconSet } from '../../../../shared_components/icon_select/icon_select';
export const annotationsIconSet: IconSet = [
{
diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/reference_line_config_panel/icon_set.ts b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/reference_line_config_panel/icon_set.ts
index eda5d06cd3ef1..d64fde37ebd25 100644
--- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/reference_line_config_panel/icon_set.ts
+++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/reference_line_config_panel/icon_set.ts
@@ -7,7 +7,7 @@
import { i18n } from '@kbn/i18n';
import { AvailableReferenceLineIcon } from '@kbn/expression-xy-plugin/common';
-import { IconSet } from '../shared/icon_select';
+import { IconSet } from '../../../../shared_components/icon_select/icon_select';
export const referenceLineIconsSet: IconSet = [
{
diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/shared/marker_decoration_settings.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/shared/marker_decoration_settings.tsx
index 4d70a99bc4192..8b7fba475dc2e 100644
--- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/shared/marker_decoration_settings.tsx
+++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/shared/marker_decoration_settings.tsx
@@ -12,7 +12,11 @@ import { IconPosition } from '@kbn/expression-xy-plugin/common';
import { YAxisMode } from '../../types';
import { TooltipWrapper } from '../../../../shared_components';
-import { hasIcon, IconSelect, IconSet } from './icon_select';
+import {
+ hasIcon,
+ IconSelect,
+ IconSet,
+} from '../../../../shared_components/icon_select/icon_select';
import { idPrefix } from '../dimension_editor';
interface LabelConfigurationOptions {
From 098c3fe2ed1e515b296e02df1da761bfc2a0688e Mon Sep 17 00:00:00 2001
From: Dima Arnautov
Date: Tue, 4 Apr 2023 13:51:11 +0200
Subject: [PATCH 010/112] [ML] Fix relative time range for Change Point
Detection (#154313)
## Summary
Fixes support for relative time bounds
### Checklist
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
---
.../change_point_detection_context.tsx | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detection_context.tsx b/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detection_context.tsx
index a30ef3cc4a997..772e9c2794da0 100644
--- a/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detection_context.tsx
+++ b/x-pack/plugins/aiops/public/components/change_point_detection/change_point_detection_context.tsx
@@ -19,7 +19,6 @@ import { startWith } from 'rxjs';
import type { Filter, Query } from '@kbn/es-query';
import { usePageUrlState } from '@kbn/ml-url-state';
import { useTimefilter, useTimeRangeUpdates } from '@kbn/ml-date-picker';
-import moment from 'moment';
import { ES_FIELD_TYPES } from '@kbn/field-types';
import { DEFAULT_AGG_FUNCTION } from './constants';
import { useSplitFieldCardinality } from './use_split_field_cardinality';
@@ -238,8 +237,8 @@ export const ChangePointDetectionContextProvider: FC = ({ children }) => {
mergedQuery.bool!.filter.push({
range: {
[dataView.timeFieldName!]: {
- from: moment(timeRange.from).valueOf(),
- to: moment(timeRange.to).valueOf(),
+ from: timeRange.from,
+ to: timeRange.to,
},
},
});
From 4cd0460f5a9595ba09e6da5a85e2512578a604da Mon Sep 17 00:00:00 2001
From: Christos Nasikas
Date: Tue, 4 Apr 2023 15:24:23 +0300
Subject: [PATCH 011/112] [Cases ]Expose the bulk get cases API from the cases
UI client (#154235)
## Summary
This PR exposes the bulk get cases API from the cases UI client.
Fixes: https://github.com/elastic/kibana/issues/153926
### Checklist
Delete any items that are not applicable to this PR.
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
### For maintainers
- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
---------
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
x-pack/plugins/cases/public/api/decoders.ts | 14 +++++++++
x-pack/plugins/cases/public/api/index.test.ts | 31 +++++++++++++++++--
x-pack/plugins/cases/public/api/index.ts | 29 ++++++++++++++++-
.../cases/public/client/api/index.test.ts | 30 +++++++++++++++++-
.../plugins/cases/public/client/api/index.ts | 3 +-
x-pack/plugins/cases/public/mocks.ts | 7 ++++-
x-pack/plugins/cases/public/types.ts | 7 +++++
.../cases/server/client/cases/client.ts | 4 +--
8 files changed, 117 insertions(+), 8 deletions(-)
diff --git a/x-pack/plugins/cases/public/api/decoders.ts b/x-pack/plugins/cases/public/api/decoders.ts
index 1022e0583f8dc..6402b2d56a342 100644
--- a/x-pack/plugins/cases/public/api/decoders.ts
+++ b/x-pack/plugins/cases/public/api/decoders.ts
@@ -15,10 +15,14 @@ import type {
CasesFindResponse,
CasesStatusResponse,
CasesMetricsResponse,
+ CasesBulkGetResponseCertainFields,
+ CaseResponse,
} from '../../common/api';
import {
CasesFindResponseRt,
CasesStatusResponseRt,
+ CasesResponseRt,
+ getTypeForCertainFieldsFromArray,
CasesMetricsResponseRt,
} from '../../common/api';
@@ -36,3 +40,13 @@ export const decodeCasesMetricsResponse = (metrics?: CasesMetricsResponse) =>
CasesMetricsResponseRt.decode(metrics),
fold(throwErrors(createToasterPlainError), identity)
);
+
+export const decodeCasesBulkGetResponse = (
+ res: CasesBulkGetResponseCertainFields,
+ fields?: string[]
+) => {
+ const typeToDecode = getTypeForCertainFieldsFromArray(CasesResponseRt, fields);
+ pipe(typeToDecode.decode(res.cases), fold(throwErrors(createToasterPlainError), identity));
+
+ return res;
+};
diff --git a/x-pack/plugins/cases/public/api/index.test.ts b/x-pack/plugins/cases/public/api/index.test.ts
index 321a1db206846..c64a204183bed 100644
--- a/x-pack/plugins/cases/public/api/index.test.ts
+++ b/x-pack/plugins/cases/public/api/index.test.ts
@@ -6,8 +6,8 @@
*/
import { httpServiceMock } from '@kbn/core/public/mocks';
-import { getCases, getCasesMetrics } from '.';
-import { allCases, allCasesSnake } from '../containers/mock';
+import { bulkGetCases, getCases, getCasesMetrics } from '.';
+import { allCases, allCasesSnake, casesSnake } from '../containers/mock';
describe('api', () => {
beforeEach(() => {
@@ -47,4 +47,31 @@ describe('api', () => {
});
});
});
+
+ describe('bulkGetCases', () => {
+ const http = httpServiceMock.createStartContract({ basePath: '' });
+ http.post.mockResolvedValue({ cases: [{ title: 'test' }], errors: [] });
+
+ it('should return the correct cases with a subset of fields', async () => {
+ expect(await bulkGetCases({ http, params: { ids: ['test'], fields: ['title'] } })).toEqual({
+ cases: [{ title: 'test' }],
+ errors: [],
+ });
+ });
+
+ it('should return the correct cases with all fields', async () => {
+ http.post.mockResolvedValueOnce({ cases: casesSnake, errors: [] });
+ expect(await bulkGetCases({ http, params: { ids: ['test'] } })).toEqual({
+ cases: casesSnake,
+ errors: [],
+ });
+ });
+
+ it('should have been called with the correct path', async () => {
+ await bulkGetCases({ http, params: { ids: ['test'], fields: ['title'] } });
+ expect(http.post).toHaveBeenCalledWith('/internal/cases/_bulk_get', {
+ body: '{"ids":["test"],"fields":["title"]}',
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/cases/public/api/index.ts b/x-pack/plugins/cases/public/api/index.ts
index 47cc0b6f108a8..c89b1ff94f9e9 100644
--- a/x-pack/plugins/cases/public/api/index.ts
+++ b/x-pack/plugins/cases/public/api/index.ts
@@ -7,8 +7,16 @@
import type { HttpStart } from '@kbn/core/public';
import type { Cases, CasesStatus, CasesMetrics } from '../../common/ui';
-import { CASE_FIND_URL, CASE_METRICS_URL, CASE_STATUS_URL } from '../../common/constants';
+import {
+ CASE_FIND_URL,
+ CASE_METRICS_URL,
+ CASE_STATUS_URL,
+ INTERNAL_BULK_GET_CASES_URL,
+} from '../../common/constants';
import type {
+ CaseResponse,
+ CasesBulkGetRequestCertainFields,
+ CasesBulkGetResponseCertainFields,
CasesFindRequest,
CasesFindResponse,
CasesMetricsRequest,
@@ -18,6 +26,7 @@ import type {
} from '../../common/api';
import { convertAllCasesToCamel, convertToCamelCase } from './utils';
import {
+ decodeCasesBulkGetResponse,
decodeCasesFindResponse,
decodeCasesMetricsResponse,
decodeCasesStatusResponse,
@@ -58,3 +67,21 @@ export const getCasesMetrics = async ({
const res = await http.get(CASE_METRICS_URL, { signal, query });
return convertToCamelCase(decodeCasesMetricsResponse(res));
};
+
+export const bulkGetCases = async ({
+ http,
+ signal,
+ params,
+}: HTTPService & { params: CasesBulkGetRequestCertainFields }): Promise<
+ CasesBulkGetResponseCertainFields
+> => {
+ const res = await http.post>(
+ INTERNAL_BULK_GET_CASES_URL,
+ {
+ body: JSON.stringify({ ...params }),
+ signal,
+ }
+ );
+
+ return decodeCasesBulkGetResponse(res, params.fields);
+};
diff --git a/x-pack/plugins/cases/public/client/api/index.test.ts b/x-pack/plugins/cases/public/client/api/index.test.ts
index dacea3350bd4a..f53de5eb20f18 100644
--- a/x-pack/plugins/cases/public/client/api/index.test.ts
+++ b/x-pack/plugins/cases/public/client/api/index.test.ts
@@ -7,7 +7,7 @@
import { httpServiceMock } from '@kbn/core/public/mocks';
import { createClientAPI } from '.';
-import { allCases, allCasesSnake } from '../../containers/mock';
+import { allCases, allCasesSnake, casesSnake } from '../../containers/mock';
describe('createClientAPI', () => {
beforeEach(() => {
@@ -80,5 +80,33 @@ describe('createClientAPI', () => {
});
});
});
+
+ describe('bulkGet', () => {
+ const http = httpServiceMock.createStartContract({ basePath: '' });
+ const api = createClientAPI({ http });
+ http.post.mockResolvedValue({ cases: [{ title: 'test' }], errors: [] });
+
+ it('should return the correct cases with a subset of fields', async () => {
+ expect(await api.cases.bulkGet({ ids: ['test'], fields: ['title'] })).toEqual({
+ cases: [{ title: 'test' }],
+ errors: [],
+ });
+ });
+
+ it('should return the correct cases with all fields', async () => {
+ http.post.mockResolvedValueOnce({ cases: casesSnake, errors: [] });
+ expect(await api.cases.bulkGet({ ids: ['test'], fields: ['title'] })).toEqual({
+ cases: casesSnake,
+ errors: [],
+ });
+ });
+
+ it('should have been called with the correct path', async () => {
+ await api.cases.bulkGet({ ids: ['test'], fields: ['title'] });
+ expect(http.post).toHaveBeenCalledWith('/internal/cases/_bulk_get', {
+ body: '{"ids":["test"],"fields":["title"]}',
+ });
+ });
+ });
});
});
diff --git a/x-pack/plugins/cases/public/client/api/index.ts b/x-pack/plugins/cases/public/client/api/index.ts
index 6d902940c2200..09a172121d08c 100644
--- a/x-pack/plugins/cases/public/client/api/index.ts
+++ b/x-pack/plugins/cases/public/client/api/index.ts
@@ -15,7 +15,7 @@ import type {
} from '../../../common/api';
import { getCasesFromAlertsUrl } from '../../../common/api';
import type { Cases, CasesStatus, CasesMetrics } from '../../../common/ui';
-import { getCases, getCasesMetrics, getCasesStatus } from '../../api';
+import { bulkGetCases, getCases, getCasesMetrics, getCasesStatus } from '../../api';
import type { CasesUiStart } from '../../types';
export const createClientAPI = ({ http }: { http: HttpStart }): CasesUiStart['api'] => {
@@ -32,6 +32,7 @@ export const createClientAPI = ({ http }: { http: HttpStart }): CasesUiStart['ap
getCasesStatus({ http, query, signal }),
getCasesMetrics: (query: CasesMetricsRequest, signal?: AbortSignal): Promise =>
getCasesMetrics({ http, signal, query }),
+ bulkGet: (params, signal?: AbortSignal) => bulkGetCases({ http, signal, params }),
},
};
};
diff --git a/x-pack/plugins/cases/public/mocks.ts b/x-pack/plugins/cases/public/mocks.ts
index b16e4895a6463..2c4a653254195 100644
--- a/x-pack/plugins/cases/public/mocks.ts
+++ b/x-pack/plugins/cases/public/mocks.ts
@@ -10,7 +10,12 @@ import type { CasesUiStart } from './types';
const apiMock: jest.Mocked = {
getRelatedCases: jest.fn(),
- cases: { find: jest.fn(), getCasesMetrics: jest.fn(), getCasesStatus: jest.fn() },
+ cases: {
+ find: jest.fn(),
+ getCasesMetrics: jest.fn(),
+ getCasesStatus: jest.fn(),
+ bulkGet: jest.fn(),
+ },
};
const uiMock: jest.Mocked = {
diff --git a/x-pack/plugins/cases/public/types.ts b/x-pack/plugins/cases/public/types.ts
index 7df2e8f950271..e2cb59095f849 100644
--- a/x-pack/plugins/cases/public/types.ts
+++ b/x-pack/plugins/cases/public/types.ts
@@ -25,6 +25,9 @@ import type { LicensingPluginStart } from '@kbn/licensing-plugin/public';
import type { FilesSetup, FilesStart } from '@kbn/files-plugin/public';
import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
import type {
+ CaseResponse,
+ CasesBulkGetRequestCertainFields,
+ CasesBulkGetResponseCertainFields,
CasesByAlertId,
CasesByAlertIDRequest,
CasesFindRequest,
@@ -102,6 +105,10 @@ export interface CasesUiStart {
find: (query: CasesFindRequest, signal?: AbortSignal) => Promise;
getCasesStatus: (query: CasesStatusRequest, signal?: AbortSignal) => Promise;
getCasesMetrics: (query: CasesMetricsRequest, signal?: AbortSignal) => Promise;
+ bulkGet: (
+ params: CasesBulkGetRequestCertainFields,
+ signal?: AbortSignal
+ ) => Promise>;
};
};
ui: {
diff --git a/x-pack/plugins/cases/server/client/cases/client.ts b/x-pack/plugins/cases/server/client/cases/client.ts
index 28f4662b1bc6d..bf3253fda6558 100644
--- a/x-pack/plugins/cases/server/client/cases/client.ts
+++ b/x-pack/plugins/cases/server/client/cases/client.ts
@@ -66,8 +66,8 @@ export interface CasesSubClient {
* Retrieves multiple cases with the specified IDs.
*/
bulkGet(
- params: CasesBulkGetRequestCertainFields
- ): Promise>;
+ params: CasesBulkGetRequestCertainFields
+ ): Promise>;
/**
* Pushes a specific case to an external system.
*/
From 66ad9e0ba596b4e4472067bf7da012bf31d11424 Mon Sep 17 00:00:00 2001
From: Christos Nasikas
Date: Tue, 4 Apr 2023 15:24:55 +0300
Subject: [PATCH 012/112] [Cases] Delete alerts when deleting all comments
(#154202)
## Summary
This PR remove the case id from the alerts when deleting all cases
comments
### Checklist
Delete any items that are not applicable to this PR.
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
### For maintainers
- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
---
.../server/client/attachments/delete.test.ts | 97 +++-
.../cases/server/client/attachments/delete.ts | 22 +-
x-pack/plugins/cases/server/mocks.ts | 4 +-
.../common/lib/alerts.ts | 95 ++++
.../tests/common/comments/delete_comment.ts | 158 +-----
.../tests/common/comments/delete_comments.ts | 495 ++++++++++++++++++
.../security_and_spaces/tests/common/index.ts | 1 +
7 files changed, 693 insertions(+), 179 deletions(-)
create mode 100644 x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/delete_comments.ts
diff --git a/x-pack/plugins/cases/server/client/attachments/delete.test.ts b/x-pack/plugins/cases/server/client/attachments/delete.test.ts
index c74f621346728..d484515f4eeaf 100644
--- a/x-pack/plugins/cases/server/client/attachments/delete.test.ts
+++ b/x-pack/plugins/cases/server/client/attachments/delete.test.ts
@@ -7,40 +7,93 @@
import { mockCaseComments } from '../../mocks';
import { createCasesClientMockArgs } from '../mocks';
-import { deleteComment } from './delete';
+import { deleteComment, deleteAll } from './delete';
-describe('deleteComment', () => {
- const clientArgs = createCasesClientMockArgs();
+describe('delete', () => {
+ describe('deleteComment', () => {
+ const clientArgs = createCasesClientMockArgs();
- beforeEach(() => {
- jest.clearAllMocks();
- });
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
- describe('Alerts', () => {
- const commentSO = mockCaseComments[0];
- const alertsSO = mockCaseComments[3];
- clientArgs.services.attachmentService.getter.get.mockResolvedValue(alertsSO);
+ describe('Alerts', () => {
+ const commentSO = mockCaseComments[0];
+ const alertsSO = mockCaseComments[3];
+ clientArgs.services.attachmentService.getter.get.mockResolvedValue(alertsSO);
- it('delete alerts correctly', async () => {
- await deleteComment({ caseID: 'mock-id-4', attachmentID: 'mock-comment-4' }, clientArgs);
+ it('delete alerts correctly', async () => {
+ await deleteComment({ caseID: 'mock-id-4', attachmentID: 'mock-comment-4' }, clientArgs);
- expect(clientArgs.services.alertsService.ensureAlertsAuthorized).toHaveBeenCalledWith({
- alerts: [{ id: 'test-id', index: 'test-index' }],
+ expect(clientArgs.services.alertsService.ensureAlertsAuthorized).toHaveBeenCalledWith({
+ alerts: [{ id: 'test-id', index: 'test-index' }],
+ });
+
+ expect(clientArgs.services.alertsService.removeCaseIdFromAlerts).toHaveBeenCalledWith({
+ alerts: [{ id: 'test-id', index: 'test-index' }],
+ caseId: 'mock-id-4',
+ });
});
- expect(clientArgs.services.alertsService.removeCaseIdFromAlerts).toHaveBeenCalledWith({
- alerts: [{ id: 'test-id', index: 'test-index' }],
- caseId: 'mock-id-4',
+ it('does not call the alert service when the attachment is not an alert', async () => {
+ clientArgs.services.attachmentService.getter.get.mockResolvedValue(commentSO);
+ await deleteComment({ caseID: 'mock-id-1', attachmentID: 'mock-comment-1' }, clientArgs);
+
+ expect(clientArgs.services.alertsService.ensureAlertsAuthorized).not.toHaveBeenCalledWith();
+ expect(clientArgs.services.alertsService.removeCaseIdFromAlerts).not.toHaveBeenCalledWith();
});
});
+ });
+
+ describe('deleteAll', () => {
+ const clientArgs = createCasesClientMockArgs();
+ const getAllCaseCommentsResponse = {
+ saved_objects: mockCaseComments.map((so) => ({ ...so, score: 0 })),
+ total: mockCaseComments.length,
+ page: 1,
+ per_page: mockCaseComments.length,
+ };
- it('does not call the alert service when the attachment is not an alert', async () => {
- clientArgs.services.attachmentService.getter.get.mockResolvedValue(commentSO);
- await deleteComment({ caseID: 'mock-id-1', attachmentID: 'mock-comment-1' }, clientArgs);
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
- expect(clientArgs.services.alertsService.ensureAlertsAuthorized).not.toHaveBeenCalledWith();
+ describe('Alerts', () => {
+ clientArgs.services.caseService.getAllCaseComments.mockResolvedValue(
+ getAllCaseCommentsResponse
+ );
- expect(clientArgs.services.alertsService.removeCaseIdFromAlerts).not.toHaveBeenCalledWith();
+ it('delete alerts correctly', async () => {
+ await deleteAll({ caseID: 'mock-id-4' }, clientArgs);
+
+ expect(clientArgs.services.alertsService.ensureAlertsAuthorized).toHaveBeenCalledWith({
+ alerts: [
+ { id: 'test-id', index: 'test-index' },
+ { id: 'test-id-2', index: 'test-index-2' },
+ { id: 'test-id-3', index: 'test-index-3' },
+ ],
+ });
+
+ expect(clientArgs.services.alertsService.removeCaseIdFromAlerts).toHaveBeenCalledWith({
+ alerts: [
+ { id: 'test-id', index: 'test-index' },
+ { id: 'test-id-2', index: 'test-index-2' },
+ { id: 'test-id-3', index: 'test-index-3' },
+ ],
+ caseId: 'mock-id-4',
+ });
+ });
+
+ it('does not call the alert service when the attachment is not an alert', async () => {
+ clientArgs.services.caseService.getAllCaseComments.mockResolvedValue({
+ ...getAllCaseCommentsResponse,
+ saved_objects: [{ ...mockCaseComments[0], score: 0 }],
+ });
+ await deleteAll({ caseID: 'mock-id-1' }, clientArgs);
+
+ expect(clientArgs.services.alertsService.ensureAlertsAuthorized).not.toHaveBeenCalledWith();
+ expect(clientArgs.services.alertsService.removeCaseIdFromAlerts).not.toHaveBeenCalledWith();
+ });
});
});
});
diff --git a/x-pack/plugins/cases/server/client/attachments/delete.ts b/x-pack/plugins/cases/server/client/attachments/delete.ts
index 50291f6a684c2..07d3091e8d348 100644
--- a/x-pack/plugins/cases/server/client/attachments/delete.ts
+++ b/x-pack/plugins/cases/server/client/attachments/delete.ts
@@ -7,7 +7,7 @@
import Boom from '@hapi/boom';
-import type { CommentAttributes } from '../../../common/api';
+import type { CommentRequest, CommentRequestAlertType } from '../../../common/api';
import { Actions, ActionTypes } from '../../../common/api';
import { CASE_SAVED_OBJECT } from '../../../common/constants';
import { getAlertInfoFromComments, isCommentRequestTypeAlert } from '../../common/utils';
@@ -25,7 +25,7 @@ export async function deleteAll(
): Promise {
const {
user,
- services: { caseService, attachmentService, userActionService },
+ services: { caseService, attachmentService, userActionService, alertsService },
logger,
authorization,
} = clientArgs;
@@ -61,6 +61,10 @@ export async function deleteAll(
})),
user,
});
+
+ const attachments = comments.saved_objects.map((comment) => comment.attributes);
+
+ await handleAlerts({ alertsService, attachments, caseId: caseID });
} catch (error) {
throw createCaseError({
message: `Failed to delete all comments case id: ${caseID}: ${error}`,
@@ -121,7 +125,7 @@ export async function deleteComment(
owner: attachment.attributes.owner,
});
- await handleAlerts({ alertsService, attachment: attachment.attributes, caseId: id });
+ await handleAlerts({ alertsService, attachments: [attachment.attributes], caseId: id });
} catch (error) {
throw createCaseError({
message: `Failed to delete comment: ${caseID} comment id: ${attachmentID}: ${error}`,
@@ -133,16 +137,20 @@ export async function deleteComment(
interface HandleAlertsArgs {
alertsService: CasesClientArgs['services']['alertsService'];
- attachment: CommentAttributes;
+ attachments: CommentRequest[];
caseId: string;
}
-const handleAlerts = async ({ alertsService, attachment, caseId }: HandleAlertsArgs) => {
- if (!isCommentRequestTypeAlert(attachment)) {
+const handleAlerts = async ({ alertsService, attachments, caseId }: HandleAlertsArgs) => {
+ const alertAttachments = attachments.filter((attachment): attachment is CommentRequestAlertType =>
+ isCommentRequestTypeAlert(attachment)
+ );
+
+ if (alertAttachments.length === 0) {
return;
}
- const alerts = getAlertInfoFromComments([attachment]);
+ const alerts = getAlertInfoFromComments(alertAttachments);
await alertsService.ensureAlertsAuthorized({ alerts });
await alertsService.removeCaseIdFromAlerts({ alerts, caseId });
};
diff --git a/x-pack/plugins/cases/server/mocks.ts b/x-pack/plugins/cases/server/mocks.ts
index 6a4d76cf035c8..823acef076fe3 100644
--- a/x-pack/plugins/cases/server/mocks.ts
+++ b/x-pack/plugins/cases/server/mocks.ts
@@ -370,8 +370,8 @@ export const mockCaseComments: Array> = [
id: 'mock-comment-6',
attributes: {
type: CommentType.alert,
- index: 'test-index',
- alertId: 'test-id',
+ index: 'test-index-3',
+ alertId: 'test-id-3',
created_at: '2019-11-25T22:32:30.608Z',
created_by: {
full_name: 'elastic',
diff --git a/x-pack/test/cases_api_integration/common/lib/alerts.ts b/x-pack/test/cases_api_integration/common/lib/alerts.ts
index ff29579c91607..08da41280f9be 100644
--- a/x-pack/test/cases_api_integration/common/lib/alerts.ts
+++ b/x-pack/test/cases_api_integration/common/lib/alerts.ts
@@ -5,12 +5,15 @@
* 2.0.
*/
+import expect from '@kbn/expect';
import type SuperTest from 'supertest';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { ToolingLog } from '@kbn/tooling-log';
import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '@kbn/security-solution-plugin/common/constants';
import { DetectionAlert } from '@kbn/security-solution-plugin/common/detection_engine/schemas/alerts';
import { RiskEnrichmentFields } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_types/utils/enrichments/types';
+import { CommentType } from '@kbn/cases-plugin/common';
+import { ALERT_CASE_IDS } from '@kbn/rule-data-utils';
import {
getRuleForSignalTesting,
createRule,
@@ -22,6 +25,9 @@ import {
import { superUser } from './authentication/users';
import { User } from './authentication/types';
import { getSpaceUrlPrefix } from './api/helpers';
+import { createCase } from './api/case';
+import { createComment, deleteAllComments } from './api';
+import { postCaseReq } from './mock';
export const createSecuritySolutionAlerts = async (
supertest: SuperTest.SuperTest,
@@ -74,3 +80,92 @@ export const getAlertById = async ({
return alert;
};
+
+export type Alerts = Array<{ _id: string; _index: string }>;
+
+export const createCaseAttachAlertAndDeleteAlert = async ({
+ supertest,
+ totalCases,
+ indexOfCaseToDelete,
+ owner,
+ expectedHttpCode = 204,
+ deleteCommentAuth = { user: superUser, space: 'space1' },
+ alerts,
+ getAlerts,
+}: {
+ supertest: SuperTest.SuperTest;
+ totalCases: number;
+ indexOfCaseToDelete: number;
+ owner: string;
+ expectedHttpCode?: number;
+ deleteCommentAuth?: { user: User; space: string | null };
+ alerts: Alerts;
+ getAlerts: (alerts: Alerts) => Promise>>;
+}) => {
+ const cases = await Promise.all(
+ [...Array(totalCases).keys()].map((index) =>
+ createCase(
+ supertest,
+ {
+ ...postCaseReq,
+ owner,
+ settings: { syncAlerts: false },
+ },
+ 200,
+ { user: superUser, space: 'space1' }
+ )
+ )
+ );
+
+ const updatedCases = [];
+
+ for (const theCase of cases) {
+ const updatedCase = await createComment({
+ supertest,
+ caseId: theCase.id,
+ params: {
+ alertId: alerts.map((alert) => alert._id),
+ index: alerts.map((alert) => alert._index),
+ rule: {
+ id: 'id',
+ name: 'name',
+ },
+ owner,
+ type: CommentType.alert,
+ },
+ auth: { user: superUser, space: 'space1' },
+ });
+
+ updatedCases.push(updatedCase);
+ }
+
+ const caseIds = updatedCases.map((theCase) => theCase.id);
+
+ const updatedAlerts = await getAlerts(alerts);
+
+ for (const alert of updatedAlerts) {
+ expect(alert[ALERT_CASE_IDS]).eql(caseIds);
+ }
+
+ const caseToDelete = updatedCases[indexOfCaseToDelete];
+
+ await deleteAllComments({
+ supertest,
+ caseId: caseToDelete.id,
+ expectedHttpCode,
+ auth: deleteCommentAuth,
+ });
+
+ const alertAfterDeletion = await getAlerts(alerts);
+
+ const caseIdsWithoutRemovedCase =
+ expectedHttpCode === 204
+ ? updatedCases
+ .filter((theCase) => theCase.id !== caseToDelete.id)
+ .map((theCase) => theCase.id)
+ : updatedCases.map((theCase) => theCase.id);
+
+ for (const alert of alertAfterDeletion) {
+ expect(alert[ALERT_CASE_IDS]).eql(caseIdsWithoutRemovedCase);
+ }
+};
diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts
index b0c4d670855a3..317bd2797245b 100644
--- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts
+++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts
@@ -6,9 +6,8 @@
*/
import expect from '@kbn/expect';
-import { CommentType } from '@kbn/cases-plugin/common';
-import { ALERT_CASE_IDS } from '@kbn/rule-data-utils';
import {
+ createCaseAttachAlertAndDeleteAlert,
createSecuritySolutionAlerts,
getAlertById,
getSecuritySolutionAlerts,
@@ -19,7 +18,6 @@ import {
deleteAllRules,
} from '../../../../../detection_engine_api_integration/utils';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
-import { User } from '../../../../common/lib/authentication/types';
import { getPostCaseRequest, postCaseReq, postCommentUserReq } from '../../../../common/lib/mock';
import {
@@ -30,7 +28,6 @@ import {
createCase,
createComment,
deleteComment,
- deleteAllComments,
superUserSpace1Auth,
} from '../../../../common/lib/api';
import {
@@ -114,93 +111,6 @@ export default ({ getService }: FtrProviderContext): void => {
describe('alerts', () => {
type Alerts = Array<{ _id: string; _index: string }>;
- const createCaseAttachAlertAndDeleteAlert = async ({
- totalCases,
- indexOfCaseToDelete,
- owner,
- expectedHttpCode = 204,
- deleteCommentAuth = { user: superUser, space: 'space1' },
- alerts,
- getAlerts,
- }: {
- totalCases: number;
- indexOfCaseToDelete: number;
- owner: string;
- expectedHttpCode?: number;
- deleteCommentAuth?: { user: User; space: string | null };
- alerts: Alerts;
- getAlerts: (alerts: Alerts) => Promise>>;
- }) => {
- const cases = await Promise.all(
- [...Array(totalCases).keys()].map((index) =>
- createCase(
- supertestWithoutAuth,
- {
- ...postCaseReq,
- owner,
- settings: { syncAlerts: false },
- },
- 200,
- { user: superUser, space: 'space1' }
- )
- )
- );
-
- const updatedCases = [];
-
- for (const theCase of cases) {
- const updatedCase = await createComment({
- supertest: supertestWithoutAuth,
- caseId: theCase.id,
- params: {
- alertId: alerts.map((alert) => alert._id),
- index: alerts.map((alert) => alert._index),
- rule: {
- id: 'id',
- name: 'name',
- },
- owner,
- type: CommentType.alert,
- },
- auth: { user: superUser, space: 'space1' },
- });
-
- updatedCases.push(updatedCase);
- }
-
- const caseIds = updatedCases.map((theCase) => theCase.id);
-
- const updatedAlerts = await getAlerts(alerts);
-
- for (const alert of updatedAlerts) {
- expect(alert[ALERT_CASE_IDS]).eql(caseIds);
- }
-
- const caseToDelete = updatedCases[indexOfCaseToDelete];
- const commentId = caseToDelete.comments![0].id;
-
- await deleteComment({
- supertest: supertestWithoutAuth,
- caseId: caseToDelete.id,
- commentId,
- expectedHttpCode,
- auth: deleteCommentAuth,
- });
-
- const alertAfterDeletion = await getAlerts(alerts);
-
- const caseIdsWithoutRemovedCase =
- expectedHttpCode === 204
- ? updatedCases
- .filter((theCase) => theCase.id !== caseToDelete.id)
- .map((theCase) => theCase.id)
- : updatedCases.map((theCase) => theCase.id);
-
- for (const alert of alertAfterDeletion) {
- expect(alert[ALERT_CASE_IDS]).eql(caseIdsWithoutRemovedCase);
- }
- };
-
describe('security_solution', () => {
let alerts: Alerts = [];
@@ -229,6 +139,7 @@ export default ({ getService }: FtrProviderContext): void => {
it('removes a case from the alert schema when deleting an alert attachment', async () => {
await createCaseAttachAlertAndDeleteAlert({
+ supertest: supertestWithoutAuth,
totalCases: 1,
indexOfCaseToDelete: 0,
owner: 'securitySolutionFixture',
@@ -239,6 +150,7 @@ export default ({ getService }: FtrProviderContext): void => {
it('should remove only one case', async () => {
await createCaseAttachAlertAndDeleteAlert({
+ supertest: supertestWithoutAuth,
totalCases: 3,
indexOfCaseToDelete: 1,
owner: 'securitySolutionFixture',
@@ -249,6 +161,7 @@ export default ({ getService }: FtrProviderContext): void => {
it('should delete case ID from the alert schema when the user has write access to the indices and only read access to the siem solution', async () => {
await createCaseAttachAlertAndDeleteAlert({
+ supertest: supertestWithoutAuth,
totalCases: 1,
indexOfCaseToDelete: 0,
owner: 'securitySolutionFixture',
@@ -261,6 +174,7 @@ export default ({ getService }: FtrProviderContext): void => {
it('should NOT delete case ID from the alert schema when the user does NOT have access to the alert', async () => {
await createCaseAttachAlertAndDeleteAlert({
+ supertest: supertestWithoutAuth,
totalCases: 1,
indexOfCaseToDelete: 0,
owner: 'securitySolutionFixture',
@@ -273,6 +187,7 @@ export default ({ getService }: FtrProviderContext): void => {
it('should delete the case ID from the alert schema when the user has read access to the kibana feature but no read access to the ES index', async () => {
await createCaseAttachAlertAndDeleteAlert({
+ supertest: supertestWithoutAuth,
totalCases: 1,
indexOfCaseToDelete: 0,
owner: 'securitySolutionFixture',
@@ -315,6 +230,7 @@ export default ({ getService }: FtrProviderContext): void => {
it('removes a case from the alert schema when deleting an alert attachment', async () => {
await createCaseAttachAlertAndDeleteAlert({
+ supertest: supertestWithoutAuth,
totalCases: 1,
indexOfCaseToDelete: 0,
owner: 'observabilityFixture',
@@ -325,6 +241,7 @@ export default ({ getService }: FtrProviderContext): void => {
it('should remove only one case', async () => {
await createCaseAttachAlertAndDeleteAlert({
+ supertest: supertestWithoutAuth,
totalCases: 3,
indexOfCaseToDelete: 1,
owner: 'observabilityFixture',
@@ -335,6 +252,7 @@ export default ({ getService }: FtrProviderContext): void => {
it('should delete case ID from the alert schema when the user has read access only', async () => {
await createCaseAttachAlertAndDeleteAlert({
+ supertest: supertestWithoutAuth,
totalCases: 1,
indexOfCaseToDelete: 0,
expectedHttpCode: 204,
@@ -347,6 +265,7 @@ export default ({ getService }: FtrProviderContext): void => {
it('should NOT delete case ID from the alert schema when the user does NOT have access to the alert', async () => {
await createCaseAttachAlertAndDeleteAlert({
+ supertest: supertestWithoutAuth,
totalCases: 1,
indexOfCaseToDelete: 0,
expectedHttpCode: 403,
@@ -387,35 +306,6 @@ export default ({ getService }: FtrProviderContext): void => {
});
});
- it('should delete multiple comments from the appropriate owner', async () => {
- const secCase = await createCase(
- supertestWithoutAuth,
- getPostCaseRequest({ owner: 'securitySolutionFixture' }),
- 200,
- { user: secOnly, space: 'space1' }
- );
-
- await createComment({
- supertest: supertestWithoutAuth,
- caseId: secCase.id,
- params: postCommentUserReq,
- auth: { user: secOnly, space: 'space1' },
- });
-
- await createComment({
- supertest: supertestWithoutAuth,
- caseId: secCase.id,
- params: postCommentUserReq,
- auth: { user: secOnly, space: 'space1' },
- });
-
- await deleteAllComments({
- supertest: supertestWithoutAuth,
- caseId: secCase.id,
- auth: { user: secOnly, space: 'space1' },
- });
- });
-
it('should not delete a comment from a different owner', async () => {
const secCase = await createCase(
supertestWithoutAuth,
@@ -438,13 +328,6 @@ export default ({ getService }: FtrProviderContext): void => {
auth: { user: obsOnly, space: 'space1' },
expectedHttpCode: 403,
});
-
- await deleteAllComments({
- supertest: supertestWithoutAuth,
- caseId: secCase.id,
- auth: { user: obsOnly, space: 'space1' },
- expectedHttpCode: 403,
- });
});
for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) {
@@ -472,13 +355,6 @@ export default ({ getService }: FtrProviderContext): void => {
auth: { user, space: 'space1' },
expectedHttpCode: 403,
});
-
- await deleteAllComments({
- supertest: supertestWithoutAuth,
- caseId: postedCase.id,
- auth: { user, space: 'space1' },
- expectedHttpCode: 403,
- });
});
}
@@ -504,13 +380,6 @@ export default ({ getService }: FtrProviderContext): void => {
auth: { user: secOnly, space: 'space2' },
expectedHttpCode: 403,
});
-
- await deleteAllComments({
- supertest: supertestWithoutAuth,
- caseId: postedCase.id,
- auth: { user: secOnly, space: 'space2' },
- expectedHttpCode: 403,
- });
});
it('should NOT delete a comment created in space2 by making a request to space1', async () => {
@@ -535,13 +404,6 @@ export default ({ getService }: FtrProviderContext): void => {
auth: { user: secOnly, space: 'space1' },
expectedHttpCode: 404,
});
-
- await deleteAllComments({
- supertest: supertestWithoutAuth,
- caseId: postedCase.id,
- auth: { user: secOnly, space: 'space1' },
- expectedHttpCode: 404,
- });
});
});
});
diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/delete_comments.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/delete_comments.ts
new file mode 100644
index 0000000000000..7ac1cc4f9de77
--- /dev/null
+++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/delete_comments.ts
@@ -0,0 +1,495 @@
+/*
+ * 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 {
+ Alerts,
+ createCaseAttachAlertAndDeleteAlert,
+ createSecuritySolutionAlerts,
+ getAlertById,
+ getSecuritySolutionAlerts,
+} from '../../../../common/lib/alerts';
+import {
+ createSignalsIndex,
+ deleteSignalsIndex,
+ deleteAllRules,
+} from '../../../../../detection_engine_api_integration/utils';
+import { FtrProviderContext } from '../../../../common/ftr_provider_context';
+
+import {
+ getPostCaseRequest,
+ persistableStateAttachment,
+ postCaseReq,
+ postCommentActionsReleaseReq,
+ postCommentActionsReq,
+ postCommentAlertReq,
+ postCommentUserReq,
+ postExternalReferenceESReq,
+ postExternalReferenceSOReq,
+} from '../../../../common/lib/mock';
+import {
+ deleteAllCaseItems,
+ deleteCasesByESQuery,
+ deleteCasesUserActions,
+ deleteComments,
+ createCase,
+ createComment,
+ deleteAllComments,
+ superUserSpace1Auth,
+ bulkCreateAttachments,
+ getAllComments,
+} from '../../../../common/lib/api';
+import {
+ globalRead,
+ noKibanaPrivileges,
+ obsOnly,
+ obsOnlyRead,
+ obsOnlyReadAlerts,
+ obsSec,
+ obsSecRead,
+ secOnly,
+ secOnlyRead,
+ secOnlyReadAlerts,
+ secSolutionOnlyReadNoIndexAlerts,
+ superUser,
+} from '../../../../common/lib/authentication/users';
+
+// eslint-disable-next-line import/no-default-export
+export default ({ getService }: FtrProviderContext): void => {
+ const supertest = getService('supertest');
+ const es = getService('es');
+ const esArchiver = getService('esArchiver');
+ const log = getService('log');
+ const supertestWithoutAuth = getService('supertestWithoutAuth');
+
+ describe('delete_comments', () => {
+ afterEach(async () => {
+ await deleteCasesByESQuery(es);
+ await deleteComments(es);
+ await deleteCasesUserActions(es);
+ });
+
+ describe('happy path', () => {
+ it('should delete all comments', async () => {
+ const postedCase = await createCase(supertest, postCaseReq);
+
+ await createComment({
+ supertest,
+ caseId: postedCase.id,
+ params: postCommentUserReq,
+ });
+
+ const comment = await deleteAllComments({
+ supertest,
+ caseId: postedCase.id,
+ });
+
+ expect(comment).to.eql({});
+ });
+ });
+
+ describe('unhappy path', () => {
+ it('404s when comment belongs to different case', async () => {
+ const postedCase = await createCase(supertest, postCaseReq);
+ await createComment({
+ supertest,
+ caseId: postedCase.id,
+ params: postCommentUserReq,
+ });
+
+ const error = (await deleteAllComments({
+ supertest,
+ caseId: 'fake-id',
+ expectedHttpCode: 404,
+ })) as Error;
+
+ expect(error.message).to.be('No comments found for fake-id.');
+ });
+ });
+
+ describe('alerts', () => {
+ describe('security_solution', () => {
+ let alerts: Alerts = [];
+
+ const getAlerts = async (_alerts: Alerts) => {
+ await es.indices.refresh({ index: _alerts.map((alert) => alert._index) });
+ const updatedAlerts = await getSecuritySolutionAlerts(
+ supertest,
+ alerts.map((alert) => alert._id)
+ );
+
+ return updatedAlerts.hits.hits.map((alert) => ({ ...alert._source }));
+ };
+
+ beforeEach(async () => {
+ await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts');
+ await createSignalsIndex(supertest, log);
+ const signals = await createSecuritySolutionAlerts(supertest, log);
+ alerts = [signals.hits.hits[0], signals.hits.hits[1]];
+ });
+
+ afterEach(async () => {
+ await deleteSignalsIndex(supertest, log);
+ await deleteAllRules(supertest, log);
+ await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts');
+ });
+
+ it('deletes alerts and comments', async () => {
+ const postedCase = await createCase(supertest, postCaseReq);
+
+ await createComment({
+ supertest,
+ caseId: postedCase.id,
+ params: postCommentUserReq,
+ });
+
+ await bulkCreateAttachments({
+ supertest,
+ caseId: postedCase.id,
+ params: [
+ {
+ ...postCommentAlertReq,
+ alertId: alerts[0]._id,
+ index: alerts[0]._index,
+ },
+ {
+ ...postCommentAlertReq,
+ alertId: alerts[1]._id,
+ index: alerts[1]._index,
+ },
+ postCommentUserReq,
+ postCommentActionsReq,
+ postCommentActionsReleaseReq,
+ postExternalReferenceESReq,
+ postExternalReferenceSOReq,
+ persistableStateAttachment,
+ ],
+ });
+
+ await deleteAllComments({
+ supertest,
+ caseId: postedCase.id,
+ });
+
+ const comments = await getAllComments({ supertest, caseId: postedCase.id });
+ expect(comments.length).to.eql(0);
+ });
+
+ it('removes a case from the alert schema when deleting all alert attachments', async () => {
+ await createCaseAttachAlertAndDeleteAlert({
+ supertest: supertestWithoutAuth,
+ totalCases: 1,
+ indexOfCaseToDelete: 0,
+ owner: 'securitySolutionFixture',
+ alerts,
+ getAlerts,
+ });
+ });
+
+ it('should remove only one case', async () => {
+ await createCaseAttachAlertAndDeleteAlert({
+ supertest: supertestWithoutAuth,
+ totalCases: 3,
+ indexOfCaseToDelete: 1,
+ owner: 'securitySolutionFixture',
+ alerts,
+ getAlerts,
+ });
+ });
+
+ it('should delete case ID from the alert schema when the user has write access to the indices and only read access to the siem solution', async () => {
+ await createCaseAttachAlertAndDeleteAlert({
+ supertest: supertestWithoutAuth,
+ totalCases: 1,
+ indexOfCaseToDelete: 0,
+ owner: 'securitySolutionFixture',
+ alerts,
+ getAlerts,
+ expectedHttpCode: 204,
+ deleteCommentAuth: { user: secOnlyReadAlerts, space: 'space1' },
+ });
+ });
+
+ it('should NOT delete case ID from the alert schema when the user does NOT have access to the alert', async () => {
+ await createCaseAttachAlertAndDeleteAlert({
+ supertest: supertestWithoutAuth,
+ totalCases: 1,
+ indexOfCaseToDelete: 0,
+ owner: 'securitySolutionFixture',
+ alerts,
+ getAlerts,
+ expectedHttpCode: 403,
+ deleteCommentAuth: { user: obsSec, space: 'space1' },
+ });
+ });
+
+ it('should delete the case ID from the alert schema when the user has read access to the kibana feature but no read access to the ES index', async () => {
+ await createCaseAttachAlertAndDeleteAlert({
+ supertest: supertestWithoutAuth,
+ totalCases: 1,
+ indexOfCaseToDelete: 0,
+ owner: 'securitySolutionFixture',
+ alerts,
+ getAlerts,
+ expectedHttpCode: 204,
+ deleteCommentAuth: { user: secSolutionOnlyReadNoIndexAlerts, space: 'space1' },
+ });
+ });
+ });
+
+ describe('observability', () => {
+ const alerts = [
+ { _id: 'NoxgpHkBqbdrfX07MqXV', _index: '.alerts-observability.apm.alerts' },
+ { _id: 'space1alert', _index: '.alerts-observability.apm.alerts' },
+ ];
+
+ const getAlerts = async (_alerts: Alerts) => {
+ const updatedAlerts = await Promise.all(
+ _alerts.map((alert) =>
+ getAlertById({
+ supertest: supertestWithoutAuth,
+ id: alert._id,
+ index: alert._index,
+ auth: { user: superUser, space: 'space1' },
+ })
+ )
+ );
+
+ return updatedAlerts as Array>;
+ };
+
+ beforeEach(async () => {
+ await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts');
+ });
+
+ afterEach(async () => {
+ await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts');
+ });
+
+ it('deletes alerts and comments', async () => {
+ const postedCase = await createCase(supertest, postCaseReq);
+
+ await createComment({
+ supertest,
+ caseId: postedCase.id,
+ params: postCommentUserReq,
+ });
+
+ await bulkCreateAttachments({
+ supertest,
+ caseId: postedCase.id,
+ params: [
+ {
+ ...postCommentAlertReq,
+ alertId: alerts[0]._id,
+ index: alerts[0]._index,
+ },
+ {
+ ...postCommentAlertReq,
+ alertId: alerts[1]._id,
+ index: alerts[1]._index,
+ },
+ postCommentUserReq,
+ postCommentActionsReq,
+ postCommentActionsReleaseReq,
+ postExternalReferenceESReq,
+ postExternalReferenceSOReq,
+ persistableStateAttachment,
+ ],
+ });
+
+ await deleteAllComments({
+ supertest,
+ caseId: postedCase.id,
+ });
+
+ const comments = await getAllComments({ supertest, caseId: postedCase.id });
+ expect(comments.length).to.eql(0);
+ });
+
+ it('removes a case from the alert schema when deleting all alert attachments', async () => {
+ await createCaseAttachAlertAndDeleteAlert({
+ supertest: supertestWithoutAuth,
+ totalCases: 1,
+ indexOfCaseToDelete: 0,
+ owner: 'observabilityFixture',
+ alerts,
+ getAlerts,
+ });
+ });
+
+ it('should remove only one case', async () => {
+ await createCaseAttachAlertAndDeleteAlert({
+ supertest: supertestWithoutAuth,
+ totalCases: 3,
+ indexOfCaseToDelete: 1,
+ owner: 'observabilityFixture',
+ alerts,
+ getAlerts,
+ });
+ });
+
+ it('should delete case ID from the alert schema when the user has read access only', async () => {
+ await createCaseAttachAlertAndDeleteAlert({
+ supertest: supertestWithoutAuth,
+ totalCases: 1,
+ indexOfCaseToDelete: 0,
+ expectedHttpCode: 204,
+ owner: 'observabilityFixture',
+ alerts,
+ getAlerts,
+ deleteCommentAuth: { user: obsOnlyReadAlerts, space: 'space1' },
+ });
+ });
+
+ it('should NOT delete case ID from the alert schema when the user does NOT have access to the alert', async () => {
+ await createCaseAttachAlertAndDeleteAlert({
+ supertest: supertestWithoutAuth,
+ totalCases: 1,
+ indexOfCaseToDelete: 0,
+ expectedHttpCode: 403,
+ owner: 'observabilityFixture',
+ alerts,
+ getAlerts,
+ deleteCommentAuth: { user: obsSec, space: 'space1' },
+ });
+ });
+ });
+ });
+
+ describe('rbac', () => {
+ afterEach(async () => {
+ await deleteAllCaseItems(es);
+ });
+
+ it('should delete multiple comments from the appropriate owner', async () => {
+ const secCase = await createCase(
+ supertestWithoutAuth,
+ getPostCaseRequest({ owner: 'securitySolutionFixture' }),
+ 200,
+ { user: secOnly, space: 'space1' }
+ );
+
+ await createComment({
+ supertest: supertestWithoutAuth,
+ caseId: secCase.id,
+ params: postCommentUserReq,
+ auth: { user: secOnly, space: 'space1' },
+ });
+
+ await createComment({
+ supertest: supertestWithoutAuth,
+ caseId: secCase.id,
+ params: postCommentUserReq,
+ auth: { user: secOnly, space: 'space1' },
+ });
+
+ await deleteAllComments({
+ supertest: supertestWithoutAuth,
+ caseId: secCase.id,
+ auth: { user: secOnly, space: 'space1' },
+ });
+ });
+
+ it('should not delete a comment from a different owner', async () => {
+ const secCase = await createCase(
+ supertestWithoutAuth,
+ getPostCaseRequest({ owner: 'securitySolutionFixture' }),
+ 200,
+ { user: secOnly, space: 'space1' }
+ );
+
+ await createComment({
+ supertest: supertestWithoutAuth,
+ caseId: secCase.id,
+ params: postCommentUserReq,
+ auth: { user: secOnly, space: 'space1' },
+ });
+
+ await deleteAllComments({
+ supertest: supertestWithoutAuth,
+ caseId: secCase.id,
+ auth: { user: obsOnly, space: 'space1' },
+ expectedHttpCode: 403,
+ });
+ });
+
+ for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) {
+ it(`User ${
+ user.username
+ } with role(s) ${user.roles.join()} - should NOT delete all comments`, async () => {
+ const postedCase = await createCase(
+ supertestWithoutAuth,
+ getPostCaseRequest({ owner: 'securitySolutionFixture' }),
+ 200,
+ superUserSpace1Auth
+ );
+
+ await createComment({
+ supertest: supertestWithoutAuth,
+ caseId: postedCase.id,
+ params: postCommentUserReq,
+ auth: superUserSpace1Auth,
+ });
+
+ await deleteAllComments({
+ supertest: supertestWithoutAuth,
+ caseId: postedCase.id,
+ auth: { user, space: 'space1' },
+ expectedHttpCode: 403,
+ });
+ });
+ }
+
+ it('should NOT delete a comment in a space with where the user does not have permissions', async () => {
+ const postedCase = await createCase(
+ supertestWithoutAuth,
+ getPostCaseRequest({ owner: 'securitySolutionFixture' }),
+ 200,
+ { user: superUser, space: 'space2' }
+ );
+
+ await createComment({
+ supertest: supertestWithoutAuth,
+ caseId: postedCase.id,
+ params: postCommentUserReq,
+ auth: { user: superUser, space: 'space2' },
+ });
+
+ await deleteAllComments({
+ supertest: supertestWithoutAuth,
+ caseId: postedCase.id,
+ auth: { user: secOnly, space: 'space2' },
+ expectedHttpCode: 403,
+ });
+ });
+
+ it('should NOT delete a comment created in space2 by making a request to space1', async () => {
+ const postedCase = await createCase(
+ supertestWithoutAuth,
+ getPostCaseRequest({ owner: 'securitySolutionFixture' }),
+ 200,
+ { user: superUser, space: 'space2' }
+ );
+
+ await createComment({
+ supertest: supertestWithoutAuth,
+ caseId: postedCase.id,
+ params: postCommentUserReq,
+ auth: { user: superUser, space: 'space2' },
+ });
+
+ await deleteAllComments({
+ supertest: supertestWithoutAuth,
+ caseId: postedCase.id,
+ auth: { user: secOnly, space: 'space1' },
+ expectedHttpCode: 404,
+ });
+ });
+ });
+ });
+};
diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts
index 5da47becd8faf..f99b2e65e50d5 100644
--- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts
+++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts
@@ -12,6 +12,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
describe('Common', function () {
loadTestFile(require.resolve('./client/update_alert_status'));
loadTestFile(require.resolve('./comments/delete_comment'));
+ loadTestFile(require.resolve('./comments/delete_comments'));
loadTestFile(require.resolve('./comments/find_comments'));
loadTestFile(require.resolve('./comments/get_comment'));
loadTestFile(require.resolve('./comments/get_all_comments'));
From 98df0c25d3774c7b11df3caa79ec590dcc8f60da Mon Sep 17 00:00:00 2001
From: Robert Oskamp
Date: Tue, 4 Apr 2023 15:53:56 +0200
Subject: [PATCH 013/112] [FTR] Switch to new browser headless mode (#153828)
## Summary
This PR updates the way how we start the headless browser for testing.
The current way of starting in headless mode is eventually going away
and the new headless mode offers more capabilities and stability, see
https://www.selenium.dev/blog/2023/headless-is-going-away/ and
https://developer.chrome.com/articles/new-headless/.
### Test adjustments
All the adjusted discover, dashboard, maps and infra tests showed the
same pattern during failure investigation that's around the fact that
the new headless mode is closer to the regular / non-headless mode:
* Tests passed with the old headless mode
* Tests failed in regular / non-headless mode the same way they failed
in new headless mode
* The failure reasons were mostly around slightly different font
rendering and slightly different browser sizes
---
.../group2/_data_grid_copy_to_clipboard.ts | 4 ++--
test/functional/apps/discover/group2/index.ts | 2 +-
.../screenshots/baseline/area_chart.png | Bin 72517 -> 73126 bytes
test/functional/services/remote/webdriver.ts | 11 +++++++++--
.../test/functional/apps/infra/hosts_view.ts | 1 +
.../apps/maps/group1/blended_vector_layer.js | 2 +-
.../group1/documents_source/search_hits.js | 2 +-
.../apps/maps/group2/es_geo_grid_source.js | 2 +-
.../screenshots/baseline/flights_map.png | Bin 66823 -> 65355 bytes
.../screenshots/baseline/web_logs_map.png | Bin 137249 -> 138468 bytes
10 files changed, 16 insertions(+), 8 deletions(-)
diff --git a/test/functional/apps/discover/group2/_data_grid_copy_to_clipboard.ts b/test/functional/apps/discover/group2/_data_grid_copy_to_clipboard.ts
index fb759bc53099d..06fe279dbd534 100644
--- a/test/functional/apps/discover/group2/_data_grid_copy_to_clipboard.ts
+++ b/test/functional/apps/discover/group2/_data_grid_copy_to_clipboard.ts
@@ -64,7 +64,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
if (canReadClipboard) {
const copiedSourceData = await browser.getClipboardValue();
- expect(copiedSourceData.startsWith('"_source"\n{"@message":["238.171.34.42')).to.be(true);
+ expect(copiedSourceData.startsWith('Document\n{"@message":["238.171.34.42')).to.be(true);
expect(copiedSourceData.endsWith('}')).to.be(true);
}
@@ -89,7 +89,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
if (canReadClipboard) {
const copiedSourceName = await browser.getClipboardValue();
- expect(copiedSourceName).to.be('"_source"');
+ expect(copiedSourceName).to.be('Document');
}
expect(await toasts.getToastCount()).to.be(1);
diff --git a/test/functional/apps/discover/group2/index.ts b/test/functional/apps/discover/group2/index.ts
index 54854a5243365..d6a0aeb9cd9ec 100644
--- a/test/functional/apps/discover/group2/index.ts
+++ b/test/functional/apps/discover/group2/index.ts
@@ -13,7 +13,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
describe('discover/group2', function () {
before(async function () {
- await browser.setWindowSize(1300, 800);
+ await browser.setWindowSize(1600, 1200);
});
after(async function unloadMakelogs() {
diff --git a/test/functional/screenshots/baseline/area_chart.png b/test/functional/screenshots/baseline/area_chart.png
index 5f3e3e7238dfd1567ae78460a7a8384b97e31b9f..b60760eb460d4aba0d97c5ab8bbf83b6a748a101 100644
GIT binary patch
literal 73126
zcmb@tby!qi*fl(efRcjJC7>YE-KCTwf*{@99YcenGPFoHk}BQZ-6;)2Hv_`}GxU4F
z-}8RopWpRe&-{ZN=j^l3-uJ%mwbs1|R(>yshfRSE0)g<}$V;n$K|jKdKQ?P|0h`KVoDMB*4LI
zeez_&kb{x_-=`7>8~aO!Bvsz~57sV^$UvX)gI>4R{u>8En(+Da1AMLzUMq6<2I&|v
zHvhXx-xwP^ll$rSmhVf34#hIkcc1JxIQ)qZa8*uq|NY*P{~5R&_rc*QHwDC(gEEf8
zVw}!_BRjZfrY&TBcg6&H`f8~A{o-V#$Hhr!!&RsLp0M2AtsQb0_glVh{rnxtALaN?q@PKU?*fT{_2{xMt
z(x0UfS7?1~;NxluF<`y@*hg2NE
zI-0IBXQ+SgW;#*x#F*1WF}S`aQ21!NY0pf*hx|2?nL+*+0vh13Eq>y!HPQJVxjVS%
zt(0R`;9-U~R}{Y4RJy>K?u1Mm)ahYJx`^}b+Cx(xkfQ#R$`E8DI(dC1tnX!MP?P>7@1-E;RHYspmLa(2&
zjOm>X6sN8H$vzEBtWK!~nZvq+QW8}dEUOm4$zCtz+glJ`LL!T$E
z8I60InJ55$iLL`0yl{ce
zZzdw=%SAn1M4#9%hbIE#uqB>e*&FWUQ}((Drh6ZjZzgz{aLj^jm7H%z2RvB9RgSda
zEtdN7SlANA#(7c=C6D(E28|qtr9`i{6K8Zpu%-r#R!$l%pBzQxlRN;jL%g6osGcu30^PAtM1pq>VA%Mb_KMb`dG-x_hq#!JO|
zdBofB*BZSA_E+az{zg-lqI+*d=KA_@nEO~;Ol8U?3cCpVFREO4J2waFg(f=P&a*a&
zt6X9wV>PVWAF64_uPaR)(gh$758^&E
zq*pVYtXPDew#8X*%Dd@oa-x<`ra$c?>@%(=Vq+!|myhoyDaTF|o8qz_5
ze_EVovNN!5%R^v3dtBNQ|m1Cb^wgo(MmZJ^kXgnrl;di5QHvz=|Msg;ZOJ0;`VL
z^0;`CdlFb|kT?{!?)~$a!B4=2i?UBOK?tb@+x^iUlspE|6+qtPzGxCmet6*A%6sE8H4?myf6bKNv|lBXmZ
zlY3ws-#%2dKUBEt3ANwM$coY1_AU(qn;0JK)b7keAA=%P)SGL+{b0Y^rimLqtu{xj
zXpz#TSc}P(l<&s&zVmcr$f54$wtFS;wpn{1?Duf#)klpkMOrDQ$0MG&O5);i5r4Un
zZ~@T{U*AE)5c3`mbEg}(%2c|pi>4?!M%ND9XY;5>Fqg9&mgwF
zHYYbpp+<|9=5HlD0`e{FMK8`aqnmyZ>+E&B^Z3+Rj%DIL|G--g(e>wKuXZM65I98bAFyERyn{litPbZxqC%agQdr=
z;+W@W@lZQd{_&$%$U$ld}14GzlElTlAoFKlS;x
z7-}2>QWX#EO*N*;^wT7w-siry?WqnF3$I0=<*A-sMK~X2WFe-a*fLX3TGd&iK=>k0
zVpOm@-1oKy{E~%<(oHcLByW~E-S-MoXF{uZ@(UT5pwv`JCM&_>N{*)Xah_IO;obq7`Z$V
zy=w7ldDm9?I1#zS`i)njTYDIH2<`1SYIiSBvj+YsM@rqez`X@G@4dvcGQ||ca20)e
z1O3vWvFtN8@hgz(Gc1>uwDKRdN&^;lUd$B`#D|n
zRG)K|e^Qq}g3jI4U)V_)B*77#IE}Id6>xD$C$;$fISrchil|wPu61ErX;0nVO?VPG
z6ZKwS|M$hEH7=gh^*;u6x^mxCag!9@nPRY&e%f9b{511E)UH;yMu$7maC)PQUyrSGNK0LQy8vU+lGOUf^YTkH}VW
z=)LD!ND#aBG8WftsWW5g6*Du_y(uq;hIgH$?Fo4ce#B30+usxW9<*TkoH5slvX*c0
zzf=?-JLN11APMH?IsEkdSpSNOa#6SAw6vT|*ogIEdeNE#BL1*l@#
zz2{os$23vsPhn`!_aU6=%5^iwp=pKmBesGeVPEh-_+zs3Syi7VTk-t;My`XanChv$
zobiHY#Hikhj+>%7raqK*x0}+_d(w4)!DRZR`eaS$S7pT2{4Q;OjbCWu#f-7O15X^w
zOy^GZWVH7_B73L`aXc+#8%o(aQ19)MS#n>*+&I&u#k8H`;of4~t`bMr$20qjgtI
zv4*cO4xg^0hoi7~PISS&HJGBweqU^Xrpp6Jk*`?pldkZCo{*`j!*hdz`IwJ=QwMAo
zfhPC*Z?h0kc_ruMFAkMsZhK9-Z9l4sGChK$$Xx|;o
zp1LF#STyGKoL_3Cg))B?VRAL?%%_DPf5c%Bp)p>-I!h2yGSZc3q1hhj%VV>cyd*Op
zCev+hZnqwY8Zy~GpK}=qPww246}h=f1#8Ymwi?Y7b9g5XL{m=pnrlX}>otzflY9!M
zB>Kxp8Hv)7%*72A34c8oY7TOq4>Tx4Syctho6beSr<~=R={D1o#`NRGg(R1ii7ox1
z7F_IJv@9FV1C$1{+lWw&1pk|y1DgnPSPjgW7kUVx_4S|0Z@YWCZih(REp!$?AB|Gk#I}eBed~`?Qz@
z(Z<3uLn-#n(_z3^-h+3QCcX1lgl9pTKyr1t7mssGEp&?SDrIK-i{I57&Z!=9{Ad8{Qd`vj(k-;NMlF8dPMF99b1co|5Y;)&)YKkV
zd)3$|{2x)1l8CvU{?%RVu!t)AUhO3um^LpzvBdn2lBPY4k7Xt>RtL+Yq)Gc9VaiU*
zO_X9vS1cYfhZzWzG11bm)^i9r-s)_=^6^wA^GGTo;eZ|6NuQ;Flw2
z@+(;DR#g*xNy(q09`N7)te{tP{x?_$)(c6}|K1A?d*naX=M!+x_1{H8+Km5s?*HGf
zGCFWb0yAGzF?~X7xDYj%9Z!|fW|U>~ktonumF$UkY;`=qTU9vjp5EbL7Q4;~-uyvP
zZT5urX%-5!9`QKLTBJpR1z4)z5_j*sQmt<00D3CK9qcU!ECAX4^KZHDs6
z?GhS%z^emj9IDVc2ruA+6S^ho=r>CR7kdRNY{{>?;=r5v{{&s(5;MJ5aazf)j;H
zV2WW?HMbi8#TL*4X@X68MmJuxDUwW5&)kzt`0qB{e*@1E1%ZaT9F=m|mX;m#&4K6h
z!5$Mvo!9$KTSm}wi@nOqUB(^a(DF7movn&WF|08{|3Bw?)plp8{d)
zCj!u;tt8w~5)QM0LNGiO%fB18s<%kk!`dFW>*s8dEmFj6xGES$2?8fUN{Hr1|1^(B&A#@HrMbO%*ew0I-zFI
z&%ad}|8AH5TjpZ7@VA*cLf9Z1c|kPEUscti@{g;Tkvi8F-8ZYP{*67aZ>||`%1!8!
zkD3anxSGzra2RC6Ev6wlniU?*z){(osZakPa1-|D&r9SgjStG{_DPHj5~-g*--9Lc
zhUfIB&g_MRV5cQsd4I#DZ7*Cs_)Yli0JsY)cfhct$7x>Tj1Hgn3@reu?$^`a>~jy4
zJlJ)R?j784X;N9_wDn!8;dCdo!uoGn*M7dlWdc^75*DnPJE69A|qP}bd#$(G=t%rjpX#c1W#*LIk3&F3z^2QkMO+=
zE{b~d^1|{J|GIDg*p&_Q72K7ve#!Kq0XYn$80kYfm-ZNRo*8jSXI1q`MyUU0-RPT$eX|RLz_3pYRgZ
zN2`O#KURU2l)`$H0V)$dweUdjWB#rP@^wG`>LR_Z$x0m1r%#_mkC#5e
znr!a%Umq0DJ0VSB37ipgO!IH$<>d=^iwX+XP;hQ~`nH8?J{uA#t8J&3EG!RkkLP8N
z_N{K{*amA|=$_|nYU#j-v)N$u^ol)g3$!IKz0tMIgaUX<-V7?5z=oP@Hs6Gzk_(Qq
zK$?m}VQ;(5oj|rMJ%_VN(hX+7ai%IK9`7USJRVy05MQTbbaTWM|6MCGpMK-)$s(q;
zVcYW|l*oI5vbi!M-0en~9flo2%>pu<9+ArSt9=+S!6CrqGrgppZt(Hr=LLI{y_$M*
zE^ATX1Z_4VCDuH)oj@DnBB6TQiD6Ca_%zq+kM|a;Z&GZ6LqgV~$j4EC2e7nC9J$hq
zZZ1jq0w6^BB6PwtgZRR;v9L7RZPc~%}WtlWE#oE>4+3Ck7q>~t|
zn;Ad_belEBBo2*N_-1D7Po6v}v+U)zC-bX|Y#pJSf{&%^XITuVPfRb;kdX8t>yb;2
zf+d=)m&ZFz4g5EX@6$xzNJ-K0(46r>Z5JZimksBK8B4)XKR^M6gjE>p3itF(56bx)
zUY?j@;B)qd-hd
zOd4!S^!a*Q4#wT~ah=pUv#$_D%%oOUl@@^(D5#{ua)dmq=($4(fP~eX-2WK3G25De%)BZ4X^mbB}Ps*&e@7e?CWRItn|X0!oB?c86lj2ptJ~Y
zO6mXzs!nZt7nk=#(6SUspOR2HOKRc6fvqE+@GPQc(}~1x%}ne0v`2)YLYxUcI)p%;
z4kwIZT(38_VA_iqT$%R1z#etsA0ARqlBsUAIGk?qHf_hm`E219&SbsX#ADbZ!u1T&Jmk|q)v
z$`0#a)Eo4D?BC^(=JjPdAuikR!~xVa+dynn?wFQ-@k{`pw&M?h1lrS0OPV>bPxpbj
zL{}CMBLddP_fcKl3&93~c&N$3(QYAIOSpz$cer$rV4!oB(|3qOpSTI8mm7R}$nHQV
z(#*?Mk$$nF@@X`%mXqHdiRg`ko%90!Ub+z8ERmvlm75c$2rkcuc($46X_Y&Ist!;Q
zKLfGuIjRlYo6?HD0=hJ}&(E+a>N3U7H0$4}P=J^~uWgG94Wv7g`>6RzuEBgDbQ7-{
zp74i7iKFLZjMoE#{^5w)x`@FPgguV}zH$-Q=P~JQ8b$PsNSiO_@)_muQdz4I6jt+t
z5eI5tbbl{9u4bpx%cSVbA7f+O*f^n9e|L`6tGBjDo{$iG
z`z+AYHyNs(Zm!LRKuUQnBwZsZkmQN6Va9`@BsV50)m)n|MoaEv)F*daG1l>700C4*
zEC>#B4RwtJQ(Tdk^|^7WchI6_@Xn9G5^JO6(N^(f-AR!WCRY}}=Xssw_X+SFBhE#<
z`-(TNLweSW=Z#ha_wZ~76h&BcjBV|T+1XjYiRut)tqUa)v-vSWVFeNr8Ugr@W~QP+
zEY36Ttbt8MVVOPa)@Gltg^}a5B1x5%GM)l8>`KFhg_H2|lK@#r;
zKH&SBa^hi9U(da@5*fC?A4O*|l}@?x$^>MgSJbmUWQlKKI*eA(Iy9yn%M*;kN+%d;
zZ-2$ilArNwT8QGs>P4fpT=o5@IU3;Wvrb0PbvaMqml~D_Km`UasWe{;;0FbMZlf>5
zr^$VNAzj>vMM$W`)s&)mAR5uEkM!2xMUnvtNbsdnX+NKPUY%t*MQ89$-%FRYYNwyK
zGckUxV~F=Kugyp`fIhzRBE-L&!GkeYA_Y4Z#lTw|#MC{n+`I$Ow8-bjao^T`Q
zo{L@%_Os46YU}u|ml^Q}?M|~6P2#9eKKlDJ6oqrkvErlVh*BT3tGJ9O)Vn;y0sBif
z6+Un=bPUgx`b(bJ;mQ~CSi)FW`E7yOwWpia*F+$lZTi@H*bud+lKuhi(%i(hmcIN@
z+b%2Hdz*FIzGcm!$y<%)Om$BR4pMBBe@QcR^u_x`VmNIVMh=dh8dTNmfg}HWUN4^Pjoea4q%}Yd>Nu=}
zQPC6gym2rc9fl?R2M?Y?_19o*DtaH~1%p_^M!!(}W)9y42&M&Ur1eWskmuToK58|e
zO5EoQP{Xa~n_%O^sPkR6hI$`DH8oB|>CI1H6?y?&2yQeCB)~J{1($1FFXzVaAm`&y04;X
zR}Q0SK^_k4L86xm7Rol|GpH*Uj;MJ5f=NDnYHrAszHgLzq4s2r?@16XV(FNY*
z$`6)zbjOO=eiXD#Ko4BND;esfAcBckhciORpXV{rKv2m5)`uhS6U%re0Ma-M=I)y7s&iNo)#AA+81+T~dEh_R$Z~*%aX
z1lUK-?yy{U_k2zla%)HV+DX-X2u#bv^@@f!Ll6ECrcfT)@n0h^j)iG0j?j^hXlP{n
z;8QD*Nj1Gqd2AwW8KLUZ3B8%;
zE^vj>BE;e}n^}*bk^yFU{=jyE9-5bGl#K-VbxT&&kuYtZ+uruUV<#sK$30CnQ2RH0
z`jdV|5Wr-Em)osr9rXTJN$kN1z}^cc&F^D+@!#j)8JepIE>y*PC@*{(z304%F*$qAO(wxIyRzu|Q7u(Of$EVm5_A%jk|iyx
zjV9=Wg47SytLEVgQsqrION3TbIGhdgC{3JS{(8ak^XJcktBbb);Tx)J)-%4xA%4S}
z_da#yW>K#<#Ugz()y!eitKguie+f|XGAk-#G_H@a{Jh-K5k+FZ_3lAo^PQDb$5=~q
zsoNC_1>R={udT-!<5E)Qg~im#T>hg5Z~2&lsH=euaIqd
zvw>q$7dhNqz33NIR_2V2jjdH-suhmE^*TtmTfyHeJWb4B#l+iNQ}YG%U@@?Lxy88j
za6JFwX!cS-$n)qK2FBL`kF_bEayJnG>NrUxcP#o|JjBMP=N@|-uuDcxUVpZAunPi^
zi6}RM+5F~Is4I-7x2)Qr+0YT+v41*Q6Ln4m2nKxw5^INDnbl-T|?B~PLzSNb^K(I921_iG}q|l2mK*AfEzi``k`k3!DQE!8GMd
zXqH}G>fdMhPiKvnN7)y4eNvX&i#CgbRhc3Ol)j)
z!lPjxr5lQX19!8-lU%kia0-k=IQT^{^Ve(9QusN|39z*Mo$IFZUPok5}0xyYQ#y7XxK?hyo2Pj;8kaVikv&
zDP<6|?bYjs1|mRj<}w!n+mdt_KCGPjoH~5kb};k3JY1?&gR2pNlGXg#lhgk~yk$OB
z0bm51i}(V6cWZ$%dNhkXAT%#Kj7J;RMLnb!{{=vKK*i2%Gg>|ESa!32aK79cz9;6F
zu$|h?^@2YqN`tv+DRr;$@&H-iQ#tqR&!6nd=P7dauK6xlNSxBS{V+1Vv`X4$1DilW
zCv6~S`QMm|X;fG#9snwbtjA_1Ae#4>83UpdNLXuE!{45>s*}@>NR|0V@cK8;%oML&%_hy`DHOj!Ge8%G}$8%9X!~oE@$tBjP%#IZq*io8yAn{W{
z85ICQ3+R?N3#A&q*OX}DYpngy%N2HoD|WqT+4>%u{}b=w$A`(NVgOkI;j8(&ZF{1?
zuRbR%ojiOz=G)=q4~#|q&(H+nb^YAQRrn5o<($bKErxf6ZZ6+->2BkO>pZ443%l1a
zjq8VPGSiMn&T@)!PzU(<);qG2#0G#6;+tZ^Hf60f#v
z8;m&_hih&1v4=$VR1Gt<}n&a
za!VxgFJFw3a`nB;wH6jTr$=VZ;sb0K6L=CGnytV&s62E&d7BZ}YsVChrY<3H>ervr7q#9n3&iC*Sq*BV}`kJ%@ja{JH
z8pyPXhoJuCT!=5PAA{uw6&Zj@K4*$V|E}g|l|$aO3Ghn#CIujM6H2JVN@{sN$)aZ{
ztp5_l5Zt9M?~feg;|>N|L%cnuGCTP?ABliogl>5{I)q;?DycpYI|$Fk@K7e4nAbfR
z$WN6D(mwA2AS|})qr9hfZ1p!bY~>1A=p}hi8t(|&KS&54A)NNMU2{3jk32p7_Hal-
zv?i*tn*d#TxukZNh6jL$_1$Zjo_JB`WHaud+oBIh`(NuNemOxq?UCWw4>uh#*5*P3
zvN1JtFPXIthfGw+iZuAJ9
zy)4fBzMIQ-|mBgUmrbFxav>NPhgbnb;9&RM~V@&k#UaH<0Q*bPCOSSg=+z#A^nO)7>D(YnGo=P5AbpSXpru@l%@+mbey!+
zT}2Y(yIm01{$gDjlDMEFha{4HX+yE?&k7^!*by^COJm9$5j3B>^b|&o9?|K(1K6!
z%k)XIjaps5967e{O!VvGZ$@y+AP#ldW=;#1LNfE#;Fvr=R_V
ziIh*Q_qC^^M+q3v_E^L+<_G`28sq*Z}Ji!FP|xBikVZa&El
z<>0QX(NQGC^RqPx`BVm^vb#GVC|(mvub5gRo?zK=ArqWN2#3(}&W(|M%OcLV7gI42
zthagz)om&CV{GT6U}7(Tmu))MMJeZYDF?@sliS-kwwn_XXG1u6s?r5y$yH4u2#YsB
zmV{>Ang^`^ey1*%VUH6LB(OpW>tZmEeI$3=zg3jP?y_4Lmlsnh;qEY=^|NP-=C%?J
zVZ!s4snC@y3h%raIO1bbs4Bg9_u4+jM5l!KO$Z*PK)t_#ib~)7Omn6T;>DtNQ%K`Q
zIi!N$WOl9IYE*RWDuqL(`q7goJ!0VbxA8-GBD9*)+04oebZ~w^1OCWhIx>hhgwT-+
z;pLN+z#4o7OvH~c1Il@Mjwt2QBBqx)~(gkO8n
zTX1u4b-5pZ%NEC3$q#FKz!mpPR;X!oSc=p@#wQXgQ$WdYZ+=)Q@%-u2fPIlBldYvs
zAl<*^lrvb7%34~zr_@lct)zUe#Lt#9WkUNzE7R2ybNwv9VUv(m&SsxlVaucJ;Pb)M
z^CjufAphj@t9RbA*yz}8BteQ0x}t%0-Wx4Z#%9NKsqPS=I<47IIggHhHD1m=h^s#Cm9eLj3kie;&u(k
zu(yPuNSd)n1ng#Ho5jT*J`^7&J^=l4zCPF!k(SQZ&wQPtSaEcW@xCFu)I1~o_hn4g
zdB}m~9*(MGU2)*(rj)c#AoTuLY;0_{-xWtub5Zj}mAnlPHja|N73)jp&RN#aB!djS
z66#w`VmF#U>pfE-5DCNt?lDiQ@*%zcm?qB%UKj5dTQ_G?+Lhp8-B`|G6l3$lk@4&3
z_j&klq@`1uKBtZ4i`-lqKL*{*Buc%SC%%t~`2)E0+u(*Ws9j3Rh)@fQVEm_VW_28p
z2CKvDjwxP0UmQsQMwTU)t6i1OYWybeOP*dEuGN=K#7GETBh{Nn6Q4UIjy`B??6RBE
z8;^XhpmEeO?*H^$
zu-J3d`L)xzhCV)Q1>H4#>=z$A2`>v+$XEie|Ia9*jQpv~m7tZU+5g_P?7mokkL0DVl>`9_J{E
z4m{4v{X=W8P|!)rdpZ=g$89lqwlt|xTV0N{OWYgEw#obi0W$;o2mJA3pEcQx0x6x<>C5O}FV-=|awpzr7*_8X^G%ET!(j5RM*JrwA>Z0)2+K3`2VdF#7iM3zS1I9lqKc
z5lD2@j-HuKM4-um|2?A1Bx6az$*t=53KW}O+!mX>X;h{*INrjq-5BI4>jlFRVZa@-7I`XY>msg2v~4rQ|nq)M4#21vV^
zzb3^jKs0VYomrP_1jzXZz-_mJ)gXjU{oaA6XoSiY9>AJ)Js+5r3lPLSe>)E0VFnqB
zNGC$xu69zB$GYK?<$cccVoK5-B6rb)<}f5FKsFbikHt+N-dgi$4cK?CqHAmpi-vxH
z$pZc~9DpG@E|C7+D&|m0b%J?krSy{V{qo$RU(C5%a8w(vt4V$7sF`MVsfRP>`AMlS
zCl*FD&}|2TwZs+51VCd;vtXdho=kCdB7@u2sibtQ|0~={>g_78OoPH5nmF75;wEa8
zi()dEmjuXRN)X6;6TSc!XD$|Xawp-FrAzB$<;`?L`!e41-B7Zy(DbyuMrFNMUtDPF
za@`;v@wWj(yUD^c5*~^^d;KDOP!-6olS>aUrIAaMUs|8ZRiQ6Q&Jad6R6$zj#)Xa9
zWU~Z)yFxvp|J}GxzGZPVnWAqcEXs%8$WL7e2l*uC6WXk3Ev8Kx;YGAik-huco~zG5
zuYZ?ay2+_?8e`SC$Xq~I&ALtMUWwMvk*8T
zChQKm+$>{|1}^lgce5Mpi>CO8ix!QgsxQl*_1oAyvf8=(dX1n_5>@9f-flsDE>UQb`Jlua5
z_bl~NN8jj^%UI!xV7sF)CV%o{G?`E04j!8ht6dE)VzgU)LOFZS#gG$dLHkaIdVIRu
zXmis)jWeTl!dcNiY1~_50aYZXM*z!0ygGLVy;X`s=$~4|Tx)Lpbt9jfuLk;9Hmfp8
z_e`eL;{X^-bC(^0$Y*dIz!l=-!K&Em*+xKcdk9@oBWu$D03Q%&R$Fjk6e-e)}D7f2-BJ7=C6{9=EBjx{2WnpwSgS>WZL`6?;ESe!&lY5C=e@iTI
zQ}|uaO0rwF4y0m3aec1mI>&u07WjZMtRf1x2{h_TJ-(D1u@U?uFtBJ
zH{s&m4Ae0I81p^JdI&%4UES40q`nU2?TUXZ_MPmm>GCp<15y;6_ilgoj!D4Ko(%Mk
zuU7XXd8tek4if9RkM^5JHznuhJ0JV(moEaj(SGt5wJLp@+0p4e0C2g(IBdnoMzyoK
zq;)buo9al=J%Ce7?~L*SBFTVME9_mGK;pergLt5oIymNQvrUrNf$E^U?f0(7_O7>(
zg3YN|`@g>YbuaC$jwH~Z1uSBmFHT=Na44>o<>B6jW*@Tj+~su`OGe*I4Rsmzg^cMZ
zMD04N%O$0!J?8pv{kz4$4NCo{tWvG|deDu_R6wU+zxMsw-USfj4VZ2w;m5J3VY0Se
zuxAb|SEU1dGCxy5Q+DK?Sei8#=zyp9aA#IJ>E7K6^i7#(Py!&Kh5Q$U_Y0}lupwXu
z3g9t`AIs#aX2J*j4Fm`5xYIG!j{~CYE)d$y-M2=R@g6G&!4WB2nAqhu3}R%
z06)=C-J1q*nH>_q)>^w6p|d?!8yFA$3X#7ebMtTFMqe-t^HJ5ywrJxS&B%TUI
z1CHXZ-mRy!=4Fl!LCGKprfesk;WF%R_N~4m&B3nBjzeDb-y(=>;R_DCWQDC(b^N?_
zFawgztuu4vgEU2&f$7Li6U@cy`dQ-UrudtwA?h=ag-wu8r6#wzbI4x;Joxu86Sj
zl29_9=POpgrVv`qUeZerU-}VD;q76yoCKcpEamxvrkhqpzTwk6Xi*cZkMccy03dc-
zD4aU5sFos{84|~qJkCxR;d@cuSmj3;Ri_QKCiGTG&C4E{mo5Dg?F7Q$wrjrrS13`B
zueIAJyT3}8rcdc?5hGnwBd`v5<9}u^`h3=IUwDDe#~%XmO!2|v)bqYTL3@#}4H&Tj
zb0ct^Q5mz%3VmszyzJ@FmFv|=`~f<;f^$bUk*2{6J!2hMGKvQj5~^S|x^nEEvGE})
zCNA!SrshCw>TJD3WQ4txQB;$B2;^WcAsMp;@MYDoIXwU=Abs0_YKkhdlvv1WFeq|C
zNz*J{s+>8WivWt8TB`5*5AyIbA_kwV4{=67i;aJM-xP=>z8#15cOwvRn$i@J!&qB@IagXndwZ@P3ozU2ij;6@%>iqry0I4qSEZ#drB2|ATI+IX5ZqIHce7#m>sw`Zz|;shmjf`anS~kfLKGW?
z(DV2XOW`e+IiLL9f4~%+egGK3l0LD=KLgZBz^0d2D-vImOjsmd69pK@ygkj{KO`)j
zPvT6NBT7N@+#QL-4N-(2{gF<>1nfDAxld>H3H$w#dss`!c2FYH%}Y71|4;rL?@BFV
zLg-YU88@i-2FwT8cjX5cmW!?s?n}n^mwa^?dH(E!>SUig0jCs@{r2k&a-^N%VYz`#
zWrNibqd~F%;|1XFnk~~K9h7FD1dPNr5~@IEv0UF-Y62W@fGXEmTcMqL*h)i4AJ39r
zEQM7AWQnu>BP8>S7nyFhEI=Ni3;^6r-9K_zJXf|Q9ub?F0Wt;1(p$#H>tw5My!nbj
zN{SIyiy*m&?t88M1@hol+6F{TAm~}=uRdM41Ems>#|T#T%Z(DiH3#fFl-^eK^mu=OKgi(79$4zFIQxhOH!AJW%-ouLNuD2HU6UM`
zvRYh4>@y7Lvw)q53D`!!Ca_azH||>O=$Va`buQz8%Wn8E6u_w2%)R9z)nZ*ioz8$j$WgpeVIM_3
z9dp43K!aNew(T7Wa&M>Oa1to!Z#eI{?vq7@n(e*nV=qpO
z9SZfh`_mQL=ns2)waYfExj-$NCz43uaITKBU~$IU1o{%9)1E2u8cf#%Vpzf*NQ#bB
zX6}yK=;y#&Nj04?y!6Y|lcq#s4CRfHi~zxNR%KjO=g!dit?KXy+eV{E*p)C+*{BMr
zhP|x4_nxv(Ae*iLJ=PhBB9vRT_k6Y3;=X-&@(+a{2PQ5V!`-ObftRXTg0&vwB9Tl3
zrnlyMASDC6g)BhcH7OUYlXE*fJ+kIxC$Y%K*Xv-oLyg~Wt3X)yl#Nq(?Fe8VRD2W0
zQg1ae5ULvnh?$AwCLtF{z+LF5(Du|`@o?e#4e(boK!Zs>`c7aEGOcMkrfeWoF6~u7PZ?3A9%{ez%eWATYEXQG&ur%_F5?1jvlXp7v?|?H2mxG|K*K2grVY~&-*HGr
zB9}E&H&ax?ng}b8<9!Y(un_F}?zJxc9u-=O{4#6*KvCN?jEoj`;Z~DT5W=em*rcxp
zgeK|T)PP0|I0Ds+Aff
zm4fOMV57Y&wmZ%_*I`Ew{4F26q))r!O>(Ay!|cNEsOE
z;H`u;;gC~sycpZM+b58JzJvgo0sBdgB70ovMH>~_-g@jrL?gOc*XDLZTPQI>gx|!&bs2O
z3d*wd^@AZ6e>{fUgt6S?|8t(v!i2(!vz$+p{Igv!8S>y(}b|%D}DDW%ajx
zWWnn=@bOKMe3t6%Hkkq01h5bRjmc8Xi*72*z|uu*x3V||(-;L*G}%-QUwGP&^TVl7
z75WwO+g)lHNEJh7*}iqdR#{W2N$$Y->f!nP#3JQFYgUT>WT_Z$g%?M!gXeY;JQCq%
z-B5}Sa==z;2V2j(b-nfTBz1g6$(0xE80(_NIYmjW-IRk5oSY`XO)U}w)R^DM+0pOg
zK1wnq8i53N+3;%BtL1kkiJgq8=@i<5=l&MKsx0eYE}HoJcoO#bp)hpE+9
z2IpFbDLx~8ARp8J=z&o{Ex|x4qMN4~&Dc1=GmYGQ1L4M6$=g41?jn_P&xwY2ouc%23u7=ZX`slj$cu39+H2iM
zOk1{bG6*^fTx}j!O|;y}fi5jx9eR_O(G>rOv9}D1@{87ohY%2?yAh;I=|*s*yFt3U
zyQNd48>CZGx?AZE>Fyjl-)H=tbFTN}{{wLm^XyoAt$W?;UVC~m6)it}q@|R~Ycra3
zuan1_&G(>&m`zF$qxf<~X)9d!Z{(YvJ9Qc$P^xVd}o-LhZZ#eL|I;JZr
z;?pS|*^?wR-;9TcDVe`p>z#YPFcMgJVsk}npds@sJoS4our9GV7)J$L9t)Dkq;GhC
zoV+8NAR0EDCleQLLI+L_gWEp$oV5lPMv7NvEY^q~64wkK5l;m}J%@iT@6{ZKc%mKBy3
zFy{hi2gs1n(}h{%kBZVNp+OA+r<79-2IPu%DlI@`InmcxD3c#ENAs;Fo^wPbo7IWf
zR92n1lGb!wl$qiyJ#&nQJIP3AS)>0@N-_Hk%jJSW|f6;3bZvQ=UkPw*T6PJ*p%wT0Bh#Hwq`^(
z_r>1liMH0zJKCI#1Isz5qKaXpEJubSt=Ht@K^Oyd^LrVh|F3=5r|gbk+#S!Q)-B{`
z5(QB%jxe}Ft|^{6e+gaz5x|V$9BS2K?f&eo8S^QIT5Z1O8wn7W=6mkoyX9JOY8BqB
zMpI`c1cAMu3Z|8>k#U4EKbVr3sP{FK8Kn~ROj5=LH=h4;n#S2i
zQXfYL6?3W?%9c!j%B`qE1{Mt6KV6lB8v6IS8R131@zIAKAA}84S!cgd1WK3M_`E`M
zJaB3#Z{Pq5^5(=^)M{jB>qjGr0Y~$%f07U!0#brk2KbyQKUYbTU@8)Y{Z)yl@lskU
znwxx}f*~Nx7$5i@!iIkM>pb9#HGfsJGq3ft(L6uROn$PNDgr}pJ
zrcQ{iHgHs2
zaG5Q%NeiYVI>W`$gtK`B5&c=@gn7vKC%_!9
z1GUJ?q6RnylqM!%{}bn?bM<6h|FVkPGQyaSCM$HA&ti{fU+wf)zkZs(=pJg^MBz5v
zOCdcyw(4;^X(6_4Ktz9ebg*u}VJTJ_-lNUE#OAhJn4ECqix)$#r^#(~$cv%n%S`{3
zy7=zI-82&uZZo$xP>{ee5ovIM3ML9ZLz#0?(|N<1Q$rt}XJVH)t@^bH_zxi5oY{Bo
z(YC(-y$O9(h%^fbl;N@?HGhwY8QFKX(#}=ha;9z0VVRJmrO?obuq;@w!kQko7t~u4
z$YV1^gL^(L&V`<<-HU~e1s=HzB<<+JTSOC>yzve*uU&9d}j`^EC2&zG(4I&T-0O#sIqu;eaT~y@dA&HGD
zQcKV1Z=Ulz4D=KiIlGZGX44H_Ml-I;PV5)Ep+6lw@&`ZC+;VBd!tngRxkD(*y1XMQ
zhh+@w`Q_E<=EA;R`m{s6SHU8(lc2BXs36^xbT))BGI=SA_QEP#)Xr$cD@DJV_IN#`q2{}W+?7fVZqvG+rUPO55zlIYim88Vr_g5NfDm#BS+Y$z*Byi%Hm=pF7
zp|+-#0Vg1v`e6E((CC8)usQ#3;IMqLf5efc6@t4eJt8W4En_0(033}+w%o`E&g|!R
zWG|FF#Q2LJ;e+D;4O6ry{jlW;N`M?K5}r
z43YOqNc7Q~92liP$NkC11JBg~9o(~bPVx$LL}W1_F&+#!5Dj$)wKR3He1UJ-x%<6h
zDor_CIC`lbz3yOJG0Z=#RItvOITgLph9HPbIkp^Sj?r2%NwzFHo`vsVoBk4k{
zEzOOHxdu=GAYecaq98%Z>zRoJ{JR*eoOP*A_73i(=v%H5@Q5qdMA;UT{(ta|sdvFS`>nyyDs6z{2i-8b#qcSmZ(39nBYFu>
z#>8?*fcNq~LT7*8JYedabVnx?p9vqd8
zP-B6)UmSH@O_l5o5o6g1egIwdOt2FQ&B-4LMLEVwe~-2QUHW_5Ph3Rsy?6}o>hD5G
z?Ra