From cf379f6e6439439e95b84c88ce9ea5a06ce716a7 Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Date: Wed, 8 Sep 2021 20:07:53 +0300 Subject: [PATCH 001/139] [Discover] Fix opening the same saved search (#111127) (#111540) * [Discover] fix opening the same saved search * [Discover] fix functional test * [Discover] apply suggestion * [Discover] apply suggestion Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/chart/discover_chart.test.tsx | 2 +- .../main/components/chart/discover_chart.tsx | 6 ++-- .../layout/discover_layout.test.tsx | 2 +- .../components/layout/discover_layout.tsx | 5 ++-- .../apps/main/components/layout/types.ts | 2 +- .../top_nav/discover_topnav.test.tsx | 1 + .../components/top_nav/discover_topnav.tsx | 30 +++++++++++++++++-- .../top_nav/get_top_nav_links.test.ts | 1 + .../components/top_nav/get_top_nav_links.ts | 4 ++- .../top_nav/open_search_panel.test.tsx | 8 +++-- .../components/top_nav/open_search_panel.tsx | 4 +-- .../top_nav/show_open_search_panel.tsx | 6 ++-- .../apps/main/discover_main_app.tsx | 4 +-- .../apps/main/services/use_discover_state.ts | 5 ++-- 14 files changed, 58 insertions(+), 22 deletions(-) diff --git a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx index dc3c9ebbc75ca..732dee6106b36 100644 --- a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx @@ -96,7 +96,7 @@ function getProps(timefield?: string) { }) as DataCharts$; return { - resetQuery: jest.fn(), + resetSavedSearch: jest.fn(), savedSearch: savedSearchMock, savedSearchDataChart$: charts$, savedSearchDataTotalHits$: totalHits$, diff --git a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx index 7d761aa93b808..2a4e4a06b6120 100644 --- a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx +++ b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx @@ -21,7 +21,7 @@ import { DiscoverServices } from '../../../../../build_services'; const TimechartHeaderMemoized = memo(TimechartHeader); const DiscoverHistogramMemoized = memo(DiscoverHistogram); export function DiscoverChart({ - resetQuery, + resetSavedSearch, savedSearch, savedSearchDataChart$, savedSearchDataTotalHits$, @@ -30,7 +30,7 @@ export function DiscoverChart({ stateContainer, timefield, }: { - resetQuery: () => void; + resetSavedSearch: () => void; savedSearch: SavedSearch; savedSearchDataChart$: DataCharts$; savedSearchDataTotalHits$: DataTotalHits$; @@ -88,7 +88,7 @@ export function DiscoverChart({ {!state.hideChart && ( diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx index 7343760f32d13..79dfc9b77f90b 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx @@ -135,7 +135,7 @@ function getProps(indexPattern: IndexPattern): DiscoverLayoutProps { navigateTo: jest.fn(), onChangeIndexPattern: jest.fn(), onUpdateQuery: jest.fn(), - resetQuery: jest.fn(), + resetSavedSearch: jest.fn(), savedSearch: savedSearchMock, savedSearchData$, savedSearchRefetch$: new Subject(), diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx index 6d241468bdf74..ce745fecbbfad 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx @@ -57,7 +57,7 @@ export function DiscoverLayout({ onChangeIndexPattern, onUpdateQuery, savedSearchRefetch$, - resetQuery, + resetSavedSearch, savedSearchData$, savedSearch, searchSource, @@ -165,6 +165,7 @@ export function DiscoverLayout({ services={services} stateContainer={stateContainer} updateQuery={onUpdateQuery} + resetSavedSearch={resetSavedSearch} /> @@ -246,7 +247,7 @@ export function DiscoverLayout({ void; onChangeIndexPattern: (id: string) => void; onUpdateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; - resetQuery: () => void; + resetSavedSearch: () => void; savedSearch: SavedSearch; savedSearchData$: SavedSearchData; savedSearchRefetch$: DataRefetch$; diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.test.tsx b/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.test.tsx index 687532cd94f08..4b572f6e348b8 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.test.tsx @@ -33,6 +33,7 @@ function getProps(savePermissions = true): DiscoverTopNavProps { updateQuery: jest.fn(), onOpenInspector: jest.fn(), searchSource: {} as ISearchSource, + resetSavedSearch: () => {}, }; } diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.tsx b/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.tsx index 9afda73401084..5e3e2dfd96954 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.tsx +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.tsx @@ -5,7 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; +import { useHistory } from 'react-router-dom'; import { DiscoverLayoutProps } from '../layout/types'; import { getTopNavLinks } from './get_top_nav_links'; import { Query, TimeRange } from '../../../../../../../data/common/query'; @@ -21,6 +22,7 @@ export type DiscoverTopNavProps = Pick< savedQuery?: string; updateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; stateContainer: GetStateReturn; + resetSavedSearch: () => void; }; export const DiscoverTopNav = ({ @@ -34,9 +36,23 @@ export const DiscoverTopNav = ({ navigateTo, savedSearch, services, + resetSavedSearch, }: DiscoverTopNavProps) => { + const history = useHistory(); const showDatePicker = useMemo(() => indexPattern.isTimeBased(), [indexPattern]); const { TopNavMenu } = services.navigation.ui; + + const onOpenSavedSearch = useCallback( + (newSavedSearchId: string) => { + if (savedSearch.id && savedSearch.id === newSavedSearchId) { + resetSavedSearch(); + } else { + history.push(`/view/${encodeURIComponent(newSavedSearchId)}`); + } + }, + [history, resetSavedSearch, savedSearch.id] + ); + const topNavMenu = useMemo( () => getTopNavLinks({ @@ -47,8 +63,18 @@ export const DiscoverTopNav = ({ state: stateContainer, onOpenInspector, searchSource, + onOpenSavedSearch, }), - [indexPattern, navigateTo, onOpenInspector, searchSource, stateContainer, savedSearch, services] + [ + indexPattern, + navigateTo, + savedSearch, + services, + stateContainer, + onOpenInspector, + searchSource, + onOpenSavedSearch, + ] ); const updateSavedQueryId = (newSavedQueryId: string | undefined) => { diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.test.ts b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.test.ts index 6a6fb8a44a5cf..fd918429b57da 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.test.ts +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.test.ts @@ -35,6 +35,7 @@ test('getTopNavLinks result', () => { services, state, searchSource: {} as ISearchSource, + onOpenSavedSearch: () => {}, }); expect(topNavLinks).toMatchInlineSnapshot(` Array [ diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts index f19b30cda5f8a..f7b4a35b4cf8b 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts @@ -29,6 +29,7 @@ export const getTopNavLinks = ({ state, onOpenInspector, searchSource, + onOpenSavedSearch, }: { indexPattern: IndexPattern; navigateTo: (url: string) => void; @@ -37,6 +38,7 @@ export const getTopNavLinks = ({ state: GetStateReturn; onOpenInspector: () => void; searchSource: ISearchSource; + onOpenSavedSearch: (id: string) => void; }) => { const options = { id: 'options', @@ -89,7 +91,7 @@ export const getTopNavLinks = ({ testId: 'discoverOpenButton', run: () => showOpenSearchPanel({ - makeUrl: (searchId) => `#/view/${encodeURIComponent(searchId)}`, + onOpenSavedSearch, I18nContext: services.core.i18n.Context, }), }; diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/open_search_panel.test.tsx b/src/plugins/discover/public/application/apps/main/components/top_nav/open_search_panel.test.tsx index 5080d1d61c88a..dc5d3e81744db 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/open_search_panel.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/open_search_panel.test.tsx @@ -29,7 +29,9 @@ import { OpenSearchPanel } from './open_search_panel'; describe('OpenSearchPanel', () => { test('render', () => { - const component = shallow(); + const component = shallow( + + ); expect(component).toMatchSnapshot(); }); @@ -40,7 +42,9 @@ describe('OpenSearchPanel', () => { delete: false, }, }); - const component = shallow(); + const component = shallow( + + ); expect(component.find('[data-test-subj="manageSearches"]').exists()).toBe(false); }); }); diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/open_search_panel.tsx b/src/plugins/discover/public/application/apps/main/components/top_nav/open_search_panel.tsx index 31026a1e0ab59..1b34e6ffa0b9a 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/open_search_panel.tsx +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/open_search_panel.tsx @@ -27,7 +27,7 @@ const SEARCH_OBJECT_TYPE = 'search'; interface OpenSearchPanelProps { onClose: () => void; - makeUrl: (id: string) => string; + onOpenSavedSearch: (id: string) => void; } export function OpenSearchPanel(props: OpenSearchPanelProps) { @@ -70,7 +70,7 @@ export function OpenSearchPanel(props: OpenSearchPanelProps) { }, ]} onChoose={(id) => { - window.location.assign(props.makeUrl(id)); + props.onOpenSavedSearch(id); props.onClose(); }} uiSettings={uiSettings} diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/show_open_search_panel.tsx b/src/plugins/discover/public/application/apps/main/components/top_nav/show_open_search_panel.tsx index bb306396c4ca5..1a9bfd7e30c57 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/show_open_search_panel.tsx +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/show_open_search_panel.tsx @@ -14,11 +14,11 @@ import { OpenSearchPanel } from './open_search_panel'; let isOpen = false; export function showOpenSearchPanel({ - makeUrl, I18nContext, + onOpenSavedSearch, }: { - makeUrl: (path: string) => string; I18nContext: I18nStart['Context']; + onOpenSavedSearch: (id: string) => void; }) { if (isOpen) { return; @@ -35,7 +35,7 @@ export function showOpenSearchPanel({ document.body.appendChild(container); const element = ( - + ); ReactDOM.render(element, container); diff --git a/src/plugins/discover/public/application/apps/main/discover_main_app.tsx b/src/plugins/discover/public/application/apps/main/discover_main_app.tsx index 456f4ebfab62f..7ee9ab44f9a75 100644 --- a/src/plugins/discover/public/application/apps/main/discover_main_app.tsx +++ b/src/plugins/discover/public/application/apps/main/discover_main_app.tsx @@ -92,7 +92,7 @@ export function DiscoverMainApp(props: DiscoverMainProps) { addHelpMenuToAppChrome(chrome, docLinks); }, [stateContainer, chrome, docLinks]); - const resetQuery = useCallback(() => { + const resetCurrentSavedSearch = useCallback(() => { resetSavedSearch(savedSearch.id); }, [resetSavedSearch, savedSearch]); @@ -103,7 +103,7 @@ export function DiscoverMainApp(props: DiscoverMainProps) { inspectorAdapters={inspectorAdapters} onChangeIndexPattern={onChangeIndexPattern} onUpdateQuery={onUpdateQuery} - resetQuery={resetQuery} + resetSavedSearch={resetCurrentSavedSearch} navigateTo={navigateTo} savedSearch={savedSearch} savedSearchData$={data$} diff --git a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts index afe010379cff3..e11a9937111a1 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts +++ b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts @@ -148,7 +148,8 @@ export function useDiscoverState({ const resetSavedSearch = useCallback( async (id?: string) => { const newSavedSearch = await services.getSavedSearchById(id); - newSavedSearch.searchSource.setField('index', indexPattern); + const newIndexPattern = newSavedSearch.searchSource.getField('index') || indexPattern; + newSavedSearch.searchSource.setField('index', newIndexPattern); const newAppState = getStateDefaults({ config, data, @@ -157,7 +158,7 @@ export function useDiscoverState({ await stateContainer.replaceUrlAppState(newAppState); setState(newAppState); }, - [services, indexPattern, config, data, stateContainer] + [indexPattern, services, config, data, stateContainer] ); /** From bfa3b9dd0ed473a3dddf49440c84884d044fe022 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Sep 2021 13:32:51 -0400 Subject: [PATCH 002/139] [ML] Fix "Exclude jobs or groups" control (#111525) (#111545) Co-authored-by: Dima Arnautov --- .../anomaly_detection_jobs_health_rule_trigger.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/x-pack/plugins/ml/public/alerting/jobs_health_rule/anomaly_detection_jobs_health_rule_trigger.tsx b/x-pack/plugins/ml/public/alerting/jobs_health_rule/anomaly_detection_jobs_health_rule_trigger.tsx index 3cb2a2d426a56..d8643c95ce92b 100644 --- a/x-pack/plugins/ml/public/alerting/jobs_health_rule/anomaly_detection_jobs_health_rule_trigger.tsx +++ b/x-pack/plugins/ml/public/alerting/jobs_health_rule/anomaly_detection_jobs_health_rule_trigger.tsx @@ -20,6 +20,7 @@ import { TestsSelectionControl } from './tests_selection_control'; import { isPopulatedObject } from '../../../common'; import { ALL_JOBS_SELECTION } from '../../../common/constants/alerts'; import { BetaBadge } from '../beta_badge'; +import { isDefined } from '../../../common/types/guards'; export type MlAnomalyAlertTriggerProps = AlertTypeParamsExpressionProps; @@ -79,6 +80,19 @@ const AnomalyDetectionJobsHealthRuleTrigger: FC = ({ }), options: jobs.map((v) => ({ label: v.job_id })), }, + { + label: i18n.translate('xpack.ml.jobSelector.groupOptionsLabel', { + defaultMessage: 'Groups', + }), + options: [ + ...new Set( + jobs + .map((v) => v.groups) + .flat() + .filter((v) => isDefined(v) && !alertParams.includeJobs.groupIds?.includes(v)) + ), + ].map((v) => ({ label: v! })), + }, ]); }); }, From 68115f984865bfa47c2e0f93b024b29fb4494656 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Sep 2021 13:33:32 -0400 Subject: [PATCH 003/139] [Security Solution] [Endpoint] Add new policy tabs layout (#110966) (#111542) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add new policy tabs layout with policy settings and trusted apps tab. Only visible with feature flag enabled * Add URL state of which tab is selected. Also refactored policy_details component. Hide sticky bar at bottom when trustedApps tab is selected * Fix wrong constant path in routing * Don't refresh policyItem if is not necessary * Remove old code and use new form layout component even if FF is disabled * Split test file * Clean test file with unused mocks * Fixes failing test * Address pr comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: David Sánchez --- .../public/management/common/constants.ts | 5 +- .../public/management/common/routing.ts | 12 +- .../components/administration_list_page.tsx | 4 +- .../pages/endpoint_hosts/view/index.test.tsx | 6 +- .../public/management/pages/policy/index.tsx | 23 +- .../policy/store/policy_details/middleware.ts | 3 +- .../policy/store/policy_details/selectors.ts | 35 +- .../pages/policy/view/policy_details.test.tsx | 241 +----------- .../pages/policy/view/policy_details.tsx | 261 ++----------- .../view/policy_forms/components/index.tsx | 8 + .../components/policy_form_confirm_update.tsx | 70 ++++ .../components/policy_form_layout.test.tsx | 353 ++++++++++++++++++ .../components/policy_form_layout.tsx | 196 ++++++++++ .../pages/policy/view/tabs/index.ts | 8 + .../pages/policy/view/tabs/policy_tabs.tsx | 92 +++++ .../policy/view/trusted_apps/layout/index.ts | 8 + .../layout/policy_trusted_apps_layout.tsx | 65 ++++ 17 files changed, 899 insertions(+), 491 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/index.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_confirm_update.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/index.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/index.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx diff --git a/x-pack/plugins/security_solution/public/management/common/constants.ts b/x-pack/plugins/security_solution/public/management/common/constants.ts index f6b147d729322..01569eae59c12 100644 --- a/x-pack/plugins/security_solution/public/management/common/constants.ts +++ b/x-pack/plugins/security_solution/public/management/common/constants.ts @@ -11,7 +11,10 @@ import { ManagementStoreGlobalNamespace, AdministrationSubTab } from '../types'; // --[ ROUTING ]--------------------------------------------------------------------------- export const MANAGEMENT_ROUTING_ENDPOINTS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.endpoints})`; export const MANAGEMENT_ROUTING_POLICIES_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})`; -export const MANAGEMENT_ROUTING_POLICY_DETAILS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId`; +export const MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId/settings`; +export const MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId/trustedApps`; +/** @deprecated use the paths defined above instead */ +export const MANAGEMENT_ROUTING_POLICY_DETAILS_PATH_OLD = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId`; export const MANAGEMENT_ROUTING_TRUSTED_APPS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.trustedApps})`; export const MANAGEMENT_ROUTING_EVENT_FILTERS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.eventFilters})`; diff --git a/x-pack/plugins/security_solution/public/management/common/routing.ts b/x-pack/plugins/security_solution/public/management/common/routing.ts index d044fc0f1f2f6..ecffc04ff7b8c 100644 --- a/x-pack/plugins/security_solution/public/management/common/routing.ts +++ b/x-pack/plugins/security_solution/public/management/common/routing.ts @@ -17,7 +17,8 @@ import { MANAGEMENT_ROUTING_ENDPOINTS_PATH, MANAGEMENT_ROUTING_EVENT_FILTERS_PATH, MANAGEMENT_ROUTING_POLICIES_PATH, - MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, + MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH, + MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH, MANAGEMENT_ROUTING_TRUSTED_APPS_PATH, } from './constants'; import { AdministrationSubTab } from '../types'; @@ -115,7 +116,14 @@ export const getPoliciesPath = (search?: string) => { }; export const getPolicyDetailPath = (policyId: string, search?: string) => { - return `${generatePath(MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, { + return `${generatePath(MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH, { + tabName: AdministrationSubTab.policies, + policyId, + })}${appendSearch(search)}`; +}; + +export const getPolicyTrustedAppsPath = (policyId: string, search?: string) => { + return `${generatePath(MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH, { tabName: AdministrationSubTab.policies, policyId, })}${appendSearch(search)}`; diff --git a/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx b/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx index 581df61efc3e4..c96deabfa245a 100644 --- a/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx +++ b/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx @@ -25,6 +25,7 @@ interface AdministrationListPageProps { subtitle?: React.ReactNode; actions?: React.ReactNode; restrictWidth?: boolean | number; + hasBottomBorder?: boolean; headerBackComponent?: React.ReactNode; } @@ -35,6 +36,7 @@ export const AdministrationListPage: FC { @@ -64,7 +66,7 @@ export const AdministrationListPage: FC { const firstPolicyName = (await renderResult.findAllByTestId('policyNameCellLink'))[0]; expect(firstPolicyName).not.toBeNull(); expect(firstPolicyName.getAttribute('href')).toEqual( - `${APP_PATH}${MANAGEMENT_PATH}/policy/${firstPolicyID}` + `${APP_PATH}${MANAGEMENT_PATH}/policy/${firstPolicyID}/settings` ); }); @@ -710,7 +710,7 @@ describe('when on the endpoint list page', () => { const policyDetailsLink = await renderResult.findByTestId('policyDetailsValue'); expect(policyDetailsLink).not.toBeNull(); expect(policyDetailsLink.getAttribute('href')).toEqual( - `${APP_PATH}${MANAGEMENT_PATH}/policy/${hostDetails.metadata.Endpoint.policy.applied.id}` + `${APP_PATH}${MANAGEMENT_PATH}/policy/${hostDetails.metadata.Endpoint.policy.applied.id}/settings` ); }); @@ -732,7 +732,7 @@ describe('when on the endpoint list page', () => { }); const changedUrlAction = await userChangedUrlChecker; expect(changedUrlAction.payload.pathname).toEqual( - `${MANAGEMENT_PATH}/policy/${hostDetails.metadata.Endpoint.policy.applied.id}` + `${MANAGEMENT_PATH}/policy/${hostDetails.metadata.Endpoint.policy.applied.id}/settings` ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/index.tsx index 0fb9371a14cc3..79a72c75d0c0a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/index.tsx @@ -6,15 +6,32 @@ */ import React, { memo } from 'react'; -import { Route, Switch } from 'react-router-dom'; +import { Route, Switch, Redirect } from 'react-router-dom'; import { PolicyDetails } from './view'; -import { MANAGEMENT_ROUTING_POLICY_DETAILS_PATH } from '../../common/constants'; +import { + MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH, + MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH, + MANAGEMENT_ROUTING_POLICY_DETAILS_PATH_OLD, +} from '../../common/constants'; import { NotFoundPage } from '../../../app/404'; +import { getPolicyDetailPath } from '../../common/routing'; export const PolicyContainer = memo(() => { return ( - + + } + /> ); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts index 93c279db8a55b..6f8a41f4559de 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts @@ -13,6 +13,7 @@ import { isOnPolicyDetailsPage, policyDetails, policyDetailsForUpdate, + needsToRefresh, } from './selectors'; import { sendGetPackagePolicy, @@ -31,7 +32,7 @@ export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory): boolean => { + return !state.policyItem && !state.apiError; +}; + +/** Returns a boolean of whether the user is on the policy form page or not */ +export const isOnPolicyFormPage = (state: Immutable) => { + return ( + matchPath(state.location?.pathname ?? '', { + path: MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH, + exact: true, + }) !== null + ); +}; + /** Returns a boolean of whether the user is on the policy details page or not */ -export const isOnPolicyDetailsPage = (state: Immutable) => { +export const isOnPolicyTrustedAppsPage = (state: Immutable) => { return ( matchPath(state.location?.pathname ?? '', { - path: MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, + path: MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH, exact: true, }) !== null ); }; +/** Returns a boolean of whether the user is on some of the policy details page or not */ +export const isOnPolicyDetailsPage = (state: Immutable) => + isOnPolicyFormPage(state) || isOnPolicyTrustedAppsPage(state); + /** Returns the license info fetched from the license service */ export const license = (state: Immutable) => { return state.license; @@ -91,7 +115,10 @@ export const policyIdFromParams: (state: Immutable) => strin (location: PolicyDetailsState['location']) => { return ( matchPath(location?.pathname ?? '', { - path: MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, + path: [ + MANAGEMENT_ROUTING_POLICY_DETAILS_FORM_PATH, + MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH, + ], exact: true, })?.params?.policyId ?? '' ); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx index d7a2beee956f2..0aed93500453b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx @@ -9,25 +9,20 @@ import React from 'react'; import { mount } from 'enzyme'; import { PolicyDetails } from './policy_details'; -import '../../../../common/mock/match_media.ts'; import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; import { getPolicyDetailPath, getEndpointListPath } from '../../../common/routing'; import { policyListApiPathHandlers } from '../store/test_mock_utils'; -import { licenseService } from '../../../../common/hooks/use_license'; -jest.mock('../../../../common/hooks/use_license'); +jest.mock('./policy_forms/components/policy_form_layout'); describe('Policy Details', () => { - type FindReactWrapperResponse = ReturnType['find']>; - const policyDetailsPathUrl = getPolicyDetailPath('1'); const endpointListPath = getEndpointListPath({ name: 'endpointList' }); const sleep = (ms = 100) => new Promise((wakeup) => setTimeout(wakeup, ms)); const generator = new EndpointDocGenerator(); let history: AppContextTestRender['history']; let coreStart: AppContextTestRender['coreStart']; - let middlewareSpy: AppContextTestRender['middlewareSpy']; let http: typeof coreStart.http; let render: (ui: Parameters[0]) => ReturnType; let policyPackagePolicy: ReturnType; @@ -37,26 +32,13 @@ describe('Policy Details', () => { const appContextMockRenderer = createAppRootMockRenderer(); const AppWrapper = appContextMockRenderer.AppWrapper; - ({ history, coreStart, middlewareSpy } = appContextMockRenderer); + ({ history, coreStart } = appContextMockRenderer); render = (ui) => mount(ui, { wrappingComponent: AppWrapper }); http = coreStart.http; }); - afterEach(() => { - if (policyView) { - policyView.unmount(); - } - jest.clearAllMocks(); - }); - describe('when displayed with invalid id', () => { - let releaseApiFailure: () => void; beforeEach(() => { - http.get.mockImplementation(async () => { - await new Promise((_, reject) => { - releaseApiFailure = reject.bind(null, new Error('policy not found')); - }); - }); history.push(policyDetailsPathUrl); policyView = render(); }); @@ -64,17 +46,6 @@ describe('Policy Details', () => { it('should NOT display timeline', async () => { expect(policyView.find('flyoutOverlay')).toHaveLength(0); }); - - it('should show loader followed by error message', async () => { - expect(policyView.find('EuiLoadingSpinner').length).toBe(1); - releaseApiFailure(); - await middlewareSpy.waitForAction('serverFailedToReturnPolicyDetailsData'); - policyView.update(); - const callout = policyView.find('EuiCallOut'); - expect(callout).toHaveLength(1); - expect(callout.prop('color')).toEqual('danger'); - expect(callout.text()).toEqual('policy not found'); - }); }); describe('when displayed with valid id', () => { let asyncActions: Promise = Promise.resolve(); @@ -152,213 +123,5 @@ describe('Policy Details', () => { expect(agentsSummary).toHaveLength(1); expect(agentsSummary.text()).toBe('Total agents5Healthy3Unhealthy1Offline1'); }); - it('should display cancel button', async () => { - await asyncActions; - policyView.update(); - const cancelbutton = policyView.find( - 'EuiButtonEmpty[data-test-subj="policyDetailsCancelButton"]' - ); - expect(cancelbutton).toHaveLength(1); - expect(cancelbutton.text()).toEqual('Cancel'); - }); - it('should redirect to policy list when cancel button is clicked', async () => { - await asyncActions; - policyView.update(); - const cancelbutton = policyView.find( - 'EuiButtonEmpty[data-test-subj="policyDetailsCancelButton"]' - ); - expect(history.location.pathname).toEqual(policyDetailsPathUrl); - cancelbutton.simulate('click', { button: 0 }); - const navigateToAppMockedCalls = coreStart.application.navigateToApp.mock.calls; - expect(navigateToAppMockedCalls[navigateToAppMockedCalls.length - 1]).toEqual([ - 'securitySolution', - { path: endpointListPath }, - ]); - }); - it('should display save button', async () => { - await asyncActions; - policyView.update(); - const saveButton = policyView.find('EuiButton[data-test-subj="policyDetailsSaveButton"]'); - expect(saveButton).toHaveLength(1); - expect(saveButton.text()).toEqual('Save'); - }); - describe('when the save button is clicked', () => { - let saveButton: FindReactWrapperResponse; - let confirmModal: FindReactWrapperResponse; - let modalCancelButton: FindReactWrapperResponse; - let modalConfirmButton: FindReactWrapperResponse; - - beforeEach(async () => { - await asyncActions; - policyView.update(); - saveButton = policyView.find('EuiButton[data-test-subj="policyDetailsSaveButton"]'); - saveButton.simulate('click'); - policyView.update(); - confirmModal = policyView.find( - 'EuiConfirmModal[data-test-subj="policyDetailsConfirmModal"]' - ); - modalCancelButton = confirmModal.find('button[data-test-subj="confirmModalCancelButton"]'); - modalConfirmButton = confirmModal.find( - 'button[data-test-subj="confirmModalConfirmButton"]' - ); - http.put.mockImplementation((...args) => { - asyncActions = asyncActions.then(async () => sleep()); - const [path] = args; - if (typeof path === 'string') { - if (path === '/api/fleet/package_policies/1') { - return Promise.resolve({ - item: policyPackagePolicy, - success: true, - }); - } - } - - return Promise.reject(new Error('unknown PUT path!')); - }); - }); - - it('should show a modal confirmation', () => { - expect(confirmModal).toHaveLength(1); - expect(confirmModal.find('div[data-test-subj="confirmModalTitleText"]').text()).toEqual( - 'Save and deploy changes' - ); - expect(modalCancelButton.text()).toEqual('Cancel'); - expect(modalConfirmButton.text()).toEqual('Save and deploy changes'); - }); - it('should show info callout if policy is in use', () => { - const warningCallout = confirmModal.find( - 'EuiCallOut[data-test-subj="policyDetailsWarningCallout"]' - ); - expect(warningCallout).toHaveLength(1); - expect(warningCallout.text()).toEqual( - 'This action will update 5 hostsSaving these changes will apply updates to all endpoints assigned to this agent policy.' - ); - }); - it('should close dialog if cancel button is clicked', () => { - modalCancelButton.simulate('click'); - expect( - policyView.find('EuiConfirmModal[data-test-subj="policyDetailsConfirmModal"]') - ).toHaveLength(0); - }); - it('should update policy and show success notification when confirm button is clicked', async () => { - modalConfirmButton.simulate('click'); - policyView.update(); - // Modal should be closed - expect( - policyView.find('EuiConfirmModal[data-test-subj="policyDetailsConfirmModal"]') - ).toHaveLength(0); - - // API should be called - await asyncActions; - expect(http.put.mock.calls[0][0]).toEqual(`/api/fleet/package_policies/1`); - policyView.update(); - - // Toast notification should be shown - const toastAddMock = coreStart.notifications.toasts.addSuccess.mock; - expect(toastAddMock.calls).toHaveLength(1); - expect(toastAddMock.calls[0][0]).toMatchObject({ - title: 'Success!', - text: expect.any(Function), - }); - }); - it('should show an error notification toast if update fails', async () => { - policyPackagePolicy.id = 'invalid'; - modalConfirmButton.simulate('click'); - - await asyncActions; - policyView.update(); - - // Toast notification should be shown - const toastAddMock = coreStart.notifications.toasts.addDanger.mock; - expect(toastAddMock.calls).toHaveLength(1); - expect(toastAddMock.calls[0][0]).toMatchObject({ - title: 'Failed!', - text: expect.any(String), - }); - }); - }); - describe('when the subscription tier is platinum or higher', () => { - beforeEach(() => { - (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(true); - policyView = render(); - }); - - it('malware popup, message customization options and tooltip are shown', () => { - // use query for finding stuff, if it doesn't find it, just returns null - const userNotificationCheckbox = policyView.find( - 'EuiCheckbox[data-test-subj="malwareUserNotificationCheckbox"]' - ); - const userNotificationCustomMessageTextArea = policyView.find( - 'EuiTextArea[data-test-subj="malwareUserNotificationCustomMessage"]' - ); - const tooltip = policyView.find('EuiIconTip[data-test-subj="malwareTooltip"]'); - expect(userNotificationCheckbox).toHaveLength(1); - expect(userNotificationCustomMessageTextArea).toHaveLength(1); - expect(tooltip).toHaveLength(1); - }); - - it('memory protection card and user notification checkbox are shown', () => { - const memory = policyView.find('EuiPanel[data-test-subj="memoryProtectionsForm"]'); - const userNotificationCheckbox = policyView.find( - 'EuiCheckbox[data-test-subj="memory_protectionUserNotificationCheckbox"]' - ); - - expect(memory).toHaveLength(1); - expect(userNotificationCheckbox).toHaveLength(1); - }); - - it('behavior protection card and user notification checkbox are shown', () => { - const behavior = policyView.find('EuiPanel[data-test-subj="behaviorProtectionsForm"]'); - const userNotificationCheckbox = policyView.find( - 'EuiCheckbox[data-test-subj="behavior_protectionUserNotificationCheckbox"]' - ); - - expect(behavior).toHaveLength(1); - expect(userNotificationCheckbox).toHaveLength(1); - }); - - it('ransomware card is shown', () => { - const ransomware = policyView.find('EuiPanel[data-test-subj="ransomwareProtectionsForm"]'); - expect(ransomware).toHaveLength(1); - }); - }); - describe('when the subscription tier is gold or lower', () => { - beforeEach(() => { - (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false); - policyView = render(); - }); - - it('malware popup, message customization options, and tooltip are hidden', () => { - const userNotificationCheckbox = policyView.find( - 'EuiCheckbox[data-test-subj="malwareUserNotificationCheckbox"]' - ); - const userNotificationCustomMessageTextArea = policyView.find( - 'EuiTextArea[data-test-subj="malwareUserNotificationCustomMessage"]' - ); - const tooltip = policyView.find('EuiIconTip[data-test-subj="malwareTooltip"]'); - expect(userNotificationCheckbox).toHaveLength(0); - expect(userNotificationCustomMessageTextArea).toHaveLength(0); - expect(tooltip).toHaveLength(0); - }); - - it('memory protection card, and user notification checkbox are hidden', () => { - const memory = policyView.find('EuiPanel[data-test-subj="memoryProtectionsForm"]'); - expect(memory).toHaveLength(0); - const userNotificationCheckbox = policyView.find( - 'EuiCheckbox[data-test-subj="memoryUserNotificationCheckbox"]' - ); - expect(userNotificationCheckbox).toHaveLength(0); - }); - - it('ransomware card is hidden', () => { - const ransomware = policyView.find('EuiPanel[data-test-subj="ransomwareProtectionsForm"]'); - expect(ransomware).toHaveLength(0); - }); - - it('shows the locked card in place of 1 paid feature', () => { - const lockedCard = policyView.find('EuiCard[data-test-subj="lockedPolicyCard"]'); - expect(lockedCard).toHaveLength(3); - }); - }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx index c8b34f97ee6d6..4538e86a841d9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx @@ -5,151 +5,31 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiButton, - EuiButtonEmpty, - EuiSpacer, - EuiConfirmModal, - EuiCallOut, - EuiLoadingSpinner, - EuiBottomBar, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; -import { useDispatch } from 'react-redux'; -import { useLocation } from 'react-router-dom'; -import { ApplicationStart } from 'kibana/public'; import { usePolicyDetailsSelector } from './policy_hooks'; -import { - policyDetails, - agentStatusSummary, - updateStatus, - isLoading, - apiError, -} from '../store/policy_details/selectors'; -import { useKibana, toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; +import { policyDetails, agentStatusSummary } from '../store/policy_details/selectors'; import { AgentsSummary } from './agents_summary'; -import { useToasts } from '../../../../common/lib/kibana'; -import { AppAction } from '../../../../common/store/actions'; -import { SpyRoute } from '../../../../common/utils/route/spy_routes'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { SecurityPageName } from '../../../../app/types'; -import { getEndpointListPath } from '../../../common/routing'; -import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; -import { APP_ID } from '../../../../../common/constants'; -import { PolicyDetailsRouteState } from '../../../../../common/endpoint/types'; -import { SecuritySolutionPageWrapper } from '../../../../common/components/page_wrapper'; import { HeaderLinkBack } from '../../../../common/components/header_page'; -import { PolicyDetailsForm } from './policy_details_form'; +import { PolicyTabs } from './tabs'; import { AdministrationListPage } from '../../../components/administration_list_page'; +import { PolicyFormLayout } from './policy_forms/components'; export const PolicyDetails = React.memo(() => { - const dispatch = useDispatch<(action: AppAction) => void>(); - const { - services: { - application: { navigateToApp }, - }, - } = useKibana<{ application: ApplicationStart }>(); - const toasts = useToasts(); - const { state: locationRouteState } = useLocation(); + // TODO: Remove this and related code when removing FF + const isTrustedAppsByPolicyEnabled = useIsExperimentalFeatureEnabled( + 'trustedAppsByPolicyEnabled' + ); // Store values const policyItem = usePolicyDetailsSelector(policyDetails); const policyAgentStatusSummary = usePolicyDetailsSelector(agentStatusSummary); - const policyUpdateStatus = usePolicyDetailsSelector(updateStatus); - const isPolicyLoading = usePolicyDetailsSelector(isLoading); - const policyApiError = usePolicyDetailsSelector(apiError); // Local state - const [showConfirm, setShowConfirm] = useState(false); - const [routeState, setRouteState] = useState(); const policyName = policyItem?.name ?? ''; const policyDescription = policyItem?.description ?? undefined; - const hostListRouterPath = getEndpointListPath({ name: 'endpointList' }); - - // Handle showing update statuses - useEffect(() => { - if (policyUpdateStatus) { - if (policyUpdateStatus.success) { - toasts.addSuccess({ - title: i18n.translate( - 'xpack.securitySolution.endpoint.policy.details.updateSuccessTitle', - { - defaultMessage: 'Success!', - } - ), - text: toMountPoint( - - - - ), - }); - - if (routeState && routeState.onSaveNavigateTo) { - navigateToApp(...routeState.onSaveNavigateTo); - } - } else { - toasts.addDanger({ - title: i18n.translate('xpack.securitySolution.endpoint.policy.details.updateErrorTitle', { - defaultMessage: 'Failed!', - }), - text: policyUpdateStatus.error!.message, - }); - } - } - }, [navigateToApp, toasts, policyName, policyUpdateStatus, routeState]); - - const routingOnCancelNavigateTo = routeState?.onCancelNavigateTo; - const navigateToAppArguments = useMemo((): Parameters => { - return routingOnCancelNavigateTo ?? [APP_ID, { path: hostListRouterPath }]; - }, [hostListRouterPath, routingOnCancelNavigateTo]); - - const handleCancelOnClick = useNavigateToAppEventHandler(...navigateToAppArguments); - - const handleSaveOnClick = useCallback(() => { - setShowConfirm(true); - }, []); - - const handleSaveConfirmation = useCallback(() => { - dispatch({ - type: 'userClickedPolicyDetailsSaveButton', - }); - setShowConfirm(false); - }, [dispatch]); - - const handleSaveCancel = useCallback(() => { - setShowConfirm(false); - }, []); - - useEffect(() => { - if (!routeState && locationRouteState) { - setRouteState(locationRouteState); - } - }, [locationRouteState, routeState]); - - // Before proceeding - check if we have a policy data. - // If not, and we are still loading, show spinner. - // Else, if we have an error, then show error on the page. - if (!policyItem) { - return ( - - {isPolicyLoading ? ( - - ) : policyApiError ? ( - - {policyApiError?.message} - - ) : null} - - - ); - } const headerRightContent = ( { ); return ( - <> - {showConfirm && ( - - )} - - - - - - - - - - - - - - - - - - - > - ); -}); - -PolicyDetails.displayName = 'PolicyDetails'; - -const ConfirmUpdate = React.memo<{ - hostCount: number; - onConfirm: () => void; - onCancel: () => void; -}>(({ hostCount, onCancel, onConfirm }) => { - return ( - - {hostCount > 0 && ( - <> - - - - - > + {isTrustedAppsByPolicyEnabled ? ( + + ) : ( + // TODO: Remove this and related code when removing FF + )} - - - - + ); }); -ConfirmUpdate.displayName = 'ConfirmUpdate'; +PolicyDetails.displayName = 'PolicyDetails'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/index.tsx new file mode 100644 index 0000000000000..cf989e6b4e0ee --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/index.tsx @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { PolicyFormLayout } from './policy_form_layout'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_confirm_update.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_confirm_update.tsx new file mode 100644 index 0000000000000..d3bc78732aae6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_confirm_update.tsx @@ -0,0 +1,70 @@ +/* + * 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, EuiConfirmModal, EuiCallOut } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +export const ConfirmUpdate = React.memo<{ + hostCount: number; + onConfirm: () => void; + onCancel: () => void; +}>(({ hostCount, onCancel, onConfirm }) => { + return ( + + {hostCount > 0 && ( + <> + + + + + > + )} + + + + + ); +}); + +ConfirmUpdate.displayName = 'ConfirmUpdate'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx new file mode 100644 index 0000000000000..87c16e411c702 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx @@ -0,0 +1,353 @@ +/* + * 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 { mount } from 'enzyme'; + +import { PolicyFormLayout } from './policy_form_layout'; +import '../../../../../../common/mock/match_media.ts'; +import { EndpointDocGenerator } from '../../../../../../../common/endpoint/generate_data'; +import { + AppContextTestRender, + createAppRootMockRenderer, +} from '../../../../../../common/mock/endpoint'; +import { getPolicyDetailPath, getEndpointListPath } from '../../../../../common/routing'; +import { policyListApiPathHandlers } from '../../../store/test_mock_utils'; +import { licenseService } from '../../../../../../common/hooks/use_license'; + +jest.mock('../../../../../../common/hooks/use_license'); + +describe('Policy Form Layout', () => { + type FindReactWrapperResponse = ReturnType['find']>; + + const policyDetailsPathUrl = getPolicyDetailPath('1'); + const endpointListPath = getEndpointListPath({ name: 'endpointList' }); + const sleep = (ms = 100) => new Promise((wakeup) => setTimeout(wakeup, ms)); + const generator = new EndpointDocGenerator(); + let history: AppContextTestRender['history']; + let coreStart: AppContextTestRender['coreStart']; + let middlewareSpy: AppContextTestRender['middlewareSpy']; + let http: typeof coreStart.http; + let render: (ui: Parameters[0]) => ReturnType; + let policyPackagePolicy: ReturnType; + let policyFormLayoutView: ReturnType; + + beforeEach(() => { + const appContextMockRenderer = createAppRootMockRenderer(); + const AppWrapper = appContextMockRenderer.AppWrapper; + + ({ history, coreStart, middlewareSpy } = appContextMockRenderer); + render = (ui) => mount(ui, { wrappingComponent: AppWrapper }); + http = coreStart.http; + }); + + afterEach(() => { + if (policyFormLayoutView) { + policyFormLayoutView.unmount(); + } + jest.clearAllMocks(); + }); + + describe('when displayed with invalid id', () => { + let releaseApiFailure: () => void; + beforeEach(() => { + http.get.mockImplementation(async () => { + await new Promise((_, reject) => { + releaseApiFailure = reject.bind(null, new Error('policy not found')); + }); + }); + history.push(policyDetailsPathUrl); + policyFormLayoutView = render(); + }); + + it('should NOT display timeline', async () => { + expect(policyFormLayoutView.find('flyoutOverlay')).toHaveLength(0); + }); + + it('should show loader followed by error message', async () => { + expect(policyFormLayoutView.find('EuiLoadingSpinner').length).toBe(1); + releaseApiFailure(); + await middlewareSpy.waitForAction('serverFailedToReturnPolicyDetailsData'); + policyFormLayoutView.update(); + const callout = policyFormLayoutView.find('EuiCallOut'); + expect(callout).toHaveLength(1); + expect(callout.prop('color')).toEqual('danger'); + expect(callout.text()).toEqual('policy not found'); + }); + }); + describe('when displayed with valid id', () => { + let asyncActions: Promise = Promise.resolve(); + + beforeEach(() => { + policyPackagePolicy = generator.generatePolicyPackagePolicy(); + policyPackagePolicy.id = '1'; + + const policyListApiHandlers = policyListApiPathHandlers(); + + http.get.mockImplementation((...args) => { + const [path] = args; + if (typeof path === 'string') { + // GET datasouce + if (path === '/api/fleet/package_policies/1') { + asyncActions = asyncActions.then(async (): Promise => sleep()); + return Promise.resolve({ + item: policyPackagePolicy, + success: true, + }); + } + + // GET Agent status for agent policy + if (path === '/api/fleet/agent-status') { + asyncActions = asyncActions.then(async () => sleep()); + return Promise.resolve({ + results: { events: 0, total: 5, online: 3, error: 1, offline: 1 }, + success: true, + }); + } + + // Get package data + // Used in tests that route back to the list + if (policyListApiHandlers[path]) { + asyncActions = asyncActions.then(async () => sleep()); + return Promise.resolve(policyListApiHandlers[path]()); + } + } + + return Promise.reject(new Error(`unknown API call (not MOCKED): ${path}`)); + }); + history.push(policyDetailsPathUrl); + policyFormLayoutView = render(); + }); + + it('should NOT display timeline', async () => { + expect(policyFormLayoutView.find('flyoutOverlay')).toHaveLength(0); + }); + + it('should display cancel button', async () => { + await asyncActions; + policyFormLayoutView.update(); + const cancelbutton = policyFormLayoutView.find( + 'EuiButtonEmpty[data-test-subj="policyDetailsCancelButton"]' + ); + expect(cancelbutton).toHaveLength(1); + expect(cancelbutton.text()).toEqual('Cancel'); + }); + it('should redirect to policy list when cancel button is clicked', async () => { + await asyncActions; + policyFormLayoutView.update(); + const cancelbutton = policyFormLayoutView.find( + 'EuiButtonEmpty[data-test-subj="policyDetailsCancelButton"]' + ); + expect(history.location.pathname).toEqual(policyDetailsPathUrl); + cancelbutton.simulate('click', { button: 0 }); + const navigateToAppMockedCalls = coreStart.application.navigateToApp.mock.calls; + expect(navigateToAppMockedCalls[navigateToAppMockedCalls.length - 1]).toEqual([ + 'securitySolution', + { path: endpointListPath }, + ]); + }); + it('should display save button', async () => { + await asyncActions; + policyFormLayoutView.update(); + const saveButton = policyFormLayoutView.find( + 'EuiButton[data-test-subj="policyDetailsSaveButton"]' + ); + expect(saveButton).toHaveLength(1); + expect(saveButton.text()).toEqual('Save'); + }); + describe('when the save button is clicked', () => { + let saveButton: FindReactWrapperResponse; + let confirmModal: FindReactWrapperResponse; + let modalCancelButton: FindReactWrapperResponse; + let modalConfirmButton: FindReactWrapperResponse; + + beforeEach(async () => { + await asyncActions; + policyFormLayoutView.update(); + saveButton = policyFormLayoutView.find( + 'EuiButton[data-test-subj="policyDetailsSaveButton"]' + ); + saveButton.simulate('click'); + policyFormLayoutView.update(); + confirmModal = policyFormLayoutView.find( + 'EuiConfirmModal[data-test-subj="policyDetailsConfirmModal"]' + ); + modalCancelButton = confirmModal.find('button[data-test-subj="confirmModalCancelButton"]'); + modalConfirmButton = confirmModal.find( + 'button[data-test-subj="confirmModalConfirmButton"]' + ); + http.put.mockImplementation((...args) => { + asyncActions = asyncActions.then(async () => sleep()); + const [path] = args; + if (typeof path === 'string') { + if (path === '/api/fleet/package_policies/1') { + return Promise.resolve({ + item: policyPackagePolicy, + success: true, + }); + } + } + + return Promise.reject(new Error('unknown PUT path!')); + }); + }); + + it('should show a modal confirmation', () => { + expect(confirmModal).toHaveLength(1); + expect(confirmModal.find('div[data-test-subj="confirmModalTitleText"]').text()).toEqual( + 'Save and deploy changes' + ); + expect(modalCancelButton.text()).toEqual('Cancel'); + expect(modalConfirmButton.text()).toEqual('Save and deploy changes'); + }); + it('should show info callout if policy is in use', () => { + const warningCallout = confirmModal.find( + 'EuiCallOut[data-test-subj="policyDetailsWarningCallout"]' + ); + expect(warningCallout).toHaveLength(1); + expect(warningCallout.text()).toEqual( + 'This action will update 5 hostsSaving these changes will apply updates to all endpoints assigned to this agent policy.' + ); + }); + it('should close dialog if cancel button is clicked', () => { + modalCancelButton.simulate('click'); + expect( + policyFormLayoutView.find('EuiConfirmModal[data-test-subj="policyDetailsConfirmModal"]') + ).toHaveLength(0); + }); + it('should update policy and show success notification when confirm button is clicked', async () => { + modalConfirmButton.simulate('click'); + policyFormLayoutView.update(); + // Modal should be closed + expect( + policyFormLayoutView.find('EuiConfirmModal[data-test-subj="policyDetailsConfirmModal"]') + ).toHaveLength(0); + + // API should be called + await asyncActions; + expect(http.put.mock.calls[0][0]).toEqual(`/api/fleet/package_policies/1`); + policyFormLayoutView.update(); + + // Toast notification should be shown + const toastAddMock = coreStart.notifications.toasts.addSuccess.mock; + expect(toastAddMock.calls).toHaveLength(1); + expect(toastAddMock.calls[0][0]).toMatchObject({ + title: 'Success!', + text: expect.any(Function), + }); + }); + it('should show an error notification toast if update fails', async () => { + policyPackagePolicy.id = 'invalid'; + modalConfirmButton.simulate('click'); + + await asyncActions; + policyFormLayoutView.update(); + + // Toast notification should be shown + const toastAddMock = coreStart.notifications.toasts.addDanger.mock; + expect(toastAddMock.calls).toHaveLength(1); + expect(toastAddMock.calls[0][0]).toMatchObject({ + title: 'Failed!', + text: expect.any(String), + }); + }); + }); + describe('when the subscription tier is platinum or higher', () => { + beforeEach(() => { + (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(true); + policyFormLayoutView = render(); + }); + + it('malware popup, message customization options and tooltip are shown', () => { + // use query for finding stuff, if it doesn't find it, just returns null + const userNotificationCheckbox = policyFormLayoutView.find( + 'EuiCheckbox[data-test-subj="malwareUserNotificationCheckbox"]' + ); + const userNotificationCustomMessageTextArea = policyFormLayoutView.find( + 'EuiTextArea[data-test-subj="malwareUserNotificationCustomMessage"]' + ); + const tooltip = policyFormLayoutView.find('EuiIconTip[data-test-subj="malwareTooltip"]'); + expect(userNotificationCheckbox).toHaveLength(1); + expect(userNotificationCustomMessageTextArea).toHaveLength(1); + expect(tooltip).toHaveLength(1); + }); + + it('memory protection card and user notification checkbox are shown', () => { + const memory = policyFormLayoutView.find( + 'EuiPanel[data-test-subj="memoryProtectionsForm"]' + ); + const userNotificationCheckbox = policyFormLayoutView.find( + 'EuiCheckbox[data-test-subj="memory_protectionUserNotificationCheckbox"]' + ); + + expect(memory).toHaveLength(1); + expect(userNotificationCheckbox).toHaveLength(1); + }); + + it('behavior protection card and user notification checkbox are shown', () => { + const behavior = policyFormLayoutView.find( + 'EuiPanel[data-test-subj="behaviorProtectionsForm"]' + ); + const userNotificationCheckbox = policyFormLayoutView.find( + 'EuiCheckbox[data-test-subj="behavior_protectionUserNotificationCheckbox"]' + ); + + expect(behavior).toHaveLength(1); + expect(userNotificationCheckbox).toHaveLength(1); + }); + + it('ransomware card is shown', () => { + const ransomware = policyFormLayoutView.find( + 'EuiPanel[data-test-subj="ransomwareProtectionsForm"]' + ); + expect(ransomware).toHaveLength(1); + }); + }); + describe('when the subscription tier is gold or lower', () => { + beforeEach(() => { + (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false); + policyFormLayoutView = render(); + }); + + it('malware popup, message customization options, and tooltip are hidden', () => { + const userNotificationCheckbox = policyFormLayoutView.find( + 'EuiCheckbox[data-test-subj="malwareUserNotificationCheckbox"]' + ); + const userNotificationCustomMessageTextArea = policyFormLayoutView.find( + 'EuiTextArea[data-test-subj="malwareUserNotificationCustomMessage"]' + ); + const tooltip = policyFormLayoutView.find('EuiIconTip[data-test-subj="malwareTooltip"]'); + expect(userNotificationCheckbox).toHaveLength(0); + expect(userNotificationCustomMessageTextArea).toHaveLength(0); + expect(tooltip).toHaveLength(0); + }); + + it('memory protection card, and user notification checkbox are hidden', () => { + const memory = policyFormLayoutView.find( + 'EuiPanel[data-test-subj="memoryProtectionsForm"]' + ); + expect(memory).toHaveLength(0); + const userNotificationCheckbox = policyFormLayoutView.find( + 'EuiCheckbox[data-test-subj="memoryUserNotificationCheckbox"]' + ); + expect(userNotificationCheckbox).toHaveLength(0); + }); + + it('ransomware card is hidden', () => { + const ransomware = policyFormLayoutView.find( + 'EuiPanel[data-test-subj="ransomwareProtectionsForm"]' + ); + expect(ransomware).toHaveLength(0); + }); + + it('shows the locked card in place of 1 paid feature', () => { + const lockedCard = policyFormLayoutView.find('EuiCard[data-test-subj="lockedPolicyCard"]'); + expect(lockedCard).toHaveLength(3); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.tsx new file mode 100644 index 0000000000000..a4a2ee65c84e7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.tsx @@ -0,0 +1,196 @@ +/* + * 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, { useCallback, useEffect, useMemo, useState } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiLoadingSpinner, + EuiBottomBar, + EuiSpacer, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { useDispatch } from 'react-redux'; +import { useLocation } from 'react-router-dom'; +import { ApplicationStart } from 'kibana/public'; +import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { + policyDetails, + agentStatusSummary, + updateStatus, + isLoading, + apiError, +} from '../../../store/policy_details/selectors'; + +import { toMountPoint } from '../../../../../../../../../../src/plugins/kibana_react/public'; +import { useToasts, useKibana } from '../../../../../../common/lib/kibana'; +import { AppAction } from '../../../../../../common/store/actions'; +import { SpyRoute } from '../../../../../../common/utils/route/spy_routes'; +import { SecurityPageName } from '../../../../../../app/types'; +import { getEndpointListPath } from '../../../../../common/routing'; +import { useNavigateToAppEventHandler } from '../../../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; +import { APP_ID } from '../../../../../../../common/constants'; +import { PolicyDetailsRouteState } from '../../../../../../../common/endpoint/types'; +import { SecuritySolutionPageWrapper } from '../../../../../../common/components/page_wrapper'; +import { PolicyDetailsForm } from '../../policy_details_form'; +import { ConfirmUpdate } from './policy_form_confirm_update'; + +export const PolicyFormLayout = React.memo(() => { + const dispatch = useDispatch<(action: AppAction) => void>(); + const { + services: { + application: { navigateToApp }, + }, + } = useKibana(); + const toasts = useToasts(); + const { state: locationRouteState } = useLocation(); + + // Store values + const policyItem = usePolicyDetailsSelector(policyDetails); + const policyAgentStatusSummary = usePolicyDetailsSelector(agentStatusSummary); + const policyUpdateStatus = usePolicyDetailsSelector(updateStatus); + const isPolicyLoading = usePolicyDetailsSelector(isLoading); + const policyApiError = usePolicyDetailsSelector(apiError); + + // Local state + const [showConfirm, setShowConfirm] = useState(false); + const [routeState, setRouteState] = useState(); + const policyName = policyItem?.name ?? ''; + const hostListRouterPath = getEndpointListPath({ name: 'endpointList' }); + + const routingOnCancelNavigateTo = routeState?.onCancelNavigateTo; + const navigateToAppArguments = useMemo((): Parameters => { + return routingOnCancelNavigateTo ?? [APP_ID, { path: hostListRouterPath }]; + }, [hostListRouterPath, routingOnCancelNavigateTo]); + + // Handle showing update statuses + useEffect(() => { + if (policyUpdateStatus) { + if (policyUpdateStatus.success) { + toasts.addSuccess({ + title: i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.updateSuccessTitle', + { + defaultMessage: 'Success!', + } + ), + text: toMountPoint( + + + + ), + }); + + if (routeState && routeState.onSaveNavigateTo) { + navigateToApp(...routeState.onSaveNavigateTo); + } + } else { + toasts.addDanger({ + title: i18n.translate('xpack.securitySolution.endpoint.policy.details.updateErrorTitle', { + defaultMessage: 'Failed!', + }), + text: policyUpdateStatus.error!.message, + }); + } + } + }, [navigateToApp, toasts, policyName, policyUpdateStatus, routeState]); + + const handleCancelOnClick = useNavigateToAppEventHandler(...navigateToAppArguments); + + const handleSaveOnClick = useCallback(() => { + setShowConfirm(true); + }, []); + + const handleSaveConfirmation = useCallback(() => { + dispatch({ + type: 'userClickedPolicyDetailsSaveButton', + }); + setShowConfirm(false); + }, [dispatch]); + + const handleSaveCancel = useCallback(() => { + setShowConfirm(false); + }, []); + + useEffect(() => { + if (!routeState && locationRouteState) { + setRouteState(locationRouteState); + } + }, [locationRouteState, routeState]); + + // Before proceeding - check if we have a policy data. + // If not, and we are still loading, show spinner. + // Else, if we have an error, then show error on the page. + if (!policyItem) { + return ( + + {isPolicyLoading ? ( + + ) : policyApiError ? ( + + {policyApiError?.message} + + ) : null} + + + ); + } + + return ( + <> + {showConfirm && ( + + )} + + + + + + + + + + + + + + + + + > + ); +}); + +PolicyFormLayout.displayName = 'PolicyFormLayout'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/index.ts new file mode 100644 index 0000000000000..86526fb77cff1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { PolicyTabs } from './policy_tabs'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx new file mode 100644 index 0000000000000..80ee88e826852 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo } from 'react'; +import { useHistory } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { EuiTabbedContent, EuiSpacer, EuiTabbedContentTab } from '@elastic/eui'; + +import { usePolicyDetailsSelector } from '../policy_hooks'; +import { + isOnPolicyFormPage, + isOnPolicyTrustedAppsPage, + policyIdFromParams, +} from '../../store/policy_details/selectors'; + +import { PolicyTrustedAppsLayout } from '../trusted_apps/layout'; +import { PolicyFormLayout } from '../policy_forms/components'; +import { getPolicyDetailPath, getPolicyTrustedAppsPath } from '../../../../common/routing'; + +export const PolicyTabs = React.memo(() => { + const history = useHistory(); + const isInSettingsTab = usePolicyDetailsSelector(isOnPolicyFormPage); + const isInTrustedAppsTab = usePolicyDetailsSelector(isOnPolicyTrustedAppsPage); + const policyId = usePolicyDetailsSelector(policyIdFromParams); + + const tabs = useMemo( + () => [ + { + id: 'settings', + name: i18n.translate('xpack.securitySolution.endpoint.policy.details.tabs.policyForm', { + defaultMessage: 'Policy settings', + }), + content: ( + <> + + + > + ), + }, + { + id: 'trustedApps', + name: i18n.translate('xpack.securitySolution.endpoint.policy.details.tabs.trustedApps', { + defaultMessage: 'Trusted applications', + }), + content: ( + <> + + + > + ), + }, + ], + [] + ); + + const getInitialSelectedTab = () => { + let initialTab = tabs[0]; + + if (isInSettingsTab) initialTab = tabs[0]; + else if (isInTrustedAppsTab) initialTab = tabs[1]; + else initialTab = tabs[0]; + + return initialTab; + }; + + const onTabClickHandler = useCallback( + (selectedTab: EuiTabbedContentTab) => { + const path = + selectedTab.id === 'settings' + ? getPolicyDetailPath(policyId) + : getPolicyTrustedAppsPath(policyId); + history.push(path); + }, + [history, policyId] + ); + + return ( + + ); +}); + +PolicyTabs.displayName = 'PolicyTabs'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/index.ts new file mode 100644 index 0000000000000..6819bc1695cfa --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { PolicyTrustedAppsLayout } from './policy_trusted_apps_layout'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx new file mode 100644 index 0000000000000..d89f2612403ca --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx @@ -0,0 +1,65 @@ +/* + * 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, { useMemo, useCallback } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { + EuiButton, + EuiTitle, + EuiPageHeader, + EuiPageHeaderSection, + EuiPageContent, +} from '@elastic/eui'; + +export const PolicyTrustedAppsLayout = React.memo(() => { + const onClickAssignTrustedAppButton = useCallback(() => { + /* TODO: to be implemented*/ + }, []); + const assignTrustedAppButton = useMemo( + () => ( + + {i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.layout.assignToPolicy', + { + defaultMessage: 'Assign trusted applications to policy', + } + )} + + ), + [onClickAssignTrustedAppButton] + ); + + return ( + + + + + + {i18n.translate('xpack.securitySolution.endpoint.policy.trustedApps.layout.title', { + defaultMessage: 'Assigned trusted applications', + })} + + + + {assignTrustedAppButton} + + + {/* TODO: To be implemented */} + {'Policy trusted apps layout content'} + + + ); +}); + +PolicyTrustedAppsLayout.displayName = 'PolicyTrustedAppsLayout'; From d794c5bad53f385c3a4bca9bb3d2152f8d873014 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Sep 2021 13:37:00 -0400 Subject: [PATCH 004/139] Add a loading spinner to alerts page (#111310) (#111397) Co-authored-by: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com> --- .../pages/detection_engine/detection_engine.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index 52e0a03ca73a8..063dc849027a7 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -12,6 +12,7 @@ import { EuiFlexGroup, EuiFlexItem, + EuiLoadingSpinner, EuiSpacer, EuiWindowEvent, EuiHorizontalRule, @@ -273,6 +274,17 @@ const DetectionEnginePageComponent: React.FC = ({ [docLinks] ); + if (loading) { + return ( + + + + + + + ); + } + if (isUserAuthenticated != null && !isUserAuthenticated && !loading) { return ( From d8b805f87dd14d9e07ebc9e88f6b6fac5229d8dc Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Sep 2021 13:43:54 -0400 Subject: [PATCH 005/139] [ML] Functional tests - omit node_name in job message assertions (#111529) (#111553) This PR removes the node_name from job audit message assertions as it can vary depending on test environments, particularly in cloud. Co-authored-by: Robert Oskamp --- .../apis/ml/job_audit_messages/clear_messages.ts | 3 +-- .../ml/job_audit_messages/get_job_audit_messages.ts | 12 ++++-------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/x-pack/test/api_integration/apis/ml/job_audit_messages/clear_messages.ts b/x-pack/test/api_integration/apis/ml/job_audit_messages/clear_messages.ts index d085f360859ec..e96759c70fcae 100644 --- a/x-pack/test/api_integration/apis/ml/job_audit_messages/clear_messages.ts +++ b/x-pack/test/api_integration/apis/ml/job_audit_messages/clear_messages.ts @@ -65,11 +65,10 @@ export default ({ getService }: FtrProviderContext) => { expect(getBody.messages.length).to.eql(1); - expect(omit(getBody.messages[0], 'timestamp')).to.eql({ + expect(omit(getBody.messages[0], ['timestamp', 'node_name'])).to.eql({ job_id: 'test_get_job_audit_messages_1', message: 'Job created', level: 'info', - node_name: 'node-01', job_type: 'anomaly_detector', cleared: true, }); diff --git a/x-pack/test/api_integration/apis/ml/job_audit_messages/get_job_audit_messages.ts b/x-pack/test/api_integration/apis/ml/job_audit_messages/get_job_audit_messages.ts index 2211103b2d404..c653f01c1027b 100644 --- a/x-pack/test/api_integration/apis/ml/job_audit_messages/get_job_audit_messages.ts +++ b/x-pack/test/api_integration/apis/ml/job_audit_messages/get_job_audit_messages.ts @@ -42,18 +42,16 @@ export default ({ getService }: FtrProviderContext) => { const messagesDict = keyBy(body.messages, 'job_id'); - expect(omit(messagesDict.test_get_job_audit_messages_2, 'timestamp')).to.eql({ + expect(omit(messagesDict.test_get_job_audit_messages_2, ['timestamp', 'node_name'])).to.eql({ job_id: 'test_get_job_audit_messages_2', message: 'Job created', level: 'info', - node_name: 'node-01', job_type: 'anomaly_detector', }); - expect(omit(messagesDict.test_get_job_audit_messages_1, 'timestamp')).to.eql({ + expect(omit(messagesDict.test_get_job_audit_messages_1, ['timestamp', 'node_name'])).to.eql({ job_id: 'test_get_job_audit_messages_1', message: 'Job created', level: 'info', - node_name: 'node-01', job_type: 'anomaly_detector', }); expect(body.notificationIndices).to.eql(['.ml-notifications-000002']); @@ -67,11 +65,10 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); expect(body.messages.length).to.eql(1); - expect(omit(body.messages[0], 'timestamp')).to.eql({ + expect(omit(body.messages[0], ['timestamp', 'node_name'])).to.eql({ job_id: 'test_get_job_audit_messages_1', message: 'Job created', level: 'info', - node_name: 'node-01', job_type: 'anomaly_detector', }); expect(body.notificationIndices).to.eql(['.ml-notifications-000002']); @@ -85,11 +82,10 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); expect(body.messages.length).to.eql(1); - expect(omit(body.messages[0], 'timestamp')).to.eql({ + expect(omit(body.messages[0], ['timestamp', 'node_name'])).to.eql({ job_id: 'test_get_job_audit_messages_1', message: 'Job created', level: 'info', - node_name: 'node-01', job_type: 'anomaly_detector', }); expect(body.notificationIndices).to.eql(['.ml-notifications-000002']); From 4115f5e3d543956d03b3b4c463eed998a6bbf25b Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Sep 2021 13:48:41 -0400 Subject: [PATCH 006/139] Allow kuery in get summary request in order to be able to filter summaries by policyId (or something else in the future) (#111507) (#111546) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: David Sánchez --- .../endpoint/schema/trusted_apps.test.ts | 17 +++++++ .../common/endpoint/schema/trusted_apps.ts | 6 +++ .../common/endpoint/types/trusted_apps.ts | 4 ++ .../routes/trusted_apps/handlers.test.ts | 44 +++++++++++++++---- .../endpoint/routes/trusted_apps/handlers.ts | 10 ++++- .../endpoint/routes/trusted_apps/index.ts | 3 +- .../routes/trusted_apps/service.test.ts | 15 ++++++- .../endpoint/routes/trusted_apps/service.ts | 7 +-- 8 files changed, 91 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts index df0d0d7acf4c7..ae8fce4efc6e9 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts @@ -7,6 +7,7 @@ import { GetTrustedAppsRequestSchema, + GetTrustedAppsSummaryRequestSchema, PostTrustedAppCreateRequestSchema, PutTrustedAppUpdateRequestSchema, } from './trusted_apps'; @@ -81,6 +82,22 @@ describe('When invoking Trusted Apps Schema', () => { }); }); + describe('for GET Summary', () => { + const getListQueryParams = (kuery?: string) => ({ kuery }); + const query = GetTrustedAppsSummaryRequestSchema.query; + + describe('query param validation', () => { + it('should return query params if valid without kuery', () => { + expect(query.validate(getListQueryParams())).toEqual({}); + }); + + it('should return query params if valid with kuery', () => { + const kuery = `exception-list-agnostic.attributes.tags:"policy:caf1a334-53f3-4be9-814d-a32245f43d34" OR exception-list-agnostic.attributes.tags:"policy:all"`; + expect(query.validate(getListQueryParams(kuery))).toEqual({ kuery }); + }); + }); + }); + describe('for POST Create', () => { const createConditionEntry = (data?: T): ConditionEntry => ({ field: ConditionEntryField.PATH, diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts index d6f1307b5d1be..6e99db7e1ed0b 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts @@ -29,6 +29,12 @@ export const GetTrustedAppsRequestSchema = { }), }; +export const GetTrustedAppsSummaryRequestSchema = { + query: schema.object({ + kuery: schema.maybe(schema.string()), + }), +}; + const ConditionEntryTypeSchema = schema.conditional( schema.siblingRef('field'), ConditionEntryField.PATH, diff --git a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts index 94a2e7f236beb..4c6d2f6037356 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts @@ -13,6 +13,7 @@ import { GetTrustedAppsRequestSchema, PostTrustedAppCreateRequestSchema, PutTrustedAppUpdateRequestSchema, + GetTrustedAppsSummaryRequestSchema, } from '../schema/trusted_apps'; import { OperatingSystem } from './os'; @@ -28,6 +29,9 @@ export interface GetOneTrustedAppResponse { /** API request params for retrieving a list of Trusted Apps */ export type GetTrustedAppsListRequest = TypeOf; +/** API request params for retrieving summary of Trusted Apps */ +export type GetTrustedAppsSummaryRequest = TypeOf; + export interface GetTrustedListAppsResponse { per_page: number; page: number; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts index 5b1c0f5c3deb3..156bcd0de2cc9 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts @@ -337,14 +337,7 @@ describe('handlers', () => { describe('getTrustedAppsSummaryHandler', () => { let getTrustedAppsSummaryHandler: ReturnType; - - beforeEach(() => { - getTrustedAppsSummaryHandler = getTrustedAppsSummaryRouteHandler(appContextMock); - }); - - it('should return ok with list when no errors', async () => { - const mockResponse = httpServerMock.createResponseFactory(); - + const getExceptionsListClientMokcResolvedValue = () => { exceptionsListClient.findExceptionListItem.mockResolvedValue({ data: [ // Linux === 5 @@ -373,6 +366,16 @@ describe('handlers', () => { per_page: 100, total: 23, }); + }; + + beforeEach(() => { + getTrustedAppsSummaryHandler = getTrustedAppsSummaryRouteHandler(appContextMock); + }); + + it('should return ok with list when no errors', async () => { + const mockResponse = httpServerMock.createResponseFactory(); + + getExceptionsListClientMokcResolvedValue(); await getTrustedAppsSummaryHandler( createHandlerContextMock(), @@ -388,6 +391,31 @@ describe('handlers', () => { }); }); + it('should return ok with list when no errors filtering by policyId', async () => { + const mockResponse = httpServerMock.createResponseFactory(); + + const policyId = 'caf1a334-53f3-4be9-814d-a32245f43d34'; + + getExceptionsListClientMokcResolvedValue(); + + await getTrustedAppsSummaryHandler( + createHandlerContextMock(), + httpServerMock.createKibanaRequest({ + query: { + kuery: `exception-list-agnostic.attributes.tags:"policy:${policyId}" OR exception-list-agnostic.attributes.tags:"policy:all"`, + }, + }), + mockResponse + ); + + assertResponse(mockResponse, 'ok', { + linux: 5, + macos: 3, + windows: 15, + total: 23, + }); + }); + it('should return internalError when errors happen', async () => { const mockResponse = httpServerMock.createResponseFactory(); const error = new Error('Something went wrong'); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts index 05194dc856d58..13282bfacd5b1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts @@ -17,6 +17,7 @@ import { PostTrustedAppCreateRequest, PutTrustedAppsRequestParams, PutTrustedAppUpdateRequest, + GetTrustedAppsSummaryRequest, } from '../../../../common/endpoint/types'; import { EndpointAppContext } from '../../types'; @@ -216,13 +217,18 @@ export const getTrustedAppsUpdateRouteHandler = ( export const getTrustedAppsSummaryRouteHandler = ( endpointAppContext: EndpointAppContext -): RequestHandler => { +): RequestHandler< + unknown, + GetTrustedAppsSummaryRequest, + unknown, + SecuritySolutionRequestHandlerContext +> => { const logger = endpointAppContext.logFactory.get('trusted_apps'); return async (context, req, res) => { try { return res.ok({ - body: await getTrustedAppsSummary(exceptionListClientFromContext(context)), + body: await getTrustedAppsSummary(exceptionListClientFromContext(context), req.query), }); } catch (error) { return errorHandler(logger, res, error); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts index 4e61f14408f47..1d5df9c6e88b8 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts @@ -11,6 +11,7 @@ import { GetTrustedAppsRequestSchema, PostTrustedAppCreateRequestSchema, PutTrustedAppUpdateRequestSchema, + GetTrustedAppsSummaryRequestSchema, } from '../../../../common/endpoint/schema/trusted_apps'; import { TRUSTED_APPS_CREATE_API, @@ -90,7 +91,7 @@ export const registerTrustedAppsRoutes = ( router.get( { path: TRUSTED_APPS_SUMMARY_API, - validate: false, + validate: GetTrustedAppsSummaryRequestSchema, options: { authRequired: true }, }, getTrustedAppsSummaryRouteHandler(endpointAppContext) diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts index ea3354a650521..3323080851801 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts @@ -274,7 +274,20 @@ describe('service', () => { }); it('should return summary of trusted app items', async () => { - expect(await getTrustedAppsSummary(exceptionsListClient)).toEqual({ + expect(await getTrustedAppsSummary(exceptionsListClient, {})).toEqual({ + linux: 45, + windows: 55, + macos: 30, + total: 130, + }); + }); + + it('should return summary of trusted app items when filtering by policyId', async () => { + expect( + await getTrustedAppsSummary(exceptionsListClient, { + kuery: `exception-list-agnostic.attributes.tags:"policy:caf1a334-53f3-4be9-814d-a32245f43d34" OR exception-list-agnostic.attributes.tags:"policy:all"`, + }) + ).toEqual({ linux: 45, windows: 55, macos: 30, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts index cfadaa98ad466..a427f13859f03 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts @@ -21,6 +21,7 @@ import { PostTrustedAppCreateResponse, PutTrustedAppUpdateRequest, PutTrustedAppUpdateResponse, + GetTrustedAppsSummaryRequest, } from '../../../../common/endpoint/types'; import { @@ -205,11 +206,11 @@ export const updateTrustedApp = async ( }; export const getTrustedAppsSummary = async ( - exceptionsListClient: ExceptionListClient + exceptionsListClient: ExceptionListClient, + { kuery }: GetTrustedAppsSummaryRequest ): Promise => { // Ensure list is created if it does not exist await exceptionsListClient.createTrustedAppsList(); - const summary = { linux: 0, windows: 0, @@ -225,7 +226,7 @@ export const getTrustedAppsSummary = async ( listId: ENDPOINT_TRUSTED_APPS_LIST_ID, page, perPage, - filter: undefined, + filter: kuery, namespaceType: 'agnostic', sortField: undefined, sortOrder: undefined, From e1ae48578f1ba1a73476a3d9c2be8d845b3055a4 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Sep 2021 14:23:26 -0400 Subject: [PATCH 007/139] [Canvas] `SidebarHeader` refactor. (#110176) (#111569) * Removed recompose. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Yaroslav Kuznietsov --- .../__stories__/sidebar_header.stories.tsx | 2 +- .../public/components/sidebar_header/index.js | 66 ------ .../components/sidebar_header/index.tsx | 9 + .../sidebar_header.component.tsx | 161 +++++++++++++ .../sidebar_header/sidebar_header.tsx | 215 +++++------------- 5 files changed, 229 insertions(+), 224 deletions(-) delete mode 100644 x-pack/plugins/canvas/public/components/sidebar_header/index.js create mode 100644 x-pack/plugins/canvas/public/components/sidebar_header/index.tsx create mode 100644 x-pack/plugins/canvas/public/components/sidebar_header/sidebar_header.component.tsx diff --git a/x-pack/plugins/canvas/public/components/sidebar_header/__stories__/sidebar_header.stories.tsx b/x-pack/plugins/canvas/public/components/sidebar_header/__stories__/sidebar_header.stories.tsx index f92ec99995eed..b98d994460dee 100644 --- a/x-pack/plugins/canvas/public/components/sidebar_header/__stories__/sidebar_header.stories.tsx +++ b/x-pack/plugins/canvas/public/components/sidebar_header/__stories__/sidebar_header.stories.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; -import { SidebarHeader } from '../sidebar_header'; +import { SidebarHeader } from '../sidebar_header.component'; const handlers = { bringToFront: action('bringToFront'), diff --git a/x-pack/plugins/canvas/public/components/sidebar_header/index.js b/x-pack/plugins/canvas/public/components/sidebar_header/index.js deleted file mode 100644 index 6e0fafc2f6bc2..0000000000000 --- a/x-pack/plugins/canvas/public/components/sidebar_header/index.js +++ /dev/null @@ -1,66 +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 { connect } from 'react-redux'; -import { compose, withHandlers } from 'recompose'; -import { insertNodes, elementLayer, removeElements } from '../../state/actions/elements'; -import { getSelectedPage, getNodes, getSelectedToplevelNodes } from '../../state/selectors/workpad'; -import { flatten } from '../../lib/aeroelastic/functional'; -import { - layerHandlerCreators, - clipboardHandlerCreators, - basicHandlerCreators, - groupHandlerCreators, - alignmentDistributionHandlerCreators, -} from '../../lib/element_handler_creators'; -import { crawlTree } from '../workpad_page/integration_utils'; -import { selectToplevelNodes } from './../../state/actions/transient'; -import { SidebarHeader as Component } from './sidebar_header'; - -/* - * TODO: this is all copied from interactive_workpad_page and workpad_shortcuts - */ -const mapStateToProps = (state) => { - const pageId = getSelectedPage(state); - const nodes = getNodes(state, pageId); - const selectedToplevelNodes = getSelectedToplevelNodes(state); - const selectedPrimaryShapeObjects = selectedToplevelNodes - .map((id) => nodes.find((s) => s.id === id)) - .filter((shape) => shape); - const selectedPersistentPrimaryNodes = flatten( - selectedPrimaryShapeObjects.map((shape) => - nodes.find((n) => n.id === shape.id) // is it a leaf or a persisted group? - ? [shape.id] - : nodes.filter((s) => s.parent === shape.id).map((s) => s.id) - ) - ); - const selectedNodeIds = flatten(selectedPersistentPrimaryNodes.map(crawlTree(nodes))); - - return { - pageId, - selectedNodes: selectedNodeIds.map((id) => nodes.find((s) => s.id === id)), - }; -}; - -const mapDispatchToProps = (dispatch) => ({ - insertNodes: (selectedNodes, pageId) => dispatch(insertNodes(selectedNodes, pageId)), - removeNodes: (nodeIds, pageId) => dispatch(removeElements(nodeIds, pageId)), - selectToplevelNodes: (nodes) => - dispatch(selectToplevelNodes(nodes.filter((e) => !e.position.parent).map((e) => e.id))), - elementLayer: (pageId, elementId, movement) => { - dispatch(elementLayer({ pageId, elementId, movement })); - }, -}); - -export const SidebarHeader = compose( - connect(mapStateToProps, mapDispatchToProps), - withHandlers(basicHandlerCreators), - withHandlers(clipboardHandlerCreators), - withHandlers(layerHandlerCreators), - withHandlers(groupHandlerCreators), - withHandlers(alignmentDistributionHandlerCreators) -)(Component); diff --git a/x-pack/plugins/canvas/public/components/sidebar_header/index.tsx b/x-pack/plugins/canvas/public/components/sidebar_header/index.tsx new file mode 100644 index 0000000000000..64e8013b1ddfa --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar_header/index.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { SidebarHeader } from './sidebar_header'; +export { SidebarHeader as SidebarHeaderComponent } from './sidebar_header.component'; diff --git a/x-pack/plugins/canvas/public/components/sidebar_header/sidebar_header.component.tsx b/x-pack/plugins/canvas/public/components/sidebar_header/sidebar_header.component.tsx new file mode 100644 index 0000000000000..08785af9b4b96 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/sidebar_header/sidebar_header.component.tsx @@ -0,0 +1,161 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { ToolTipShortcut } from '../tool_tip_shortcut'; +import { ShortcutStrings } from '../../../i18n/shortcuts'; + +const strings = { + getBringForwardAriaLabel: () => + i18n.translate('xpack.canvas.sidebarHeader.bringForwardArialLabel', { + defaultMessage: 'Move element up one layer', + }), + getBringToFrontAriaLabel: () => + i18n.translate('xpack.canvas.sidebarHeader.bringToFrontArialLabel', { + defaultMessage: 'Move element to top layer', + }), + getSendBackwardAriaLabel: () => + i18n.translate('xpack.canvas.sidebarHeader.sendBackwardArialLabel', { + defaultMessage: 'Move element down one layer', + }), + getSendToBackAriaLabel: () => + i18n.translate('xpack.canvas.sidebarHeader.sendToBackArialLabel', { + defaultMessage: 'Move element to bottom layer', + }), +}; + +const shortcutHelp = ShortcutStrings.getShortcutHelp(); + +interface Props { + /** + * title to display in the header + */ + title: string; + /** + * indicated whether or not layer controls should be displayed + */ + showLayerControls?: boolean; + /** + * moves selected element to top layer + */ + bringToFront: () => void; + /** + * moves selected element up one layer + */ + bringForward: () => void; + /** + * moves selected element down one layer + */ + sendBackward: () => void; + /** + * moves selected element to bottom layer + */ + sendToBack: () => void; +} + +export const SidebarHeader: FunctionComponent = ({ + title, + showLayerControls = false, + bringToFront, + bringForward, + sendBackward, + sendToBack, +}) => ( + + + + {title} + + + {showLayerControls ? ( + + + + + {shortcutHelp.BRING_TO_FRONT} + + + } + > + + + + + + {shortcutHelp.BRING_FORWARD} + + + } + > + + + + + + {shortcutHelp.SEND_BACKWARD} + + + } + > + + + + + + {shortcutHelp.SEND_TO_BACK} + + + } + > + + + + + + ) : null} + +); diff --git a/x-pack/plugins/canvas/public/components/sidebar_header/sidebar_header.tsx b/x-pack/plugins/canvas/public/components/sidebar_header/sidebar_header.tsx index 4ba3a7f90f64b..119195f190252 100644 --- a/x-pack/plugins/canvas/public/components/sidebar_header/sidebar_header.tsx +++ b/x-pack/plugins/canvas/public/components/sidebar_header/sidebar_header.tsx @@ -5,171 +5,72 @@ * 2.0. */ -import React, { FunctionComponent } from 'react'; -import PropTypes from 'prop-types'; -import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; +import React from 'react'; +import deepEqual from 'react-fast-compare'; +import { useDispatch, useSelector } from 'react-redux'; +// @ts-expect-error unconverted component +import { elementLayer } from '../../state/actions/elements'; +import { getSelectedPage, getNodes, getSelectedToplevelNodes } from '../../state/selectors/workpad'; +// @ts-expect-error unconverted lib +import { flatten } from '../../lib/aeroelastic/functional'; +import { layerHandlerCreators } from '../../lib/element_handler_creators'; +// @ts-expect-error unconverted component +import { crawlTree } from '../workpad_page/integration_utils'; +import { State } from '../../../types'; +import { SidebarHeader as Component } from './sidebar_header.component'; -import { ToolTipShortcut } from '../tool_tip_shortcut/'; -import { ShortcutStrings } from '../../../i18n/shortcuts'; - -const strings = { - getBringForwardAriaLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.bringForwardArialLabel', { - defaultMessage: 'Move element up one layer', - }), - getBringToFrontAriaLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.bringToFrontArialLabel', { - defaultMessage: 'Move element to top layer', - }), - getSendBackwardAriaLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.sendBackwardArialLabel', { - defaultMessage: 'Move element down one layer', - }), - getSendToBackAriaLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.sendToBackArialLabel', { - defaultMessage: 'Move element to bottom layer', - }), +const getSelectedNodes = (state: State, pageId: string): Array => { + const nodes = getNodes(state, pageId); + const selectedToplevelNodes = getSelectedToplevelNodes(state); + const selectedPrimaryShapeObjects = selectedToplevelNodes + .map((id) => nodes.find((s) => s.id === id)) + .filter((shape) => shape); + const selectedPersistentPrimaryNodes = flatten( + selectedPrimaryShapeObjects.map((shape) => + nodes.find((n) => n.id === shape?.id) // is it a leaf or a persisted group? + ? [shape?.id] + : nodes.filter((s) => s.position?.parent === shape?.id).map((s) => s.id) + ) + ); + const selectedNodeIds = flatten(selectedPersistentPrimaryNodes.map(crawlTree(nodes))); + return selectedNodeIds.map((id: string) => nodes.find((s) => s.id === id)); }; -const shortcutHelp = ShortcutStrings.getShortcutHelp(); +const createHandlers = function ( + handlers: Record any>, + context: Record +) { + return Object.keys(handlers).reduce any>>((acc, val) => { + acc[val as keyof T] = handlers[val as keyof T](context); + return acc; + }, {} as Record any>); +}; interface Props { - /** - * title to display in the header - */ title: string; - /** - * indicated whether or not layer controls should be displayed - */ - showLayerControls?: boolean; - /** - * moves selected element to top layer - */ - bringToFront: () => void; - /** - * moves selected element up one layer - */ - bringForward: () => void; - /** - * moves selected element down one layer - */ - sendBackward: () => void; - /** - * moves selected element to bottom layer - */ - sendToBack: () => void; } -export const SidebarHeader: FunctionComponent = ({ - title, - showLayerControls, - bringToFront, - bringForward, - sendBackward, - sendToBack, -}) => ( - - - - {title} - - - {showLayerControls ? ( - - - - - {shortcutHelp.BRING_TO_FRONT} - - - } - > - - - - - - {shortcutHelp.BRING_FORWARD} - - - } - > - - - - - - {shortcutHelp.SEND_BACKWARD} - - - } - > - - - - - - {shortcutHelp.SEND_TO_BACK} - - - } - > - - - - - - ) : null} - -); +export const SidebarHeader: React.FC = (props) => { + const pageId = useSelector((state) => getSelectedPage(state)); + const selectedNodes = useSelector>( + (state) => getSelectedNodes(state, pageId), + deepEqual + ); -SidebarHeader.propTypes = { - title: PropTypes.string.isRequired, - showLayerControls: PropTypes.bool, // TODO: remove when we support relayering multiple elements - bringToFront: PropTypes.func.isRequired, - bringForward: PropTypes.func.isRequired, - sendBackward: PropTypes.func.isRequired, - sendToBack: PropTypes.func.isRequired, -}; + const dispatch = useDispatch(); + + const elementLayerDispatch = (selectedPageId: string, elementId: string, movement: number) => { + dispatch(elementLayer({ pageId: selectedPageId, elementId, movement })); + }; + + const handlersContext = { + ...props, + pageId, + selectedNodes, + elementLayer: elementLayerDispatch, + }; + + const layerHandlers = createHandlers(layerHandlerCreators, handlersContext); -SidebarHeader.defaultProps = { - showLayerControls: false, + return ; }; From d381d3631140139d0dbd75e03e01b5dfb100cf8a Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Sep 2021 14:27:35 -0400 Subject: [PATCH 008/139] fix topN popover for case view (#111514) (#111567) * fix topN popover for case view * unit tests Co-authored-by: Angela Chuang <6295984+angorayc@users.noreply.github.com> --- .../use_hover_action_items.test.tsx | 90 +++++++++++++++++++ .../hover_actions/use_hover_action_items.tsx | 82 ++++++++--------- 2 files changed, 128 insertions(+), 44 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/hover_actions/use_hover_action_items.test.tsx b/x-pack/plugins/security_solution/public/common/components/hover_actions/use_hover_action_items.test.tsx index f37f801982d2b..345f79521aa99 100644 --- a/x-pack/plugins/security_solution/public/common/components/hover_actions/use_hover_action_items.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/hover_actions/use_hover_action_items.test.tsx @@ -184,4 +184,94 @@ describe('useHoverActionItems', () => { ); }); }); + + test('if not on CaseView, overflow button is enabled, ShowTopNButton should disable popOver (e.g.: alerts flyout)', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => { + const testProps = { + ...defaultProps, + enableOverflowButton: true, + }; + return useHoverActionItems(testProps); + }); + await waitForNextUpdate(); + expect(result.current.allActionItems[4].props.enablePopOver).toEqual(false); + }); + }); + + test('if not on CaseView, overflow button is disabled, ShowTopNButton should disable popOver (e.g.: alerts table - reason field)', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => { + const testProps = { + ...defaultProps, + enableOverflowButton: false, + }; + return useHoverActionItems(testProps); + }); + await waitForNextUpdate(); + expect(result.current.allActionItems[4].props.enablePopOver).toEqual(false); + }); + }); + + test('if on CaseView, ShowTopNButton should enable popOver', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => { + const testProps = { + ...defaultProps, + isCaseView: true, + enableOverflowButton: false, + }; + return useHoverActionItems(testProps); + }); + await waitForNextUpdate(); + + expect(result.current.allActionItems[1].props.enablePopOver).toEqual(true); + }); + }); + + test('if on CaseView, it should show all items when shoTopN is true', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => { + const testProps = { + ...defaultProps, + showTopN: true, + isCaseView: true, + enableOverflowButton: false, + }; + return useHoverActionItems(testProps); + }); + await waitForNextUpdate(); + + expect(result.current.allActionItems).toHaveLength(3); + expect(result.current.allActionItems[0].props['data-test-subj']).toEqual( + 'hover-actions-add-timeline' + ); + expect(result.current.allActionItems[1].props['data-test-subj']).toEqual( + 'hover-actions-show-top-n' + ); + expect(result.current.allActionItems[2].props['data-test-subj']).toEqual( + 'hover-actions-copy-button' + ); + }); + }); + + test('when disable OverflowButton, it should show only "showTopNBtn" when shoTopN is true', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => { + const testProps = { + ...defaultProps, + showTopN: true, + isCaseView: false, + enableOverflowButton: false, + }; + return useHoverActionItems(testProps); + }); + await waitForNextUpdate(); + + expect(result.current.allActionItems).toHaveLength(1); + expect(result.current.allActionItems[0].props['data-test-subj']).toEqual( + 'hover-actions-show-top-n' + ); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/hover_actions/use_hover_action_items.tsx b/x-pack/plugins/security_solution/public/common/components/hover_actions/use_hover_action_items.tsx index 9ff844c608dd9..f717a72ab8ad5 100644 --- a/x-pack/plugins/security_solution/public/common/components/hover_actions/use_hover_action_items.tsx +++ b/x-pack/plugins/security_solution/public/common/components/hover_actions/use_hover_action_items.tsx @@ -5,8 +5,6 @@ * 2.0. */ -/* eslint-disable complexity */ - import { EuiContextMenuItem } from '@elastic/eui'; import React, { useMemo } from 'react'; import { DraggableId } from 'react-beautiful-dnd'; @@ -124,6 +122,36 @@ export const useHoverActionItems = ({ values != null && (enableOverflowButton || (!showTopN && !enableOverflowButton)) && !isCaseView; const shouldDisableColumnToggle = (isObjectArray && field !== 'geo_point') || isCaseView; + const showTopNBtn = useMemo( + () => ( + + ), + [ + enableOverflowButton, + field, + isCaseView, + onFilterAdded, + ownFocus, + showTopN, + timelineId, + toggleTopN, + values, + ] + ); + const allItems = useMemo( () => [ @@ -191,21 +219,9 @@ export const useHoverActionItems = ({ browserField: getAllFieldsByName(browserFields)[field], fieldName: field, hideTopN, - }) ? ( - - ) : null, + }) + ? showTopNBtn + : null, field != null ? ( {getCopyButton({ @@ -244,34 +260,13 @@ export const useHoverActionItems = ({ ownFocus, shouldDisableColumnToggle, showFilters, - showTopN, + showTopNBtn, stKeyboardEvent, - timelineId, toggleColumn, - toggleTopN, values, ] ) as JSX.Element[]; - const showTopNBtn = useMemo( - () => ( - - ), - [enableOverflowButton, field, onFilterAdded, ownFocus, showTopN, timelineId, toggleTopN, values] - ); - const overflowActionItems = useMemo( () => [ @@ -311,11 +306,10 @@ export const useHoverActionItems = ({ ] ); - const allActionItems = useMemo(() => (showTopN ? [showTopNBtn] : allItems), [ - allItems, - showTopNBtn, - showTopN, - ]); + const allActionItems = useMemo( + () => (showTopN && !enableOverflowButton && !isCaseView ? [showTopNBtn] : allItems), + [showTopN, enableOverflowButton, isCaseView, showTopNBtn, allItems] + ); return { overflowActionItems, From 705d2b07c894b9ffe8d8c8cf7cf3cbc209b5a2d6 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Wed, 8 Sep 2021 21:41:12 +0300 Subject: [PATCH 009/139] [7.x] [Vislib] Removes old implementation of xy chart (#110786) (#111558) * [Vislib] Removes old implementation of xy chart (#110786) * [Vislib] Remove xy chart * Update i18n * Remove uncecessary file * Fix types * More fixes * Fix functional tests part 1 * Fix functional tests part 2 * Fix bug with shard-delay * Fix functional tests part 3 * fix functional tests part4 * Fix async_serch FT * Fix functional dashboard async test * REplace screenshot area chart image * Cleanup vislib from xy charts * Remove unused fixtures * Address PR comments * Remove miaou :D * Address PR comments * Fix i18n Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> # Conflicts: # docs/management/advanced-options.asciidoc # test/functional/screenshots/baseline/area_chart.png * Fixes * Remove setting from docs Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/management/advanced-options.asciidoc | 4 - .../server/collectors/management/schema.ts | 4 - .../server/collectors/management/types.ts | 1 - src/plugins/telemetry/schema/oss_plugins.json | 6 - .../components/timelion_vis_component.tsx | 2 +- src/plugins/vis_types/vislib/public/area.ts | 18 - .../dispatch_bar_chart_config_normal.json | 31 -- .../dispatch_bar_chart_config_percentage.json | 40 -- .../fixtures/dispatch_bar_chart_d3.json | 19 - .../dispatch_bar_chart_data_point.json | 9 - .../vis_types/vislib/public/histogram.ts | 18 - .../vis_types/vislib/public/horizontal_bar.ts | 18 - src/plugins/vis_types/vislib/public/line.ts | 18 - src/plugins/vis_types/vislib/public/plugin.ts | 12 +- src/plugins/vis_types/vislib/public/types.ts | 5 - .../public/vis_type_vislib_vis_types.ts | 14 - .../vis_types/vislib/public/vislib/VISLIB.md | 11 +- .../vislib/public/vislib/_index.scss | 1 - .../public/vislib/lib/axis/axis.test.js | 2 +- .../public/vislib/lib/axis/axis_title.test.js | 2 +- .../public/vislib/lib/axis/x_axis.test.js | 2 +- .../public/vislib/lib/axis/y_axis.test.js | 2 +- .../public/vislib/lib/chart_title.test.js | 2 +- .../vislib/public/vislib/lib/dispatch.test.js | 68 ++- .../lib/dispatch_vertical_bar_chart.test.js | 48 -- .../vislib/public/vislib/lib/handler.test.js | 20 +- .../public/vislib/lib/layout/_layout.scss | 4 - .../public/vislib/lib/layout/layout.test.js | 24 +- .../vislib/public/vislib/lib/types/index.js | 5 - .../public/vislib/lib/types/point_series.js | 35 -- .../vislib/lib/types/point_series.test.js | 72 +-- .../types/testdata_linechart_percentile.json | 464 ------------------ ...data_linechart_percentile_float_value.json | 463 ----------------- ...nechart_percentile_float_value_result.json | 456 ----------------- .../testdata_linechart_percentile_result.json | 458 ----------------- .../public/vislib/lib/vis_config.test.js | 2 +- .../vislib/public/vislib/vis.test.js | 20 +- .../vislib/visualizations/_vis_fixture.js | 2 +- .../vislib/visualizations/chart.test.js | 26 +- .../vislib/visualizations/point_series.js | 8 +- .../visualizations/point_series/_index.scss | 1 - .../visualizations/point_series/_labels.scss | 20 - .../visualizations/point_series/area_chart.js | 247 ---------- .../point_series/area_chart.test.js | 264 ---------- .../point_series/column_chart.js | 383 --------------- .../point_series/column_chart.test.js | 401 --------------- .../visualizations/point_series/line_chart.js | 230 --------- .../point_series/line_chart.test.js | 225 --------- .../point_series/series_types.js | 19 - src/plugins/vis_types/xy/common/index.ts | 2 - src/plugins/vis_types/xy/kibana.json | 2 +- .../xy/public/editor/common_config.tsx | 54 +- .../components/common/validation_wrapper.tsx | 12 +- .../editor/components/options/index.tsx | 22 +- .../__snapshots__/index.test.tsx.snap | 1 - .../value_axes_panel.test.tsx.snap | 2 - .../options/metrics_axes/index.test.tsx | 24 +- .../components/options/metrics_axes/index.tsx | 14 +- .../options/metrics_axes/value_axes_panel.tsx | 2 - .../metrics_axes/value_axis_options.tsx | 4 +- .../options/point_series/grid_panel.tsx | 30 +- .../point_series/point_series.test.tsx | 52 +- .../options/point_series/point_series.tsx | 27 +- src/plugins/vis_types/xy/public/index.ts | 1 - src/plugins/vis_types/xy/public/plugin.ts | 29 +- .../vis_types/xy/public/utils/accessors.tsx | 3 +- .../vis_types/xy/public/vis_types/area.ts | 11 +- .../xy/public/vis_types/histogram.ts | 11 +- .../xy/public/vis_types/horizontal_bar.ts | 11 +- .../vis_types/xy/public/vis_types/index.ts | 29 +- .../vis_types/xy/public/vis_types/line.ts | 11 +- src/plugins/vis_types/xy/server/index.ts | 10 - src/plugins/vis_types/xy/server/plugin.ts | 56 --- .../components/deprecation_vis_warning.tsx | 66 --- .../components/visualize_editor_common.tsx | 13 - .../apps/dashboard/dashboard_state.ts | 45 +- test/functional/apps/dashboard/index.ts | 2 - .../apps/getting_started/_shakespeare.ts | 48 +- test/functional/apps/getting_started/index.ts | 2 - test/functional/apps/visualize/_area_chart.ts | 120 ++--- .../apps/visualize/_line_chart_split_chart.ts | 134 +++-- .../visualize/_line_chart_split_series.ts | 134 +++-- .../apps/visualize/_point_series_options.ts | 156 +++--- test/functional/apps/visualize/_timelion.ts | 82 +++- .../apps/visualize/_vertical_bar_chart.ts | 184 +++---- .../_vertical_bar_chart_nontimeindex.ts | 141 ++---- test/functional/apps/visualize/index.ts | 8 - test/functional/config.js | 1 - .../page_objects/visual_builder_page.ts | 4 - .../page_objects/visualize_chart_page.ts | 368 ++++---------- .../page_objects/visualize_editor_page.ts | 4 +- .../functional/page_objects/visualize_page.ts | 5 +- .../screenshots/baseline/area_chart.png | Bin 129876 -> 85012 bytes .../translations/translations/ja-JP.json | 16 +- .../translations/translations/zh-CN.json | 12 +- x-pack/test/functional/config.js | 1 - .../fixtures/kbn_archiver/rollup/rollup.json | 1 - .../dashboard/async_search/async_search.ts | 13 +- .../async_search/save_search_session.ts | 13 +- .../apps/dashboard/dashboard_smoke_tests.ts | 1 - 100 files changed, 883 insertions(+), 5317 deletions(-) delete mode 100644 src/plugins/vis_types/vislib/public/area.ts delete mode 100644 src/plugins/vis_types/vislib/public/fixtures/dispatch_bar_chart_config_normal.json delete mode 100644 src/plugins/vis_types/vislib/public/fixtures/dispatch_bar_chart_config_percentage.json delete mode 100644 src/plugins/vis_types/vislib/public/fixtures/dispatch_bar_chart_d3.json delete mode 100644 src/plugins/vis_types/vislib/public/fixtures/dispatch_bar_chart_data_point.json delete mode 100644 src/plugins/vis_types/vislib/public/histogram.ts delete mode 100644 src/plugins/vis_types/vislib/public/horizontal_bar.ts delete mode 100644 src/plugins/vis_types/vislib/public/line.ts delete mode 100644 src/plugins/vis_types/vislib/public/vislib/lib/dispatch_vertical_bar_chart.test.js delete mode 100644 src/plugins/vis_types/vislib/public/vislib/lib/types/testdata_linechart_percentile.json delete mode 100644 src/plugins/vis_types/vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value.json delete mode 100644 src/plugins/vis_types/vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value_result.json delete mode 100644 src/plugins/vis_types/vislib/public/vislib/lib/types/testdata_linechart_percentile_result.json delete mode 100644 src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/_index.scss delete mode 100644 src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/_labels.scss delete mode 100644 src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/area_chart.js delete mode 100644 src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/area_chart.test.js delete mode 100644 src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/column_chart.js delete mode 100644 src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/column_chart.test.js delete mode 100644 src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/line_chart.js delete mode 100644 src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/line_chart.test.js delete mode 100644 src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/series_types.js delete mode 100644 src/plugins/vis_types/xy/server/index.ts delete mode 100644 src/plugins/vis_types/xy/server/plugin.ts delete mode 100644 src/plugins/visualize/public/application/components/deprecation_vis_warning.tsx diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 651a63fb8f3cc..26eeff0a8641b 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -548,10 +548,6 @@ The maximum geoHash precision displayed in tile maps. 7 is high, 10 is very high and 12 is the maximum. For more information, refer to {ref}/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator[Cell dimensions at the equator]. -[[visualization-visualize-chartslibrary]]`visualization:visualize:legacyChartsLibrary`:: -**The legacy XY charts are deprecated and will not be supported as of 7.16.** -The visualize editor uses a new XY charts library with improved performance, color palettes, fill capacity, and more. Enable this option if you prefer to use the legacy charts library. - [[visualization-visualize-pieChartslibrary]]`visualization:visualize:legacyPieChartsLibrary`:: The visualize editor uses new pie charts with improved performance, color palettes, label positioning, and more. Enable this option if you prefer to use to the legacy charts library. diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index b8455b9547bad..79ebf69acc4fe 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -408,10 +408,6 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, - 'visualization:visualize:legacyChartsLibrary': { - type: 'boolean', - _meta: { description: 'Non-default value of setting.' }, - }, 'visualization:visualize:legacyPieChartsLibrary': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index 2fdbd5825df8a..b27feda208c2c 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -28,7 +28,6 @@ export interface UsageStats { 'autocomplete:useTimeRange': boolean; 'autocomplete:valueSuggestionMethod': string; 'search:timeout': number; - 'visualization:visualize:legacyChartsLibrary': boolean; 'visualization:visualize:legacyPieChartsLibrary': boolean; 'doc_table:legacy': boolean; 'discover:modifyColumnsOnSwitch': boolean; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 3a308b1b69c35..8dec1740e3cc8 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -7759,12 +7759,6 @@ "description": "Non-default value of setting." } }, - "visualization:visualize:legacyChartsLibrary": { - "type": "boolean", - "_meta": { - "description": "Non-default value of setting." - } - }, "visualization:visualize:legacyPieChartsLibrary": { "type": "boolean", "_meta": { diff --git a/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx b/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx index 858ba0ad64add..3cc335392b7c4 100644 --- a/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx +++ b/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx @@ -177,7 +177,7 @@ const TimelionVisComponent = ({ }, [chart]); return ( - + {title && ( {title} diff --git a/src/plugins/vis_types/vislib/public/area.ts b/src/plugins/vis_types/vislib/public/area.ts deleted file mode 100644 index f4ac79e12bbe2..0000000000000 --- a/src/plugins/vis_types/vislib/public/area.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { xyVisTypes } from '../../xy/public'; -import { VisTypeDefinition } from '../../../visualizations/public'; - -import { toExpressionAst } from './to_ast'; -import { BasicVislibParams } from './types'; - -export const areaVisTypeDefinition = { - ...xyVisTypes.area(), - toExpressionAst, -} as VisTypeDefinition; diff --git a/src/plugins/vis_types/vislib/public/fixtures/dispatch_bar_chart_config_normal.json b/src/plugins/vis_types/vislib/public/fixtures/dispatch_bar_chart_config_normal.json deleted file mode 100644 index a72cebcfdf2c8..0000000000000 --- a/src/plugins/vis_types/vislib/public/fixtures/dispatch_bar_chart_config_normal.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "seriesParams": [ - { - "data": { - "id": "1", - "label": "Count" - }, - "drawLinesBetweenPoints": true, - "interpolate": "cardinal", - "mode": "stacked", - "show": "true", - "showCircles": true, - "type": "histogram", - "valueAxis": "ValueAxis-1" - } - ], - "valueAxes": [ - { - "id": "ValueAxis-1", - "name": "LeftAxis-1", - "position": "left", - "scale": { - "type": "linear", - "mode": "normal" - }, - "show": true, - "style": {}, - "type": "value" - } - ] -} \ No newline at end of file diff --git a/src/plugins/vis_types/vislib/public/fixtures/dispatch_bar_chart_config_percentage.json b/src/plugins/vis_types/vislib/public/fixtures/dispatch_bar_chart_config_percentage.json deleted file mode 100644 index 1fb4bc89bf4e9..0000000000000 --- a/src/plugins/vis_types/vislib/public/fixtures/dispatch_bar_chart_config_percentage.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "seriesParams": [ - { - "data": { - "id": "1", - "label": "Count" - }, - "drawLinesBetweenPoints": true, - "interpolate": "cardinal", - "mode": "stacked", - "show": "true", - "showCircles": true, - "type": "histogram", - "valueAxis": "ValueAxis-1" - } - ], - "valueAxes": [ - { - "id": "ValueAxis-1", - "labels": { - "show": true, - "rotate": 0, - "filter": false, - "truncate": 100 - }, - "name": "LeftAxis-1", - "position": "left", - "scale": { - "type": "linear", - "mode": "percentage" - }, - "show": true, - "style": {}, - "title": { - "text": "Count" - }, - "type": "value" - } - ] -} \ No newline at end of file diff --git a/src/plugins/vis_types/vislib/public/fixtures/dispatch_bar_chart_d3.json b/src/plugins/vis_types/vislib/public/fixtures/dispatch_bar_chart_d3.json deleted file mode 100644 index f614ab64d7b34..0000000000000 --- a/src/plugins/vis_types/vislib/public/fixtures/dispatch_bar_chart_d3.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "series": [ - { - "id": "1", - "rawId": "Late Aircraft Delay-col-2-1", - "label": "Late Aircraft Delay" - }, - { - "id": "1", - "rawId": "No Delay-col-2-1", - "label": "No Delay" - }, - { - "id": "1", - "rawId": "NAS Delay-col-2-1", - "label": "NAS Delay" - } - ] -} \ No newline at end of file diff --git a/src/plugins/vis_types/vislib/public/fixtures/dispatch_bar_chart_data_point.json b/src/plugins/vis_types/vislib/public/fixtures/dispatch_bar_chart_data_point.json deleted file mode 100644 index 19bb7b30d6e6a..0000000000000 --- a/src/plugins/vis_types/vislib/public/fixtures/dispatch_bar_chart_data_point.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "parent": { - "accessor": "col-1-3", - "column": 1, - "params": {} - }, - "series": "No Delay", - "seriesId": "No Delay-col-2-1" -} \ No newline at end of file diff --git a/src/plugins/vis_types/vislib/public/histogram.ts b/src/plugins/vis_types/vislib/public/histogram.ts deleted file mode 100644 index bb4f570c6a2d8..0000000000000 --- a/src/plugins/vis_types/vislib/public/histogram.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { xyVisTypes } from '../../xy/public'; -import { VisTypeDefinition } from '../../../visualizations/public'; - -import { toExpressionAst } from './to_ast'; -import { BasicVislibParams } from './types'; - -export const histogramVisTypeDefinition = { - ...xyVisTypes.histogram(), - toExpressionAst, -} as VisTypeDefinition; diff --git a/src/plugins/vis_types/vislib/public/horizontal_bar.ts b/src/plugins/vis_types/vislib/public/horizontal_bar.ts deleted file mode 100644 index 37aa79a0b1aee..0000000000000 --- a/src/plugins/vis_types/vislib/public/horizontal_bar.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { xyVisTypes } from '../../xy/public'; -import { VisTypeDefinition } from '../../../visualizations/public'; - -import { toExpressionAst } from './to_ast'; -import { BasicVislibParams } from './types'; - -export const horizontalBarVisTypeDefinition = { - ...xyVisTypes.horizontalBar(), - toExpressionAst, -} as VisTypeDefinition; diff --git a/src/plugins/vis_types/vislib/public/line.ts b/src/plugins/vis_types/vislib/public/line.ts deleted file mode 100644 index 0f33c393e0643..0000000000000 --- a/src/plugins/vis_types/vislib/public/line.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { xyVisTypes } from '../../xy/public'; -import { VisTypeDefinition } from '../../../visualizations/public'; - -import { toExpressionAst } from './to_ast'; -import { BasicVislibParams } from './types'; - -export const lineVisTypeDefinition = { - ...xyVisTypes.line(), - toExpressionAst, -} as VisTypeDefinition; diff --git a/src/plugins/vis_types/vislib/public/plugin.ts b/src/plugins/vis_types/vislib/public/plugin.ts index 24ba7741cab91..b0385475f7105 100644 --- a/src/plugins/vis_types/vislib/public/plugin.ts +++ b/src/plugins/vis_types/vislib/public/plugin.ts @@ -13,16 +13,11 @@ import { VisualizationsSetup } from '../../../visualizations/public'; import { ChartsPluginSetup } from '../../../charts/public'; import { DataPublicPluginStart } from '../../../data/public'; import { KibanaLegacyStart } from '../../../kibana_legacy/public'; -import { LEGACY_CHARTS_LIBRARY } from '../../xy/common/index'; import { LEGACY_PIE_CHARTS_LIBRARY } from '../../pie/common/index'; import { createVisTypeVislibVisFn } from './vis_type_vislib_vis_fn'; import { createPieVisFn } from './pie_fn'; -import { - convertedTypeDefinitions, - pieVisTypeDefinition, - visLibVisTypeDefinitions, -} from './vis_type_vislib_vis_types'; +import { visLibVisTypeDefinitions, pieVisTypeDefinition } from './vis_type_vislib_vis_types'; import { setFormatService, setDataActions } from './services'; import { getVislibVisRenderer } from './vis_renderer'; @@ -51,11 +46,8 @@ export class VisTypeVislibPlugin core: VisTypeVislibCoreSetup, { expressions, visualizations, charts }: VisTypeVislibPluginSetupDependencies ) { - const typeDefinitions = !core.uiSettings.get(LEGACY_CHARTS_LIBRARY, false) - ? convertedTypeDefinitions - : visLibVisTypeDefinitions; // register vislib XY axis charts - typeDefinitions.forEach(visualizations.createBaseVisualization); + visLibVisTypeDefinitions.forEach(visualizations.createBaseVisualization); expressions.registerRenderer(getVislibVisRenderer(core, charts)); expressions.registerFunction(createVisTypeVislibVisFn()); diff --git a/src/plugins/vis_types/vislib/public/types.ts b/src/plugins/vis_types/vislib/public/types.ts index 5196f0e33f404..9184e2a4c884c 100644 --- a/src/plugins/vis_types/vislib/public/types.ts +++ b/src/plugins/vis_types/vislib/public/types.ts @@ -37,12 +37,7 @@ export const GaugeType = Object.freeze({ export type GaugeType = $Values; export const VislibChartType = Object.freeze({ - Histogram: 'histogram' as const, - HorizontalBar: 'horizontal_bar' as const, - Line: 'line' as const, Pie: 'pie' as const, - Area: 'area' as const, - PointSeries: 'point_series' as const, Heatmap: 'heatmap' as const, Gauge: 'gauge' as const, Goal: 'goal' as const, diff --git a/src/plugins/vis_types/vislib/public/vis_type_vislib_vis_types.ts b/src/plugins/vis_types/vislib/public/vis_type_vislib_vis_types.ts index 325c9256d7184..6ecb63ca31b37 100644 --- a/src/plugins/vis_types/vislib/public/vis_type_vislib_vis_types.ts +++ b/src/plugins/vis_types/vislib/public/vis_type_vislib_vis_types.ts @@ -7,27 +7,13 @@ */ import { VisTypeDefinition } from 'src/plugins/visualizations/public'; -import { histogramVisTypeDefinition } from './histogram'; -import { lineVisTypeDefinition } from './line'; -import { areaVisTypeDefinition } from './area'; import { heatmapVisTypeDefinition } from './heatmap'; -import { horizontalBarVisTypeDefinition } from './horizontal_bar'; import { gaugeVisTypeDefinition } from './gauge'; import { goalVisTypeDefinition } from './goal'; export { pieVisTypeDefinition } from './pie'; export const visLibVisTypeDefinitions: Array> = [ - histogramVisTypeDefinition, - lineVisTypeDefinition, - areaVisTypeDefinition, - heatmapVisTypeDefinition, - horizontalBarVisTypeDefinition, - gaugeVisTypeDefinition, - goalVisTypeDefinition, -]; - -export const convertedTypeDefinitions: Array> = [ heatmapVisTypeDefinition, gaugeVisTypeDefinition, goalVisTypeDefinition, diff --git a/src/plugins/vis_types/vislib/public/vislib/VISLIB.md b/src/plugins/vis_types/vislib/public/vislib/VISLIB.md index 05ca9a51b19eb..1f17228dda7ab 100644 --- a/src/plugins/vis_types/vislib/public/vislib/VISLIB.md +++ b/src/plugins/vis_types/vislib/public/vislib/VISLIB.md @@ -1,4 +1,8 @@ -# Vislib general overview +# Charts supported + +Vislib supports the heatmap and gauge/goal charts from the aggregation-based visualizations. It also contains the legacy implemementation of the pie chart (enabled by the visualization:visualize:legacyPieChartsLibrary advanced setting). + +# General overview `vis.js` constructor accepts vis parameters and render method accepts data. it exposes event emitter interface so we can listen to certain events like 'renderComplete'. @@ -18,7 +22,4 @@ All base visualizations extend from `visualizations/_chart` ### Point series chart -`visualizations/point_series` takes care of drawing the point series chart (no axes or titles, just the chart itself). It creates all the series defined and calls render method on them. - -currently there are 3 series types available (line, area, bars), they all extend from `visualizations/point_series/_point_series`. - +`visualizations/point_series` takes care of drawing the point series chart (no axes or titles, just the chart itself). It creates all the series defined and calls render method on them. \ No newline at end of file diff --git a/src/plugins/vis_types/vislib/public/vislib/_index.scss b/src/plugins/vis_types/vislib/public/vislib/_index.scss index 78e16224a67a3..00b22df06f10d 100644 --- a/src/plugins/vis_types/vislib/public/vislib/_index.scss +++ b/src/plugins/vis_types/vislib/public/vislib/_index.scss @@ -5,5 +5,4 @@ @import './components/tooltip/index'; @import './components/legend/index'; -@import './visualizations/point_series/index'; @import './visualizations/gauges/index'; diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/axis/axis.test.js b/src/plugins/vis_types/vislib/public/vislib/lib/axis/axis.test.js index f8dcf8edc6bee..ef4f08cac35f6 100644 --- a/src/plugins/vis_types/vislib/public/vislib/lib/axis/axis.test.js +++ b/src/plugins/vis_types/vislib/public/vislib/lib/axis/axis.test.js @@ -96,7 +96,7 @@ describe('Vislib Axis Class Test Suite', function () { const visConfig = new VisConfig( { - type: 'histogram', + type: 'heatmap', }, data, mockUiState, diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/axis/axis_title.test.js b/src/plugins/vis_types/vislib/public/vislib/lib/axis/axis_title.test.js index 90e5a4ee6defb..b5a158e173b0d 100644 --- a/src/plugins/vis_types/vislib/public/vislib/lib/axis/axis_title.test.js +++ b/src/plugins/vis_types/vislib/public/vislib/lib/axis/axis_title.test.js @@ -103,7 +103,7 @@ describe('Vislib AxisTitle Class Test Suite', function () { dataObj = new Data(data, getMockUiState(), () => undefined); visConfig = new VisConfig( { - type: 'histogram', + type: 'heatmap', }, data, getMockUiState(), diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/axis/x_axis.test.js b/src/plugins/vis_types/vislib/public/vislib/lib/axis/x_axis.test.js index 5b2ff31727074..1ded9e48fcfd3 100644 --- a/src/plugins/vis_types/vislib/public/vislib/lib/axis/x_axis.test.js +++ b/src/plugins/vis_types/vislib/public/vislib/lib/axis/x_axis.test.js @@ -101,7 +101,7 @@ describe('Vislib xAxis Class Test Suite', function () { const visConfig = new VisConfig( { - type: 'histogram', + type: 'heatmap', }, data, mockUiState, diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/axis/y_axis.test.js b/src/plugins/vis_types/vislib/public/vislib/lib/axis/y_axis.test.js index c69a029fca18c..5bbfde01197e5 100644 --- a/src/plugins/vis_types/vislib/public/vislib/lib/axis/y_axis.test.js +++ b/src/plugins/vis_types/vislib/public/vislib/lib/axis/y_axis.test.js @@ -81,7 +81,7 @@ function createData(seriesData) { buildYAxis = function (params) { const visConfig = new VisConfig( { - type: 'histogram', + type: 'heatmap', }, data, mockUiState, diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/chart_title.test.js b/src/plugins/vis_types/vislib/public/vislib/lib/chart_title.test.js index 291b2da81b8ce..54b326a292845 100644 --- a/src/plugins/vis_types/vislib/public/vislib/lib/chart_title.test.js +++ b/src/plugins/vis_types/vislib/public/vislib/lib/chart_title.test.js @@ -99,7 +99,7 @@ describe('Vislib ChartTitle Class Test Suite', function () { const visConfig = new VisConfig( { - type: 'histogram', + type: 'heatmap', title: { text: 'rows', }, diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/dispatch.test.js b/src/plugins/vis_types/vislib/public/vislib/lib/dispatch.test.js index dfc36a364e7ad..21a3dc069d8c6 100644 --- a/src/plugins/vis_types/vislib/public/vislib/lib/dispatch.test.js +++ b/src/plugins/vis_types/vislib/public/vislib/lib/dispatch.test.js @@ -8,6 +8,7 @@ import _ from 'lodash'; import d3 from 'd3'; +import $ from 'jquery'; import { setHTMLElementClientSizes, setSVGElementGetBBox, @@ -23,6 +24,7 @@ import { getVis } from '../visualizations/_vis_fixture'; let mockedHTMLElementClientSizes; let mockedSVGElementGetBBox; let mockedSVGElementGetComputedTextLength; +let mockWidth; describe('Vislib Dispatch Class Test Suite', function () { function destroyVis(vis) { @@ -37,22 +39,43 @@ describe('Vislib Dispatch Class Test Suite', function () { mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); mockedSVGElementGetBBox = setSVGElementGetBBox(100); mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100); + mockWidth = jest.spyOn($.prototype, 'width').mockReturnValue(900); }); afterAll(() => { mockedHTMLElementClientSizes.mockRestore(); mockedSVGElementGetBBox.mockRestore(); mockedSVGElementGetComputedTextLength.mockRestore(); + mockWidth.mockRestore(); }); describe('', function () { let vis; let mockUiState; - beforeEach(() => { - vis = getVis(); + const vislibParams = { + type: 'heatmap', + addLegend: true, + addTooltip: true, + colorsNumber: 4, + colorSchema: 'Greens', + setColorRange: false, + percentageMode: true, + percentageFormatPattern: '0.0%', + invertColors: false, + colorsRange: [], + }; + + function generateVis(opts = {}) { + const config = _.defaultsDeep({}, opts, vislibParams); + vis = getVis(config); mockUiState = getMockUiState(); + vis.on('brush', _.noop); vis.render(data, mockUiState); + } + + beforeEach(() => { + generateVis(); }); afterEach(function () { @@ -74,11 +97,29 @@ describe('Vislib Dispatch Class Test Suite', function () { let vis; let mockUiState; - beforeEach(() => { + const vislibParams = { + type: 'heatmap', + addLegend: true, + addTooltip: true, + colorsNumber: 4, + colorSchema: 'Greens', + setColorRange: false, + percentageMode: true, + percentageFormatPattern: '0.0%', + invertColors: false, + colorsRange: [], + }; + + function generateVis(opts = {}) { + const config = _.defaultsDeep({}, opts, vislibParams); + vis = getVis(config); mockUiState = getMockUiState(); - vis = getVis(); vis.on('brush', _.noop); vis.render(data, mockUiState); + } + + beforeEach(() => { + generateVis(); }); afterEach(function () { @@ -183,9 +224,22 @@ describe('Vislib Dispatch Class Test Suite', function () { }); describe('Custom event handlers', function () { + const vislibParams = { + type: 'heatmap', + addLegend: true, + addTooltip: true, + colorsNumber: 4, + colorSchema: 'Greens', + setColorRange: false, + percentageMode: true, + percentageFormatPattern: '0.0%', + invertColors: false, + colorsRange: [], + }; + const config = _.defaultsDeep({}, vislibParams); + const vis = getVis(config); + const mockUiState = getMockUiState(); test('should attach whatever gets passed on vis.on() to chart.events', function (done) { - const vis = getVis(); - const mockUiState = getMockUiState(); vis.on('someEvent', _.noop); vis.render(data, mockUiState); @@ -198,8 +252,6 @@ describe('Vislib Dispatch Class Test Suite', function () { }); test('can be added after rendering', function () { - const vis = getVis(); - const mockUiState = getMockUiState(); vis.render(data, mockUiState); vis.on('someEvent', _.noop); diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/dispatch_vertical_bar_chart.test.js b/src/plugins/vis_types/vislib/public/vislib/lib/dispatch_vertical_bar_chart.test.js deleted file mode 100644 index 0374f082f1676..0000000000000 --- a/src/plugins/vis_types/vislib/public/vislib/lib/dispatch_vertical_bar_chart.test.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import mockDispatchDataD3 from '../../fixtures/dispatch_bar_chart_d3.json'; -import { Dispatch } from './dispatch'; -import mockdataPoint from '../../fixtures/dispatch_bar_chart_data_point.json'; -import mockConfigPercentage from '../../fixtures/dispatch_bar_chart_config_percentage.json'; -import mockConfigNormal from '../../fixtures/dispatch_bar_chart_config_normal.json'; - -jest.mock('d3', () => ({ - event: { - target: { - nearestViewportElement: { - __data__: mockDispatchDataD3, - }, - }, - }, -})); - -function getHandlerMock(config = {}, data = {}) { - return { - visConfig: { get: (id, fallback) => config[id] || fallback }, - data, - }; -} - -describe('Vislib event responses dispatcher', () => { - test('return data for a vertical bars popover in percentage mode', () => { - const dataPoint = mockdataPoint; - const handlerMock = getHandlerMock(mockConfigPercentage); - const dispatch = new Dispatch(handlerMock); - const actual = dispatch.eventResponse(dataPoint, 0); - expect(actual.isPercentageMode).toBeTruthy(); - }); - - test('return data for a vertical bars popover in normal mode', () => { - const dataPoint = mockdataPoint; - const handlerMock = getHandlerMock(mockConfigNormal); - const dispatch = new Dispatch(handlerMock); - const actual = dispatch.eventResponse(dataPoint, 0); - expect(actual.isPercentageMode).toBeFalsy(); - }); -}); diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/handler.test.js b/src/plugins/vis_types/vislib/public/vislib/lib/handler.test.js index 326a29f3690bc..60ffaf3f3d19c 100644 --- a/src/plugins/vis_types/vislib/public/vislib/lib/handler.test.js +++ b/src/plugins/vis_types/vislib/public/vislib/lib/handler.test.js @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +import _ from 'lodash'; import $ from 'jquery'; import { setHTMLElementClientSizes, @@ -26,6 +26,7 @@ const names = ['series', 'columns', 'rows', 'stackedSeries']; let mockedHTMLElementClientSizes; let mockedSVGElementGetBBox; let mockedSVGElementGetComputedTextLength; +let mockWidth; dateHistogramArray.forEach(function (data, i) { describe('Vislib Handler Test Suite for ' + names[i] + ' Data', function () { @@ -36,10 +37,24 @@ dateHistogramArray.forEach(function (data, i) { mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); mockedSVGElementGetBBox = setSVGElementGetBBox(100); mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100); + mockWidth = jest.spyOn($.prototype, 'width').mockReturnValue(900); }); beforeEach(() => { - vis = getVis(); + const vislibParams = { + type: 'heatmap', + addLegend: true, + addTooltip: true, + colorsNumber: 4, + colorSchema: 'Greens', + setColorRange: false, + percentageMode: true, + percentageFormatPattern: '0.0%', + invertColors: false, + colorsRange: [], + }; + const config = _.defaultsDeep({}, vislibParams); + vis = getVis(config); vis.render(data, getMockUiState()); }); @@ -51,6 +66,7 @@ dateHistogramArray.forEach(function (data, i) { mockedHTMLElementClientSizes.mockRestore(); mockedSVGElementGetBBox.mockRestore(); mockedSVGElementGetComputedTextLength.mockRestore(); + mockWidth.mockRestore(); }); describe('render Method', function () { diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/layout/_layout.scss b/src/plugins/vis_types/vislib/public/vislib/lib/layout/_layout.scss index a6896a9181b4e..7ead0b314c7ad 100644 --- a/src/plugins/vis_types/vislib/public/vislib/lib/layout/_layout.scss +++ b/src/plugins/vis_types/vislib/public/vislib/lib/layout/_layout.scss @@ -187,10 +187,6 @@ fill: $visHoverBackgroundColor; } - .visAreaChart__overlapArea { - opacity: .8; - } - .series > path, .series > rect { stroke-opacity: 1; diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/layout/layout.test.js b/src/plugins/vis_types/vislib/public/vislib/lib/layout/layout.test.js index f4ea2d3898d25..af59f011515d0 100644 --- a/src/plugins/vis_types/vislib/public/vislib/lib/layout/layout.test.js +++ b/src/plugins/vis_types/vislib/public/vislib/lib/layout/layout.test.js @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +import _ from 'lodash'; import d3 from 'd3'; import $ from 'jquery'; import { @@ -30,6 +30,7 @@ const names = ['series', 'columns', 'rows', 'stackedSeries']; let mockedHTMLElementClientSizes; let mockedSVGElementGetBBox; let mockedSVGElementGetComputedTextLength; +let mockWidth; dateHistogramArray.forEach(function (data, i) { describe('Vislib Layout Class Test Suite for ' + names[i] + ' Data', function () { @@ -42,10 +43,24 @@ dateHistogramArray.forEach(function (data, i) { mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); mockedSVGElementGetBBox = setSVGElementGetBBox(100); mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100); + mockWidth = jest.spyOn($.prototype, 'width').mockReturnValue(900); }); beforeEach(() => { - vis = getVis(); + const vislibParams = { + type: 'heatmap', + addLegend: true, + addTooltip: true, + colorsNumber: 4, + colorSchema: 'Greens', + setColorRange: false, + percentageMode: true, + percentageFormatPattern: '0.0%', + invertColors: false, + colorsRange: [], + }; + const config = _.defaultsDeep({}, vislibParams); + vis = getVis(config); mockUiState = getMockUiState(); vis.render(data, mockUiState); numberOfCharts = vis.handler.charts.length; @@ -59,6 +74,7 @@ dateHistogramArray.forEach(function (data, i) { mockedHTMLElementClientSizes.mockRestore(); mockedSVGElementGetBBox.mockRestore(); mockedSVGElementGetComputedTextLength.mockRestore(); + mockWidth.mockRestore(); }); describe('createLayout Method', function () { @@ -81,7 +97,7 @@ dateHistogramArray.forEach(function (data, i) { beforeEach(function () { const visConfig = new VisConfig( { - type: 'histogram', + type: 'heatmap', }, data, mockUiState, @@ -125,7 +141,7 @@ dateHistogramArray.forEach(function (data, i) { expect(function () { testLayout.layout({ - parent: 'histogram', + parent: 'heatmap', type: 'div', }); }).toThrowError(); diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/types/index.js b/src/plugins/vis_types/vislib/public/vislib/lib/types/index.js index dcfff3618ab91..4a9dd0bd512ca 100644 --- a/src/plugins/vis_types/vislib/public/vislib/lib/types/index.js +++ b/src/plugins/vis_types/vislib/public/vislib/lib/types/index.js @@ -11,12 +11,7 @@ import { vislibPieConfig } from './pie'; import { vislibGaugeConfig } from './gauge'; export const vislibTypesConfig = { - histogram: pointSeries.column, - horizontal_bar: pointSeries.column, - line: pointSeries.line, pie: vislibPieConfig, - area: pointSeries.area, - point_series: pointSeries.line, heatmap: pointSeries.heatmap, gauge: vislibGaugeConfig, goal: vislibGaugeConfig, diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/types/point_series.js b/src/plugins/vis_types/vislib/public/vislib/lib/types/point_series.js index 9753cbb78ea5c..2328a09205dd6 100644 --- a/src/plugins/vis_types/vislib/public/vislib/lib/types/point_series.js +++ b/src/plugins/vis_types/vislib/public/vislib/lib/types/point_series.js @@ -193,41 +193,6 @@ function create(opts) { } export const vislibPointSeriesTypes = { - line: create(), - - column: create({ - expandLastBucket: true, - }), - - area: create({ - alerts: [ - { - type: 'warning', - msg: - 'Positive and negative values are not accurately represented by stacked ' + - 'area charts. Either changing the chart mode to "overlap" or using a ' + - 'bar chart is recommended.', - test: function (_, data) { - if (!data.shouldBeStacked() || data.maxNumberOfSeries() < 2) return; - - const hasPos = data.getYMax(data._getY) > 0; - const hasNeg = data.getYMin(data._getY) < 0; - return hasPos && hasNeg; - }, - }, - { - type: 'warning', - msg: - 'Parts of or the entire area chart might not be displayed due to null ' + - 'values in the data. A line chart is recommended when displaying data ' + - 'with null values.', - test: function (_, data) { - return data.hasNullValues(); - }, - }, - ], - }), - heatmap: (cfg, data) => { const defaults = create()(cfg, data); const hasCharts = defaults.charts.length; diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/types/point_series.test.js b/src/plugins/vis_types/vislib/public/vislib/lib/types/point_series.test.js index aa2fe39c34ec3..c0764e6a39ed6 100644 --- a/src/plugins/vis_types/vislib/public/vislib/lib/types/point_series.test.js +++ b/src/plugins/vis_types/vislib/public/vislib/lib/types/point_series.test.js @@ -8,10 +8,6 @@ import stackedSeries from '../../../fixtures/mock_data/date_histogram/_stacked_series'; import { vislibPointSeriesTypes } from './point_series'; -import percentileTestdata from './testdata_linechart_percentile.json'; -import percentileTestdataResult from './testdata_linechart_percentile_result.json'; -import percentileTestdataFloatValue from './testdata_linechart_percentile_float_value.json'; -import percentileTestdataFloatValueResult from './testdata_linechart_percentile_float_value_result.json'; const maxBucketData = { get: (prop) => { @@ -84,7 +80,7 @@ describe('vislibPointSeriesTypes', () => { describe('axis formatters', () => { it('should create a value axis config with the default y axis formatter', () => { - const parsedConfig = vislibPointSeriesTypes.line({}, maxBucketData); + const parsedConfig = vislibPointSeriesTypes.heatmap({}, maxBucketData); expect(parsedConfig.valueAxes.length).toEqual(1); expect(parsedConfig.valueAxes[0].labels.axisFormatter).toBe( maxBucketData.data.yAxisFormatter @@ -95,7 +91,7 @@ describe('vislibPointSeriesTypes', () => { const axisFormatter1 = jest.fn(); const axisFormatter2 = jest.fn(); const axisFormatter3 = jest.fn(); - const parsedConfig = vislibPointSeriesTypes.line( + const parsedConfig = vislibPointSeriesTypes.heatmap( { valueAxes: [ { @@ -166,67 +162,3 @@ describe('vislibPointSeriesTypes', () => { }); }); }); - -describe('Point Series Config Type Class Test Suite', function () { - let parsedConfig; - const histogramConfig = { - type: 'histogram', - addLegend: true, - tooltip: { - show: true, - }, - categoryAxes: [ - { - id: 'CategoryAxis-1', - type: 'category', - title: {}, - }, - ], - valueAxes: [ - { - id: 'ValueAxis-1', - type: 'value', - labels: {}, - title: {}, - }, - ], - }; - - describe('histogram chart', function () { - beforeEach(function () { - parsedConfig = vislibPointSeriesTypes.column(histogramConfig, maxBucketData); - }); - it('should not throw an error when more than 25 series are provided', function () { - expect(parsedConfig.error).toBeUndefined(); - }); - - it('should set axis title and formatter from data', () => { - expect(parsedConfig.categoryAxes[0].title.text).toEqual(maxBucketData.data.xAxisLabel); - expect(parsedConfig.valueAxes[0].labels.axisFormatter).toBeDefined(); - }); - }); - - describe('line chart', function () { - function prepareData({ cfg, data }) { - const percentileDataObj = { - get: (prop) => { - return maxBucketData[prop] || maxBucketData.data[prop] || null; - }, - getLabels: () => [], - data: data, - }; - const parsedConfig = vislibPointSeriesTypes.line(cfg, percentileDataObj); - return parsedConfig; - } - - it('should render a percentile line chart', function () { - const parsedConfig = prepareData(percentileTestdata); - expect(parsedConfig).toMatchObject(percentileTestdataResult); - }); - - it('should render a percentile line chart when value is float', function () { - const parsedConfig = prepareData(percentileTestdataFloatValue); - expect(parsedConfig).toMatchObject(percentileTestdataFloatValueResult); - }); - }); -}); diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/types/testdata_linechart_percentile.json b/src/plugins/vis_types/vislib/public/vislib/lib/types/testdata_linechart_percentile.json deleted file mode 100644 index 50d6eab03e3f7..0000000000000 --- a/src/plugins/vis_types/vislib/public/vislib/lib/types/testdata_linechart_percentile.json +++ /dev/null @@ -1,464 +0,0 @@ -{ - "cfg": { - "addLegend": true, - "addTimeMarker": false, - "addTooltip": true, - "categoryAxes": [ - { - "id": "CategoryAxis-1", - "labels": { - "show": true, - "truncate": 100 - }, - "position": "bottom", - "scale": { - "type": "linear" - }, - "show": true, - "style": {}, - "title": {}, - "type": "category" - } - ], - "dimensions": { - "x": { - "accessor": 0, - "format": { - "id": "date", - "params": { - "pattern": "YYYY-MM-DD" - } - }, - "params": { - "date": true, - "interval": 86400000, - "format": "YYYY-MM-DD", - "bounds": { - "min": "2019-05-10T04:00:00.000Z", - "max": "2019-05-12T10:18:57.342Z" - } - }, - "aggType": "date_histogram" - }, - "y": [ - { - "accessor": 1, - "format": { - "id": "number", - "params": { - "pattern": "$0,0.[00]" - } - }, - "params": {}, - "aggType": "percentiles" - }, - { - "accessor": 2, - "format": { - "id": "number", - "params": { - "pattern": "$0,0.[00]" - } - }, - "params": {}, - "aggType": "percentiles" - } - ] - }, - "grid": { - "categoryLines": false, - "style": { - "color": "#eee" - } - }, - "legendPosition": "right", - "seriesParams": [ - { - "data": { - "id": "1", - "label": "Percentiles of AvgTicketPrice" - }, - "drawLinesBetweenPoints": true, - "interpolate": "cardinal", - "mode": "normal", - "show": "true", - "showCircles": true, - "type": "line", - "valueAxis": "ValueAxis-1" - } - ], - "times": [], - "type": "area", - "valueAxes": [ - { - "id": "ValueAxis-1", - "labels": { - "filter": false, - "rotate": 0, - "show": true, - "truncate": 100 - }, - "name": "LeftAxis-1", - "position": "left", - "scale": { - "mode": "normal", - "type": "linear" - }, - "show": true, - "style": {}, - "title": { - "text": "Percentiles of AvgTicketPrice" - }, - "type": "value" - } - ] - }, - "data": { - "uiState": {}, - "data": { - "xAxisOrderedValues": [ - 1557460800000, - 1557547200000 - ], - "xAxisFormat": { - "id": "date", - "params": { - "pattern": "YYYY-MM-DD" - } - }, - "xAxisLabel": "timestamp per day", - "ordered": { - "interval": 86400000, - "date": true, - "min": 1557460800000, - "max": 1557656337342 - }, - "yAxisFormat": { - "id": "number", - "params": { - "pattern": "$0,0.[00]" - } - }, - "yAxisLabel": "", - "hits": 2 - }, - "series": [ - { - "id": "1.1", - "rawId": "col-1-1.1", - "label": "1st percentile of AvgTicketPrice", - "values": [ - { - "x": 1557460800000, - "y": 116.33676605224609, - "extraMetrics": [], - "xRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 0, - "row": 0, - "value": 1557460800000 - }, - "yRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 1, - "row": 0, - "value": 116.33676605224609 - }, - "parent": null, - "series": "1st percentile of AvgTicketPrice", - "seriesId": "col-1-1.1" - }, - { - "x": 1557547200000, - "y": 223, - "extraMetrics": [], - "xRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 0, - "row": 1, - "value": 1557547200000 - }, - "yRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 1, - "row": 1, - "value": 223 - }, - "parent": null, - "series": "1st percentile of AvgTicketPrice", - "seriesId": "col-1-1.1" - } - ] - }, - { - "id": "1.50", - "rawId": "col-2-1.50", - "label": "50th percentile of AvgTicketPrice", - "values": [ - { - "x": 1557460800000, - "y": 658.8453063964844, - "extraMetrics": [], - "xRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 0, - "row": 0, - "value": 1557460800000 - }, - "yRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 2, - "row": 0, - "value": 658 - }, - "parent": null, - "series": "50th percentile of AvgTicketPrice", - "seriesId": "col-2-1.50" - }, - { - "x": 1557547200000, - "y": 756, - "extraMetrics": [], - "xRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 0, - "row": 1, - "value": 1557547200000 - }, - "yRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 2, - "row": 1, - "value": 756.2283554077148 - }, - "parent": null, - "series": "50th percentile of AvgTicketPrice", - "seriesId": "col-2-1.50" - } - ] - } - ], - "type": "series", - "labels": [ - "1st percentile of AvgTicketPrice", - "50th percentile of AvgTicketPrice" - ] - } - -} diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value.json b/src/plugins/vis_types/vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value.json deleted file mode 100644 index 1987c59f6722b..0000000000000 --- a/src/plugins/vis_types/vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value.json +++ /dev/null @@ -1,463 +0,0 @@ -{ - "cfg": { - "addLegend": true, - "addTimeMarker": false, - "addTooltip": true, - "categoryAxes": [ - { - "id": "CategoryAxis-1", - "labels": { - "show": true, - "truncate": 100 - }, - "position": "bottom", - "scale": { - "type": "linear" - }, - "show": true, - "style": {}, - "title": {}, - "type": "category" - } - ], - "dimensions": { - "x": { - "accessor": 0, - "format": { - "id": "date", - "params": { - "pattern": "YYYY-MM-DD" - } - }, - "params": { - "date": true, - "interval": 86400000, - "format": "YYYY-MM-DD", - "bounds": { - "min": "2019-05-10T04:00:00.000Z", - "max": "2019-05-12T10:18:57.342Z" - } - }, - "aggType": "date_histogram" - }, - "y": [ - { - "accessor": 1, - "format": { - "id": "number", - "params": { - "pattern": "$0,0.[00]" - } - }, - "params": {}, - "aggType": "percentiles" - }, - { - "accessor": 2, - "format": { - "id": "number", - "params": { - "pattern": "$0,0.[00]" - } - }, - "params": {}, - "aggType": "percentiles" - } - ] - }, - "grid": { - "categoryLines": false, - "style": { - "color": "#eee" - } - }, - "legendPosition": "right", - "seriesParams": [ - { - "data": { - "id": "1", - "label": "Percentiles of AvgTicketPrice" - }, - "drawLinesBetweenPoints": true, - "interpolate": "cardinal", - "mode": "normal", - "show": "true", - "showCircles": true, - "type": "line", - "valueAxis": "ValueAxis-1" - } - ], - "times": [], - "type": "area", - "valueAxes": [ - { - "id": "ValueAxis-1", - "labels": { - "filter": false, - "rotate": 0, - "show": true, - "truncate": 100 - }, - "name": "LeftAxis-1", - "position": "left", - "scale": { - "mode": "normal", - "type": "linear" - }, - "show": true, - "style": {}, - "title": { - "text": "Percentiles of AvgTicketPrice" - }, - "type": "value" - } - ] - }, - "data": { - "uiState": {}, - "data": { - "xAxisOrderedValues": [ - 1557460800000, - 1557547200000 - ], - "xAxisFormat": { - "id": "date", - "params": { - "pattern": "YYYY-MM-DD" - } - }, - "xAxisLabel": "timestamp per day", - "ordered": { - "interval": 86400000, - "date": true, - "min": 1557460800000, - "max": 1557656337342 - }, - "yAxisFormat": { - "id": "number", - "params": { - "pattern": "$0,0.[00]" - } - }, - "yAxisLabel": "", - "hits": 2 - }, - "series": [ - { - "id": "1.['1.1']", - "rawId": "col-1-1.['1.1']", - "label": "1.1th percentile of AvgTicketPrice", - "values": [ - { - "x": 1557460800000, - "y": 116.33676605224609, - "extraMetrics": [], - "xRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.['1.1']", - "name": "1.1th percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.['1.1']": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.['1.1']": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 0, - "row": 0, - "value": 1557460800000 - }, - "yRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.['1.1']", - "name": "1.1th percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.['1.1']": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.['1.1']": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 1, - "row": 0, - "value": 116.33676605224609 - }, - "parent": null, - "series": "1.1th percentile of AvgTicketPrice", - "seriesId": "col-1-1.['1.1']" - }, - { - "x": 1557547200000, - "y": 223, - "extraMetrics": [], - "xRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.['1.1']", - "name": "1.1th percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.['1.1']": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.['1.1']": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 0, - "row": 1, - "value": 1557547200000 - }, - "yRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.['1.1']", - "name": "1.1th percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.['1.1']": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.['1.1']": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 1, - "row": 1, - "value": 223 - }, - "parent": null, - "series": "1.1th percentile of AvgTicketPrice", - "seriesId": "col-1-1.['1.1']" - } - ] - }, - { - "id": "1.50", - "rawId": "col-2-1.50", - "label": "50th percentile of AvgTicketPrice", - "values": [ - { - "x": 1557460800000, - "y": 658.8453063964844, - "extraMetrics": [], - "xRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.['1.1']", - "name": "1.1th percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.['1.1']": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.['1.1']": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 0, - "row": 0, - "value": 1557460800000 - }, - "yRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.['1.1']", - "name": "1.1th percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.['1.1']": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.['1.1']": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 2, - "row": 0, - "value": 658 - }, - "parent": null, - "series": "50th percentile of AvgTicketPrice", - "seriesId": "col-2-1.50" - }, - { - "x": 1557547200000, - "y": 756, - "extraMetrics": [], - "xRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.['1.1']", - "name": "1.1th percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.['1.1']": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.['1.1']": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 0, - "row": 1, - "value": 1557547200000 - }, - "yRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.['1.1']", - "name": "1.1th percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.['1.1']": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.['1.1']": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 2, - "row": 1, - "value": 756.2283554077148 - }, - "parent": null, - "series": "50th percentile of AvgTicketPrice", - "seriesId": "col-2-1.50" - } - ] - } - ], - "type": "series", - "labels": [ - "1.1th percentile of AvgTicketPrice", - "50th percentile of AvgTicketPrice" - ] - } -} diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value_result.json b/src/plugins/vis_types/vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value_result.json deleted file mode 100644 index ae1f3cbf24c33..0000000000000 --- a/src/plugins/vis_types/vislib/public/vislib/lib/types/testdata_linechart_percentile_float_value_result.json +++ /dev/null @@ -1,456 +0,0 @@ -{ - "addLegend": true, - "addTimeMarker": false, - "addTooltip": true, - "categoryAxes": [ - { - "id": "CategoryAxis-1", - "labels": { - "show": true, - "truncate": 100 - }, - "position": "bottom", - "scale": { - "type": "linear" - }, - "show": true, - "style": {}, - "title": { - "text": "Date Histogram" - }, - "type": "category" - } - ], - "dimensions": { - "x": { - "accessor": 0, - "format": { - "id": "date", - "params": { - "pattern": "YYYY-MM-DD" - } - }, - "params": { - "date": true, - "interval": 86400000, - "format": "YYYY-MM-DD", - "bounds": { - "min": "2019-05-10T04:00:00.000Z", - "max": "2019-05-12T10:18:57.342Z" - } - }, - "aggType": "date_histogram" - }, - "y": [ - { - "accessor": 1, - "format": { - "id": "number", - "params": { - "pattern": "$0,0.[00]" - } - }, - "params": {}, - "aggType": "percentiles" - }, - { - "accessor": 2, - "format": { - "id": "number", - "params": { - "pattern": "$0,0.[00]" - } - }, - "params": {}, - "aggType": "percentiles" - } - ] - }, - "grid": { - "categoryLines": false, - "style": { - "color": "#eee" - } - }, - "legendPosition": "right", - "seriesParams": [ - { - "data": { - "id": "1", - "label": "Percentiles of AvgTicketPrice" - }, - "drawLinesBetweenPoints": true, - "interpolate": "cardinal", - "mode": "normal", - "show": "true", - "showCircles": true, - "type": "line", - "valueAxis": "ValueAxis-1" - } - ], - "times": [], - "type": "point_series", - "valueAxes": [ - { - "id": "ValueAxis-1", - "labels": { - "filter": false, - "rotate": 0, - "show": true, - "truncate": 100 - }, - "name": "LeftAxis-1", - "position": "left", - "scale": { - "mode": "normal", - "type": "linear" - }, - "show": true, - "style": {}, - "title": { - "text": "Percentiles of AvgTicketPrice" - }, - "type": "value" - } - ], - "chartTitle": {}, - "mode": "normal", - "tooltip": { - "show": true - }, - "charts": [ - { - "type": "point_series", - "addTimeMarker": false, - "series": [ - { - "show": true, - "type": "area", - "mode": "normal", - "drawLinesBetweenPoints": true, - "showCircles": true, - "data": { - "id": "1.['1.1']", - "rawId": "col-1-1.['1.1']", - "label": "1.1th percentile of AvgTicketPrice", - "values": [ - { - "x": 1557460800000, - "y": 116.33676605224609, - "extraMetrics": [], - "xRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.['1.1']", - "name": "1.1th percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.['1.1']": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.['1.1']": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 0, - "row": 0, - "value": 1557460800000 - }, - "yRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.['1.1']", - "name": "1.1th percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.['1.1']": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.['1.1']": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 1, - "row": 0, - "value": 116.33676605224609 - }, - "parent": null, - "series": "1.1th percentile of AvgTicketPrice", - "seriesId": "col-1-1.['1.1']" - }, - { - "x": 1557547200000, - "y": 223, - "extraMetrics": [], - "xRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.['1.1']", - "name": "1.1th percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.['1.1']": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.['1.1']": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 0, - "row": 1, - "value": 1557547200000 - }, - "yRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.['1.1']", - "name": "1.1th percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.['1.1']": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.['1.1']": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 1, - "row": 1, - "value": 223 - }, - "parent": null, - "series": "1.1th percentile of AvgTicketPrice", - "seriesId": "col-1-1.['1.1']" - } - ] - } - }, - { - "data": { - "id": "1.50", - "rawId": "col-2-1.50", - "label": "50th percentile of AvgTicketPrice", - "values": [ - { - "x": 1557460800000, - "y": 658.8453063964844, - "extraMetrics": [], - "xRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.['1.1']", - "name": "1.1th percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.['1.1']": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.['1.1']": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 0, - "row": 0, - "value": 1557460800000 - }, - "yRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.['1.1']", - "name": "1.1th percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.['1.1']": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.['1.1']": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 2, - "row": 0, - "value": 658 - }, - "parent": null, - "series": "50th percentile of AvgTicketPrice", - "seriesId": "col-2-1.50" - }, - { - "x": 1557547200000, - "y": 756, - "extraMetrics": [], - "xRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.['1.1']", - "name": "1.1th percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.['1.1']": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.['1.1']": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 0, - "row": 1, - "value": 1557547200000 - }, - "yRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.['1.1']", - "name": "1.1th percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.['1.1']": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.['1.1']": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 2, - "row": 1, - "value": 756.2283554077148 - }, - "parent": null, - "series": "50th percentile of AvgTicketPrice", - "seriesId": "col-2-1.50" - } - ] - }, - "drawLinesBetweenPoints": true, - "interpolate": "cardinal", - "mode": "normal", - "show": "true", - "showCircles": true, - "type": "line", - "valueAxis": "ValueAxis-1" - } - ] - } - ], - "enableHover": true -} diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/types/testdata_linechart_percentile_result.json b/src/plugins/vis_types/vislib/public/vislib/lib/types/testdata_linechart_percentile_result.json deleted file mode 100644 index f2ee245a8431f..0000000000000 --- a/src/plugins/vis_types/vislib/public/vislib/lib/types/testdata_linechart_percentile_result.json +++ /dev/null @@ -1,458 +0,0 @@ -{ - "addLegend": true, - "addTimeMarker": false, - "addTooltip": true, - "categoryAxes": [ - { - "id": "CategoryAxis-1", - "labels": { - "show": true, - "truncate": 100 - }, - "position": "bottom", - "scale": { - "type": "linear" - }, - "show": true, - "style": {}, - "title": { - "text": "Date Histogram" - }, - "type": "category" - } - ], - "dimensions": { - "x": { - "accessor": 0, - "format": { - "id": "date", - "params": { - "pattern": "YYYY-MM-DD" - } - }, - "params": { - "date": true, - "interval": 86400000, - "format": "YYYY-MM-DD", - "bounds": { - "min": "2019-05-10T04:00:00.000Z", - "max": "2019-05-12T10:18:57.342Z" - } - }, - "aggType": "date_histogram" - }, - "y": [ - { - "accessor": 1, - "format": { - "id": "number", - "params": { - "pattern": "$0,0.[00]" - } - }, - "params": {}, - "aggType": "percentiles" - }, - { - "accessor": 2, - "format": { - "id": "number", - "params": { - "pattern": "$0,0.[00]" - } - }, - "params": {}, - "aggType": "percentiles" - } - ] - }, - "grid": { - "categoryLines": false, - "style": { - "color": "#eee" - } - }, - "legendPosition": "right", - "seriesParams": [ - { - "data": { - "id": "1", - "label": "Percentiles of AvgTicketPrice" - }, - "drawLinesBetweenPoints": true, - "interpolate": "cardinal", - "mode": "normal", - "show": "true", - "showCircles": true, - "type": "line", - "valueAxis": "ValueAxis-1" - } - ], - "times": [], - "type": "point_series", - "valueAxes": [ - { - "id": "ValueAxis-1", - "labels": { - "filter": false, - "rotate": 0, - "show": true, - "truncate": 100 - }, - "name": "LeftAxis-1", - "position": "left", - "scale": { - "mode": "normal", - "type": "linear" - }, - "show": true, - "style": {}, - "title": { - "text": "Percentiles of AvgTicketPrice" - }, - "type": "value" - } - ], - "chartTitle": {}, - "mode": "normal", - "tooltip": { - "show": true - }, - "charts": [ - { - "type": "point_series", - "addTimeMarker": false, - "series": [ - { - "data": { - "id": "1.1", - "rawId": "col-1-1.1", - "label": "1st percentile of AvgTicketPrice", - "values": [ - { - "x": 1557460800000, - "y": 116.33676605224609, - "extraMetrics": [], - "xRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 0, - "row": 0, - "value": 1557460800000 - }, - "yRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 1, - "row": 0, - "value": 116.33676605224609 - }, - "parent": null, - "series": "1st percentile of AvgTicketPrice", - "seriesId": "col-1-1.1" - }, - { - "x": 1557547200000, - "y": 223, - "extraMetrics": [], - "xRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 0, - "row": 1, - "value": 1557547200000 - }, - "yRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 1, - "row": 1, - "value": 223 - }, - "parent": null, - "series": "1st percentile of AvgTicketPrice", - "seriesId": "col-1-1.1" - } - ] - }, - "drawLinesBetweenPoints": true, - "interpolate": "cardinal", - "mode": "normal", - "show": "true", - "showCircles": true, - "type": "line", - "valueAxis": "ValueAxis-1" - }, - { - "data": { - "id": "1.50", - "rawId": "col-2-1.50", - "label": "50th percentile of AvgTicketPrice", - "values": [ - { - "x": 1557460800000, - "y": 658.8453063964844, - "extraMetrics": [], - "xRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 0, - "row": 0, - "value": 1557460800000 - }, - "yRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 2, - "row": 0, - "value": 658 - }, - "parent": null, - "series": "50th percentile of AvgTicketPrice", - "seriesId": "col-2-1.50" - }, - { - "x": 1557547200000, - "y": 756, - "extraMetrics": [], - "xRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 0, - "row": 1, - "value": 1557547200000 - }, - "yRaw": { - "table": { - "columns": [ - { - "id": "col-0-2", - "name": "timestamp per day" - }, - { - "id": "col-1-1.1", - "name": "1st percentile of AvgTicketPrice" - }, - { - "id": "col-2-1.50", - "name": "50th percentile of AvgTicketPrice" - } - ], - "rows": [ - { - "col-0-2": 1557460800000, - "col-1-1.1": 116, - "col-2-1.50": 658 - }, - { - "col-0-2": 1557547200000, - "col-1-1.1": 223, - "col-2-1.50": 756 - } - ] - }, - "column": 2, - "row": 1, - "value": 756.2283554077148 - }, - "parent": null, - "series": "50th percentile of AvgTicketPrice", - "seriesId": "col-2-1.50" - } - ] - }, - "drawLinesBetweenPoints": true, - "interpolate": "cardinal", - "mode": "normal", - "show": "true", - "showCircles": true, - "type": "line", - "valueAxis": "ValueAxis-1" - } - ] - } - ], - "enableHover": true -} diff --git a/src/plugins/vis_types/vislib/public/vislib/lib/vis_config.test.js b/src/plugins/vis_types/vislib/public/vislib/lib/vis_config.test.js index 441c5d9969c4f..1ae32aa4b5473 100644 --- a/src/plugins/vis_types/vislib/public/vislib/lib/vis_config.test.js +++ b/src/plugins/vis_types/vislib/public/vislib/lib/vis_config.test.js @@ -78,7 +78,7 @@ describe('Vislib VisConfig Class Test Suite', function () { visConfig = new VisConfig( { - type: 'point_series', + type: 'heatmap', }, data, getMockUiState(), diff --git a/src/plugins/vis_types/vislib/public/vislib/vis.test.js b/src/plugins/vis_types/vislib/public/vislib/vis.test.js index 1614175f7e2a4..46afbd1c62f69 100644 --- a/src/plugins/vis_types/vislib/public/vislib/vis.test.js +++ b/src/plugins/vis_types/vislib/public/vislib/vis.test.js @@ -26,6 +26,7 @@ const names = ['series', 'columns', 'rows', 'stackedSeries']; let mockedHTMLElementClientSizes; let mockedSVGElementGetBBox; let mockedSVGElementGetComputedTextLength; +let mockWidth; dataArray.forEach(function (data, i) { describe('Vislib Vis Test Suite for ' + names[i] + ' Data', function () { @@ -35,16 +36,30 @@ dataArray.forEach(function (data, i) { let mockUiState; let secondVis; let numberOfCharts; + let config; beforeAll(() => { mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); mockedSVGElementGetBBox = setSVGElementGetBBox(100); mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100); + mockWidth = jest.spyOn($.prototype, 'width').mockReturnValue(900); }); beforeEach(() => { - vis = getVis(); - secondVis = getVis(); + config = { + type: 'heatmap', + addLegend: true, + addTooltip: true, + colorsNumber: 4, + colorSchema: 'Greens', + setColorRange: false, + percentageMode: true, + percentageFormatPattern: '0.0%', + invertColors: false, + colorsRange: [], + }; + vis = getVis(config); + secondVis = getVis(config); mockUiState = getMockUiState(); }); @@ -57,6 +72,7 @@ dataArray.forEach(function (data, i) { mockedHTMLElementClientSizes.mockRestore(); mockedSVGElementGetBBox.mockRestore(); mockedSVGElementGetComputedTextLength.mockRestore(); + mockWidth.mockRestore(); }); describe('render Method', function () { diff --git a/src/plugins/vis_types/vislib/public/vislib/visualizations/_vis_fixture.js b/src/plugins/vis_types/vislib/public/vislib/visualizations/_vis_fixture.js index f4e2e4b977b8f..7313d1c8f8eac 100644 --- a/src/plugins/vis_types/vislib/public/vislib/visualizations/_vis_fixture.js +++ b/src/plugins/vis_types/vislib/public/vislib/visualizations/_vis_fixture.js @@ -51,7 +51,7 @@ export function getVis(vislibParams, element) { defaultYExtents: false, setYExtents: false, yAxis: {}, - type: 'histogram', + type: 'heatmap', }), coreMock.createSetup(), chartPluginMock.createStartContract() diff --git a/src/plugins/vis_types/vislib/public/vislib/visualizations/chart.test.js b/src/plugins/vis_types/vislib/public/vislib/visualizations/chart.test.js index 97ea3313d81de..c105102dc6ab9 100644 --- a/src/plugins/vis_types/vislib/public/vislib/visualizations/chart.test.js +++ b/src/plugins/vis_types/vislib/public/vislib/visualizations/chart.test.js @@ -7,7 +7,12 @@ */ import d3 from 'd3'; -import { setHTMLElementClientSizes, setSVGElementGetBBox } from '@kbn/test/jest'; +import $ from 'jquery'; +import { + setHTMLElementClientSizes, + setSVGElementGetBBox, + setSVGElementGetComputedTextLength, +} from '@kbn/test/jest'; import { Chart } from './_chart'; import { getMockUiState } from '../../fixtures/mocks'; import { getVis } from './_vis_fixture'; @@ -96,22 +101,31 @@ describe('Vislib _chart Test Suite', function () { let mockedHTMLElementClientSizes; let mockedSVGElementGetBBox; + let mockedSVGElementGetComputedTextLength; + let mockWidth; beforeAll(() => { mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); mockedSVGElementGetBBox = setSVGElementGetBBox(100); + mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100); + mockWidth = jest.spyOn($.prototype, 'width').mockReturnValue(900); }); beforeEach(() => { el = d3.select('body').append('div').attr('class', 'column-chart'); config = { - type: 'histogram', - addTooltip: true, + type: 'heatmap', addLegend: true, - zeroFill: true, + addTooltip: true, + colorsNumber: 4, + colorSchema: 'Greens', + setColorRange: false, + percentageMode: true, + percentageFormatPattern: '0.0%', + invertColors: false, + colorsRange: [], }; - vis = getVis(config, el[0][0]); vis.render(data, getMockUiState()); @@ -126,6 +140,8 @@ describe('Vislib _chart Test Suite', function () { afterAll(() => { mockedHTMLElementClientSizes.mockRestore(); mockedSVGElementGetBBox.mockRestore(); + mockedSVGElementGetComputedTextLength.mockRestore(); + mockWidth.mockRestore(); }); test('should be a constructor for visualization modules', function () { diff --git a/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series.js b/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series.js index b4ab2ea2992c5..dae60fda47631 100644 --- a/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series.js +++ b/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series.js @@ -13,10 +13,8 @@ import $ from 'jquery'; import { Tooltip } from '../components/tooltip'; import { Chart } from './_chart'; import { TimeMarker } from './time_marker'; -import { seriesTypes } from './point_series/series_types'; import { touchdownTemplate } from '../partials/touchdown_template'; - -const seriTypes = seriesTypes; +import { HeatmapChart } from './point_series/heatmap_chart'; /** * Line Chart Visualization @@ -233,9 +231,7 @@ export class PointSeries extends Chart { self.series = []; _.each(self.chartConfig.series, (seriArgs, i) => { if (!seriArgs.show) return; - const SeriClass = - seriTypes[seriArgs.type || self.handler.visConfig.get('chart.type')] || seriTypes.line; - const series = new SeriClass( + const series = new HeatmapChart( self.handler, svg, data.series[i], diff --git a/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/_index.scss b/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/_index.scss deleted file mode 100644 index 53fce786ecc15..0000000000000 --- a/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './labels'; diff --git a/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/_labels.scss b/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/_labels.scss deleted file mode 100644 index 8bcd17fd55ddf..0000000000000 --- a/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/_labels.scss +++ /dev/null @@ -1,20 +0,0 @@ -$visColumnChartBarLabelDarkColor: #000; // EUI doesn't yet have a variable for fully black in all themes; -$visColumnChartBarLabelLightColor: $euiColorGhost; - -.visColumnChart__barLabel { - font-size: 8pt; - pointer-events: none; -} - -.visColumnChart__barLabel--stack { - dominant-baseline: central; - text-anchor: middle; -} - -.visColumnChart__bar-label--dark { - fill: $visColumnChartBarLabelDarkColor; -} - -.visColumnChart__bar-label--light { - fill: $visColumnChartBarLabelLightColor; -} diff --git a/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/area_chart.js b/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/area_chart.js deleted file mode 100644 index 2e2ce79247c3d..0000000000000 --- a/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/area_chart.js +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import d3 from 'd3'; -import _ from 'lodash'; -import $ from 'jquery'; -import { PointSeries } from './_point_series'; - -const defaults = { - mode: 'normal', - showCircles: true, - radiusRatio: 9, - showLines: true, - interpolate: 'linear', - color: undefined, - fillColor: undefined, -}; -/** - * Area chart visualization - * - * @class AreaChart - * @constructor - * @extends Chart - * @param handler {Object} Reference to the Handler Class Constructor - * @param el {HTMLElement} HTML element to which the chart will be appended - * @param chartData {Object} Elasticsearch query results for this specific - * chart - */ -export class AreaChart extends PointSeries { - constructor(handler, chartEl, chartData, seriesConfigArgs, uiSettings) { - super(handler, chartEl, chartData, seriesConfigArgs, uiSettings); - - this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults); - this.isOverlapping = this.seriesConfig.mode !== 'stacked'; - if (this.isOverlapping) { - // Default opacity should return to 0.6 on mouseout - const defaultOpacity = 0.6; - this.seriesConfig.defaultOpacity = defaultOpacity; - handler.highlight = function (element) { - const label = this.getAttribute('data-label'); - if (!label) return; - - const highlightOpacity = 0.8; - const highlightElements = $('[data-label]', element.parentNode).filter(function (els, el) { - return `${$(el).data('label')}` === label; - }); - $('[data-label]', element.parentNode) - .not(highlightElements) - .css('opacity', defaultOpacity / 2); // half of the default opacity - highlightElements.css('opacity', highlightOpacity); - }; - handler.unHighlight = function (element) { - $('[data-label]', element).css('opacity', defaultOpacity); - - //The legend should keep max opacity - $('[data-label]', $(element).siblings()).css('opacity', 1); - }; - } - } - - addPath(svg, data) { - const ordered = this.handler.data.get('ordered'); - const isTimeSeries = ordered && ordered.date; - const isOverlapping = this.isOverlapping; - const color = this.handler.data.getColorFunc(); - const xScale = this.getCategoryAxis().getScale(); - const yScale = this.getValueAxis().getScale(); - const interpolate = this.seriesConfig.interpolate; - const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); - - // Data layers - const layer = svg.append('g').attr('class', function (d, i) { - return 'series series-' + i; - }); - - // Append path - const path = layer - .append('path') - .attr('data-label', data.label) - .style('fill', () => color(data.label)) - .style('stroke', () => color(data.label)) - .classed('visAreaChart__overlapArea', function () { - return isOverlapping; - }) - .attr('clip-path', 'url(#' + this.baseChart.clipPathId + ')'); - - function x(d) { - if (isTimeSeries) { - return xScale(d.x); - } - return xScale(d.x) + xScale.rangeBand() / 2; - } - - function y1(d) { - const y0 = d.y0 || 0; - const y = d.y || 0; - return yScale(y0 + y); - } - - function y0(d) { - const y0 = d.y0 || 0; - return yScale(y0); - } - - function getArea() { - if (isHorizontal) { - return d3.svg.area().x(x).y0(y0).y1(y1); - } else { - return d3.svg.area().y(x).x0(y0).x1(y1); - } - } - - // update - path - .attr('d', function () { - const area = getArea() - .defined(function (d) { - return !_.isNull(d.y); - }) - .interpolate(interpolate); - return area(data.values); - }) - .style('stroke-width', '1px'); - - return path; - } - - /** - * Adds SVG circles to area chart - * - * @method addCircles - * @param svg {HTMLElement} SVG to which circles are appended - * @param data {Array} Chart data array - * @returns {D3.UpdateSelection} SVG with circles added - */ - addCircles(svg, data) { - const color = this.handler.data.getColorFunc(); - const xScale = this.getCategoryAxis().getScale(); - const yScale = this.getValueAxis().getScale(); - const ordered = this.handler.data.get('ordered'); - const circleRadius = 12; - const circleStrokeWidth = 0; - const tooltip = this.baseChart.tooltip; - const isTooltip = this.handler.visConfig.get('tooltip.show'); - const isOverlapping = this.isOverlapping; - const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); - - const layer = svg - .append('g') - .attr('class', 'points area') - .attr('clip-path', 'url(#' + this.baseChart.clipPathId + ')'); - - // append the circles - const circles = layer.selectAll('circles').data(function appendData() { - return data.values.filter(function isZeroOrNull(d) { - return d.y !== 0 && !_.isNull(d.y); - }); - }); - - // exit - circles.exit().remove(); - - // enter - circles - .enter() - .append('circle') - .attr('data-label', data.label) - .attr('stroke', () => { - return color(data.label); - }) - .attr('fill', 'transparent') - .attr('stroke-width', circleStrokeWidth); - - function cx(d) { - if (ordered && ordered.date) { - return xScale(d.x); - } - return xScale(d.x) + xScale.rangeBand() / 2; - } - - function cy(d) { - const y = d.y || 0; - if (isOverlapping) { - return yScale(y); - } - return yScale(d.y0 + y); - } - - // update - circles - .attr('cx', isHorizontal ? cx : cy) - .attr('cy', isHorizontal ? cy : cx) - .attr('r', circleRadius); - - // Add tooltip - if (isTooltip) { - circles.call(tooltip.render()); - } - - return circles; - } - - addPathEvents(path) { - const events = this.events; - if (this.handler.visConfig.get('enableHover')) { - const hover = events.addHoverEvent(); - const mouseout = events.addMouseoutEvent(); - path.call(hover).call(mouseout); - } - } - - /** - * Renders d3 visualization - * - * @method draw - * @returns {Function} Creates the area chart - */ - draw() { - const self = this; - - return function (selection) { - selection.each(function () { - const svg = self.chartEl.append('g'); - svg.data([self.chartData]); - - const path = self.addPath(svg, self.chartData); - self.addPathEvents(path); - const circles = self.addCircles(svg, self.chartData); - self.addCircleEvents(circles); - - if (self.thresholdLineOptions.show) { - self.addThresholdLine(self.chartEl); - } - self.events.emit('rendered', { - chart: self.chartData, - }); - - return svg; - }); - }; - } -} diff --git a/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/area_chart.test.js b/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/area_chart.test.js deleted file mode 100644 index 68b0728026498..0000000000000 --- a/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/area_chart.test.js +++ /dev/null @@ -1,264 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import d3 from 'd3'; -import _ from 'lodash'; -import $ from 'jquery'; -import { - setHTMLElementClientSizes, - setSVGElementGetBBox, - setSVGElementGetComputedTextLength, -} from '@kbn/test/jest'; - -import { getMockUiState } from '../../../fixtures/mocks'; -import { getVis } from '../_vis_fixture'; - -const dataTypesArray = { - 'series pos': import('../../../fixtures/mock_data/date_histogram/_series'), - 'series pos neg': import('../../../fixtures/mock_data/date_histogram/_series_pos_neg'), - 'series neg': import('../../../fixtures/mock_data/date_histogram/_series_neg'), - 'term columns': import('../../../fixtures/mock_data/terms/_columns'), - 'range rows': import('../../../fixtures/mock_data/range/_rows'), - stackedSeries: import('../../../fixtures/mock_data/date_histogram/_stacked_series'), -}; - -const vislibParams = { - type: 'area', - addLegend: true, - addTooltip: true, - mode: 'stacked', -}; - -let mockedHTMLElementClientSizes; -let mockedSVGElementGetBBox; -let mockedSVGElementGetComputedTextLength; - -_.forOwn(dataTypesArray, function (dataType, dataTypeName) { - describe('Vislib Area Chart Test Suite for ' + dataTypeName + ' Data', function () { - let vis; - let mockUiState; - - beforeAll(() => { - mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); - mockedSVGElementGetBBox = setSVGElementGetBBox(100); - mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100); - }); - - beforeEach(async () => { - vis = getVis(vislibParams); - mockUiState = getMockUiState(); - vis.on('brush', _.noop); - vis.render(await dataType, mockUiState); - }); - - afterEach(function () { - vis.destroy(); - }); - - afterAll(() => { - mockedHTMLElementClientSizes.mockRestore(); - mockedSVGElementGetBBox.mockRestore(); - mockedSVGElementGetComputedTextLength.mockRestore(); - }); - - describe('stackData method', function () { - let stackedData; - let isStacked; - - beforeEach(function () { - vis.handler.charts.forEach(function (chart) { - stackedData = chart.chartData; - - isStacked = stackedData.series.every(function (arr) { - return arr.values.every(function (d) { - return _.isNumber(d.y0); - }); - }); - }); - }); - - test('should append a d.y0 key to the data object', function () { - expect(isStacked).toBe(true); - }); - }); - - describe('addPath method', function () { - test('should append a area paths', function () { - vis.handler.charts.forEach(function (chart) { - expect($(chart.chartEl).find('path').length).toBeGreaterThan(0); - }); - }); - }); - - describe('addPathEvents method', function () { - let path; - let d3selectedPath; - let onMouseOver; - - beforeEach(function () { - vis.handler.charts.forEach(function (chart) { - path = $(chart.chartEl).find('path')[0]; - d3selectedPath = d3.select(path)[0][0]; - - // d3 instance of click and hover - onMouseOver = !!d3selectedPath.__onmouseover; - }); - }); - - test('should attach a hover event', function () { - vis.handler.charts.forEach(function () { - expect(onMouseOver).toBe(true); - }); - }); - }); - - describe('addCircleEvents method', function () { - let circle; - let brush; - let d3selectedCircle; - let onBrush; - let onClick; - let onMouseOver; - - beforeEach(() => { - vis.handler.charts.forEach(function (chart) { - circle = $(chart.chartEl).find('circle')[0]; - brush = $(chart.chartEl).find('.brush'); - d3selectedCircle = d3.select(circle)[0][0]; - - // d3 instance of click and hover - onBrush = !!brush; - onClick = !!d3selectedCircle.__onclick; - onMouseOver = !!d3selectedCircle.__onmouseover; - }); - }); - - // D3 brushing requires that a g element is appended that - // listens for mousedown events. This g element includes - // listeners, however, I was not able to test for the listener - // function being present. I will need to update this test - // in the future. - test('should attach a brush g element', function () { - vis.handler.charts.forEach(function () { - expect(onBrush).toBe(true); - }); - }); - - test('should attach a click event', function () { - vis.handler.charts.forEach(function () { - expect(onClick).toBe(true); - }); - }); - - test('should attach a hover event', function () { - vis.handler.charts.forEach(function () { - expect(onMouseOver).toBe(true); - }); - }); - }); - - describe('addCircles method', function () { - test('should append circles', function () { - vis.handler.charts.forEach(function (chart) { - expect($(chart.chartEl).find('circle').length).toBeGreaterThan(0); - }); - }); - - test('should not draw circles where d.y === 0', function () { - vis.handler.charts.forEach(function (chart) { - const series = chart.chartData.series; - const isZero = series.some(function (d) { - return d.y === 0; - }); - const circles = $.makeArray($(chart.chartEl).find('circle')); - const isNotDrawn = circles.some(function (d) { - return d.__data__.y === 0; - }); - - if (isZero) { - expect(isNotDrawn).toBe(false); - } - }); - }); - }); - - describe('draw method', function () { - test('should return a function', function () { - vis.handler.charts.forEach(function (chart) { - expect(_.isFunction(chart.draw())).toBe(true); - }); - }); - - test('should return a yMin and yMax', function () { - vis.handler.charts.forEach(function (chart) { - const yAxis = chart.handler.valueAxes[0]; - const domain = yAxis.getScale().domain(); - - expect(domain[0]).not.toBe(undefined); - expect(domain[1]).not.toBe(undefined); - }); - }); - - test('should render a zero axis line', function () { - vis.handler.charts.forEach(function (chart) { - const yAxis = chart.handler.valueAxes[0]; - - if (yAxis.yMin < 0 && yAxis.yMax > 0) { - expect($(chart.chartEl).find('line.zero-line').length).toBe(1); - } - }); - }); - }); - - describe('defaultYExtents is true', function () { - beforeEach(async function () { - vis.visConfigArgs.defaultYExtents = true; - vis.render(await dataType, mockUiState); - }); - - test('should return yAxis extents equal to data extents', function () { - vis.handler.charts.forEach(function (chart) { - const yAxis = chart.handler.valueAxes[0]; - const min = vis.handler.valueAxes[0].axisScale.getYMin(); - const max = vis.handler.valueAxes[0].axisScale.getYMax(); - const domain = yAxis.getScale().domain(); - expect(domain[0]).toEqual(min); - expect(domain[1]).toEqual(max); - }); - }); - }); - [0, 2, 4, 8].forEach(function (boundsMarginValue) { - describe('defaultYExtents is true and boundsMargin is defined', function () { - beforeEach(async function () { - vis.visConfigArgs.defaultYExtents = true; - vis.visConfigArgs.boundsMargin = boundsMarginValue; - vis.render(await dataType, mockUiState); - }); - - test('should return yAxis extents equal to data extents with boundsMargin', function () { - vis.handler.charts.forEach(function (chart) { - const yAxis = chart.handler.valueAxes[0]; - const min = vis.handler.valueAxes[0].axisScale.getYMin(); - const max = vis.handler.valueAxes[0].axisScale.getYMax(); - const domain = yAxis.getScale().domain(); - if (min < 0 && max < 0) { - expect(domain[0]).toEqual(min); - expect(domain[1] - boundsMarginValue).toEqual(max); - } else if (min > 0 && max > 0) { - expect(domain[0] + boundsMarginValue).toEqual(min); - expect(domain[1]).toEqual(max); - } else { - expect(domain[0]).toEqual(min); - expect(domain[1]).toEqual(max); - } - }); - }); - }); - }); - }); -}); diff --git a/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/column_chart.js b/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/column_chart.js deleted file mode 100644 index 1c543d06e9be9..0000000000000 --- a/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/column_chart.js +++ /dev/null @@ -1,383 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import _ from 'lodash'; -import d3 from 'd3'; -import { isColorDark } from '@elastic/eui/lib/services'; -import { PointSeries } from './_point_series'; - -const defaults = { - mode: 'normal', - showTooltip: true, - color: undefined, - fillColor: undefined, - showLabel: true, -}; - -/** - * Histogram intervals are not always equal widths, e.g, monthly time intervals. - * It is more visually appealing to vary bar width so that gutter width is constant. - */ -function datumWidth(defaultWidth, datum, nextDatum, scale, gutterWidth, groupCount = 1) { - let datumWidth = defaultWidth; - if (nextDatum) { - datumWidth = (scale(nextDatum.x) - scale(datum.x) - gutterWidth) / groupCount; - // To handle data-sets with holes, do not let width be larger than default. - if (datumWidth > defaultWidth) { - datumWidth = defaultWidth; - } - } - return datumWidth; -} - -/** - * Vertical Bar Chart Visualization: renders vertical and/or stacked bars - * - * @class ColumnChart - * @constructor - * @extends Chart - * @param handler {Object} Reference to the Handler Class Constructor - * @param el {HTMLElement} HTML element to which the chart will be appended - * @param chartData {Object} Elasticsearch query results for this specific chart - */ -export class ColumnChart extends PointSeries { - constructor(handler, chartEl, chartData, seriesConfigArgs, uiSettings) { - super(handler, chartEl, chartData, seriesConfigArgs, uiSettings); - this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults); - this.labelOptions = _.defaults(handler.visConfig.get('labels', {}), defaults.showLabel); - } - - addBars(svg, data) { - const self = this; - const color = this.handler.data.getColorFunc(); - const tooltip = this.baseChart.tooltip; - const isTooltip = this.handler.visConfig.get('tooltip.show'); - - const layer = svg - .append('g') - .attr('class', 'series histogram') - .attr('clip-path', 'url(#' + this.baseChart.clipPathId + ')'); - - const bars = layer.selectAll('rect').data( - data.values.filter(function (d) { - return !_.isNull(d.y); - }) - ); - - bars.exit().remove(); - - bars - .enter() - .append('rect') - .attr('data-label', data.label) - .attr('fill', () => color(data.label)) - .attr('stroke', () => color(data.label)); - - self.updateBars(bars); - - // Add tooltip - if (isTooltip) { - bars.call(tooltip.render()); - } - - return bars; - } - - /** - * Determines whether bars are grouped or stacked and updates the D3 - * selection - * - * @method updateBars - * @param bars {D3.UpdateSelection} SVG with rect added - * @returns {D3.UpdateSelection} - */ - updateBars(bars) { - if (this.seriesConfig.mode === 'stacked') { - return this.addStackedBars(bars); - } - return this.addGroupedBars(bars); - } - - /** - * Adds stacked bars to column chart visualization - * - * @method addStackedBars - * @param bars {D3.UpdateSelection} SVG with rect added - * @returns {D3.UpdateSelection} - */ - addStackedBars(bars) { - const xScale = this.getCategoryAxis().getScale(); - const yScale = this.getValueAxis().getScale(); - const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); - const isTimeScale = this.getCategoryAxis().axisConfig.isTimeDomain(); - const isLabels = this.labelOptions.show; - const yMin = yScale.domain()[0]; - const gutterSpacingPercentage = 0.15; - const chartData = this.chartData; - const getGroupedNum = this.getGroupedNum.bind(this); - const groupCount = this.getGroupedCount(); - - let barWidth; - let gutterWidth; - - if (isTimeScale) { - const { min, interval } = this.handler.data.get('ordered'); - let intervalWidth = xScale(min + interval) - xScale(min); - intervalWidth = Math.abs(intervalWidth); - - gutterWidth = intervalWidth * gutterSpacingPercentage; - barWidth = (intervalWidth - gutterWidth) / groupCount; - } - - function x(d, i) { - const groupNum = getGroupedNum(d.seriesId); - - if (isTimeScale) { - return ( - xScale(d.x) + - datumWidth(barWidth, d, bars.data()[i + 1], xScale, gutterWidth, groupCount) * groupNum - ); - } - return xScale(d.x) + (xScale.rangeBand() / groupCount) * groupNum; - } - - function y(d) { - if ((isHorizontal && d.y < 0) || (!isHorizontal && d.y > 0)) { - return yScale(d.y0); - } - return yScale(d.y0 + d.y); - } - - function labelX(d, i) { - return x(d, i) + widthFunc(d, i) / 2; - } - - function labelY(d) { - return y(d) + heightFunc(d) / 2; - } - - function labelDisplay(d, i) { - if (isHorizontal && this.getBBox().width > widthFunc(d, i)) return 'none'; - if (!isHorizontal && this.getBBox().width > heightFunc(d)) return 'none'; - if (isHorizontal && this.getBBox().height > heightFunc(d)) return 'none'; - if (!isHorizontal && this.getBBox().height > widthFunc(d, i)) return 'none'; - return 'block'; - } - - function widthFunc(d, i) { - if (isTimeScale) { - return datumWidth(barWidth, d, bars.data()[i + 1], xScale, gutterWidth, groupCount); - } - return xScale.rangeBand() / groupCount; - } - - function heightFunc(d) { - // for split bars or for one series, - // last series will have d.y0 = 0 - if (d.y0 === 0 && yMin > 0) { - return yScale(yMin) - yScale(d.y); - } - return Math.abs(yScale(d.y0) - yScale(d.y0 + d.y)); - } - - function formatValue(d) { - return chartData.yAxisFormatter(d.y); - } - - // update - bars - .attr('x', isHorizontal ? x : y) - .attr('width', isHorizontal ? widthFunc : heightFunc) - .attr('y', isHorizontal ? y : x) - .attr('height', isHorizontal ? heightFunc : widthFunc); - - const layer = d3.select(bars[0].parentNode); - const barLabels = layer.selectAll('text').data( - chartData.values.filter(function (d) { - return !_.isNull(d.y); - }) - ); - - if (isLabels) { - const colorFunc = this.handler.data.getColorFunc(); - const d3Color = d3.rgb(colorFunc(chartData.label)); - let labelClass; - if (isColorDark(d3Color.r, d3Color.g, d3Color.b)) { - labelClass = 'visColumnChart__bar-label--light'; - } else { - labelClass = 'visColumnChart__bar-label--dark'; - } - - barLabels - .enter() - .append('text') - .text(formatValue) - .attr('class', `visColumnChart__barLabel visColumnChart__barLabel--stack ${labelClass}`) - .attr('x', isHorizontal ? labelX : labelY) - .attr('y', isHorizontal ? labelY : labelX) - - // display must apply last, because labelDisplay decision it based - // on text bounding box which depends on actual applied style. - .attr('display', labelDisplay); - } - - return bars; - } - - /** - * Adds grouped bars to column chart visualization - * - * @method addGroupedBars - * @param bars {D3.UpdateSelection} SVG with rect added - * @returns {D3.UpdateSelection} - */ - addGroupedBars(bars) { - const xScale = this.getCategoryAxis().getScale(); - const yScale = this.getValueAxis().getScale(); - const chartData = this.chartData; - const groupCount = this.getGroupedCount(); - const gutterSpacingPercentage = 0.15; - const isTimeScale = this.getCategoryAxis().axisConfig.isTimeDomain(); - const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); - const isLogScale = this.getValueAxis().axisConfig.isLogScale(); - const isLabels = this.labelOptions.show; - const getGroupedNum = this.getGroupedNum.bind(this); - - let barWidth; - let gutterWidth; - - if (isTimeScale) { - const { min, interval } = this.handler.data.get('ordered'); - let intervalWidth = xScale(min + interval) - xScale(min); - intervalWidth = Math.abs(intervalWidth); - - gutterWidth = intervalWidth * gutterSpacingPercentage; - barWidth = (intervalWidth - gutterWidth) / groupCount; - } - - function x(d, i) { - const groupNum = getGroupedNum(d.seriesId); - if (isTimeScale) { - return ( - xScale(d.x) + - datumWidth(barWidth, d, bars.data()[i + 1], xScale, gutterWidth, groupCount) * groupNum - ); - } - return xScale(d.x) + (xScale.rangeBand() / groupCount) * groupNum; - } - - function y(d) { - if ((isHorizontal && d.y < 0) || (!isHorizontal && d.y > 0)) { - return yScale(0); - } - return yScale(d.y); - } - - function labelX(d, i) { - return x(d, i) + widthFunc(d, i) / 2; - } - - function labelY(d) { - if (isHorizontal) { - return d.y >= 0 ? y(d) - 4 : y(d) + heightFunc(d) + this.getBBox().height; - } - return d.y >= 0 ? y(d) + heightFunc(d) + 4 : y(d) - this.getBBox().width - 4; - } - - function labelDisplay(d, i) { - if (isHorizontal && this.getBBox().width > widthFunc(d, i)) { - return 'none'; - } - if (!isHorizontal && this.getBBox().height > widthFunc(d)) { - return 'none'; - } - return 'block'; - } - function widthFunc(d, i) { - if (isTimeScale) { - return datumWidth(barWidth, d, bars.data()[i + 1], xScale, gutterWidth, groupCount); - } - return xScale.rangeBand() / groupCount; - } - - function heightFunc(d) { - const baseValue = isLogScale ? 1 : 0; - return Math.abs(yScale(baseValue) - yScale(d.y)); - } - - function formatValue(d) { - return chartData.yAxisFormatter(d.y); - } - - // update - bars - .attr('x', isHorizontal ? x : y) - .attr('width', isHorizontal ? widthFunc : heightFunc) - .attr('y', isHorizontal ? y : x) - .attr('height', isHorizontal ? heightFunc : widthFunc); - - const layer = d3.select(bars[0].parentNode); - const barLabels = layer.selectAll('text').data( - chartData.values.filter(function (d) { - return !_.isNull(d.y); - }) - ); - - barLabels.exit().remove(); - - if (isLabels) { - const labelColor = this.handler.data.getColorFunc()(chartData.label); - - barLabels - .enter() - .append('text') - .text(formatValue) - .attr('class', 'visColumnChart__barLabel') - .attr('x', isHorizontal ? labelX : labelY) - .attr('y', isHorizontal ? labelY : labelX) - .attr('dominant-baseline', isHorizontal ? 'auto' : 'central') - .attr('text-anchor', isHorizontal ? 'middle' : 'start') - .attr('fill', labelColor) - - // display must apply last, because labelDisplay decision it based - // on text bounding box which depends on actual applied style. - .attr('display', labelDisplay); - } - return bars; - } - - /** - * Renders d3 visualization - * - * @method draw - * @returns {Function} Creates the vertical bar chart - */ - draw() { - const self = this; - - return function (selection) { - selection.each(function () { - const svg = self.chartEl.append('g'); - svg.data([self.chartData]); - - const bars = self.addBars(svg, self.chartData); - self.addCircleEvents(bars); - - if (self.thresholdLineOptions.show) { - self.addThresholdLine(self.chartEl); - } - - self.events.emit('rendered', { - chart: self.chartData, - }); - - return svg; - }); - }; - } -} diff --git a/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/column_chart.test.js b/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/column_chart.test.js deleted file mode 100644 index 8f0db3ab18393..0000000000000 --- a/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/column_chart.test.js +++ /dev/null @@ -1,401 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import _ from 'lodash'; -import d3 from 'd3'; -import $ from 'jquery'; -import { - setHTMLElementClientSizes, - setSVGElementGetBBox, - setSVGElementGetComputedTextLength, -} from '@kbn/test/jest'; - -// Data -import series from '../../../fixtures/mock_data/date_histogram/_series'; -import seriesPosNeg from '../../../fixtures/mock_data/date_histogram/_series_pos_neg'; -import seriesNeg from '../../../fixtures/mock_data/date_histogram/_series_neg'; -import termsColumns from '../../../fixtures/mock_data/terms/_columns'; -import histogramRows from '../../../fixtures/mock_data/histogram/_rows'; -import stackedSeries from '../../../fixtures/mock_data/date_histogram/_stacked_series'; - -import { seriesMonthlyInterval } from '../../../fixtures/mock_data/date_histogram/_series_monthly_interval'; -import { rowsSeriesWithHoles } from '../../../fixtures/mock_data/date_histogram/_rows_series_with_holes'; -import rowsWithZeros from '../../../fixtures/mock_data/date_histogram/_rows'; -import { getMockUiState } from '../../../fixtures/mocks'; -import { getVis } from '../_vis_fixture'; - -// tuple, with the format [description, mode, data] -const dataTypesArray = [ - ['series', 'stacked', series], - ['series with positive and negative values', 'stacked', seriesPosNeg], - ['series with negative values', 'stacked', seriesNeg], - ['terms columns', 'grouped', termsColumns], - ['histogram rows', 'percentage', histogramRows], - ['stackedSeries', 'stacked', stackedSeries], -]; - -let mockedHTMLElementClientSizes; -let mockedSVGElementGetBBox; -let mockedSVGElementGetComputedTextLength; - -dataTypesArray.forEach(function (dataType) { - const name = dataType[0]; - const mode = dataType[1]; - const data = dataType[2]; - - describe('Vislib Column Chart Test Suite for ' + name + ' Data', function () { - let vis; - let mockUiState; - const vislibParams = { - type: 'histogram', - addLegend: true, - addTooltip: true, - mode: mode, - zeroFill: true, - grid: { - categoryLines: true, - valueAxis: 'ValueAxis-1', - }, - }; - - beforeAll(() => { - mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); - mockedSVGElementGetBBox = setSVGElementGetBBox(100); - mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100); - }); - - beforeEach(() => { - vis = getVis(vislibParams); - mockUiState = getMockUiState(); - vis.on('brush', _.noop); - vis.render(data, mockUiState); - }); - - afterEach(function () { - vis.destroy(); - }); - - afterAll(() => { - mockedHTMLElementClientSizes.mockRestore(); - mockedSVGElementGetBBox.mockRestore(); - mockedSVGElementGetComputedTextLength.mockRestore(); - }); - - describe('stackData method', function () { - let stackedData; - let isStacked; - - beforeEach(function () { - vis.handler.charts.forEach(function (chart) { - stackedData = chart.chartData; - - isStacked = stackedData.series.every(function (arr) { - return arr.values.every(function (d) { - return _.isNumber(d.y0); - }); - }); - }); - }); - - test('should stack values when mode is stacked', function () { - if (mode === 'stacked') { - expect(isStacked).toBe(true); - } - }); - - test('should stack values when mode is percentage', function () { - if (mode === 'percentage') { - expect(isStacked).toBe(true); - } - }); - }); - - describe('addBars method', function () { - test('should append rects', function () { - let numOfSeries; - let numOfValues; - let product; - - vis.handler.charts.forEach(function (chart) { - numOfSeries = chart.chartData.series.length; - numOfValues = chart.chartData.series[0].values.length; - product = numOfSeries * numOfValues; - expect($(chart.chartEl).find('.series rect')).toHaveLength(product); - }); - }); - }); - - describe('addBarEvents method', function () { - function checkChart(chart) { - const rect = $(chart.chartEl).find('.series rect').get(0); - - // check for existence of stuff and things - return { - click: !!rect.__onclick, - mouseOver: !!rect.__onmouseover, - // D3 brushing requires that a g element is appended that - // listens for mousedown events. This g element includes - // listeners, however, I was not able to test for the listener - // function being present. I will need to update this test - // in the future. - brush: !!d3.select('.brush')[0][0], - }; - } - - test('should attach the brush if data is a set is ordered', function () { - vis.handler.charts.forEach(function (chart) { - const has = checkChart(chart); - const ordered = vis.handler.data.get('ordered'); - const allowBrushing = Boolean(ordered); - expect(has.brush).toBe(allowBrushing); - }); - }); - - test('should attach a click event', function () { - vis.handler.charts.forEach(function (chart) { - const has = checkChart(chart); - expect(has.click).toBe(true); - }); - }); - - test('should attach a hover event', function () { - vis.handler.charts.forEach(function (chart) { - const has = checkChart(chart); - expect(has.mouseOver).toBe(true); - }); - }); - }); - - describe('draw method', function () { - test('should return a function', function () { - vis.handler.charts.forEach(function (chart) { - expect(_.isFunction(chart.draw())).toBe(true); - }); - }); - - test('should return a yMin and yMax', function () { - vis.handler.charts.forEach(function (chart) { - const yAxis = chart.handler.valueAxes[0]; - const domain = yAxis.getScale().domain(); - - expect(domain[0]).not.toBe(undefined); - expect(domain[1]).not.toBe(undefined); - }); - }); - - test('should render a zero axis line', function () { - vis.handler.charts.forEach(function (chart) { - const yAxis = chart.handler.valueAxes[0]; - - if (yAxis.yMin < 0 && yAxis.yMax > 0) { - expect($(chart.chartEl).find('line.zero-line').length).toBe(1); - } - }); - }); - }); - - describe('defaultYExtents is true', function () { - beforeEach(function () { - vis.visConfigArgs.defaultYExtents = true; - vis.render(data, mockUiState); - }); - - test('should return yAxis extents equal to data extents', function () { - vis.handler.charts.forEach(function (chart) { - const yAxis = chart.handler.valueAxes[0]; - const min = vis.handler.valueAxes[0].axisScale.getYMin(); - const max = vis.handler.valueAxes[0].axisScale.getYMax(); - const domain = yAxis.getScale().domain(); - expect(domain[0]).toEqual(min); - expect(domain[1]).toEqual(max); - }); - }); - }); - [0, 2, 4, 8].forEach(function (boundsMarginValue) { - describe('defaultYExtents is true and boundsMargin is defined', function () { - beforeEach(function () { - vis.visConfigArgs.defaultYExtents = true; - vis.visConfigArgs.boundsMargin = boundsMarginValue; - vis.render(data, mockUiState); - }); - - test('should return yAxis extents equal to data extents with boundsMargin', function () { - vis.handler.charts.forEach(function (chart) { - const yAxis = chart.handler.valueAxes[0]; - const min = vis.handler.valueAxes[0].axisScale.getYMin(); - const max = vis.handler.valueAxes[0].axisScale.getYMax(); - const domain = yAxis.getScale().domain(); - if (min < 0 && max < 0) { - expect(domain[0]).toEqual(min); - expect(domain[1] - boundsMarginValue).toEqual(max); - } else if (min > 0 && max > 0) { - expect(domain[0] + boundsMarginValue).toEqual(min); - expect(domain[1]).toEqual(max); - } else { - expect(domain[0]).toEqual(min); - expect(domain[1]).toEqual(max); - } - }); - }); - }); - }); - }); -}); - -describe('stackData method - data set with zeros in percentage mode', function () { - let vis; - let mockUiState; - const vislibParams = { - type: 'histogram', - addLegend: true, - addTooltip: true, - mode: 'percentage', - zeroFill: true, - }; - - beforeAll(() => { - mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); - mockedSVGElementGetBBox = setSVGElementGetBBox(100); - mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100); - }); - - beforeEach(() => { - vis = getVis(vislibParams); - mockUiState = getMockUiState(); - vis.on('brush', _.noop); - }); - - afterEach(function () { - vis.destroy(); - }); - - afterAll(() => { - mockedHTMLElementClientSizes.mockRestore(); - mockedSVGElementGetBBox.mockRestore(); - mockedSVGElementGetComputedTextLength.mockRestore(); - }); - - test('should not mutate the injected zeros', function () { - vis.render(seriesMonthlyInterval, mockUiState); - - expect(vis.handler.charts).toHaveLength(1); - const chart = vis.handler.charts[0]; - expect(chart.chartData.series).toHaveLength(1); - const series = chart.chartData.series[0].values; - // with the interval set in seriesMonthlyInterval data, the point at x=1454309600000 does not exist - const point = _.find(series, ['x', 1454309600000]); - expect(point).not.toBe(undefined); - expect(point.y).toBe(0); - }); - - test('should not mutate zeros that exist in the data', function () { - vis.render(rowsWithZeros, mockUiState); - - expect(vis.handler.charts).toHaveLength(2); - const chart = vis.handler.charts[0]; - expect(chart.chartData.series).toHaveLength(5); - const series = chart.chartData.series[0].values; - const point = _.find(series, ['x', 1415826240000]); - expect(point).not.toBe(undefined); - expect(point.y).toBe(0); - }); -}); - -describe('datumWidth - split chart data set with holes', function () { - let vis; - let mockUiState; - const vislibParams = { - type: 'histogram', - addLegend: true, - addTooltip: true, - mode: 'stacked', - zeroFill: true, - }; - - beforeAll(() => { - mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); - mockedSVGElementGetBBox = setSVGElementGetBBox(100); - mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100); - }); - - beforeEach(() => { - vis = getVis(vislibParams); - mockUiState = getMockUiState(); - vis.on('brush', _.noop); - vis.render(rowsSeriesWithHoles, mockUiState); - }); - - afterEach(function () { - vis.destroy(); - }); - - afterAll(() => { - mockedHTMLElementClientSizes.mockRestore(); - mockedSVGElementGetBBox.mockRestore(); - mockedSVGElementGetComputedTextLength.mockRestore(); - }); - - test('should not have bar widths that span multiple time bins', function () { - expect(vis.handler.charts.length).toEqual(1); - const chart = vis.handler.charts[0]; - const rects = $(chart.chartEl).find('.series rect'); - const MAX_WIDTH_IN_PIXELS = 27; - rects.each(function () { - const width = parseInt($(this).attr('width'), 10); - expect(width).toBeLessThan(MAX_WIDTH_IN_PIXELS); - }); - }); -}); - -describe('datumWidth - monthly interval', function () { - let vis; - let mockUiState; - const vislibParams = { - type: 'histogram', - addLegend: true, - addTooltip: true, - mode: 'stacked', - zeroFill: true, - }; - - let mockWidth; - - beforeAll(() => { - mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); - mockedSVGElementGetBBox = setSVGElementGetBBox(100); - mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100); - mockWidth = jest.spyOn($.prototype, 'width').mockReturnValue(900); - }); - - beforeEach(() => { - vis = getVis(vislibParams); - mockUiState = getMockUiState(); - vis.on('brush', _.noop); - vis.render(seriesMonthlyInterval, mockUiState); - }); - - afterEach(function () { - vis.destroy(); - }); - - afterAll(() => { - mockedHTMLElementClientSizes.mockRestore(); - mockedSVGElementGetBBox.mockRestore(); - mockedSVGElementGetComputedTextLength.mockRestore(); - mockWidth.mockRestore(); - }); - - test('should vary bar width when date histogram intervals are not equal', function () { - expect(vis.handler.charts.length).toEqual(1); - const chart = vis.handler.charts[0]; - const rects = $(chart.chartEl).find('.series rect'); - const januaryBarWidth = parseInt($(rects.get(0)).attr('width'), 10); - const februaryBarWidth = parseInt($(rects.get(1)).attr('width'), 10); - expect(februaryBarWidth).toBeLessThan(januaryBarWidth); - }); -}); diff --git a/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/line_chart.js b/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/line_chart.js deleted file mode 100644 index 4476574c940bc..0000000000000 --- a/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/line_chart.js +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import d3 from 'd3'; -import _ from 'lodash'; -import { PointSeries } from './_point_series'; - -const defaults = { - mode: 'normal', - showCircles: true, - radiusRatio: 9, - showLines: true, - interpolate: 'linear', - lineWidth: 2, - color: undefined, - fillColor: undefined, -}; -/** - * Line Chart Visualization - * - * @class LineChart - * @constructor - * @extends Chart - * @param handler {Object} Reference to the Handler Class Constructor - * @param el {HTMLElement} HTML element to which the chart will be appended - * @param chartData {Object} Elasticsearch query results for this specific chart - */ -export class LineChart extends PointSeries { - constructor(handler, chartEl, chartData, seriesConfigArgs, uiSettings) { - super(handler, chartEl, chartData, seriesConfigArgs, uiSettings); - this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults); - } - - addCircles(svg, data) { - const self = this; - const showCircles = this.seriesConfig.showCircles; - const color = this.handler.data.getColorFunc(); - const xScale = this.getCategoryAxis().getScale(); - const yScale = this.getValueAxis().getScale(); - const ordered = this.handler.data.get('ordered'); - const tooltip = this.baseChart.tooltip; - const isTooltip = this.handler.visConfig.get('tooltip.show'); - const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); - const lineWidth = this.seriesConfig.lineWidth; - - const radii = this.baseChart.radii; - - const radiusStep = - (radii.max - radii.min || radii.max * 100) / Math.pow(this.seriesConfig.radiusRatio, 2); - - const layer = svg - .append('g') - .attr('class', 'points line') - .attr('clip-path', 'url(#' + this.baseChart.clipPathId + ')'); - - const circles = layer.selectAll('circle').data(function appendData() { - return data.values.filter(function (d) { - return !_.isNull(d.y) && (d.y || !d.y0); - }); - }); - - circles.exit().remove(); - - function cx(d) { - if (ordered && ordered.date) { - return xScale(d.x); - } - return xScale(d.x) + xScale.rangeBand() / 2; - } - - function cy(d) { - const y0 = d.y0 || 0; - const y = d.y || 0; - return yScale(y0 + y); - } - - function cColor() { - return color(data.label); - } - - function colorCircle() { - const parent = d3.select(this).node().parentNode; - const lengthOfParent = d3.select(parent).data()[0].length; - const isVisible = lengthOfParent === 1; - - // If only 1 point exists, show circle - if (!showCircles && !isVisible) return 'none'; - return cColor(); - } - - function getCircleRadiusFn(modifier) { - return function getCircleRadius(d) { - const width = self.baseChart.chartConfig.width; - const height = self.baseChart.chartConfig.height; - const circleRadius = (d.z - radii.min) / radiusStep; - const baseMagicNumber = 2; - - const base = circleRadius - ? Math.sqrt(circleRadius + baseMagicNumber) + lineWidth - : lineWidth; - return _.min([base, width, height]) + (modifier || 0); - }; - } - - circles - .enter() - .append('circle') - .attr('r', getCircleRadiusFn()) - .attr('fill-opacity', this.seriesConfig.drawLinesBetweenPoints ? 1 : 0.7) - .attr('cx', isHorizontal ? cx : cy) - .attr('cy', isHorizontal ? cy : cx) - .attr('class', 'circle-decoration') - .attr('data-label', data.label) - .attr('fill', colorCircle); - - circles - .enter() - .append('circle') - .attr('r', getCircleRadiusFn(10)) - .attr('cx', isHorizontal ? cx : cy) - .attr('cy', isHorizontal ? cy : cx) - .attr('fill', 'transparent') - .attr('class', 'circle') - .attr('data-label', data.label) - .attr('stroke', cColor) - .attr('stroke-width', 0); - - if (isTooltip) { - circles.call(tooltip.render()); - } - - return circles; - } - - /** - * Adds path to SVG - * - * @method addLines - * @param svg {HTMLElement} SVG to which path are appended - * @param data {Array} Array of object data points - * @returns {D3.UpdateSelection} SVG with paths added - */ - addLine(svg, data) { - const xScale = this.getCategoryAxis().getScale(); - const yScale = this.getValueAxis().getScale(); - const color = this.handler.data.getColorFunc(); - const ordered = this.handler.data.get('ordered'); - const lineWidth = this.seriesConfig.lineWidth; - const interpolate = this.seriesConfig.interpolate; - const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); - - const line = svg - .append('g') - .attr('class', 'pathgroup lines') - .attr('clip-path', 'url(#' + this.baseChart.clipPathId + ')'); - - function cx(d) { - if (ordered && ordered.date) { - return xScale(d.x); - } - return xScale(d.x) + xScale.rangeBand() / 2; - } - - function cy(d) { - const y = d.y || 0; - const y0 = d.y0 || 0; - return yScale(y0 + y); - } - - line - .append('path') - .attr('data-label', data.label) - .attr('d', () => { - const d3Line = d3.svg - .line() - .defined(function (d) { - return !_.isNull(d.y); - }) - .interpolate(interpolate) - .x(isHorizontal ? cx : cy) - .y(isHorizontal ? cy : cx); - return d3Line(data.values); - }) - .attr('fill', 'none') - .attr('stroke', () => { - return color(data.label); - }) - .attr('stroke-width', lineWidth); - - return line; - } - - /** - * Renders d3 visualization - * - * @method draw - * @returns {Function} Creates the line chart - */ - draw() { - const self = this; - - return function (selection) { - selection.each(function () { - const svg = self.chartEl.append('g'); - svg.data([self.chartData]); - - if (self.seriesConfig.drawLinesBetweenPoints) { - self.addLine(svg, self.chartData); - } - const circles = self.addCircles(svg, self.chartData); - self.addCircleEvents(circles); - - if (self.thresholdLineOptions.show) { - self.addThresholdLine(self.chartEl); - } - - self.events.emit('rendered', { - chart: self.chartData, - }); - - return svg; - }); - }; - } -} diff --git a/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/line_chart.test.js b/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/line_chart.test.js deleted file mode 100644 index f9843f1bc83a9..0000000000000 --- a/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/line_chart.test.js +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import d3 from 'd3'; -import $ from 'jquery'; -import _ from 'lodash'; -import { - setHTMLElementClientSizes, - setSVGElementGetBBox, - setSVGElementGetComputedTextLength, -} from '@kbn/test/jest'; - -// Data -import seriesPos from '../../../fixtures/mock_data/date_histogram/_series'; -import seriesPosNeg from '../../../fixtures/mock_data/date_histogram/_series_pos_neg'; -import seriesNeg from '../../../fixtures/mock_data/date_histogram/_series_neg'; -import histogramColumns from '../../../fixtures/mock_data/histogram/_columns'; -import rangeRows from '../../../fixtures/mock_data/range/_rows'; -import termSeries from '../../../fixtures/mock_data/terms/_series'; -import { getMockUiState } from '../../../fixtures/mocks'; -import { getVis } from '../_vis_fixture'; - -const dataTypes = [ - ['series pos', seriesPos], - ['series pos neg', seriesPosNeg], - ['series neg', seriesNeg], - ['histogram columns', histogramColumns], - ['range rows', rangeRows], - ['term series', termSeries], -]; - -let mockedHTMLElementClientSizes; -let mockedSVGElementGetBBox; -let mockedSVGElementGetComputedTextLength; - -describe('Vislib Line Chart', function () { - beforeAll(() => { - mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512); - mockedSVGElementGetBBox = setSVGElementGetBBox(100); - mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100); - }); - - afterAll(() => { - mockedHTMLElementClientSizes.mockRestore(); - mockedSVGElementGetBBox.mockRestore(); - mockedSVGElementGetComputedTextLength.mockRestore(); - }); - - dataTypes.forEach(function (type) { - const name = type[0]; - const data = type[1]; - - describe(name + ' Data', function () { - let vis; - let mockUiState; - - beforeEach(() => { - const vislibParams = { - type: 'line', - addLegend: true, - addTooltip: true, - drawLinesBetweenPoints: true, - }; - - vis = getVis(vislibParams); - mockUiState = getMockUiState(); - vis.render(data, mockUiState); - vis.on('brush', _.noop); - }); - - afterEach(function () { - vis.destroy(); - }); - - describe('addCircleEvents method', function () { - let circle; - let brush; - let d3selectedCircle; - let onBrush; - let onClick; - let onMouseOver; - - beforeEach(function () { - vis.handler.charts.forEach(function (chart) { - circle = $(chart.chartEl).find('.circle')[0]; - brush = $(chart.chartEl).find('.brush'); - d3selectedCircle = d3.select(circle)[0][0]; - - // d3 instance of click and hover - onBrush = !!brush; - onClick = !!d3selectedCircle.__onclick; - onMouseOver = !!d3selectedCircle.__onmouseover; - }); - }); - - // D3 brushing requires that a g element is appended that - // listens for mousedown events. This g element includes - // listeners, however, I was not able to test for the listener - // function being present. I will need to update this test - // in the future. - test('should attach a brush g element', function () { - vis.handler.charts.forEach(function () { - expect(onBrush).toBe(true); - }); - }); - - test('should attach a click event', function () { - vis.handler.charts.forEach(function () { - expect(onClick).toBe(true); - }); - }); - - test('should attach a hover event', function () { - vis.handler.charts.forEach(function () { - expect(onMouseOver).toBe(true); - }); - }); - }); - - describe('addCircles method', function () { - test('should append circles', function () { - vis.handler.charts.forEach(function (chart) { - expect($(chart.chartEl).find('circle').length).toBeGreaterThan(0); - }); - }); - }); - - describe('addLines method', function () { - test('should append a paths', function () { - vis.handler.charts.forEach(function (chart) { - expect($(chart.chartEl).find('path').length).toBeGreaterThan(0); - }); - }); - }); - - // Cannot seem to get these tests to work on the box - // They however pass in the browsers - //describe('addClipPath method', function () { - // test('should append a clipPath', function () { - // vis.handler.charts.forEach(function (chart) { - // expect($(chart.chartEl).find('clipPath').length).to.be(1); - // }); - // }); - //}); - - describe('draw method', function () { - test('should return a function', function () { - vis.handler.charts.forEach(function (chart) { - expect(chart.draw()).toBeInstanceOf(Function); - }); - }); - - test('should return a yMin and yMax', function () { - vis.handler.charts.forEach(function (chart) { - const yAxis = chart.handler.valueAxes[0]; - const domain = yAxis.getScale().domain(); - expect(domain[0]).not.toBe(undefined); - expect(domain[1]).not.toBe(undefined); - }); - }); - - test('should render a zero axis line', function () { - vis.handler.charts.forEach(function (chart) { - const yAxis = chart.handler.valueAxes[0]; - - if (yAxis.yMin < 0 && yAxis.yMax > 0) { - expect($(chart.chartEl).find('line.zero-line').length).toBe(1); - } - }); - }); - }); - - describe('defaultYExtents is true', function () { - beforeEach(function () { - vis.visConfigArgs.defaultYExtents = true; - vis.render(data, mockUiState); - }); - - test('should return yAxis extents equal to data extents', function () { - vis.handler.charts.forEach(function (chart) { - const yAxis = chart.handler.valueAxes[0]; - const min = vis.handler.valueAxes[0].axisScale.getYMin(); - const max = vis.handler.valueAxes[0].axisScale.getYMax(); - const domain = yAxis.getScale().domain(); - expect(domain[0]).toEqual(min); - expect(domain[1]).toEqual(max); - }); - }); - }); - [0, 2, 4, 8].forEach(function (boundsMarginValue) { - describe('defaultYExtents is true and boundsMargin is defined', function () { - beforeEach(function () { - vis.visConfigArgs.defaultYExtents = true; - vis.visConfigArgs.boundsMargin = boundsMarginValue; - vis.render(data, mockUiState); - }); - - test('should return yAxis extents equal to data extents with boundsMargin', function () { - vis.handler.charts.forEach(function (chart) { - const yAxis = chart.handler.valueAxes[0]; - const min = vis.handler.valueAxes[0].axisScale.getYMin(); - const max = vis.handler.valueAxes[0].axisScale.getYMax(); - const domain = yAxis.getScale().domain(); - if (min < 0 && max < 0) { - expect(domain[0]).toEqual(min); - expect(domain[1] - boundsMarginValue).toEqual(max); - } else if (min > 0 && max > 0) { - expect(domain[0] + boundsMarginValue).toEqual(min); - expect(domain[1]).toEqual(max); - } else { - expect(domain[0]).toEqual(min); - expect(domain[1]).toEqual(max); - } - }); - }); - }); - }); - }); - }); -}); diff --git a/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/series_types.js b/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/series_types.js deleted file mode 100644 index 6a87f7e32758a..0000000000000 --- a/src/plugins/vis_types/vislib/public/vislib/visualizations/point_series/series_types.js +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { ColumnChart } from './column_chart'; -import { LineChart } from './line_chart'; -import { AreaChart } from './area_chart'; -import { HeatmapChart } from './heatmap_chart'; - -export const seriesTypes = { - histogram: ColumnChart, - line: LineChart, - area: AreaChart, - heatmap: HeatmapChart, -}; diff --git a/src/plugins/vis_types/xy/common/index.ts b/src/plugins/vis_types/xy/common/index.ts index a80946f7c62fa..f17bc8476d9a6 100644 --- a/src/plugins/vis_types/xy/common/index.ts +++ b/src/plugins/vis_types/xy/common/index.ts @@ -19,5 +19,3 @@ export enum ChartType { * Type of xy visualizations */ export type XyVisType = ChartType | 'horizontal_bar'; - -export const LEGACY_CHARTS_LIBRARY = 'visualization:visualize:legacyChartsLibrary'; diff --git a/src/plugins/vis_types/xy/kibana.json b/src/plugins/vis_types/xy/kibana.json index 1606af5944ad3..1666a346e3482 100644 --- a/src/plugins/vis_types/xy/kibana.json +++ b/src/plugins/vis_types/xy/kibana.json @@ -2,7 +2,7 @@ "id": "visTypeXy", "version": "kibana", "ui": true, - "server": true, + "server": false, "requiredPlugins": ["charts", "data", "expressions", "visualizations", "usageCollection"], "requiredBundles": ["kibanaUtils", "visDefaultEditor"], "extraPublicDirs": ["common/index"], diff --git a/src/plugins/vis_types/xy/public/editor/common_config.tsx b/src/plugins/vis_types/xy/public/editor/common_config.tsx index bd9882a15c124..6c071969f0cd8 100644 --- a/src/plugins/vis_types/xy/public/editor/common_config.tsx +++ b/src/plugins/vis_types/xy/public/editor/common_config.tsx @@ -15,37 +15,23 @@ import type { VisParams } from '../types'; import { MetricsAxisOptions, PointSeriesOptions } from './components/options'; import { ValidationWrapper } from './components/common/validation_wrapper'; -export function getOptionTabs(showElasticChartsOptions = false) { - return [ - { - name: 'advanced', - title: i18n.translate('visTypeXy.area.tabs.metricsAxesTitle', { - defaultMessage: 'Metrics & axes', - }), - editor: (props: VisEditorOptionsProps) => ( - - ), - }, - { - name: 'options', - title: i18n.translate('visTypeXy.area.tabs.panelSettingsTitle', { - defaultMessage: 'Panel settings', - }), - editor: (props: VisEditorOptionsProps) => ( - - ), - }, - ]; -} +export const optionTabs = [ + { + name: 'advanced', + title: i18n.translate('visTypeXy.area.tabs.metricsAxesTitle', { + defaultMessage: 'Metrics & axes', + }), + editor: (props: VisEditorOptionsProps) => ( + + ), + }, + { + name: 'options', + title: i18n.translate('visTypeXy.area.tabs.panelSettingsTitle', { + defaultMessage: 'Panel settings', + }), + editor: (props: VisEditorOptionsProps) => ( + + ), + }, +]; diff --git a/src/plugins/vis_types/xy/public/editor/components/common/validation_wrapper.tsx b/src/plugins/vis_types/xy/public/editor/components/common/validation_wrapper.tsx index 2088878f963ae..4d50dcd20228f 100644 --- a/src/plugins/vis_types/xy/public/editor/components/common/validation_wrapper.tsx +++ b/src/plugins/vis_types/xy/public/editor/components/common/validation_wrapper.tsx @@ -10,24 +10,22 @@ import React, { useEffect, useState, useCallback } from 'react'; import { VisEditorOptionsProps } from '../../../../../../visualizations/public'; -export interface ValidationVisOptionsProps extends VisEditorOptionsProps { +export interface ValidationVisOptionsProps extends VisEditorOptionsProps { setMultipleValidity(paramName: string, isValid: boolean): void; - extraProps?: E; } -interface ValidationWrapperProps extends VisEditorOptionsProps { - component: React.ComponentType>; - extraProps?: E; +interface ValidationWrapperProps extends VisEditorOptionsProps { + component: React.ComponentType>; } interface Item { isValid: boolean; } -function ValidationWrapper({ +function ValidationWrapper({ component: Component, ...rest -}: ValidationWrapperProps) { +}: ValidationWrapperProps) { const [panelState, setPanelState] = useState({} as { [key: string]: Item }); const isPanelValid = Object.values(panelState).every((item) => item.isValid); const { setValidity } = rest; diff --git a/src/plugins/vis_types/xy/public/editor/components/options/index.tsx b/src/plugins/vis_types/xy/public/editor/components/options/index.tsx index a3e20dd22dd5a..4e7d0e6412cb2 100644 --- a/src/plugins/vis_types/xy/public/editor/components/options/index.tsx +++ b/src/plugins/vis_types/xy/public/editor/components/options/index.tsx @@ -14,20 +14,10 @@ import { ValidationVisOptionsProps } from '../common'; const PointSeriesOptionsLazy = lazy(() => import('./point_series')); const MetricsAxisOptionsLazy = lazy(() => import('./metrics_axes')); -export const PointSeriesOptions = ( - props: ValidationVisOptionsProps< - VisParams, - { - showElasticChartsOptions: boolean; - } - > -) => ; +export const PointSeriesOptions = (props: ValidationVisOptionsProps) => ( + +); -export const MetricsAxisOptions = ( - props: ValidationVisOptionsProps< - VisParams, - { - showElasticChartsOptions: boolean; - } - > -) => ; +export const MetricsAxisOptions = (props: ValidationVisOptionsProps) => ( + +); diff --git a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/index.test.tsx.snap b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/index.test.tsx.snap index fa049199a55b6..05e2532073eaf 100644 --- a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/index.test.tsx.snap +++ b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/index.test.tsx.snap @@ -75,7 +75,6 @@ exports[`MetricsAxisOptions component should init with the default set of props /> ({ describe('MetricsAxisOptions component', () => { let setValue: jest.Mock; - let defaultProps: ValidationVisOptionsProps< - VisParams, - { - showElasticChartsOptions: boolean; - } - >; + let defaultProps: ValidationVisOptionsProps; let axis: ValueAxis; let axisRight: ValueAxis; let chart: SeriesParam; @@ -86,9 +81,6 @@ describe('MetricsAxisOptions component', () => { defaultProps = { aggs: createAggs([aggCount]), isTabSelected: true, - extraProps: { - showElasticChartsOptions: false, - }, vis: { type: { type: ChartType.Area, @@ -244,12 +236,7 @@ describe('MetricsAxisOptions component', () => { const getProps = ( valuePosition1: Position = Position.Right, valuePosition2: Position = Position.Left - ): ValidationVisOptionsProps< - VisParams, - { - showElasticChartsOptions: boolean; - } - > => ({ + ): ValidationVisOptionsProps => ({ ...defaultProps, stateParams: { ...defaultProps.stateParams, @@ -387,12 +374,7 @@ describe('MetricsAxisOptions component', () => { describe('onCategoryAxisPositionChanged', () => { const getProps = ( position: Position = Position.Bottom - ): ValidationVisOptionsProps< - VisParams, - { - showElasticChartsOptions: boolean; - } - > => ({ + ): ValidationVisOptionsProps => ({ ...defaultProps, stateParams: { ...defaultProps.stateParams, diff --git a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/index.tsx b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/index.tsx index 9b4e1c61a201f..c3eb659435b2d 100644 --- a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/index.tsx +++ b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/index.tsx @@ -43,17 +43,8 @@ export type ChangeValueAxis = ( const VALUE_AXIS_PREFIX = 'ValueAxis-'; -function MetricsAxisOptions( - props: ValidationVisOptionsProps< - VisParams, - { - // TODO: Remove when vis_type_vislib is removed - // https://github.com/elastic/kibana/issues/56143 - showElasticChartsOptions: boolean; - } - > -) { - const { stateParams, setValue, aggs, vis, isTabSelected, extraProps } = props; +function MetricsAxisOptions(props: ValidationVisOptionsProps) { + const { stateParams, setValue, aggs, vis, isTabSelected } = props; const setParamByIndex: SetParamByIndex = useCallback( (axesName, index, paramName, value) => { @@ -335,7 +326,6 @@ function MetricsAxisOptions( setMultipleValidity={props.setMultipleValidity} seriesParams={stateParams.seriesParams} valueAxes={stateParams.valueAxes} - isNewLibrary={extraProps?.showElasticChartsOptions} /> void; - isNewLibrary?: boolean; } function ValueAxesPanel(props: ValueAxesPanelProps) { @@ -150,7 +149,6 @@ function ValueAxesPanel(props: ValueAxesPanelProps) { onValueAxisPositionChanged={props.onValueAxisPositionChanged} setParamByIndex={props.setParamByIndex} setMultipleValidity={props.setMultipleValidity} - isNewLibrary={props.isNewLibrary ?? false} /> > diff --git a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/value_axis_options.tsx b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/value_axis_options.tsx index 751c61f3b1531..aa20eb84222bd 100644 --- a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/value_axis_options.tsx +++ b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/value_axis_options.tsx @@ -36,7 +36,6 @@ export interface ValueAxisOptionsParams { setParamByIndex: SetParamByIndex; valueAxis: ValueAxis; setMultipleValidity: (paramName: string, isValid: boolean) => void; - isNewLibrary?: boolean; } export function ValueAxisOptions({ @@ -46,7 +45,6 @@ export function ValueAxisOptions({ onValueAxisPositionChanged, setParamByIndex, setMultipleValidity, - isNewLibrary = false, }: ValueAxisOptionsParams) { const setValueAxis = useCallback( (paramName: T, value: ValueAxis[T]) => @@ -193,7 +191,7 @@ export function ValueAxisOptions({ setMultipleValidity={setMultipleValidity} setValueAxisScale={setValueAxisScale} setValueAxis={setValueAxis} - disableAxisExtents={isNewLibrary && axis.scale.mode === 'percentage'} + disableAxisExtents={axis.scale.mode === 'percentage'} /> > diff --git a/src/plugins/vis_types/xy/public/editor/components/options/point_series/grid_panel.tsx b/src/plugins/vis_types/xy/public/editor/components/options/point_series/grid_panel.tsx index 0bf5344ac7f26..c536d2866b8da 100644 --- a/src/plugins/vis_types/xy/public/editor/components/options/point_series/grid_panel.tsx +++ b/src/plugins/vis_types/xy/public/editor/components/options/point_series/grid_panel.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useMemo, useEffect, useCallback } from 'react'; +import React, { useMemo, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -16,25 +16,15 @@ import { SelectOption, SwitchOption } from '../../../../../../../vis_default_edi import { VisParams, ValueAxis } from '../../../../types'; import { ValidationVisOptionsProps } from '../../common'; -type GridPanelOptions = ValidationVisOptionsProps< - VisParams, - { - showElasticChartsOptions: boolean; - } ->; +type GridPanelOptions = ValidationVisOptionsProps; -function GridPanel({ stateParams, setValue, hasHistogramAgg, extraProps }: GridPanelOptions) { +function GridPanel({ stateParams, setValue }: GridPanelOptions) { const setGrid = useCallback( (paramName: T, value: VisParams['grid'][T]) => setValue('grid', { ...stateParams.grid, [paramName]: value }), [stateParams.grid, setValue] ); - const disableCategoryGridLines = useMemo( - () => !extraProps?.showElasticChartsOptions && hasHistogramAgg, - [extraProps?.showElasticChartsOptions, hasHistogramAgg] - ); - const options = useMemo( () => [ ...stateParams.valueAxes.map(({ id, name }: ValueAxis) => ({ @@ -51,12 +41,6 @@ function GridPanel({ stateParams, setValue, hasHistogramAgg, extraProps }: GridP [stateParams.valueAxes] ); - useEffect(() => { - if (disableCategoryGridLines) { - setGrid('categoryLines', false); - } - }, [disableCategoryGridLines, setGrid]); - return ( @@ -71,18 +55,10 @@ function GridPanel({ stateParams, setValue, hasHistogramAgg, extraProps }: GridP { + it('renders the detailedTooltip option', async () => { component = mountWithIntl(); - await act(async () => { - expect(findTestSubject(component, 'detailedTooltip').length).toBe(0); - }); - }); - - it('renders the editor options that are specific for the es charts implementation if showElasticChartsOptions is true', async () => { - const newVisProps = ({ - ...props, - extraProps: { - showElasticChartsOptions: true, - }, - } as unknown) as PointSeriesOptionsProps; - component = mountWithIntl(); await act(async () => { expect(findTestSubject(component, 'detailedTooltip').length).toBe(1); }); }); - it('not renders the long legend options if showElasticChartsOptions is false', async () => { + it('renders the long legend options', async () => { component = mountWithIntl(); - await act(async () => { - expect(findTestSubject(component, 'xyLongLegendsOptions').length).toBe(0); - }); - }); - - it('renders the long legend options if showElasticChartsOptions is true', async () => { - const newVisProps = ({ - ...props, - extraProps: { - showElasticChartsOptions: true, - }, - } as unknown) as PointSeriesOptionsProps; - component = mountWithIntl(); await act(async () => { expect(findTestSubject(component, 'xyLongLegendsOptions').length).toBe(1); }); }); it('not renders the fitting function for a bar chart', async () => { - const newVisProps = ({ - ...props, - extraProps: { - showElasticChartsOptions: true, - }, - } as unknown) as PointSeriesOptionsProps; - component = mountWithIntl(); + component = mountWithIntl(); await act(async () => { expect(findTestSubject(component, 'fittingFunction').length).toBe(0); }); @@ -142,9 +107,6 @@ describe('PointSeries Editor', function () { const newVisProps = ({ ...props, stateParams: getStateParams(ChartType.Line, false), - extraProps: { - showElasticChartsOptions: true, - }, } as unknown) as PointSeriesOptionsProps; component = mountWithIntl(); await act(async () => { @@ -153,13 +115,7 @@ describe('PointSeries Editor', function () { }); it('renders the showCategoryLines switch', async () => { - const newVisProps = ({ - ...props, - extraProps: { - showElasticChartsOptions: true, - }, - } as unknown) as PointSeriesOptionsProps; - component = mountWithIntl(); + component = mountWithIntl(); await act(async () => { expect(findTestSubject(component, 'showValuesOnChart').length).toBe(1); }); diff --git a/src/plugins/vis_types/xy/public/editor/components/options/point_series/point_series.tsx b/src/plugins/vis_types/xy/public/editor/components/options/point_series/point_series.tsx index da7bdfb0d7986..62dbd94c516d7 100644 --- a/src/plugins/vis_types/xy/public/editor/components/options/point_series/point_series.tsx +++ b/src/plugins/vis_types/xy/public/editor/components/options/point_series/point_series.tsx @@ -28,16 +28,7 @@ import { getPositions } from '../../../collections'; const legendPositions = getPositions(); -export function PointSeriesOptions( - props: ValidationVisOptionsProps< - VisParams, - { - // TODO: Remove when vis_type_vislib is removed - // https://github.com/elastic/kibana/issues/56143 - showElasticChartsOptions: boolean; - } - > -) { +export function PointSeriesOptions(props: ValidationVisOptionsProps) { const { stateParams, setValue, vis, aggs } = props; const hasBarChart = useMemo( () => @@ -62,14 +53,12 @@ export function PointSeriesOptions( - {props.extraProps?.showElasticChartsOptions && ( - - )} + {vis.data.aggs!.aggs.some( (agg) => agg.schema === 'segment' && agg.type.name === BUCKET_TYPES.DATE_HISTOGRAM @@ -109,7 +98,7 @@ export function PointSeriesOptions( /> )} - {props.extraProps?.showElasticChartsOptions && } + diff --git a/src/plugins/vis_types/xy/public/index.ts b/src/plugins/vis_types/xy/public/index.ts index 0953183fa1093..1ee96fab35253 100644 --- a/src/plugins/vis_types/xy/public/index.ts +++ b/src/plugins/vis_types/xy/public/index.ts @@ -30,7 +30,6 @@ export type { ValidationVisOptionsProps } from './editor/components/common/valid export { TruncateLabelsOption } from './editor/components/common/truncate_labels'; export { getPositions } from './editor/positions'; export { getScaleTypes } from './editor/scale_types'; -export { xyVisTypes } from './vis_types'; export { getAggId } from './config/get_agg_id'; // Export common types diff --git a/src/plugins/vis_types/xy/public/plugin.ts b/src/plugins/vis_types/xy/public/plugin.ts index 57736444f49fe..600e78b5b3949 100644 --- a/src/plugins/vis_types/xy/public/plugin.ts +++ b/src/plugins/vis_types/xy/public/plugin.ts @@ -24,7 +24,6 @@ import { } from './services'; import { visTypesDefinitions } from './vis_types'; -import { LEGACY_CHARTS_LIBRARY } from '../common/'; import { xyVisRenderer } from './vis_renderer'; import * as expressionFunctions from './expression_functions'; @@ -65,23 +64,21 @@ export class VisTypeXyPlugin core: VisTypeXyCoreSetup, { expressions, visualizations, charts, usageCollection }: VisTypeXyPluginSetupDependencies ) { - if (!core.uiSettings.get(LEGACY_CHARTS_LIBRARY, false)) { - setUISettings(core.uiSettings); - setThemeService(charts.theme); - setPalettesService(charts.palettes); + setUISettings(core.uiSettings); + setThemeService(charts.theme); + setPalettesService(charts.palettes); - expressions.registerRenderer(xyVisRenderer); - expressions.registerFunction(expressionFunctions.visTypeXyVisFn); - expressions.registerFunction(expressionFunctions.categoryAxis); - expressions.registerFunction(expressionFunctions.timeMarker); - expressions.registerFunction(expressionFunctions.valueAxis); - expressions.registerFunction(expressionFunctions.seriesParam); - expressions.registerFunction(expressionFunctions.thresholdLine); - expressions.registerFunction(expressionFunctions.label); - expressions.registerFunction(expressionFunctions.visScale); + expressions.registerRenderer(xyVisRenderer); + expressions.registerFunction(expressionFunctions.visTypeXyVisFn); + expressions.registerFunction(expressionFunctions.categoryAxis); + expressions.registerFunction(expressionFunctions.timeMarker); + expressions.registerFunction(expressionFunctions.valueAxis); + expressions.registerFunction(expressionFunctions.seriesParam); + expressions.registerFunction(expressionFunctions.thresholdLine); + expressions.registerFunction(expressionFunctions.label); + expressions.registerFunction(expressionFunctions.visScale); - visTypesDefinitions.forEach(visualizations.createBaseVisualization); - } + visTypesDefinitions.forEach(visualizations.createBaseVisualization); setTrackUiMetric(usageCollection?.reportUiCounter.bind(usageCollection, 'vis_type_xy')); diff --git a/src/plugins/vis_types/xy/public/utils/accessors.tsx b/src/plugins/vis_types/xy/public/utils/accessors.tsx index 0356e921a9d5c..748430e3b16a6 100644 --- a/src/plugins/vis_types/xy/public/utils/accessors.tsx +++ b/src/plugins/vis_types/xy/public/utils/accessors.tsx @@ -13,6 +13,7 @@ import { Aspect } from '../types'; export const COMPLEX_X_ACCESSOR = '__customXAccessor__'; export const COMPLEX_SPLIT_ACCESSOR = '__complexSplitAccessor__'; +const SHARD_DELAY = 'shard_delay'; export const getXAccessor = (aspect: Aspect): Accessor | AccessorFn => { return ( @@ -39,7 +40,7 @@ export const getComplexAccessor = (fieldName: string, isComplex: boolean = false aspect: Aspect, index?: number ): Accessor | AccessorFn | undefined => { - if (!aspect.accessor) { + if (!aspect.accessor || aspect.aggType === SHARD_DELAY) { return; } diff --git a/src/plugins/vis_types/xy/public/vis_types/area.ts b/src/plugins/vis_types/xy/public/vis_types/area.ts index b377fd54753da..6ba197ceb9424 100644 --- a/src/plugins/vis_types/xy/public/vis_types/area.ts +++ b/src/plugins/vis_types/xy/public/vis_types/area.ts @@ -22,15 +22,12 @@ import { AxisMode, ThresholdLineStyle, InterpolationMode, - XyVisTypeDefinition, } from '../types'; import { toExpressionAst } from '../to_ast'; import { ChartType } from '../../common'; -import { getOptionTabs } from '../editor/common_config'; +import { optionTabs } from '../editor/common_config'; -export const getAreaVisTypeDefinition = ( - showElasticChartsOptions = false -): XyVisTypeDefinition => ({ +export const areaVisTypeDefinition = { name: 'area', title: i18n.translate('visTypeXy.area.areaTitle', { defaultMessage: 'Area' }), icon: 'visArea', @@ -128,7 +125,7 @@ export const getAreaVisTypeDefinition = ( }, }, editorConfig: { - optionTabs: getOptionTabs(showElasticChartsOptions), + optionTabs, schemas: [ { group: AggGroupNames.Metrics, @@ -183,4 +180,4 @@ export const getAreaVisTypeDefinition = ( ], }, requiresSearch: true, -}); +}; diff --git a/src/plugins/vis_types/xy/public/vis_types/histogram.ts b/src/plugins/vis_types/xy/public/vis_types/histogram.ts index 2d22b7566175c..bd549615fe7fd 100644 --- a/src/plugins/vis_types/xy/public/vis_types/histogram.ts +++ b/src/plugins/vis_types/xy/public/vis_types/histogram.ts @@ -20,17 +20,14 @@ import { ScaleType, AxisMode, ThresholdLineStyle, - XyVisTypeDefinition, InterpolationMode, } from '../types'; import { toExpressionAst } from '../to_ast'; import { ChartType } from '../../common'; -import { getOptionTabs } from '../editor/common_config'; +import { optionTabs } from '../editor/common_config'; import { defaultCountLabel, LabelRotation } from '../../../../charts/public'; -export const getHistogramVisTypeDefinition = ( - showElasticChartsOptions = false -): XyVisTypeDefinition => ({ +export const histogramVisTypeDefinition = { name: 'histogram', title: i18n.translate('visTypeXy.histogram.histogramTitle', { defaultMessage: 'Vertical bar', @@ -131,7 +128,7 @@ export const getHistogramVisTypeDefinition = ( }, }, editorConfig: { - optionTabs: getOptionTabs(showElasticChartsOptions), + optionTabs, schemas: [ { group: AggGroupNames.Metrics, @@ -186,4 +183,4 @@ export const getHistogramVisTypeDefinition = ( ], }, requiresSearch: true, -}); +}; diff --git a/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts b/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts index 8916f3f94f6ff..5bd45fc2eb7a8 100644 --- a/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts +++ b/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts @@ -20,17 +20,14 @@ import { ScaleType, AxisMode, ThresholdLineStyle, - XyVisTypeDefinition, InterpolationMode, } from '../types'; import { toExpressionAst } from '../to_ast'; import { ChartType } from '../../common'; -import { getOptionTabs } from '../editor/common_config'; +import { optionTabs } from '../editor/common_config'; import { defaultCountLabel, LabelRotation } from '../../../../charts/public'; -export const getHorizontalBarVisTypeDefinition = ( - showElasticChartsOptions = false -): XyVisTypeDefinition => ({ +export const horizontalBarVisTypeDefinition = { name: 'horizontal_bar', title: i18n.translate('visTypeXy.horizontalBar.horizontalBarTitle', { defaultMessage: 'Horizontal bar', @@ -130,7 +127,7 @@ export const getHorizontalBarVisTypeDefinition = ( }, }, editorConfig: { - optionTabs: getOptionTabs(showElasticChartsOptions), + optionTabs, schemas: [ { group: AggGroupNames.Metrics, @@ -185,4 +182,4 @@ export const getHorizontalBarVisTypeDefinition = ( ], }, requiresSearch: true, -}); +}; diff --git a/src/plugins/vis_types/xy/public/vis_types/index.ts b/src/plugins/vis_types/xy/public/vis_types/index.ts index a8dae74eb110c..93c973b5316c9 100644 --- a/src/plugins/vis_types/xy/public/vis_types/index.ts +++ b/src/plugins/vis_types/xy/public/vis_types/index.ts @@ -6,27 +6,14 @@ * Side Public License, v 1. */ -import { getAreaVisTypeDefinition } from './area'; -import { getLineVisTypeDefinition } from './line'; -import { getHistogramVisTypeDefinition } from './histogram'; -import { getHorizontalBarVisTypeDefinition } from './horizontal_bar'; -import { XyVisTypeDefinition } from '../types'; +import { areaVisTypeDefinition } from './area'; +import { lineVisTypeDefinition } from './line'; +import { histogramVisTypeDefinition } from './histogram'; +import { horizontalBarVisTypeDefinition } from './horizontal_bar'; export const visTypesDefinitions = [ - getAreaVisTypeDefinition(true), - getLineVisTypeDefinition(true), - getHistogramVisTypeDefinition(true), - getHorizontalBarVisTypeDefinition(true), + areaVisTypeDefinition, + lineVisTypeDefinition, + histogramVisTypeDefinition, + horizontalBarVisTypeDefinition, ]; - -// TODO: Remove when vis_type_vislib is removed -// https://github.com/elastic/kibana/issues/56143 -export const xyVisTypes: Record< - string, - (showElasticChartsOptions?: boolean) => XyVisTypeDefinition -> = { - area: getAreaVisTypeDefinition, - line: getLineVisTypeDefinition, - histogram: getHistogramVisTypeDefinition, - horizontalBar: getHorizontalBarVisTypeDefinition, -}; diff --git a/src/plugins/vis_types/xy/public/vis_types/line.ts b/src/plugins/vis_types/xy/public/vis_types/line.ts index af75c38d627df..747de1679c7c5 100644 --- a/src/plugins/vis_types/xy/public/vis_types/line.ts +++ b/src/plugins/vis_types/xy/public/vis_types/line.ts @@ -22,15 +22,12 @@ import { AxisMode, ThresholdLineStyle, InterpolationMode, - XyVisTypeDefinition, } from '../types'; import { toExpressionAst } from '../to_ast'; import { ChartType } from '../../common'; -import { getOptionTabs } from '../editor/common_config'; +import { optionTabs } from '../editor/common_config'; -export const getLineVisTypeDefinition = ( - showElasticChartsOptions = false -): XyVisTypeDefinition => ({ +export const lineVisTypeDefinition = { name: 'line', title: i18n.translate('visTypeXy.line.lineTitle', { defaultMessage: 'Line' }), icon: 'visLine', @@ -128,7 +125,7 @@ export const getLineVisTypeDefinition = ( }, }, editorConfig: { - optionTabs: getOptionTabs(showElasticChartsOptions), + optionTabs, schemas: [ { group: AggGroupNames.Metrics, @@ -177,4 +174,4 @@ export const getLineVisTypeDefinition = ( ], }, requiresSearch: true, -}); +}; diff --git a/src/plugins/vis_types/xy/server/index.ts b/src/plugins/vis_types/xy/server/index.ts deleted file mode 100644 index a27ac49c0ea49..0000000000000 --- a/src/plugins/vis_types/xy/server/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -import { VisTypeXyServerPlugin } from './plugin'; - -export const plugin = () => new VisTypeXyServerPlugin(); diff --git a/src/plugins/vis_types/xy/server/plugin.ts b/src/plugins/vis_types/xy/server/plugin.ts deleted file mode 100644 index 46d6531204c24..0000000000000 --- a/src/plugins/vis_types/xy/server/plugin.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import { schema } from '@kbn/config-schema'; - -import { CoreSetup, Plugin, UiSettingsParams } from 'kibana/server'; - -import { LEGACY_CHARTS_LIBRARY } from '../common'; - -export const getUiSettingsConfig: () => Record> = () => ({ - // TODO: Remove this when vis_type_vislib is removed - // https://github.com/elastic/kibana/issues/56143 - [LEGACY_CHARTS_LIBRARY]: { - name: i18n.translate('visTypeXy.advancedSettings.visualization.legacyChartsLibrary.name', { - defaultMessage: 'XY axis legacy charts library', - }), - requiresPageReload: true, - value: false, - description: i18n.translate( - 'visTypeXy.advancedSettings.visualization.legacyChartsLibrary.description', - { - defaultMessage: 'Enables legacy charts library for area, line and bar charts in visualize.', - } - ), - deprecation: { - message: i18n.translate( - 'visTypeXy.advancedSettings.visualization.legacyChartsLibrary.deprecation', - { - defaultMessage: - 'The legacy charts library for area, line and bar charts in visualize is deprecated and will not be supported as of 7.16.', - } - ), - docLinksKey: 'visualizationSettings', - }, - category: ['visualization'], - schema: schema.boolean(), - }, -}); - -export class VisTypeXyServerPlugin implements Plugin { - public setup(core: CoreSetup) { - core.uiSettings.register(getUiSettingsConfig()); - - return {}; - } - - public start() { - return {}; - } -} diff --git a/src/plugins/visualize/public/application/components/deprecation_vis_warning.tsx b/src/plugins/visualize/public/application/components/deprecation_vis_warning.tsx deleted file mode 100644 index 6389f52996926..0000000000000 --- a/src/plugins/visualize/public/application/components/deprecation_vis_warning.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiCallOut, EuiLink } from '@elastic/eui'; -import { useKibana } from '../../../../kibana_react/public'; -import { VisualizeServices } from '../types'; - -export const LEGACY_CHARTS_LIBRARY = 'visualization:visualize:legacyChartsLibrary'; - -export const DeprecationWarning = () => { - const { services } = useKibana(); - const canEditAdvancedSettings = services.application.capabilities.advancedSettings.save; - const advancedSettingsLink = services.application.getUrlForApp('management', { - path: `/kibana/settings?query=${LEGACY_CHARTS_LIBRARY}`, - }); - - return ( - - {canEditAdvancedSettings && ( - - - - ), - }} - /> - )} - {!canEditAdvancedSettings && ( - - )} - > - ), - }} - /> - } - iconType="alert" - color="warning" - size="s" - /> - ); -}; diff --git a/src/plugins/visualize/public/application/components/visualize_editor_common.tsx b/src/plugins/visualize/public/application/components/visualize_editor_common.tsx index 22f635460c353..a03073e61f59c 100644 --- a/src/plugins/visualize/public/application/components/visualize_editor_common.tsx +++ b/src/plugins/visualize/public/application/components/visualize_editor_common.tsx @@ -13,14 +13,12 @@ import { EuiScreenReaderOnly } from '@elastic/eui'; import { AppMountParameters } from 'kibana/public'; import { VisualizeTopNav } from './visualize_top_nav'; import { ExperimentalVisInfo } from './experimental_vis_info'; -import { DeprecationWarning, LEGACY_CHARTS_LIBRARY } from './deprecation_vis_warning'; import { SavedVisInstance, VisualizeAppState, VisualizeAppStateContainer, VisualizeEditorVisInstance, } from '../types'; -import { getUISettings } from '../../services'; interface VisualizeEditorCommonProps { visInstance?: VisualizeEditorVisInstance; @@ -39,13 +37,6 @@ interface VisualizeEditorCommonProps { embeddableId?: string; } -const isXYAxis = (visType: string | undefined): boolean => { - if (!visType) { - return false; - } - return ['area', 'line', 'histogram', 'horizontal_bar', 'point_series'].includes(visType); -}; - export const VisualizeEditorCommon = ({ visInstance, appState, @@ -62,7 +53,6 @@ export const VisualizeEditorCommon = ({ embeddableId, visEditorRef, }: VisualizeEditorCommonProps) => { - const hasXYLegacyChartsEnabled = getUISettings().get(LEGACY_CHARTS_LIBRARY); return ( {visInstance && appState && currentAppState && ( @@ -83,9 +73,6 @@ export const VisualizeEditorCommon = ({ /> )} {visInstance?.vis?.type?.stage === 'experimental' && } - {/* Adds a deprecation warning for vislib xy axis charts */} - {/* Should be removed when this issue is closed https://github.com/elastic/kibana/issues/103209 */} - {isXYAxis(visInstance?.vis.type.name) && hasXYLegacyChartsEnabled && } {visInstance?.vis?.type?.getInfoMessage?.(visInstance.vis)} {visInstance && ( diff --git a/test/functional/apps/dashboard/dashboard_state.ts b/test/functional/apps/dashboard/dashboard_state.ts index 8200ba8aae92e..b5cb0194a0a78 100644 --- a/test/functional/apps/dashboard/dashboard_state.ts +++ b/test/functional/apps/dashboard/dashboard_state.ts @@ -33,9 +33,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const dashboardPanelActions = getService('dashboardPanelActions'); const dashboardAddPanel = getService('dashboardAddPanel'); + const xyChartSelector = 'visTypeXyChart'; - const enableNewChartLibraryDebug = async () => { - if (await PageObjects.visChart.isNewChartsLibraryEnabled()) { + const enableNewChartLibraryDebug = async (force = false) => { + if ((await PageObjects.visChart.isNewChartsLibraryEnabled()) || force) { await elasticChart.setNewChartUiDebugFlag(); await queryBar.submitQuery(); } @@ -52,7 +53,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { if (isNewChartsLibraryEnabled) { await kibanaServer.uiSettings.update({ - 'visualization:visualize:legacyChartsLibrary': false, 'visualization:visualize:legacyPieChartsLibrary': false, }); await browser.refresh(); @@ -69,33 +69,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.clickNewDashboard(); await PageObjects.timePicker.setHistoricalDataRange(); - const visName = await PageObjects.visChart.getExpectedValue( - AREA_CHART_VIS_NAME, - `${AREA_CHART_VIS_NAME} - new charts library` - ); + const visName = AREA_CHART_VIS_NAME; await dashboardAddPanel.addVisualization(visName); - const dashboarName = await PageObjects.visChart.getExpectedValue( - 'Overridden colors', - 'Overridden colors - new charts library' - ); - await PageObjects.dashboard.saveDashboard(dashboarName); + const dashboardName = 'Overridden colors - new charts library'; + await PageObjects.dashboard.saveDashboard(dashboardName); await PageObjects.dashboard.switchToEditMode(); await queryBar.clickQuerySubmitButton(); - await PageObjects.visChart.openLegendOptionColors('Count', `[data-title="${visName}"]`); - const overwriteColor = isNewChartsLibraryEnabled ? '#d36086' : '#EA6460'; + await PageObjects.visChart.openLegendOptionColorsForXY('Count', `[data-title="${visName}"]`); + const overwriteColor = '#d36086'; await PageObjects.visChart.selectNewLegendColorChoice(overwriteColor); - await PageObjects.dashboard.saveDashboard(dashboarName); + await PageObjects.dashboard.saveDashboard(dashboardName); await PageObjects.dashboard.gotoDashboardLandingPage(); - await PageObjects.dashboard.loadSavedDashboard(dashboarName); + await PageObjects.dashboard.loadSavedDashboard(dashboardName); - await enableNewChartLibraryDebug(); + await enableNewChartLibraryDebug(true); - const colorChoiceRetained = await PageObjects.visChart.doesSelectedLegendColorExist( - overwriteColor + const colorChoiceRetained = await PageObjects.visChart.doesSelectedLegendColorExistForXY( + overwriteColor, + xyChartSelector ); expect(colorChoiceRetained).to.be(true); @@ -210,7 +205,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.get(newUrl.toString()); const alert = await browser.getAlert(); await alert?.accept(); - await enableNewChartLibraryDebug(); + await enableNewChartLibraryDebug(true); await PageObjects.dashboard.waitForRenderComplete(); }; @@ -293,7 +288,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('updates a pie slice color on a hard refresh', async function () { - await PageObjects.visChart.openLegendOptionColors( + await PageObjects.visChart.openLegendOptionColorsForPie( '80,000', `[data-title="${PIE_CHART_VIS_NAME}"]` ); @@ -318,7 +313,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('and updates the pie slice legend color', async function () { await retry.try(async () => { - const colorExists = await PageObjects.visChart.doesSelectedLegendColorExist('#FFFFFF'); + const colorExists = await PageObjects.visChart.doesSelectedLegendColorExistForPie( + '#FFFFFF' + ); expect(colorExists).to.be(true); }); }); @@ -342,7 +339,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('resets the legend color as well', async function () { await retry.try(async () => { - const colorExists = await PageObjects.visChart.doesSelectedLegendColorExist('#57c17b'); + const colorExists = await PageObjects.visChart.doesSelectedLegendColorExistForPie( + '#57c17b' + ); expect(colorExists).to.be(true); }); }); diff --git a/test/functional/apps/dashboard/index.ts b/test/functional/apps/dashboard/index.ts index e4dc04282e4ac..8627a258869bb 100644 --- a/test/functional/apps/dashboard/index.ts +++ b/test/functional/apps/dashboard/index.ts @@ -122,7 +122,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { before(async () => { await loadLogstash(); await kibanaServer.uiSettings.update({ - 'visualization:visualize:legacyChartsLibrary': false, 'visualization:visualize:legacyPieChartsLibrary': false, }); await browser.refresh(); @@ -131,7 +130,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { after(async () => { await unloadLogstash(); await kibanaServer.uiSettings.update({ - 'visualization:visualize:legacyChartsLibrary': true, 'visualization:visualize:legacyPieChartsLibrary': true, }); await browser.refresh(); diff --git a/test/functional/apps/getting_started/_shakespeare.ts b/test/functional/apps/getting_started/_shakespeare.ts index 98eeed7bcf53e..426713c912e88 100644 --- a/test/functional/apps/getting_started/_shakespeare.ts +++ b/test/functional/apps/getting_started/_shakespeare.ts @@ -28,6 +28,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'visChart', ]); + const xyChartSelector = 'visTypeXyChart'; + // https://www.elastic.co/guide/en/kibana/current/tutorial-load-dataset.html describe('Shakespeare', function describeIndexTests() { @@ -56,7 +58,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { if (isNewChartsLibraryEnabled) { await kibanaServer.uiSettings.update({ - 'visualization:visualize:legacyChartsLibrary': false, 'visualization:visualize:legacyPieChartsLibrary': false, }); await browser.refresh(); @@ -92,11 +93,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Remove refresh click when vislib is removed // https://github.com/elastic/kibana/issues/56143 - await PageObjects.visualize.clickRefresh(); + await PageObjects.visualize.clickRefresh(true); const expectedChartValues = [111396]; await retry.try(async () => { - const data = await PageObjects.visChart.getBarChartData('Count'); + const data = await PageObjects.visChart.getBarChartData(xyChartSelector, 'Count'); log.debug('data=' + data); log.debug('data.length=' + data.length); expect(data[0] - expectedChartValues[0]).to.be.lessThan(5); @@ -123,12 +124,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.clickGo(); const expectedChartValues = [935]; await retry.try(async () => { - const data = await PageObjects.visChart.getBarChartData('Speaking Parts'); + const data = await PageObjects.visChart.getBarChartData(xyChartSelector, 'Speaking Parts'); log.debug('data=' + data); log.debug('data.length=' + data.length); expect(data).to.eql(expectedChartValues); }); - const title = await PageObjects.visChart.getYAxisTitle(); + const title = await PageObjects.visChart.getYAxisTitle(xyChartSelector); expect(title).to.be('Speaking Parts'); }); @@ -149,13 +150,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const expectedChartValues = [71, 65, 62, 55, 55]; await retry.try(async () => { - const data = await PageObjects.visChart.getBarChartData('Speaking Parts'); + const data = await PageObjects.visChart.getBarChartData(xyChartSelector, 'Speaking Parts'); log.debug('data=' + data); log.debug('data.length=' + data.length); expect(data).to.eql(expectedChartValues); }); - const labels = await PageObjects.visChart.getXAxisLabels(); + const labels = await PageObjects.visChart.getXAxisLabels(xyChartSelector); expect(labels).to.eql([ 'Richard III', 'Henry VI Part 2', @@ -187,8 +188,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const expectedChartValues = [71, 65, 62, 55, 55]; const expectedChartValues2 = [177, 106, 153, 132, 162]; await retry.try(async () => { - const data = await PageObjects.visChart.getBarChartData('Speaking Parts'); - const data2 = await PageObjects.visChart.getBarChartData('Max Speaking Parts'); + const data = await PageObjects.visChart.getBarChartData(xyChartSelector, 'Speaking Parts'); + const data2 = await PageObjects.visChart.getBarChartData( + xyChartSelector, + 'Max Speaking Parts' + ); log.debug('data=' + data); log.debug('data.length=' + data.length); log.debug('data2=' + data2); @@ -197,7 +201,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(data2).to.eql(expectedChartValues2); }); - const labels = await PageObjects.visChart.getXAxisLabels(); + const labels = await PageObjects.visChart.getXAxisLabels(xyChartSelector); expect(labels).to.eql([ 'Richard III', 'Henry VI Part 2', @@ -220,8 +224,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const expectedChartValues = [71, 65, 62, 55, 55]; const expectedChartValues2 = [177, 106, 153, 132, 162]; await retry.try(async () => { - const data = await PageObjects.visChart.getBarChartData('Speaking Parts'); - const data2 = await PageObjects.visChart.getBarChartData('Max Speaking Parts'); + const data = await PageObjects.visChart.getBarChartData(xyChartSelector, 'Speaking Parts'); + const data2 = await PageObjects.visChart.getBarChartData( + xyChartSelector, + 'Max Speaking Parts' + ); log.debug('data=' + data); log.debug('data.length=' + data.length); log.debug('data2=' + data2); @@ -243,17 +250,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.clickGo(); // same values as previous test except scaled down by the 50 for Y-Axis min - const expectedChartValues = await PageObjects.visChart.getExpectedValue( - [21, 15, 12, 5, 5], - [71, 65, 62, 55, 55] // no scaled values in elastic-charts - ); - const expectedChartValues2 = await PageObjects.visChart.getExpectedValue( - [127, 56, 103, 82, 112], - [177, 106, 153, 132, 162] // no scaled values in elastic-charts - ); + const expectedChartValues = [71, 65, 62, 55, 55]; + const expectedChartValues2 = [177, 106, 153, 132, 162]; await retry.try(async () => { - const data = await PageObjects.visChart.getBarChartData('Speaking Parts'); - const data2 = await PageObjects.visChart.getBarChartData('Max Speaking Parts'); + const data = await PageObjects.visChart.getBarChartData(xyChartSelector, 'Speaking Parts'); + const data2 = await PageObjects.visChart.getBarChartData( + xyChartSelector, + 'Max Speaking Parts' + ); log.debug('data=' + data); log.debug('data.length=' + data.length); log.debug('data2=' + data2); diff --git a/test/functional/apps/getting_started/index.ts b/test/functional/apps/getting_started/index.ts index 4c1c052ef15a2..ae7fdc3c1d4fa 100644 --- a/test/functional/apps/getting_started/index.ts +++ b/test/functional/apps/getting_started/index.ts @@ -23,7 +23,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { describe('new charts library', function () { before(async () => { await kibanaServer.uiSettings.update({ - 'visualization:visualize:legacyChartsLibrary': false, 'visualization:visualize:legacyPieChartsLibrary': false, }); await browser.refresh(); @@ -31,7 +30,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { after(async () => { await kibanaServer.uiSettings.update({ - 'visualization:visualize:legacyChartsLibrary': true, 'visualization:visualize:legacyPieChartsLibrary': true, }); await browser.refresh(); diff --git a/test/functional/apps/visualize/_area_chart.ts b/test/functional/apps/visualize/_area_chart.ts index e88754823f6cb..4e4fe5e2902b9 100644 --- a/test/functional/apps/visualize/_area_chart.ts +++ b/test/functional/apps/visualize/_area_chart.ts @@ -26,18 +26,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'header', 'timePicker', ]); + const xyChartSelector = 'visTypeXyChart'; - const getVizName = async () => - await PageObjects.visChart.getExpectedValue( - 'Visualization AreaChart Name Test', - 'Visualization AreaChart Name Test - Charts library' - ); + const vizName = 'Visualization AreaChart Name Test - Charts library'; describe('area charts', function indexPatternCreation() { - let isNewChartsLibraryEnabled = false; before(async () => { - isNewChartsLibraryEnabled = await PageObjects.visChart.isNewChartsLibraryEnabled(); - await PageObjects.visualize.initTests(isNewChartsLibraryEnabled); + await PageObjects.visualize.initTests(); }); const initAreaChart = async () => { log.debug('navigateToApp visualize'); @@ -58,7 +53,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const intervalValue = await PageObjects.visEditor.getInterval(); log.debug('intervalValue = ' + intervalValue); expect(intervalValue[0]).to.be('Auto'); - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); }; before(async function () { @@ -75,49 +70,38 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should save and load with special characters', async function () { - const vizNamewithSpecialChars = (await getVizName()) + '/?&=%'; + const vizNamewithSpecialChars = vizName + '/?&=%'; await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb( vizNamewithSpecialChars ); }); it('should save and load with non-ascii characters', async function () { - const vizNamewithSpecialChars = `${await getVizName()} with Umlaut ä`; + const vizNamewithSpecialChars = `${vizName} with Umlaut ä`; await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb( vizNamewithSpecialChars ); }); it('should save and load', async function () { - await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(await getVizName()); - await PageObjects.visualize.loadSavedVisualization(await getVizName()); + await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(vizName); + await PageObjects.visualize.loadSavedVisualization(vizName); await PageObjects.visChart.waitForVisualization(); }); - // Should be removed when this issue is closed https://github.com/elastic/kibana/issues/103209 - it('should show/hide a deprecation warning depending on the library selected', async () => { - await PageObjects.visualize.getDeprecationWarningStatus(); - }); - it('should have inspector enabled', async function () { await inspector.expectIsEnabled(); }); it('should show correct chart', async function () { - const xAxisLabels = await PageObjects.visChart.getExpectedValue( - ['2015-09-20 00:00', '2015-09-21 00:00', '2015-09-22 00:00', '2015-09-23 00:00'], - [ - '2015-09-19 12:00', - '2015-09-20 12:00', - '2015-09-21 12:00', - '2015-09-22 12:00', - '2015-09-23 12:00', - ] - ); - const yAxisLabels = await PageObjects.visChart.getExpectedValue( - ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400', '1,600'], - ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400'] - ); + const xAxisLabels = [ + '2015-09-19 12:00', + '2015-09-20 12:00', + '2015-09-21 12:00', + '2015-09-22 12:00', + '2015-09-23 12:00', + ]; + const yAxisLabels = ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400']; const expectedAreaChartData = [ 37, 202, @@ -146,14 +130,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ]; await retry.try(async function tryingForTime() { - const labels = await PageObjects.visChart.getXAxisLabels(); + const labels = await PageObjects.visChart.getXAxisLabels(xyChartSelector); log.debug('X-Axis labels = ' + labels); expect(labels).to.eql(xAxisLabels); }); - const labels = await PageObjects.visChart.getYAxisLabels(); + const labels = await PageObjects.visChart.getYAxisLabels(xyChartSelector); log.debug('Y-Axis labels = ' + labels); expect(labels).to.eql(yAxisLabels); - const paths = await PageObjects.visChart.getAreaChartData('Count'); + const paths = await PageObjects.visChart.getAreaChartData('Count', xyChartSelector); log.debug('expectedAreaChartData = ' + expectedAreaChartData); log.debug('actual chart data = ' + paths); expect(paths).to.eql(expectedAreaChartData); @@ -220,7 +204,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.toggleOpenEditor(2); await PageObjects.visEditor.setInterval('Second'); - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); await inspector.open(); await inspector.expectTableData(expectedTableData); await inspector.close(); @@ -252,7 +236,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.toggleAdvancedParams('2'); await PageObjects.visEditor.toggleScaleMetrics(); - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); await inspector.open(); await inspector.expectTableData(expectedTableData); await inspector.close(); @@ -286,7 +270,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.selectAggregation('Top Hit', 'metrics'); await PageObjects.visEditor.selectField('bytes', 'metrics'); await PageObjects.visEditor.selectAggregateWith('average'); - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); await inspector.open(); await inspector.expectTableData(expectedTableData); await inspector.close(); @@ -320,10 +304,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.clickYAxisOptions(axisId); await PageObjects.visEditor.selectYAxisScaleType(axisId, 'log'); await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(); - const minLabel = await PageObjects.visChart.getExpectedValue(2, 1); - const maxLabel = await PageObjects.visChart.getExpectedValue(5000, 900); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(xyChartSelector); + const minLabel = 1; + const maxLabel = 900; const numberOfLabels = 10; expect(labels.length).to.be.greaterThan(numberOfLabels); expect(labels[0]).to.eql(minLabel); @@ -332,10 +316,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show filtered ticks on selecting log scale', async () => { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(); - const minLabel = await PageObjects.visChart.getExpectedValue(2, 1); - const maxLabel = await PageObjects.visChart.getExpectedValue(5000, 900); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(xyChartSelector); + const minLabel = 1; + const maxLabel = 900; const numberOfLabels = 10; expect(labels.length).to.be.greaterThan(numberOfLabels); expect(labels[0]).to.eql(minLabel); @@ -345,47 +329,35 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show ticks on selecting square root scale', async () => { await PageObjects.visEditor.selectYAxisScaleType(axisId, 'square root'); await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = await PageObjects.visChart.getExpectedValue( - ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400', '1,600'], - ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400'] - ); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabels(xyChartSelector); + const expectedLabels = ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400']; expect(labels).to.eql(expectedLabels); }); it('should show filtered ticks on selecting square root scale', async () => { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = await PageObjects.visChart.getExpectedValue( - ['200', '400', '600', '800', '1,000', '1,200', '1,400'], - ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400'] - ); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabels(xyChartSelector); + const expectedLabels = ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400']; expect(labels).to.eql(expectedLabels); }); it('should show ticks on selecting linear scale', async () => { await PageObjects.visEditor.selectYAxisScaleType(axisId, 'linear'); await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabels(xyChartSelector); log.debug(labels); - const expectedLabels = await PageObjects.visChart.getExpectedValue( - ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400', '1,600'], - ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400'] - ); + const expectedLabels = ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400']; expect(labels).to.eql(expectedLabels); }); it('should show filtered ticks on selecting linear scale', async () => { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = await PageObjects.visChart.getExpectedValue( - ['200', '400', '600', '800', '1,000', '1,200', '1,400'], - ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400'] - ); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabels(xyChartSelector); + const expectedLabels = ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400']; expect(labels).to.eql(expectedLabels); }); }); @@ -408,11 +380,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.selectAggregation('Date Histogram'); await PageObjects.visEditor.selectField('@timestamp'); await PageObjects.visEditor.setInterval('Year'); - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); // This svg area is composed by 7 years (2013 - 2019). // 7 points are used to draw the upper line (usually called y1) // 7 points compose the lower line (usually called y0) - const paths = await PageObjects.visChart.getAreaChartPaths('Count'); + const paths = await PageObjects.visChart.getAreaChartPaths('Count', xyChartSelector); log.debug('actual chart data = ' + paths); const numberOfSegments = 7 * 2; expect(paths.length).to.eql(numberOfSegments); @@ -431,12 +403,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.selectAggregation('Date Histogram'); await PageObjects.visEditor.selectField('@timestamp'); await PageObjects.visEditor.setInterval('Month'); - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); // This svg area is composed by 67 months 3 (2013) + 5 * 12 + 4 (2019) // 67 points are used to draw the upper line (usually called y1) // 67 points compose the lower line (usually called y0) const numberOfSegments = 67 * 2; - const paths = await PageObjects.visChart.getAreaChartPaths('Count'); + const paths = await PageObjects.visChart.getAreaChartPaths('Count', xyChartSelector); log.debug('actual chart data = ' + paths); expect(paths.length).to.eql(numberOfSegments); }); diff --git a/test/functional/apps/visualize/_line_chart_split_chart.ts b/test/functional/apps/visualize/_line_chart_split_chart.ts index 9b1c12de9666e..0e44c30499ed3 100644 --- a/test/functional/apps/visualize/_line_chart_split_chart.ts +++ b/test/functional/apps/visualize/_line_chart_split_chart.ts @@ -23,9 +23,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'visChart', 'timePicker', ]); + const xyChartSelector = 'visTypeXyChart'; describe('line charts - split chart', function () { - let isNewChartsLibraryEnabled = false; const initLineChart = async function () { log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); @@ -41,12 +41,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.selectField('extension.raw'); log.debug('switch from Rows to Columns'); await PageObjects.visEditor.clickSplitDirection('Columns'); - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); }; before(async () => { - isNewChartsLibraryEnabled = await PageObjects.visChart.isNewChartsLibraryEnabled(); - await PageObjects.visualize.initTests(isNewChartsLibraryEnabled); + await PageObjects.visualize.initTests(); await initLineChart(); }); @@ -61,7 +60,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // sleep a bit before trying to get the chart data await PageObjects.common.sleep(3000); - const data = await PageObjects.visChart.getLineChartData(); + const data = await PageObjects.visChart.getLineChartData(xyChartSelector); log.debug('data=' + data); const tolerance = 10; // the y-axis scale is 10000 so 10 is 0.1% for (let x = 0; x < data.length; x++) { @@ -92,9 +91,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { log.debug('Order By = Term'); await PageObjects.visEditor.selectOrderByMetric(2, '_key'); - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); await retry.try(async function () { - const data = await PageObjects.visChart.getLineChartData(); + const data = await PageObjects.visChart.getLineChartData(xyChartSelector); log.debug('data=' + data); const tolerance = 10; // the y-axis scale is 10000 so 10 is 0.1% for (let x = 0; x < data.length; x++) { @@ -161,10 +160,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should be able to save and load', async function () { - const vizName = await PageObjects.visChart.getExpectedValue( - 'Visualization Line split chart', - 'Visualization Line split chart - chart library' - ); + const vizName = 'Visualization Line split chart - chart library'; await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(vizName); await PageObjects.visualize.loadSavedVisualization(vizName); @@ -180,10 +176,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.clickYAxisOptions(axisId); await PageObjects.visEditor.selectYAxisScaleType(axisId, 'log'); await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(); - const minLabel = await PageObjects.visChart.getExpectedValue(2, 1); - const maxLabel = await PageObjects.visChart.getExpectedValue(5000, 7000); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(xyChartSelector); + const minLabel = 1; + const maxLabel = 7000; const numberOfLabels = 10; expect(labels.length).to.be.greaterThan(numberOfLabels); expect(labels[0]).to.eql(minLabel); @@ -192,10 +188,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show filtered ticks on selecting log scale', async () => { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(); - const minLabel = await PageObjects.visChart.getExpectedValue(2, 1); - const maxLabel = await PageObjects.visChart.getExpectedValue(5000, 7000); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(xyChartSelector); + const minLabel = 1; + const maxLabel = 7000; const numberOfLabels = 10; expect(labels.length).to.be.greaterThan(numberOfLabels); expect(labels[0]).to.eql(minLabel); @@ -205,48 +201,80 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show ticks on selecting square root scale', async () => { await PageObjects.visEditor.selectYAxisScaleType(axisId, 'square root'); await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = await PageObjects.visChart.getExpectedValue( - ['0', '2,000', '4,000', '6,000', '8,000', '10,000'], - ['0', '1,000', '2,000', '3,000', '4,000', '5,000', '6,000', '7,000', '8,000', '9,000'] - ); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabels(xyChartSelector); + const expectedLabels = [ + '0', + '1,000', + '2,000', + '3,000', + '4,000', + '5,000', + '6,000', + '7,000', + '8,000', + '9,000', + ]; expect(labels).to.eql(expectedLabels); }); it('should show filtered ticks on selecting square root scale', async () => { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = await PageObjects.visChart.getExpectedValue( - ['2,000', '4,000', '6,000', '8,000'], - ['0', '1,000', '2,000', '3,000', '4,000', '5,000', '6,000', '7,000', '8,000', '9,000'] - ); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabels(xyChartSelector); + const expectedLabels = [ + '0', + '1,000', + '2,000', + '3,000', + '4,000', + '5,000', + '6,000', + '7,000', + '8,000', + '9,000', + ]; expect(labels).to.eql(expectedLabels); }); it('should show ticks on selecting linear scale', async () => { await PageObjects.visEditor.selectYAxisScaleType(axisId, 'linear'); await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabels(xyChartSelector); log.debug(labels); - const expectedLabels = await PageObjects.visChart.getExpectedValue( - ['0', '2,000', '4,000', '6,000', '8,000', '10,000'], - ['0', '1,000', '2,000', '3,000', '4,000', '5,000', '6,000', '7,000', '8,000', '9,000'] - ); + const expectedLabels = [ + '0', + '1,000', + '2,000', + '3,000', + '4,000', + '5,000', + '6,000', + '7,000', + '8,000', + '9,000', + ]; expect(labels).to.eql(expectedLabels); }); it('should show filtered ticks on selecting linear scale', async () => { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = await PageObjects.visChart.getExpectedValue( - ['2,000', '4,000', '6,000', '8,000'], - ['0', '1,000', '2,000', '3,000', '4,000', '5,000', '6,000', '7,000', '8,000', '9,000'] - ); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabels(xyChartSelector); + const expectedLabels = [ + '0', + '1,000', + '2,000', + '3,000', + '4,000', + '5,000', + '6,000', + '7,000', + '8,000', + '9,000', + ]; expect(labels).to.eql(expectedLabels); }); }); @@ -274,16 +302,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.clickBucket('X-axis'); log.debug('Aggregation = Date Histogram'); await PageObjects.visEditor.selectAggregation('Date Histogram'); - await PageObjects.visEditor.clickGo(); - const title = await PageObjects.visChart.getYAxisTitle(); + await PageObjects.visEditor.clickGo(true); + const title = await PageObjects.visChart.getYAxisTitle(xyChartSelector); expect(title).to.be('Serial Diff of Count'); }); it('should change y-axis label to custom', async () => { log.debug('set custom label of y-axis to "Custom"'); await PageObjects.visEditor.setCustomLabel('Custom', 1); - await PageObjects.visEditor.clickGo(); - const title = await PageObjects.visChart.getYAxisTitle(); + await PageObjects.visEditor.clickGo(true); + const title = await PageObjects.visChart.getYAxisTitle(xyChartSelector); expect(title).to.be('Custom'); }); @@ -297,24 +325,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should apply with selected bucket', async () => { log.debug('Metrics agg = Average Bucket'); await PageObjects.visEditor.selectAggregation('Average Bucket', 'metrics'); - await PageObjects.visEditor.clickGo(); - const title = await PageObjects.visChart.getYAxisTitle(); + await PageObjects.visEditor.clickGo(true); + const title = await PageObjects.visChart.getYAxisTitle(xyChartSelector); expect(title).to.be('Overall Average of Count'); }); it('should change sub metric custom label and calculate y-axis title', async () => { log.debug('set custom label of sub metric to "Cats"'); await PageObjects.visEditor.setCustomLabel('Cats', '1-metric'); - await PageObjects.visEditor.clickGo(); - const title = await PageObjects.visChart.getYAxisTitle(); + await PageObjects.visEditor.clickGo(true); + const title = await PageObjects.visChart.getYAxisTitle(xyChartSelector); expect(title).to.be('Overall Average of Cats'); }); it('should outer custom label', async () => { log.debug('set custom label to "Custom"'); await PageObjects.visEditor.setCustomLabel('Custom', 1); - await PageObjects.visEditor.clickGo(); - const title = await PageObjects.visChart.getYAxisTitle(); + await PageObjects.visEditor.clickGo(true); + const title = await PageObjects.visChart.getYAxisTitle(xyChartSelector); expect(title).to.be('Custom'); }); diff --git a/test/functional/apps/visualize/_line_chart_split_series.ts b/test/functional/apps/visualize/_line_chart_split_series.ts index 91d44a6fc40da..d10b4ebd9b312 100644 --- a/test/functional/apps/visualize/_line_chart_split_series.ts +++ b/test/functional/apps/visualize/_line_chart_split_series.ts @@ -23,9 +23,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'visChart', 'timePicker', ]); + const xyChartSelector = 'visTypeXyChart'; describe('line charts - split series', function () { - let isNewChartsLibraryEnabled = false; const initLineChart = async function () { log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewAggBasedVisualization(); @@ -39,12 +39,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.selectAggregation('Terms'); log.debug('Field = extension'); await PageObjects.visEditor.selectField('extension.raw'); - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); }; before(async () => { - isNewChartsLibraryEnabled = await PageObjects.visChart.isNewChartsLibraryEnabled(); - await PageObjects.visualize.initTests(isNewChartsLibraryEnabled); + await PageObjects.visualize.initTests(); await initLineChart(); }); @@ -59,7 +58,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // sleep a bit before trying to get the chart data await PageObjects.common.sleep(3000); - const data = await PageObjects.visChart.getLineChartData(); + const data = await PageObjects.visChart.getLineChartData(xyChartSelector); log.debug('data=' + data); const tolerance = 10; // the y-axis scale is 10000 so 10 is 0.1% for (let x = 0; x < data.length; x++) { @@ -90,9 +89,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { log.debug('Order By = Term'); await PageObjects.visEditor.selectOrderByMetric(2, '_key'); - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); await retry.try(async function () { - const data = await PageObjects.visChart.getLineChartData(); + const data = await PageObjects.visChart.getLineChartData(xyChartSelector); log.debug('data=' + data); const tolerance = 10; // the y-axis scale is 10000 so 10 is 0.1% for (let x = 0; x < data.length; x++) { @@ -159,10 +158,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should be able to save and load', async function () { - const vizName = await PageObjects.visChart.getExpectedValue( - 'Visualization Line split series', - 'Visualization Line split series - chart library' - ); + const vizName = 'Visualization Line split series'; await PageObjects.visualize.saveVisualizationExpectSuccessAndBreadcrumb(vizName); @@ -179,10 +175,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.clickYAxisOptions(axisId); await PageObjects.visEditor.selectYAxisScaleType(axisId, 'log'); await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(); - const minLabel = await PageObjects.visChart.getExpectedValue(2, 1); - const maxLabel = await PageObjects.visChart.getExpectedValue(5000, 900); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(xyChartSelector); + const minLabel = 1; + const maxLabel = 900; const numberOfLabels = 10; expect(labels.length).to.be.greaterThan(numberOfLabels); expect(labels[0]).to.eql(minLabel); @@ -191,10 +187,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show filtered ticks on selecting log scale', async () => { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(); - const minLabel = await PageObjects.visChart.getExpectedValue(2, 1); - const maxLabel = await PageObjects.visChart.getExpectedValue(5000, 900); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(xyChartSelector); + const minLabel = 1; + const maxLabel = 900; const numberOfLabels = 10; expect(labels.length).to.be.greaterThan(numberOfLabels); expect(labels[0]).to.eql(minLabel); @@ -204,47 +200,79 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show ticks on selecting square root scale', async () => { await PageObjects.visEditor.selectYAxisScaleType(axisId, 'square root'); await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = await PageObjects.visChart.getExpectedValue( - ['0', '2,000', '4,000', '6,000', '8,000', '10,000'], - ['0', '1,000', '2,000', '3,000', '4,000', '5,000', '6,000', '7,000', '8,000', '9,000'] - ); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabels(xyChartSelector); + const expectedLabels = [ + '0', + '1,000', + '2,000', + '3,000', + '4,000', + '5,000', + '6,000', + '7,000', + '8,000', + '9,000', + ]; expect(labels).to.eql(expectedLabels); }); it('should show filtered ticks on selecting square root scale', async () => { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = await PageObjects.visChart.getExpectedValue( - ['2,000', '4,000', '6,000', '8,000'], - ['0', '1,000', '2,000', '3,000', '4,000', '5,000', '6,000', '7,000', '8,000', '9,000'] - ); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabels(xyChartSelector); + const expectedLabels = [ + '0', + '1,000', + '2,000', + '3,000', + '4,000', + '5,000', + '6,000', + '7,000', + '8,000', + '9,000', + ]; expect(labels).to.eql(expectedLabels); }); it('should show ticks on selecting linear scale', async () => { await PageObjects.visEditor.selectYAxisScaleType(axisId, 'linear'); await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabels(xyChartSelector); log.debug(labels); - const expectedLabels = await PageObjects.visChart.getExpectedValue( - ['0', '2,000', '4,000', '6,000', '8,000', '10,000'], - ['0', '1,000', '2,000', '3,000', '4,000', '5,000', '6,000', '7,000', '8,000', '9,000'] - ); + const expectedLabels = [ + '0', + '1,000', + '2,000', + '3,000', + '4,000', + '5,000', + '6,000', + '7,000', + '8,000', + '9,000', + ]; expect(labels).to.eql(expectedLabels); }); it('should show filtered ticks on selecting linear scale', async () => { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = await PageObjects.visChart.getExpectedValue( - ['2,000', '4,000', '6,000', '8,000'], - ['0', '1,000', '2,000', '3,000', '4,000', '5,000', '6,000', '7,000', '8,000', '9,000'] - ); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabels(xyChartSelector); + const expectedLabels = [ + '0', + '1,000', + '2,000', + '3,000', + '4,000', + '5,000', + '6,000', + '7,000', + '8,000', + '9,000', + ]; expect(labels).to.eql(expectedLabels); }); }); @@ -272,16 +300,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.clickBucket('X-axis'); log.debug('Aggregation = Date Histogram'); await PageObjects.visEditor.selectAggregation('Date Histogram'); - await PageObjects.visEditor.clickGo(); - const title = await PageObjects.visChart.getYAxisTitle(); + await PageObjects.visEditor.clickGo(true); + const title = await PageObjects.visChart.getYAxisTitle(xyChartSelector); expect(title).to.be('Serial Diff of Count'); }); it('should change y-axis label to custom', async () => { log.debug('set custom label of y-axis to "Custom"'); await PageObjects.visEditor.setCustomLabel('Custom', 1); - await PageObjects.visEditor.clickGo(); - const title = await PageObjects.visChart.getYAxisTitle(); + await PageObjects.visEditor.clickGo(true); + const title = await PageObjects.visChart.getYAxisTitle(xyChartSelector); expect(title).to.be('Custom'); }); @@ -295,24 +323,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should apply with selected bucket', async () => { log.debug('Metrics agg = Average Bucket'); await PageObjects.visEditor.selectAggregation('Average Bucket', 'metrics'); - await PageObjects.visEditor.clickGo(); - const title = await PageObjects.visChart.getYAxisTitle(); + await PageObjects.visEditor.clickGo(true); + const title = await PageObjects.visChart.getYAxisTitle(xyChartSelector); expect(title).to.be('Overall Average of Count'); }); it('should change sub metric custom label and calculate y-axis title', async () => { log.debug('set custom label of sub metric to "Cats"'); await PageObjects.visEditor.setCustomLabel('Cats', '1-metric'); - await PageObjects.visEditor.clickGo(); - const title = await PageObjects.visChart.getYAxisTitle(); + await PageObjects.visEditor.clickGo(true); + const title = await PageObjects.visChart.getYAxisTitle(xyChartSelector); expect(title).to.be('Overall Average of Cats'); }); it('should outer custom label', async () => { log.debug('set custom label to "Custom"'); await PageObjects.visEditor.setCustomLabel('Custom', 1); - await PageObjects.visEditor.clickGo(); - const title = await PageObjects.visChart.getYAxisTitle(); + await PageObjects.visEditor.clickGo(true); + const title = await PageObjects.visChart.getYAxisTitle(xyChartSelector); expect(title).to.be('Custom'); }); diff --git a/test/functional/apps/visualize/_point_series_options.ts b/test/functional/apps/visualize/_point_series_options.ts index 08c26b1f3ee95..0d68ea4984ec2 100644 --- a/test/functional/apps/visualize/_point_series_options.ts +++ b/test/functional/apps/visualize/_point_series_options.ts @@ -25,6 +25,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'common', ]); const inspector = getService('inspector'); + const xyChartSelector = 'visTypeXyChart'; async function initChart() { log.debug('navigateToApp visualize'); @@ -57,14 +58,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { log.debug('Average memory value axis - ValueAxis-2'); await PageObjects.visEditor.setSeriesAxis(1, 'ValueAxis-2'); await PageObjects.visChart.waitForVisualizationRenderingStabilized(); - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); } describe('point series', function describeIndexTests() { - let isNewChartsLibraryEnabled = false; before(async () => { - isNewChartsLibraryEnabled = await PageObjects.visChart.isNewChartsLibraryEnabled(); - await PageObjects.visualize.initTests(isNewChartsLibraryEnabled); + await PageObjects.visualize.initTests(); await initChart(); }); @@ -126,7 +125,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ]; await retry.try(async () => { - const data = await PageObjects.visChart.getLineChartData('Count'); + const data = await PageObjects.visChart.getLineChartData(xyChartSelector, 'Count'); log.debug('count data=' + data); log.debug('data.length=' + data.length); expect(data).to.eql(expectedChartValues[0]); @@ -134,8 +133,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.try(async () => { const avgMemoryData = await PageObjects.visChart.getLineChartData( - 'Average machine.ram', - 'ValueAxis-2' + xyChartSelector, + 'Average machine.ram' ); log.debug('average memory data=' + avgMemoryData); log.debug('data.length=' + avgMemoryData.length); @@ -151,7 +150,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should put secondary axis on the right', async function () { - const length = await PageObjects.visChart.getAxesCountByPosition('right'); + const length = await PageObjects.visChart.getAxesCountByPosition('right', xyChartSelector); expect(length).to.be(1); }); }); @@ -159,8 +158,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('multiple chart types', function () { it('should change average series type to histogram', async function () { await PageObjects.visEditor.setSeriesType(1, 'histogram'); - await PageObjects.visEditor.clickGo(); - const length = await PageObjects.visChart.getHistogramSeriesCount(); + await PageObjects.visEditor.clickGo(true); + const length = await PageObjects.visChart.getHistogramSeriesCount(xyChartSelector); expect(length).to.be(1); }); }); @@ -172,8 +171,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show category grid lines', async function () { await PageObjects.visEditor.toggleGridCategoryLines(); - await PageObjects.visEditor.clickGo(); - const gridLines = await PageObjects.visChart.getGridLines(); + await PageObjects.visEditor.clickGo(true); + const gridLines = await PageObjects.visChart.getGridLines(xyChartSelector); // FLAKY relaxing as depends on chart size/browser size and produce differences between local and CI // The objective here is to check whenever the grid lines are rendered, not the exact quantity expect(gridLines.length).to.be.greaterThan(0); @@ -185,8 +184,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show value axis grid lines', async function () { await PageObjects.visEditor.setGridValueAxis('ValueAxis-2'); await PageObjects.visEditor.toggleGridCategoryLines(); - await PageObjects.visEditor.clickGo(); - const gridLines = await PageObjects.visChart.getGridLines(); + await PageObjects.visEditor.clickGo(true); + const gridLines = await PageObjects.visChart.getGridLines(xyChartSelector); // FLAKY relaxing as depends on chart size/browser size and produce differences between local and CI // The objective here is to check whenever the grid lines are rendered, not the exact quantity expect(gridLines.length).to.be.greaterThan(0); @@ -208,22 +207,22 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.selectAggregation('Terms'); log.debug('Field = geo.src'); await PageObjects.visEditor.selectField('geo.src'); - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); log.debug('Open Options tab'); await PageObjects.visEditor.clickOptionsTab(); }); it('should show values on bar chart', async () => { await PageObjects.visEditor.toggleValuesOnChart(); - await PageObjects.visEditor.clickGo(); - const values = await PageObjects.visChart.getChartValues(); + await PageObjects.visEditor.clickGo(true); + const values = await PageObjects.visChart.getChartValues(xyChartSelector); expect(values).to.eql(['2,592', '2,373', '1,194', '489', '415']); }); it('should hide values on bar chart', async () => { await PageObjects.visEditor.toggleValuesOnChart(); - await PageObjects.visEditor.clickGo(); - const values = await PageObjects.visChart.getChartValues(); + await PageObjects.visEditor.clickGo(true); + const values = await PageObjects.visChart.getChartValues(xyChartSelector); expect(values.length).to.be(0); }); }); @@ -237,20 +236,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visualize.clickLineChart(); await PageObjects.visualize.clickNewSearch(); await PageObjects.visEditor.selectYAxisAggregation('Average', 'bytes', customLabel, 1); - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); await PageObjects.visEditor.clickMetricsAndAxes(); await PageObjects.visEditor.clickYAxisOptions('ValueAxis-1'); }); it('should render a custom label when one is set', async function () { - const title = await PageObjects.visChart.getYAxisTitle(); + const title = await PageObjects.visChart.getYAxisTitle(xyChartSelector); expect(title).to.be(customLabel); }); it('should render a custom axis title when one is set, overriding the custom label', async function () { await PageObjects.visEditor.setAxisTitle(axisTitle); - await PageObjects.visEditor.clickGo(); - const title = await PageObjects.visChart.getYAxisTitle(); + await PageObjects.visEditor.clickGo(true); + const title = await PageObjects.visChart.getYAxisTitle(xyChartSelector); expect(title).to.be(axisTitle); }); @@ -262,43 +261,42 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.clickDataTab(); await PageObjects.visEditor.toggleOpenEditor(1); await PageObjects.visEditor.setCustomLabel('test', 1); - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); await PageObjects.visEditor.clickMetricsAndAxes(); await PageObjects.visEditor.clickYAxisOptions('ValueAxis-1'); - const title = await PageObjects.visChart.getYAxisTitle(); + const title = await PageObjects.visChart.getYAxisTitle(xyChartSelector); expect(title).to.be(axisTitle); }); }); describe('timezones', async function () { it('should show round labels in default timezone', async function () { - const expectedLabels = await PageObjects.visChart.getExpectedValue( - ['2015-09-20 00:00', '2015-09-21 00:00', '2015-09-22 00:00'], - ['2015-09-19 12:00', '2015-09-20 12:00', '2015-09-21 12:00', '2015-09-22 12:00'] - ); + const expectedLabels = [ + '2015-09-19 12:00', + '2015-09-20 12:00', + '2015-09-21 12:00', + '2015-09-22 12:00', + ]; await initChart(); - const labels = await PageObjects.visChart.getXAxisLabels(); + const labels = await PageObjects.visChart.getXAxisLabels(xyChartSelector); expect(labels.join()).to.contain(expectedLabels.join()); }); it('should show round labels in different timezone', async function () { - const expectedLabels = await PageObjects.visChart.getExpectedValue( - ['2015-09-20 00:00', '2015-09-21 00:00', '2015-09-22 00:00'], - [ - '2015-09-19 12:00', - '2015-09-20 12:00', - '2015-09-21 12:00', - '2015-09-22 12:00', - '2015-09-23 12:00', - ] - ); + const expectedLabels = [ + '2015-09-19 12:00', + '2015-09-20 12:00', + '2015-09-21 12:00', + '2015-09-22 12:00', + '2015-09-23 12:00', + ]; await kibanaServer.uiSettings.update({ 'dateFormat:tz': 'America/Phoenix' }); await browser.refresh(); await PageObjects.header.awaitKibanaChrome(); await initChart(); - const labels = await PageObjects.visChart.getXAxisLabels(); + const labels = await PageObjects.visChart.getXAxisLabels(xyChartSelector); expect(labels.join()).to.contain(expectedLabels.join()); }); @@ -314,28 +312,25 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'wait for x-axis labels to match expected for Phoenix', 5000, async () => { - const labels = (await PageObjects.visChart.getXAxisLabels()) ?? ''; + const labels = (await PageObjects.visChart.getXAxisLabels(xyChartSelector)) ?? ''; log.debug(`Labels: ${labels}`); - const xLabels = await PageObjects.visChart.getExpectedValue( - ['10:00', '11:00', '12:00', '13:00', '14:00', '15:00'], - [ - '09:30', - '10:00', - '10:30', - '11:00', - '11:30', - '12:00', - '12:30', - '13:00', - '13:30', - '14:00', - '14:30', - '15:00', - '15:30', - '16:00', - ] - ); + const xLabels = [ + '09:30', + '10:00', + '10:30', + '11:00', + '11:30', + '12:00', + '12:30', + '13:00', + '13:30', + '14:00', + '14:30', + '15:00', + '15:30', + '16:00', + ]; return labels.toString() === xLabels.toString(); } ); @@ -375,7 +370,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await browser.refresh(); // wait some time before trying to check for rendering count await PageObjects.header.awaitKibanaChrome(); - await PageObjects.visualize.clickRefresh(); + await PageObjects.visualize.clickRefresh(true); await PageObjects.visChart.waitForRenderingCount(); log.debug('getXAxisLabels'); @@ -383,28 +378,25 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'wait for x-axis labels to match expected for UTC', 5000, async () => { - const labels2 = (await PageObjects.visChart.getXAxisLabels()) ?? ''; + const labels2 = (await PageObjects.visChart.getXAxisLabels(xyChartSelector)) ?? ''; log.debug(`Labels: ${labels2}`); - const xLabels2 = await PageObjects.visChart.getExpectedValue( - ['17:00', '18:00', '19:00', '20:00', '21:00', '22:00'], - [ - '16:30', - '17:00', - '17:30', - '18:00', - '18:30', - '19:00', - '19:30', - '20:00', - '20:30', - '21:00', - '21:30', - '22:00', - '22:30', - '23:00', - ] - ); + const xLabels2 = [ + '16:30', + '17:00', + '17:30', + '18:00', + '18:30', + '19:00', + '19:30', + '20:00', + '20:30', + '21:00', + '21:30', + '22:00', + '22:30', + '23:00', + ]; return labels2.toString() === xLabels2.toString(); } ); diff --git a/test/functional/apps/visualize/_timelion.ts b/test/functional/apps/visualize/_timelion.ts index 589559c717842..a3f2c87424244 100644 --- a/test/functional/apps/visualize/_timelion.ts +++ b/test/functional/apps/visualize/_timelion.ts @@ -20,6 +20,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const elasticChart = getService('elasticChart'); const find = getService('find'); + const timelionChartSelector = 'timelionChart'; describe('Timelion visualization', () => { before(async () => { @@ -35,13 +36,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const initVisualization = async (expression: string, interval: string = '12h') => { await visEditor.setTimelionInterval(interval); await monacoEditor.setCodeEditorValue(expression); - await visEditor.clickGo(); + await visEditor.clickGo(true); }; it('should display correct data for specified index pattern and timefield', async () => { await initVisualization('.es(index=long-window-logstash-*,timefield=@timestamp)'); - const chartData = await visChart.getAreaChartData('q:* > count'); + const chartData = await visChart.getAreaChartData('q:* > count', timelionChartSelector); expect(chartData).to.eql([3, 5, 2, 6, 1, 6, 1, 7, 0, 0]); }); @@ -62,10 +63,22 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { '36h' ); - const firstAreaChartData = await visChart.getAreaChartData('q:* > avg(bytes)'); - const secondAreaChartData = await visChart.getAreaChartData('q:* > min(bytes)'); - const thirdAreaChartData = await visChart.getAreaChartData('q:* > max(bytes)'); - const forthAreaChartData = await visChart.getAreaChartData('q:* > cardinality(bytes)'); + const firstAreaChartData = await visChart.getAreaChartData( + 'q:* > avg(bytes)', + timelionChartSelector + ); + const secondAreaChartData = await visChart.getAreaChartData( + 'q:* > min(bytes)', + timelionChartSelector + ); + const thirdAreaChartData = await visChart.getAreaChartData( + 'q:* > max(bytes)', + timelionChartSelector + ); + const forthAreaChartData = await visChart.getAreaChartData( + 'q:* > cardinality(bytes)', + timelionChartSelector + ); expect(firstAreaChartData).to.eql([5732.783676366217, 5721.775973559419]); expect(secondAreaChartData).to.eql([0, 0]); @@ -84,10 +97,19 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { '.es(*).if(operator=gt,if=200,then=50,else=150).label("condition")' ); - const firstAreaChartData = await visChart.getAreaChartData('initial'); - const secondAreaChartData = await visChart.getAreaChartData('add multiply abs divide'); - const thirdAreaChartData = await visChart.getAreaChartData('query derivative min sum'); - const forthAreaChartData = await visChart.getAreaChartData('condition'); + const firstAreaChartData = await visChart.getAreaChartData('initial', timelionChartSelector); + const secondAreaChartData = await visChart.getAreaChartData( + 'add multiply abs divide', + timelionChartSelector + ); + const thirdAreaChartData = await visChart.getAreaChartData( + 'query derivative min sum', + timelionChartSelector + ); + const forthAreaChartData = await visChart.getAreaChartData( + 'condition', + timelionChartSelector + ); expect(firstAreaChartData).to.eql(firstAreaExpectedChartData); expect(secondAreaChartData).to.eql(firstAreaExpectedChartData); @@ -112,20 +134,23 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { '36h' ); - const leftAxesCount = await visChart.getAxesCountByPosition('left'); - const rightAxesCount = await visChart.getAxesCountByPosition('right'); - const firstAxesLabels = await visChart.getYAxisLabels(); - const secondAxesLabels = await visChart.getYAxisLabels(1); - const thirdAxesLabels = await visChart.getYAxisLabels(2); - const firstAreaChartData = await visChart.getAreaChartData('Average Machine RAM amount'); + const leftAxesCount = await visChart.getAxesCountByPosition('left', timelionChartSelector); + const rightAxesCount = await visChart.getAxesCountByPosition('right', timelionChartSelector); + const firstAxesLabels = await visChart.getYAxisLabels(timelionChartSelector); + const secondAxesLabels = await visChart.getYAxisLabels(timelionChartSelector, 1); + const thirdAxesLabels = await visChart.getYAxisLabels(timelionChartSelector, 2); + const firstAreaChartData = await visChart.getAreaChartData( + 'Average Machine RAM amount', + timelionChartSelector + ); const secondAreaChartData = await visChart.getAreaChartData( 'Average Bytes for request', - undefined, + timelionChartSelector, true ); const thirdAreaChartData = await visChart.getAreaChartData( 'Average Bytes for request with offset', - undefined, + timelionChartSelector, true ); @@ -144,9 +169,18 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('should display correct chart data for split expression', async () => { await initVisualization('.es(index=logstash-*, split=geo.dest:3)', '1 day'); - const firstAreaChartData = await visChart.getAreaChartData('q:* > geo.dest:CN > count'); - const secondAreaChartData = await visChart.getAreaChartData('q:* > geo.dest:IN > count'); - const thirdAreaChartData = await visChart.getAreaChartData('q:* > geo.dest:US > count'); + const firstAreaChartData = await visChart.getAreaChartData( + 'q:* > geo.dest:CN > count', + timelionChartSelector + ); + const secondAreaChartData = await visChart.getAreaChartData( + 'q:* > geo.dest:IN > count', + timelionChartSelector + ); + const thirdAreaChartData = await visChart.getAreaChartData( + 'q:* > geo.dest:US > count', + timelionChartSelector + ); expect(firstAreaChartData).to.eql([0, 905, 910, 850, 0]); expect(secondAreaChartData).to.eql([0, 763, 699, 825, 0]); @@ -156,8 +190,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('should display two areas and one bar chart items', async () => { await initVisualization('.es(*), .es(*), .es(*).bars(stack=true)'); - const areasChartsCount = await visChart.getAreaSeriesCount(); - const barsChartsCount = await visChart.getHistogramSeriesCount(); + const areasChartsCount = await visChart.getAreaSeriesCount(timelionChartSelector); + const barsChartsCount = await visChart.getHistogramSeriesCount(timelionChartSelector); expect(areasChartsCount).to.be(2); expect(barsChartsCount).to.be(1); @@ -167,7 +201,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('should correctly display the legend items names and position', async () => { await initVisualization('.es(*).label("first series"), .es(*).label("second series")'); - const legendNames = await visChart.getLegendEntries(); + const legendNames = await visChart.getLegendEntriesXYCharts(timelionChartSelector); const legendElement = await find.byClassName('echLegend'); const isLegendTopPositioned = await legendElement.elementHasClass('echLegend--top'); const isLegendLeftPositioned = await legendElement.elementHasClass('echLegend--left'); diff --git a/test/functional/apps/visualize/_vertical_bar_chart.ts b/test/functional/apps/visualize/_vertical_bar_chart.ts index a728757a485e1..93022b5d2f0e8 100644 --- a/test/functional/apps/visualize/_vertical_bar_chart.ts +++ b/test/functional/apps/visualize/_vertical_bar_chart.ts @@ -18,11 +18,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const filterBar = getService('filterBar'); const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']); + const xyChartSelector = 'visTypeXyChart'; + describe('vertical bar chart', function () { - let isNewChartsLibraryEnabled = false; before(async () => { - isNewChartsLibraryEnabled = await PageObjects.visChart.isNewChartsLibraryEnabled(); - await PageObjects.visualize.initTests(isNewChartsLibraryEnabled); + await PageObjects.visualize.initTests(); }); const vizName1 = 'Visualization VerticalBarChart'; @@ -41,21 +41,21 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { log.debug('Field = @timestamp'); await PageObjects.visEditor.selectField('@timestamp'); // leaving Interval set to Auto - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); }; describe('bar charts x axis tick labels', () => { it('should show tick labels also after rotation of the chart', async function () { await initBarChart(); - const bottomLabels = await PageObjects.visChart.getXAxisLabels(); + const bottomLabels = await PageObjects.visChart.getXAxisLabels(xyChartSelector); log.debug(`${bottomLabels.length} tick labels on bottom x axis`); await PageObjects.visEditor.clickMetricsAndAxes(); await PageObjects.visEditor.selectXAxisPosition('left'); - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); // the getYAxisLabels helper always returns the labels on the left axis - const leftLabels = await PageObjects.visChart.getYAxisLabels(); + const leftLabels = await PageObjects.visChart.getYAxisLabels(xyChartSelector); log.debug(`${leftLabels.length} tick labels on left x axis`); expect(leftLabels.length).to.be.greaterThan(bottomLabels.length * (2 / 3)); }); @@ -69,16 +69,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.selectAggregation('Date Range'); await PageObjects.visEditor.selectField('@timestamp'); - await PageObjects.visEditor.clickGo(); - const bottomLabels = await PageObjects.visChart.getXAxisLabels(); + await PageObjects.visEditor.clickGo(true); + const bottomLabels = await PageObjects.visChart.getXAxisLabels(xyChartSelector); expect(bottomLabels.length).to.be(1); await PageObjects.visEditor.clickMetricsAndAxes(); await PageObjects.visEditor.selectXAxisPosition('left'); - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); // the getYAxisLabels helper always returns the labels on the left axis - const leftLabels = await PageObjects.visChart.getYAxisLabels(); + const leftLabels = await PageObjects.visChart.getYAxisLabels(xyChartSelector); expect(leftLabels.length).to.be(1); }); }); @@ -96,8 +96,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.selectField('@timestamp'); await PageObjects.visEditor.clickAddDateRange(); await PageObjects.visEditor.setDateRangeByIndex('1', 'now-2w/w', 'now-1w/w'); - await PageObjects.visEditor.clickGo(); - const bottomLabels = await PageObjects.visChart.getXAxisLabels(); + await PageObjects.visEditor.clickGo(true); + const bottomLabels = await PageObjects.visChart.getXAxisLabels(xyChartSelector); expect(bottomLabels.length).to.be(2); }); }); @@ -146,7 +146,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // return arguments[0].getAttribute(arguments[1]);","args":[{"ELEMENT":"592"},"fill"]}] arguments[0].getAttribute is not a function // try sleeping a bit before getting that data await retry.try(async () => { - const data = await PageObjects.visChart.getBarChartData(); + const data = await PageObjects.visChart.getBarChartData(xyChartSelector); log.debug('data=' + data); log.debug('data.length=' + data.length); expect(data).to.eql(expectedChartValues); @@ -257,7 +257,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // return arguments[0].getAttribute(arguments[1]);","args":[{"ELEMENT":"592"},"fill"]}] arguments[0].getAttribute is not a function // try sleeping a bit before getting that data await retry.try(async () => { - const data = await PageObjects.visChart.getBarChartData(); + const data = await PageObjects.visChart.getBarChartData(xyChartSelector); log.debug('data=' + data); log.debug('data.length=' + data.length); expect(data).to.eql(expectedChartValues); @@ -265,7 +265,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.toggleOpenEditor(2); await PageObjects.visEditor.clickDropPartialBuckets(); - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); expectedChartValues = [ 218, @@ -333,7 +333,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // return arguments[0].getAttribute(arguments[1]);","args":[{"ELEMENT":"592"},"fill"]}] arguments[0].getAttribute is not a function // try sleeping a bit before getting that data await retry.try(async () => { - const data = await PageObjects.visChart.getBarChartData(); + const data = await PageObjects.visChart.getBarChartData(xyChartSelector); log.debug('data=' + data); log.debug('data.length=' + data.length); expect(data).to.eql(expectedChartValues); @@ -349,11 +349,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.clickYAxisOptions(axisId); await PageObjects.visEditor.selectYAxisScaleType(axisId, 'log'); await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(xyChartSelector); - const minLabel = await PageObjects.visChart.getExpectedValue(2, 1); - const maxLabel = await PageObjects.visChart.getExpectedValue(5000, 900); + const minLabel = 1; + const maxLabel = 900; const numberOfLabels = 10; expect(labels.length).to.be.greaterThan(numberOfLabels); expect(labels[0]).to.eql(minLabel); @@ -362,11 +362,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show filtered ticks on selecting log scale', async () => { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(xyChartSelector); - const minLabel = await PageObjects.visChart.getExpectedValue(2, 1); - const maxLabel = await PageObjects.visChart.getExpectedValue(5000, 900); + const minLabel = 1; + const maxLabel = 900; const numberOfLabels = 10; expect(labels.length).to.be.greaterThan(numberOfLabels); expect(labels[0]).to.eql(minLabel); @@ -376,47 +376,35 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show ticks on selecting square root scale', async () => { await PageObjects.visEditor.selectYAxisScaleType(axisId, 'square root'); await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = await PageObjects.visChart.getExpectedValue( - ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400', '1,600'], - ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400'] - ); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabels(xyChartSelector); + const expectedLabels = ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400']; expect(labels).to.eql(expectedLabels); }); it('should show filtered ticks on selecting square root scale', async () => { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = await PageObjects.visChart.getExpectedValue( - ['200', '400', '600', '800', '1,000', '1,200', '1,400'], - ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400'] - ); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabels(xyChartSelector); + const expectedLabels = ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400']; expect(labels).to.eql(expectedLabels); }); it('should show ticks on selecting linear scale', async () => { await PageObjects.visEditor.selectYAxisScaleType(axisId, 'linear'); await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabels(xyChartSelector); log.debug(labels); - const expectedLabels = await PageObjects.visChart.getExpectedValue( - ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400', '1,600'], - ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400'] - ); + const expectedLabels = ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400']; expect(labels).to.eql(expectedLabels); }); it('should show filtered ticks on selecting linear scale', async () => { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = await PageObjects.visChart.getExpectedValue( - ['200', '400', '600', '800', '1,000', '1,200', '1,400'], - ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400'] - ); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabels(xyChartSelector); + const expectedLabels = ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400']; expect(labels).to.eql(expectedLabels); }); }); @@ -429,8 +417,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.selectYAxisMode('percentage'); await PageObjects.visEditor.changeYAxisShowCheckbox(axisId, true); await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabels(xyChartSelector); expect(labels[0]).to.eql('0%'); expect(labels[labels.length - 1]).to.eql('100%'); }); @@ -445,33 +433,27 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.selectAggregation('Terms'); await PageObjects.visEditor.selectField('response.raw'); await PageObjects.visChart.waitForVisualizationRenderingStabilized(); - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); - const expectedEntries = await PageObjects.visChart.getExpectedValue( - ['200', '404', '503'], - ['503', '404', '200'] // sorting aligned with rendered geometries - ); - const legendEntries = await PageObjects.visChart.getLegendEntries(); + const expectedEntries = ['503', '404', '200']; // sorting aligned with rendered geometries + const legendEntries = await PageObjects.visChart.getLegendEntriesXYCharts(xyChartSelector); expect(legendEntries).to.eql(expectedEntries); }); it('should allow custom sorting of series', async () => { await PageObjects.visEditor.toggleOpenEditor(1, 'false'); await PageObjects.visEditor.selectCustomSortMetric(3, 'Min', 'bytes'); - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); - const expectedEntries = await PageObjects.visChart.getExpectedValue( - ['404', '200', '503'], - ['503', '200', '404'] // sorting aligned with rendered geometries - ); - const legendEntries = await PageObjects.visChart.getLegendEntries(); + const expectedEntries = ['503', '200', '404']; + const legendEntries = await PageObjects.visChart.getLegendEntriesXYCharts(xyChartSelector); expect(legendEntries).to.eql(expectedEntries); }); it('should correctly filter by legend', async () => { - await PageObjects.visChart.filterLegend('200'); + await PageObjects.visChart.filterLegend('200', true); await PageObjects.visChart.waitForVisualization(); - const legendEntries = await PageObjects.visChart.getLegendEntries(); + const legendEntries = await PageObjects.visChart.getLegendEntriesXYCharts(xyChartSelector); const expectedEntries = ['200']; expect(legendEntries).to.eql(expectedEntries); await filterBar.removeFilter('response.raw'); @@ -494,45 +476,26 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.selectAggregation('Terms'); await PageObjects.visEditor.selectField('machine.os'); await PageObjects.visChart.waitForVisualizationRenderingStabilized(); - await PageObjects.visEditor.clickGo(); - - const expectedEntries = await PageObjects.visChart.getExpectedValue( - [ - '200 - win 8', - '200 - win xp', - '200 - ios', - '200 - osx', - '200 - win 7', - '404 - ios', - '503 - ios', - '503 - osx', - '503 - win 7', - '503 - win 8', - '503 - win xp', - '404 - osx', - '404 - win 7', - '404 - win 8', - '404 - win xp', - ], - [ - '404 - win xp', - '404 - win 8', - '404 - win 7', - '404 - osx', - '503 - win xp', - '503 - win 8', - '503 - win 7', - '503 - osx', - '503 - ios', - '404 - ios', - '200 - win 7', - '200 - osx', - '200 - ios', - '200 - win xp', - '200 - win 8', - ] - ); - const legendEntries = await PageObjects.visChart.getLegendEntries(); + await PageObjects.visEditor.clickGo(true); + + const expectedEntries = [ + '404 - win xp', + '404 - win 8', + '404 - win 7', + '404 - osx', + '503 - win xp', + '503 - win 8', + '503 - win 7', + '503 - osx', + '503 - ios', + '404 - ios', + '200 - win 7', + '200 - osx', + '200 - ios', + '200 - win xp', + '200 - win 8', + ]; + const legendEntries = await PageObjects.visChart.getLegendEntriesXYCharts(xyChartSelector); expect(legendEntries).to.eql(expectedEntries); }); @@ -540,13 +503,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // this will avoid issues with the play tooltip covering the disable agg button await testSubjects.scrollIntoView('metricsAggGroup'); await PageObjects.visEditor.toggleDisabledAgg(3); - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); - const expectedEntries = await PageObjects.visChart.getExpectedValue( - ['win 8', 'win xp', 'ios', 'osx', 'win 7'], - ['win 7', 'osx', 'ios', 'win xp', 'win 8'] - ); - const legendEntries = await PageObjects.visChart.getLegendEntries(); + const expectedEntries = ['win 7', 'osx', 'ios', 'win xp', 'win 8']; + const legendEntries = await PageObjects.visChart.getLegendEntriesXYCharts(xyChartSelector); expect(legendEntries).to.eql(expectedEntries); }); }); @@ -559,10 +519,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.toggleOpenEditor(1); await PageObjects.visEditor.selectAggregation('Derivative', 'metrics'); await PageObjects.visChart.waitForVisualizationRenderingStabilized(); - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); const expectedEntries = ['Derivative of Count']; - const legendEntries = await PageObjects.visChart.getLegendEntries(); + const legendEntries = await PageObjects.visChart.getLegendEntriesXYCharts(xyChartSelector); expect(legendEntries).to.eql(expectedEntries); }); diff --git a/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.ts b/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.ts index 97817315b5801..e9f39a45d7892 100644 --- a/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.ts +++ b/test/functional/apps/visualize/_vertical_bar_chart_nontimeindex.ts @@ -16,9 +16,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const inspector = getService('inspector'); const PageObjects = getPageObjects(['common', 'visualize', 'header', 'visEditor', 'visChart']); + const xyChartSelector = 'visTypeXyChart'; + describe('vertical bar chart with index without time filter', function () { const vizName1 = 'Visualization VerticalBarChart without time filter'; - let isNewChartsLibraryEnabled = false; const initBarChart = async () => { log.debug('navigateToApp visualize'); @@ -37,12 +38,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.selectField('@timestamp'); await PageObjects.visEditor.setInterval('3h', { type: 'custom' }); await PageObjects.visChart.waitForVisualizationRenderingStabilized(); - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); }; before(async () => { - isNewChartsLibraryEnabled = await PageObjects.visChart.isNewChartsLibraryEnabled(); - await PageObjects.visualize.initTests(isNewChartsLibraryEnabled); + await PageObjects.visualize.initTests(); await initBarChart(); }); @@ -89,7 +89,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // return arguments[0].getAttribute(arguments[1]);","args":[{"ELEMENT":"592"},"fill"]}] arguments[0].getAttribute is not a function // try sleeping a bit before getting that data await retry.try(async () => { - const data = await PageObjects.visChart.getBarChartData(); + const data = await PageObjects.visChart.getBarChartData(xyChartSelector); log.debug('data=' + data); log.debug('data.length=' + data.length); expect(data).to.eql(expectedChartValues); @@ -134,10 +134,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.clickYAxisOptions(axisId); await PageObjects.visEditor.selectYAxisScaleType(axisId, 'log'); await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(); - const minLabel = await PageObjects.visChart.getExpectedValue(2, 1); - const maxLabel = await PageObjects.visChart.getExpectedValue(5000, 900); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(xyChartSelector); + const minLabel = 1; + const maxLabel = 900; const numberOfLabels = 10; expect(labels.length).to.be.greaterThan(numberOfLabels); expect(labels[0]).to.eql(minLabel); @@ -146,10 +146,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show filtered ticks on selecting log scale', async () => { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(); - const minLabel = await PageObjects.visChart.getExpectedValue(2, 1); - const maxLabel = await PageObjects.visChart.getExpectedValue(5000, 900); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(xyChartSelector); + const minLabel = 1; + const maxLabel = 900; const numberOfLabels = 10; expect(labels.length).to.be.greaterThan(numberOfLabels); expect(labels[0]).to.eql(minLabel); @@ -159,47 +159,35 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show ticks on selecting square root scale', async () => { await PageObjects.visEditor.selectYAxisScaleType(axisId, 'square root'); await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = await PageObjects.visChart.getExpectedValue( - ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400', '1,600'], - ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400'] - ); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabels(xyChartSelector); + const expectedLabels = ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400']; expect(labels).to.eql(expectedLabels); }); it('should show filtered ticks on selecting square root scale', async () => { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = await PageObjects.visChart.getExpectedValue( - ['200', '400', '600', '800', '1,000', '1,200', '1,400'], - ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400'] - ); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabels(xyChartSelector); + const expectedLabels = ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400']; expect(labels).to.eql(expectedLabels); }); it('should show ticks on selecting linear scale', async () => { await PageObjects.visEditor.selectYAxisScaleType(axisId, 'linear'); await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabels(xyChartSelector); log.debug(labels); - const expectedLabels = await PageObjects.visChart.getExpectedValue( - ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400', '1,600'], - ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400'] - ); + const expectedLabels = ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400']; expect(labels).to.eql(expectedLabels); }); it('should show filtered ticks on selecting linear scale', async () => { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); - await PageObjects.visEditor.clickGo(); - const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = await PageObjects.visChart.getExpectedValue( - ['200', '400', '600', '800', '1,000', '1,200', '1,400'], - ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400'] - ); + await PageObjects.visEditor.clickGo(true); + const labels = await PageObjects.visChart.getYAxisLabels(xyChartSelector); + const expectedLabels = ['0', '200', '400', '600', '800', '1,000', '1,200', '1,400']; expect(labels).to.eql(expectedLabels); }); }); @@ -215,15 +203,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.common.sleep(1003); - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); await PageObjects.header.waitUntilLoadingHasFinished(); - const expectedEntries = await PageObjects.visChart.getExpectedValue( - ['200', '404', '503'], - ['503', '404', '200'] // sorting aligned with rendered geometries - ); + const expectedEntries = ['503', '404', '200']; // sorting aligned with rendered geometries - const legendEntries = await PageObjects.visChart.getLegendEntries(); + const legendEntries = await PageObjects.visChart.getLegendEntriesXYCharts(xyChartSelector); expect(legendEntries).to.eql(expectedEntries); }); }); @@ -245,59 +230,37 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.common.sleep(1003); - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); await PageObjects.header.waitUntilLoadingHasFinished(); - const expectedEntries = await PageObjects.visChart.getExpectedValue( - [ - '200 - win 8', - '200 - win xp', - '200 - ios', - '200 - osx', - '200 - win 7', - '404 - ios', - '503 - ios', - '503 - osx', - '503 - win 7', - '503 - win 8', - '503 - win xp', - '404 - osx', - '404 - win 7', - '404 - win 8', - '404 - win xp', - ], - [ - '404 - win xp', - '404 - win 8', - '404 - win 7', - '404 - osx', - '503 - win xp', - '503 - win 8', - '503 - win 7', - '503 - osx', - '503 - ios', - '404 - ios', - '200 - win 7', - '200 - osx', - '200 - ios', - '200 - win xp', - '200 - win 8', - ] - ); - const legendEntries = await PageObjects.visChart.getLegendEntries(); + const expectedEntries = [ + '404 - win xp', + '404 - win 8', + '404 - win 7', + '404 - osx', + '503 - win xp', + '503 - win 8', + '503 - win 7', + '503 - osx', + '503 - ios', + '404 - ios', + '200 - win 7', + '200 - osx', + '200 - ios', + '200 - win xp', + '200 - win 8', + ]; + const legendEntries = await PageObjects.visChart.getLegendEntriesXYCharts(xyChartSelector); expect(legendEntries).to.eql(expectedEntries); }); it('should show correct series when disabling first agg', async function () { await PageObjects.visEditor.toggleDisabledAgg(3); - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); await PageObjects.header.waitUntilLoadingHasFinished(); - const expectedEntries = await PageObjects.visChart.getExpectedValue( - ['win 8', 'win xp', 'ios', 'osx', 'win 7'], - ['win 7', 'osx', 'ios', 'win xp', 'win 8'] - ); - const legendEntries = await PageObjects.visChart.getLegendEntries(); + const expectedEntries = ['win 7', 'osx', 'ios', 'win xp', 'win 8']; + const legendEntries = await PageObjects.visChart.getLegendEntriesXYCharts(xyChartSelector); expect(legendEntries).to.eql(expectedEntries); }); }); @@ -312,11 +275,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.common.sleep(1003); - await PageObjects.visEditor.clickGo(); + await PageObjects.visEditor.clickGo(true); await PageObjects.header.waitUntilLoadingHasFinished(); const expectedEntries = ['Derivative of Count']; - const legendEntries = await PageObjects.visChart.getLegendEntries(); + const legendEntries = await PageObjects.visChart.getLegendEntriesXYCharts(xyChartSelector); expect(legendEntries).to.eql(expectedEntries); }); }); diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts index 4af871bd9347d..bb5357fc456cb 100644 --- a/test/functional/apps/visualize/index.ts +++ b/test/functional/apps/visualize/index.ts @@ -30,7 +30,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { before(async () => { await kibanaServer.uiSettings.update({ - 'visualization:visualize:legacyChartsLibrary': false, 'visualization:visualize:legacyPieChartsLibrary': false, }); await browser.refresh(); @@ -38,7 +37,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { after(async () => { await kibanaServer.uiSettings.update({ - 'visualization:visualize:legacyChartsLibrary': true, 'visualization:visualize:legacyPieChartsLibrary': true, }); await browser.refresh(); @@ -59,7 +57,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { this.tags('ciGroup9'); loadTestFile(require.resolve('./_embedding_chart')); - loadTestFile(require.resolve('./_area_chart')); loadTestFile(require.resolve('./_data_table')); loadTestFile(require.resolve('./_data_table_nontimeindex')); loadTestFile(require.resolve('./_data_table_notimeindex_filters')); @@ -81,10 +78,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { describe('visualize ciGroup4', function () { this.tags('ciGroup4'); - loadTestFile(require.resolve('./_line_chart_split_series')); - loadTestFile(require.resolve('./_line_chart_split_chart')); loadTestFile(require.resolve('./_pie_chart')); - loadTestFile(require.resolve('./_point_series_options')); loadTestFile(require.resolve('./_markdown_vis')); loadTestFile(require.resolve('./_shared_item')); loadTestFile(require.resolve('./_lab_mode')); @@ -99,8 +93,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { this.tags('ciGroup12'); loadTestFile(require.resolve('./_tag_cloud')); - loadTestFile(require.resolve('./_vertical_bar_chart')); - loadTestFile(require.resolve('./_vertical_bar_chart_nontimeindex')); loadTestFile(require.resolve('./_tsvb_chart')); loadTestFile(require.resolve('./_tsvb_time_series')); loadTestFile(require.resolve('./_tsvb_markdown')); diff --git a/test/functional/config.js b/test/functional/config.js index 5d8a65d44a183..643f012205aea 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -57,7 +57,6 @@ export default async function ({ readConfigFile }) { defaults: { 'accessibility:disableAnimations': true, 'dateFormat:tz': 'UTC', - 'visualization:visualize:legacyChartsLibrary': true, 'visualization:visualize:legacyPieChartsLibrary': true, }, }, diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index 591cddd18a2b3..c96faab2dc321 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -30,7 +30,6 @@ export class VisualBuilderPageObject extends FtrService { private readonly testSubjects = this.ctx.getService('testSubjects'); private readonly comboBox = this.ctx.getService('comboBox'); private readonly elasticChart = this.ctx.getService('elasticChart'); - private readonly kibanaServer = this.ctx.getService('kibanaServer'); private readonly common = this.ctx.getPageObject('common'); private readonly header = this.ctx.getPageObject('header'); private readonly timePicker = this.ctx.getPageObject('timePicker'); @@ -843,9 +842,6 @@ export class VisualBuilderPageObject extends FtrService { } public async toggleNewChartsLibraryWithDebug(enabled: boolean) { - await this.kibanaServer.uiSettings.update({ - 'visualization:visualize:legacyChartsLibrary': !enabled, - }); await this.elasticChart.setNewChartUiDebugFlag(enabled); } diff --git a/test/functional/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts index bcee77a21c0b0..c1056b58e22d4 100644 --- a/test/functional/page_objects/visualize_chart_page.ts +++ b/test/functional/page_objects/visualize_chart_page.ts @@ -11,7 +11,6 @@ import Color from 'color'; import { FtrService } from '../ftr_provider_context'; -const xyChartSelector = 'visTypeXyChart'; const pieChartSelector = 'visTypePieChart'; export class VisualizeChartPageObject extends FtrService { @@ -37,8 +36,7 @@ export class VisualizeChartPageObject extends FtrService { public async isNewChartsLibraryEnabled(): Promise { const legacyChartsLibrary = Boolean( - (await this.kibanaServer.uiSettings.get('visualization:visualize:legacyChartsLibrary')) && - (await this.kibanaServer.uiSettings.get('visualization:visualize:legacyPieChartsLibrary')) + await this.kibanaServer.uiSettings.get('visualization:visualize:legacyPieChartsLibrary') ) ?? true; const enabled = !legacyChartsLibrary; this.log.debug(`-- isNewChartsLibraryEnabled = ${enabled}`); @@ -78,143 +76,52 @@ export class VisualizeChartPageObject extends FtrService { return true; } - /** - * Helper method to get expected values that are slightly different - * between vislib and elastic-chart inplementations - * @param vislibValue value expected for vislib chart - * @param elasticChartsValue value expected for `@elastic/charts` chart - */ - public async getExpectedValue(vislibValue: T, elasticChartsValue: T): Promise { - if (await this.isNewLibraryChart(xyChartSelector)) { - return elasticChartsValue; - } - - return vislibValue; - } - - public async getYAxisTitle() { - if (await this.isNewLibraryChart(xyChartSelector)) { - const xAxis = (await this.getEsChartDebugState(xyChartSelector))?.axes?.y ?? []; - return xAxis[0]?.title; - } - - const title = await this.find.byCssSelector('.y-axis-div .y-axis-title text'); - return await title.getVisibleText(); + public async getYAxisTitle(selector: string) { + const xAxis = (await this.getEsChartDebugState(selector))?.axes?.y ?? []; + return xAxis[0]?.title; } - public async getXAxisLabels() { - if (await this.isNewLibraryChart(xyChartSelector)) { - const [xAxis] = (await this.getEsChartDebugState(xyChartSelector))?.axes?.x ?? []; - return xAxis?.labels; - } - - const xAxis = await this.find.byCssSelector('.visAxis--x.visAxis__column--bottom'); - const $ = await xAxis.parseDomContent(); - return $('.x > g > text') - .toArray() - .map((tick) => $(tick).text().trim()); + public async getXAxisLabels(selector: string) { + const [xAxis] = (await this.getEsChartDebugState(selector))?.axes?.x ?? []; + return xAxis?.labels; } - public async getYAxisLabels(nth = 0) { - if (await this.isNewLibraryChart(xyChartSelector)) { - const yAxis = (await this.getEsChartDebugState(xyChartSelector))?.axes?.y ?? []; - return yAxis[nth]?.labels; - } - - const yAxis = await this.find.byCssSelector('.visAxis__column--y.visAxis__column--left'); - const $ = await yAxis.parseDomContent(); - return $('.y > g > text') - .toArray() - .map((tick) => $(tick).text().trim()); + public async getYAxisLabels(selector: string, nth = 0) { + const yAxis = (await this.getEsChartDebugState(selector))?.axes?.y ?? []; + return yAxis[nth]?.labels; } - public async getYAxisLabelsAsNumbers() { - if (await this.isNewLibraryChart(xyChartSelector)) { - const [yAxis] = (await this.getEsChartDebugState(xyChartSelector))?.axes?.y ?? []; - return yAxis?.values; - } - - return (await this.getYAxisLabels()).map((label) => Number(label.replace(',', ''))); + public async getYAxisLabelsAsNumbers(selector: string) { + const [yAxis] = (await this.getEsChartDebugState(selector))?.axes?.y ?? []; + return yAxis?.values; } /** * Gets the chart data and scales it based on chart height and label. * @param dataLabel data-label value - * @param axis axis value, 'ValueAxis-1' by default + * @param selector chart selector * @param shouldContainXAxisData boolean value for mapping points, false by default * * Returns an array of height values */ public async getAreaChartData( dataLabel: string, - axis = 'ValueAxis-1', + selector: string, shouldContainXAxisData = false ) { - if (await this.isNewLibraryChart(xyChartSelector)) { - const areas = (await this.getEsChartDebugState(xyChartSelector))?.areas ?? []; - const points = areas.find(({ name }) => name === dataLabel)?.lines.y1.points ?? []; - return shouldContainXAxisData ? points.map(({ x, y }) => [x, y]) : points.map(({ y }) => y); - } - - const yAxisRatio = await this.getChartYAxisRatio(axis); - - const rectangle = await this.find.byCssSelector('rect.background'); - const yAxisHeight = Number(await rectangle.getAttribute('height')); - this.log.debug(`height --------- ${yAxisHeight}`); - - const path = await this.retry.try( - async () => - await this.find.byCssSelector( - `path[data-label="${dataLabel}"]`, - this.defaultFindTimeout * 2 - ) - ); - const data = await path.getAttribute('d'); - this.log.debug(data); - // This area chart data starts with a 'M'ove to a x,y location, followed - // by a bunch of 'L'ines from that point to the next. Those points are - // the values we're going to use to calculate the data values we're testing. - // So git rid of the one 'M' and split the rest on the 'L's. - const tempArray = data - .replace('M ', '') - .replace('M', '') - .replace(/ L /g, 'L') - .replace(/ /g, ',') - .split('L'); - const chartSections = tempArray.length / 2; - const chartData = []; - for (let i = 0; i < chartSections; i++) { - chartData[i] = Math.round((yAxisHeight - Number(tempArray[i].split(',')[1])) * yAxisRatio); - this.log.debug('chartData[i] =' + chartData[i]); - } - return chartData; + const areas = (await this.getEsChartDebugState(selector))?.areas ?? []; + const points = areas.find(({ name }) => name === dataLabel)?.lines.y1.points ?? []; + return shouldContainXAxisData ? points.map(({ x, y }) => [x, y]) : points.map(({ y }) => y); } /** * Returns the paths that compose an area chart. * @param dataLabel data-label value */ - public async getAreaChartPaths(dataLabel: string) { - if (await this.isNewLibraryChart(xyChartSelector)) { - const areas = (await this.getEsChartDebugState(xyChartSelector))?.areas ?? []; - const path = areas.find(({ name }) => name === dataLabel)?.path ?? ''; - return path.split('L'); - } - - const path = await this.retry.try( - async () => - await this.find.byCssSelector( - `path[data-label="${dataLabel}"]`, - this.defaultFindTimeout * 2 - ) - ); - const data = await path.getAttribute('d'); - this.log.debug(data); - // This area chart data starts with a 'M'ove to a x,y location, followed - // by a bunch of 'L'ines from that point to the next. Those points are - // the values we're going to use to calculate the data values we're testing. - // So git rid of the one 'M' and split the rest on the 'L's. - return data.split('L'); + public async getAreaChartPaths(dataLabel: string, selector: string) { + const areas = (await this.getEsChartDebugState(selector))?.areas ?? []; + const path = areas.find(({ name }) => name === dataLabel)?.path ?? ''; + return path.split('L'); } /** @@ -222,106 +129,38 @@ export class VisualizeChartPageObject extends FtrService { * @param dataLabel data-label value * @param axis axis value, 'ValueAxis-1' by default */ - public async getLineChartData(dataLabel = 'Count', axis = 'ValueAxis-1') { - if (await this.isNewLibraryChart(xyChartSelector)) { - // For now lines are rendered as areas to enable stacking - const areas = (await this.getEsChartDebugState(xyChartSelector))?.areas ?? []; - const lines = areas.map(({ lines: { y1 }, name, color }) => ({ ...y1, name, color })); - const points = lines.find(({ name }) => name === dataLabel)?.points ?? []; - return points.map(({ y }) => y); - } - - // 1). get the range/pixel ratio - const yAxisRatio = await this.getChartYAxisRatio(axis); - // 2). find and save the y-axis pixel size (the chart height) - const rectangle = await this.find.byCssSelector('clipPath rect'); - const yAxisHeight = Number(await rectangle.getAttribute('height')); - // 3). get the visWrapper__chart elements - const chartTypes = await this.retry.try( - async () => - await this.find.allByCssSelector( - `.visWrapper__chart circle[data-label="${dataLabel}"][fill-opacity="1"]`, - this.defaultFindTimeout * 2 - ) - ); - // 4). for each chart element, find the green circle, then the cy position - const chartData = await Promise.all( - chartTypes.map(async (chart) => { - const cy = Number(await chart.getAttribute('cy')); - // the point_series_options test has data in the billions range and - // getting 11 digits of precision with these calculations is very hard - return Math.round(Number(((yAxisHeight - cy) * yAxisRatio).toPrecision(6))); - }) - ); - - return chartData; + public async getLineChartData(selector: string, dataLabel = 'Count') { + // For now lines are rendered as areas to enable stacking + const areas = (await this.getEsChartDebugState(selector))?.areas ?? []; + const lines = areas.map(({ lines: { y1 }, name, color }) => ({ ...y1, name, color })); + const points = lines.find(({ name }) => name === dataLabel)?.points ?? []; + return points.map(({ y }) => y); } /** * Returns bar chart data in pixels * @param dataLabel data-label value - * @param axis axis value, 'ValueAxis-1' by default */ - public async getBarChartData(dataLabel = 'Count', axis = 'ValueAxis-1') { - if (await this.isNewLibraryChart(xyChartSelector)) { - const bars = (await this.getEsChartDebugState(xyChartSelector))?.bars ?? []; - const values = bars.find(({ name }) => name === dataLabel)?.bars ?? []; - return values.map(({ y }) => y); - } - - const yAxisRatio = await this.getChartYAxisRatio(axis); - const svg = await this.find.byCssSelector('div.chart'); - const $ = await svg.parseDomContent(); - const chartData = $(`g > g.series > rect[data-label="${dataLabel}"]`) - .toArray() - .map((chart) => { - const barHeight = Number($(chart).attr('height')); - return Math.round(barHeight * yAxisRatio); - }); - - return chartData; + public async getBarChartData(selector: string, dataLabel = 'Count') { + const bars = (await this.getEsChartDebugState(selector))?.bars ?? []; + const values = bars.find(({ name }) => name === dataLabel)?.bars ?? []; + return values.map(({ y }) => y); } - /** - * Returns the range/pixel ratio - * @param axis axis value, 'ValueAxis-1' by default - */ - private async getChartYAxisRatio(axis = 'ValueAxis-1') { - // 1). get the maximum chart Y-Axis marker value and Y position - const maxYAxisChartMarker = await this.retry.try( - async () => - await this.find.byCssSelector( - `div.visAxis__splitAxes--y > div > svg > g.${axis} > g:last-of-type.tick` - ) - ); - const maxYLabel = (await maxYAxisChartMarker.getVisibleText()).replace(/,/g, ''); - const maxYLabelYPosition = (await maxYAxisChartMarker.getPosition()).y; - this.log.debug(`maxYLabel = ${maxYLabel}, maxYLabelYPosition = ${maxYLabelYPosition}`); - - // 2). get the minimum chart Y-Axis marker value and Y position - const minYAxisChartMarker = await this.find.byCssSelector( - 'div.visAxis__column--y.visAxis__column--left > div > div > svg:nth-child(2) > g > g:nth-child(1).tick' - ); - const minYLabel = (await minYAxisChartMarker.getVisibleText()).replace(',', ''); - const minYLabelYPosition = (await minYAxisChartMarker.getPosition()).y; - return (Number(maxYLabel) - Number(minYLabel)) / (minYLabelYPosition - maxYLabelYPosition); - } - - public async toggleLegend(show = true) { - const isVisTypeXYChart = await this.isNewLibraryChart(xyChartSelector); + private async toggleLegend(force = false) { const isVisTypePieChart = await this.isNewLibraryChart(pieChartSelector); - const legendSelector = isVisTypeXYChart || isVisTypePieChart ? '.echLegend' : '.visLegend'; + const legendSelector = force || isVisTypePieChart ? '.echLegend' : '.visLegend'; await this.retry.try(async () => { const isVisible = await this.find.existsByCssSelector(legendSelector); - if ((show && !isVisible) || (!show && isVisible)) { + if (!isVisible) { await this.testSubjects.click('vislibToggleLegend'); } }); } - public async filterLegend(name: string) { - await this.toggleLegend(); + public async filterLegend(name: string, force = false) { + await this.toggleLegend(force); await this.testSubjects.click(`legend-${name}`); const filterIn = await this.testSubjects.find(`legend-${name}-filterIn`); await filterIn.click(); @@ -336,12 +175,12 @@ export class VisualizeChartPageObject extends FtrService { await this.testSubjects.click(`visColorPickerColor-${color}`); } - public async doesSelectedLegendColorExist(color: string) { - if (await this.isNewLibraryChart(xyChartSelector)) { - const items = (await this.getEsChartDebugState(xyChartSelector))?.legend?.items ?? []; - return items.some(({ color: c }) => c === color); - } + public async doesSelectedLegendColorExistForXY(color: string, selector: string) { + const items = (await this.getEsChartDebugState(selector))?.legend?.items ?? []; + return items.some(({ color: c }) => c === color); + } + public async doesSelectedLegendColorExistForPie(color: string) { if (await this.isNewLibraryChart(pieChartSelector)) { const slices = (await this.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ?? []; @@ -355,7 +194,7 @@ export class VisualizeChartPageObject extends FtrService { } public async expectError() { - if (!this.isNewLibraryChart(xyChartSelector)) { + if (!this.isNewLibraryChart(pieChartSelector)) { await this.testSubjects.existOrFail('vislibVisualizeError'); } } @@ -395,19 +234,15 @@ export class VisualizeChartPageObject extends FtrService { public async waitForVisualization() { await this.waitForVisualizationRenderingStabilized(); + } - if (!(await this.isNewLibraryChart(xyChartSelector))) { - await this.find.byCssSelector('.visualization'); - } + public async getLegendEntriesXYCharts(selector: string) { + const items = (await this.getEsChartDebugState(selector))?.legend?.items ?? []; + return items.map(({ name }) => name); } public async getLegendEntries() { - const isVisTypeXYChart = await this.isNewLibraryChart(xyChartSelector); const isVisTypePieChart = await this.isNewLibraryChart(pieChartSelector); - if (isVisTypeXYChart) { - const items = (await this.getEsChartDebugState(xyChartSelector))?.legend?.items ?? []; - return items.map(({ name }) => name); - } if (isVisTypePieChart) { const slices = @@ -424,13 +259,29 @@ export class VisualizeChartPageObject extends FtrService { ); } - public async openLegendOptionColors(name: string, chartSelector: string) { + public async openLegendOptionColorsForXY(name: string, chartSelector: string) { + await this.waitForVisualizationRenderingStabilized(); + await this.retry.try(async () => { + const chart = await this.find.byCssSelector(chartSelector); + const legendItemColor = await chart.findByCssSelector( + `[data-ech-series-name="${name}"] .echLegendItem__color` + ); + legendItemColor.click(); + + await this.waitForVisualizationRenderingStabilized(); + // arbitrary color chosen, any available would do + const arbitraryColor = '#d36086'; + const isOpen = await this.doesLegendColorChoiceExist(arbitraryColor); + if (!isOpen) { + throw new Error('legend color selector not open'); + } + }); + } + + public async openLegendOptionColorsForPie(name: string, chartSelector: string) { await this.waitForVisualizationRenderingStabilized(); await this.retry.try(async () => { - if ( - (await this.isNewLibraryChart(xyChartSelector)) || - (await this.isNewLibraryChart(pieChartSelector)) - ) { + if (await this.isNewLibraryChart(pieChartSelector)) { const chart = await this.find.byCssSelector(chartSelector); const legendItemColor = await chart.findByCssSelector( `[data-ech-series-name="${name}"] .echLegendItem__color` @@ -444,9 +295,7 @@ export class VisualizeChartPageObject extends FtrService { await this.waitForVisualizationRenderingStabilized(); // arbitrary color chosen, any available would do - const arbitraryColor = (await this.isNewLibraryChart(xyChartSelector)) - ? '#d36086' - : '#EF843C'; + const arbitraryColor = '#EF843C'; const isOpen = await this.doesLegendColorChoiceExist(arbitraryColor); if (!isOpen) { throw new Error('legend color selector not open'); @@ -561,13 +410,12 @@ export class VisualizeChartPageObject extends FtrService { return values.filter((item) => item.length > 0); } - public async getAxesCountByPosition(axesPosition: typeof Position[keyof typeof Position]) { - if (await this.isNewLibraryChart(xyChartSelector)) { - const yAxes = (await this.getEsChartDebugState(xyChartSelector))?.axes?.y ?? []; - return yAxes.filter(({ position }) => position === axesPosition).length; - } - const axes = await this.find.allByCssSelector(`.visAxis__column--${axesPosition} g.axis`); - return axes.length; + public async getAxesCountByPosition( + axesPosition: typeof Position[keyof typeof Position], + selector: string + ) { + const yAxes = (await this.getEsChartDebugState(selector))?.axes?.y ?? []; + return yAxes.filter(({ position }) => position === axesPosition).length; } public async clickOnGaugeByLabel(label: string) { @@ -581,62 +429,26 @@ export class VisualizeChartPageObject extends FtrService { await gauge.clickMouseButton({ xOffset: 0, yOffset }); } - public async getAreaSeriesCount() { - if (await this.isNewLibraryChart(xyChartSelector)) { - const areas = (await this.getEsChartDebugState(xyChartSelector))?.areas ?? []; - return areas.filter((area) => area.lines.y1.visible).length; - } - - const series = await this.find.allByCssSelector('.points.area'); - return series.length; + public async getAreaSeriesCount(selector: string) { + const areas = (await this.getEsChartDebugState(selector))?.areas ?? []; + return areas.filter((area) => area.lines.y1.visible).length; } - public async getHistogramSeriesCount() { - if (await this.isNewLibraryChart(xyChartSelector)) { - const bars = (await this.getEsChartDebugState(xyChartSelector))?.bars ?? []; - return bars.filter(({ visible }) => visible).length; - } - - const series = await this.find.allByCssSelector('.series.histogram'); - return series.length; + public async getHistogramSeriesCount(selector: string) { + const bars = (await this.getEsChartDebugState(selector))?.bars ?? []; + return bars.filter(({ visible }) => visible).length; } - public async getGridLines(): Promise> { - if (await this.isNewLibraryChart(xyChartSelector)) { - const { x, y } = (await this.getEsChartDebugState(xyChartSelector))?.axes ?? { - x: [], - y: [], - }; - return [...x, ...y].flatMap(({ gridlines }) => gridlines); - } - - const grid = await this.find.byCssSelector('g.grid'); - const $ = await grid.parseDomContent(); - return $('path') - .toArray() - .map((line) => { - const dAttribute = $(line).attr('d'); - const firstPoint = dAttribute.split('L')[0].replace('M', '').split(','); - return { - x: parseFloat(firstPoint[0]), - y: parseFloat(firstPoint[1]), - }; - }); + public async getGridLines(selector: string): Promise> { + const { x, y } = (await this.getEsChartDebugState(selector))?.axes ?? { + x: [], + y: [], + }; + return [...x, ...y].flatMap(({ gridlines }) => gridlines); } - public async getChartValues() { - if (await this.isNewLibraryChart(xyChartSelector)) { - const barSeries = (await this.getEsChartDebugState(xyChartSelector))?.bars ?? []; - return barSeries.filter(({ visible }) => visible).flatMap((bars) => bars.labels); - } - - const elements = await this.find.allByCssSelector('.series.histogram text'); - const values = await Promise.all( - elements.map(async (element) => { - const text = await element.getVisibleText(); - return text; - }) - ); - return values; + public async getChartValues(selector: string) { + const barSeries = (await this.getEsChartDebugState(selector))?.bars ?? []; + return barSeries.filter(({ visible }) => visible).flatMap((bars) => bars.labels); } } diff --git a/test/functional/page_objects/visualize_editor_page.ts b/test/functional/page_objects/visualize_editor_page.ts index 90fc320da3cda..50b275d04eabb 100644 --- a/test/functional/page_objects/visualize_editor_page.ts +++ b/test/functional/page_objects/visualize_editor_page.ts @@ -63,8 +63,8 @@ export class VisualizeEditorPageObject extends FtrService { await this.visChart.waitForVisualizationRenderingStabilized(); } - public async clickGo() { - if (await this.visChart.isNewChartsLibraryEnabled()) { + public async clickGo(isNewChartLibrary = false) { + if ((await this.visChart.isNewChartsLibraryEnabled()) || isNewChartLibrary) { await this.elasticChart.setNewChartUiDebugFlag(); } diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index 6c0c7463e4a5f..f851126a0e633 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -56,7 +56,6 @@ export class VisualizePageObject extends FtrService { await this.kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*', [FORMATS_UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0,0.[000]b', - 'visualization:visualize:legacyChartsLibrary': !isNewLibrary, 'visualization:visualize:legacyPieChartsLibrary': !isNewLibrary, }); } @@ -113,8 +112,8 @@ export class VisualizePageObject extends FtrService { }); } - public async clickRefresh() { - if (await this.visChart.isNewChartsLibraryEnabled()) { + public async clickRefresh(isNewChartLibrary = false) { + if ((await this.visChart.isNewChartsLibraryEnabled()) || isNewChartLibrary) { await this.elasticChart.setNewChartUiDebugFlag(); } await this.queryBar.clickQuerySubmitButton(); diff --git a/test/functional/screenshots/baseline/area_chart.png b/test/functional/screenshots/baseline/area_chart.png index a2dd5e3b3465ff44d081e3489127cd19ab079bef..28ce63c1bc41b49dcac92377103aecc11c889edf 100644 GIT binary patch literal 85012 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 z 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
- -
+ +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