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 zcmd43Wmr{hw?C?)0wN_yBPAs*NQctWh$1P{ph!!H3c{ivxT7Elo>X^@g` z_>Z~7XYYN^e$REzxATE;vDTd99`_i(827vkP*ah^y+Co{)TvXrx84nOoo(@$+cK4yh>>f zm(GwUzp`JQu@7ohHTfxY?y|c2t((NZ&b7cuRnZ9AU&#yVUErJS$A5ZeTb63_;6D7>(@mb*qg>yCS%6ujdC*W4oX8!p zJvR2@`Xdv(_P(r+w(jN5%EO*pZLNz+PO~ajBWqW9@HyPh`j6CqFB+gdNrRm6tLIdB zm_*G6s!yJ9tUUSA8Ijc?FE9UaE5G7%gmi|wqcC;5u1$@CM&*+&cB2_C7Xtr=df(N7 zek~HQs<_4OWHOO03JP653YnaKyW*wf0rMK0T}`rrRf*lR(Ytm6MCm+6}d@7HTriAcl_SeE*RiXGdGliaPn zWoPt|5MRG}GyW_0NLpW?dYx3XHS>1A%VGdyRP1_rWTyOv%hTGyGFlkw&2S03+rAEgAokd_oJ zN$tU~Bmvt~Zq-*98A^Cpp17qduh^Kie<2?hHKtIIH8X$UeTWNNMS}c!sN80D?D*&f zHc`&+Uyp4*e;gQmKf)QccJo=0am(s}tF!&@pSNbZt9>df#+u+}4w+tudwqol278+e z9qIlwS^93eF`TZ!@M`*hjKg$$S=2g-FQgW(lcQNMvA+^T#y2N=>C&av*2UmE53W4y zZ3w%*J~Ph}tAFI~G_^eSiLgQFjj4MLPLk1WbQiYz|3nd>u|2 z8S~ALw`aL!Y&p3n+M7bz$cSEFWu>0drlzZG;2=J@v%BhOj}pq51Ib_1+S;k0D_&9Y z0;+OQ1{s2jP9BW=mCJtERR`_48}lPL6+=zo|9K+=WHZ;Ozr7Dsyu0 zzgwx`>iSFuU!wj0Yf1b;4pd~$+qVhXnK{q#Wax?AL_H2S1w>08;RbfhOKTr|zZAo+ zFFWStN#=1x8&jy&*2f&HVPL(?si4?F#$zOC03}s2+sQCvvpv#-N#iAb?69}mfG>VQ z?7=VH|NYZrqy`?hS;&m`O53PZ|CcKr?USz6`_uFm+R$9sS{(nl5+tY|M5Fr&nrLpQ z5Z|YVrg7~uoA%z6y%Cd2LNjx!NBeZFfkLWywN~|vPz;Cnh3WFa5;Xvcimnuy&eOH2 zcf48U)_*euxo%aG&W+X5Ncmr!Np?PJTXG&&_B8NVWlVdBn(t>KD%mMwWY_)>Tywa` zy>oC2*{dv7PUe|$Ig<572d>Yx-V%Oz3yhuk?U|3gqD3mZ{)+1>rNHxswPO|B_?MZkgEEb2W>X}XLNXMekx?s%kuzplb z43ANxZ4DHeSx>xNXM4_lyKS*;Lq!#{Pdk*a-@Y|!FD0)!J|cM+> zxnm{zamF%OmZ8PCWYEQa-gwSY7%rsp;DI>L%F%)tUsq?~v?5A2BWZQv0hMqgw)TFX zaGoZc!)&K)&Op(o559KAvT3$TyziYWSg$Vbb-5nQRGn6e7Y+aNg*b{elKDD7=64N^ zxa+R;JjZ*p=P#aTkmEJ{gsbN@$Y^3#KAUJ$GyQeo!R&%fOBp169h<;@wo`{q^VurEVC2jrU*HR`a(%=mhewy zyV2zLpiu61xsH%+T~ASF_V>>Vn=Iv1CI?4%i~@ta>!$6;Z3WdnJY5RBqj!M89P~f# zd=d}+pBA+^gyeoTGW^m?wYRa{C35T%J8R$)PA;I#z}V#1F-=dFH&u^0<3PXYB!Y>z zgYEwqC5u0~z5k|X+Me*D=Gsq;bxQ^QMBLiisDuQa^j;P(Z&v2^fBzO;8!_6pN{YZ+1_>%>n(e-%2z7V1u`#Cj^RIdX%&D@v$AGR{- z^f*cDA2Q;LbEjrh+D+WWqkcQxA12ZGsLWPpAcd-WTAX&@ny)4J|{|0-aaT{}z z)H8$<{{2RRA)x_iR8aFv6(Ou1@oiCPgDL!s!k^)CG_%RxRY77mqVMIVPJ?RY29=4+ zV>S2iTTN-oDiM>Npj(`LX6W7#_uhpot{ZlvyDc996|~Ftw7ZwQ@iJs*KF6KM6%GT= zjih8&$u3yt25~+ieQ3CGS#)h~vT8ld8M+PWzoemKD%p_ETZIGbv$etFKS7Zt3cu_t zz7qwwb!pO8@V86#uxF3wu(%Cg`)@*_q;FX`x%b7I!eLh~U&hofb|g3O+`Iv0<+sx? z$L2EC+u}M)vtp#GZ2I+=xlqNiP%uTj&!rIAH?IX-fdzMd91eESU5S&OuA*L!WzcwCO7SYP3-oog!syfj0?s?MOMaD?;pKPlkx}A zSC|o8xy98eNQEc*fXjKzfu^D-QOej}kjka%!E(*4xOJ)k=JbEg=81fWv1!m<5KO?e z$)B{~uPO_j>~%Q9q*_B5+13bbrmm~oq1>Ny-D`N$YP2f4j4R*#`)4|KFGoV$%>khb z`{l~nci=`WQp0amSnI!8$)49Bmsob=w;4qUhSzG2fJEX0s@Z9OSZHIZKM{CF-pR>a z*r|0wdW6sO+GLA<&rFvt;F+5rF7>AgInp$;i|12x*DqGPC3j7#vYQ_6-LKq;2=A<2 zu{K)%;J~S3PQi8H$uVw=2%M{3@y)%%&MW$v6;C>X9uL^{$}q?m4O*ZQvsFzG-Y*PR zxw1Vr(CK-XCZ%;SM{l(JahtPmsC>GqY~lS}I$`E_D|Dr+Z@7sa_RP!q<7u0in7q13 z_BnO3a-q9YFxj%|_jZ|Y!wtUjg+j`PrS%!xcf}E=9jWv@JmD>*5}p6h%@R2bBsYAFeX)TJWqt$9|oAau) z1P{k5-2d80yGQG>F}+b2QO9M!)8w?)7uWUN+T?>}iT1Z?9)igK*H9{VJ=-zxPIh36 zuBTn8t_XnkAY||>`B_u@$G-wF@!i0f@Yc@Pykc;`{nvO(JM=T0$O=hRq{@HUSFea( zi%iz}|8l6R3SamVBCh;&kv z`sZAA&O;sxWQ`opPa}6mX|@w>6lLU{nr`kdd}WikJavE3c(a1?_rTfj#oo+962yoWO4Y6RKFwR+bf?}87sABNt3^U#HE-fcTi}n%<*+x=idy>WXmCU zOOIAi_^eiyb_t1t<L>$t$gk0q9s>p2YGt<3_UhlMfKjGl*Tu({ko+xAQh! zyV3jCkT*-JXH@`Ekw??D<}gYr2OR%~;p3kfRhwH9JByRbU$&>#%(ZHdo<>Oo*Cee@ zHhh~BTVE$HRxol{ZM$YTGNwbx=R&$CENQ4xn8d!mXpSH+|AK!4$*hE zJP43Z1abb%wiGbpqj0rbV)LjJ33FeG_rIf|(SZu`?$|5KB|pq5VyPuhYTyu19WPdN zc%3WKSv=kI`v;{>h90*ER)4lqofUuD6sboO1`g@us)yWAIp@sbQxdTc&NAfQlgjMg zYlKllD^+2X%zAPkH_EBH@=LNEzQv1ybx`=_zFAfgCwL^R*UcA2*%CIL^|bK1V=HY! zN4i|fO(E~kH@yN+wuqDK9X@0Kgscz&Cv&Y4jpvY~9M);dz>1=b(o?FR<>VdFAK3e4y##$wH^YPbiGkNagPy2LHqO=be#|D#Qdp?xqa#I8k zS3D$CW!DDl%aqZXbR+hob*gf$SM~0Id!p-fw7`6CHJgGzUVLL|k3~z|0+Zc1Dosyf ziz_obTWw6+d~3=Aj9Ir}1|^SqQg8A;jYrfBKrdWL;Gu8xp+lHBn{ zm85thH+I#(gD}19w?77pN>?}emQ_@b0uF&>-LGn!60?=_Yit+Zyv2z$P;ZSn{1}(K zFPJ7zMrZ&9p!!?whMceqOQYKg6UCzm-vxP<@`xlW)8>uaRxYy!EP%v=$s$Dz+YV7( zy5c4~pX;eQC~d4Bb}z;QuwG?kyl%WFeN<+N!^4z8=xs?}R_0G8ZpFPm(@j59SkZOW z?BG35)j=NxI||D?-DU{1B?w;BmWlbevhW60DGlJCc z?ju_v%AksjgMtPEK!8x@DdTE$qN9XUx?3w(nT=1;dwJ-6NHiW)*Q|Tb8QV*+^dpNX zGCKOUr6oB}gI{t_Og@dBO6{n}PH&46nSh@$>!9~D2?LSmd$8|3K_sIcYdM;$-zPf+ zmr4zITqkgoyxd4bnS=)0Lnd3+7E;9GpkgkSNf*>S;%kYFFq-^XuRw(-(q7+A>*Ri| zi_v*@Rx2`}rBXy9$9bhh@Ctf`+7Pqu_KIXBlZgNNNOf!B_m*Ea0zc8qRk6Q*o>sbQ z-A_U7dgL$*f5uB6e@T}Y;=(%pgcEnC3$b5i#%qv|7?LSKagV+V3uzicY9J>&eZolC z`4?(p)62hq&@V1c>b%_{uEzVhPlly7V|YMX#4d$dWw*> zdd#89>2E`>>t(eLeCOP2%m&xYSy%^q4r)j0U){pkVNDixdbquO<~%ONvtoB=acU8h zz9jHIxLE+EjjSC8L^p5q=0rsDoC>;?0hqm^yx=#1AIl(J)ltYIZD;9Z^x^w;b%5Fc zf-~&rL-vvqs0mZ|a+2$dHZ1!?{B+6>$gi>a+|@kqIycVze&^G8<9mG-Jx*p-PIo^X zKiq6_<$e~03Ul0SS90k^)vD4rk zDlY!`Myz(lX!qwdAD8jU95OzOQ@D1!rb6ucM|_wESzvWUD-XyEc9t0VVn?gH)7Ks{ zMq{$gk1VkN!Im6RSd4g%nH=idC!&os!5bnx@g$^`rtYoy#Pp4CIxSUXfbG7kYu-%d zANhUO(~A9vUxdkLg4O9jJgM&w_89A>zJ4+1)!Fg%Pvi6LwLpevS>#ioNk;9&|Prf+!mr_=4eYmX@&llRkMcMX#q0q3cFZy7-z3A@!`~4;6 z_%p-87)_wz8=ekudp18g>vIc{TBEgH4B4O8sd>y(cOTCjFX1UMF-Ww$Wbj*E8kIb9 z{Q4Bq;&uG718L8$EU3Y`k$rXw1++9FEO^66@lv`A$R_E+!MJ_4*B(K8W52f?tO+T~ z1awi_Dm&pJ%uAn=Kf2Eky~fK|jbBZzAK=P&Eb}OX@XCS!5t1m4ub--BkvX+9+%Ld{V|*N%19#+Kb{RMiw-ENpINx>9TY{MASpsfJ<4P5kZ)4s`YRU`q9gGUDT4y zFgFj|V*(;*Sfo)J@d8sB9|~fKGorU{b4Dqacu7HDN>Z_sU9_9GDU_}>nRaerbDQt2ElKy!Y13mX$mzxG& z=jLW;_$w7qd13=D9(gZRuc&M4KG$`yeX=EAb8&Gxd1A>?rMvFUAR+yz>kDz>#NHP% z4>M~x+qA11oSdRA69oDqQbO?Z6VAZ^d%tSlv2)uQzVu(Q?D)Vc;#XApSj2%e;RcqW zP*A19Ixl6lFBqGc^Ps2GQsDOr7Y@rlPo03fSk&S+?`%wfp2Jrg*VX9?wQtqM&n1qm zuR2)AOLG&`7yhJ#3OdL+2$bX}v5~8MJyOZSSwfvftA--Tf6hE51sD*T$w&r2q7DK* zvFxs0kojaaQj3#i{)k^&Qy`FeP715{V59N0Pwz7o%rbvG!Z*26Jc-I4n_c4F;w6~E z$)Qg3){h=N!g~33r%3c>OWqbsb@yU@O5rv*@`+<#dc&AyS8~&5xy;Mbn1Ld_&nF78 zVev>>e8h?Grzd9!HKc^BX+(c6HyJ zjsG$r-u5!Wpa`FCA{kcGR$Mx5@!@KP+@QJ+RAqWT<43jbch0i#y3FcXPb|8G)aQb> z@2{Y;Ce2@+BXZeU&6$6yi62VL7#Mm?e9mzWZlZCI<&s&oGm!{r>w&Y}3(CIu z1FwQU$t-nG%R}O1?qLG-Y&_|wrkjWt(0Vb!Cn89Z*c;eoG5#}+Amvq9=r>1S!FwK> zF=4e#&$=wq6zxei@5y{Z?w=y4c2`ewKiHA41T_B5S(SK?`x_XnQBFmCPUIsU}U&=&VNI^9EoZ9t2wJ;EDEm+}{?mkw-e57$eeW|V-fUhSme%z4Wh zeB1n{BDX!ezEwO6a4@t;TTFyLi)Y^l!pQh3SwKyYwi04?b@4N=UcdKk@W&?#k==!+ zu+0{lbJH=mcV1!wR?_dq*UBS_xI|=^RW?54`2ZT!pf5w06!!b>6!sp=>))?B7ws-{ z;q6i4xG6JrB`I``OD*CpPRm<&J)gjhp_LY0_w`^Ykj8B3Z~EJ;FD3N40g0lnuntlw zvs{wJ>aDuic-lO{r>5GwE4?KZ4%+P8VpUAaZ2bG^N~qUqbOPz>>+X415u0wesTxNJp~ z3jDUn!f}&lL77T9ri^1ulVlusuP%hZ?hPN+O}oc=))@Z?NYTM6Z{~YX4FQ}xuL{Vu7k^-VEBuUk4iN@Wc43IY%+~~n^!(u{rCsfe zQQ|CK-&gpc*?_3dtn_n^vOs8a)}T6QJ}km8$#8DyFev9~7k*;T#;`I>bz!X^>36@r zbY)pN2ZZHo!2uGtt&n93pinyFbFT9J(41?RWRnv1wrJV|bZU$)GfRP3C~#lSu{lmF zy$VzJiMJAAlkjD{8+h-J;?ybAb<^weaqU5zC$(_W%w4?zJiqd#h9VQDN_DV_z9_lT zfAA3^m;|HI0)odiZ%ipEAzAHeCvYCr;yc(tS!XgoXab^*RkhLeoyTVmQd#`*2(|HwuqeIX(Vdy($wr%}yx>9Lgb0yifkzFB)CU9>k%^o9pY5&l)dC$3} zIR&Zrd?J{irl;-`_lac@Nj64S)rk{3r~6F45z-6leQtmB>iNm;R8waGSw;-?tHqNDAn7=_)r(FCkYJ*v25P? z?up)_3Di)OLAk^jsLNX_pE8|(R;hK7qCLMdY!Wy6-A5xXjAZjW2GDw0uRi&pBx-(L zbF=n%S)9Plqgc!Fb9x15_h5oeUR5cb?g-E*&)_44>9_oKmq*hSOrtJh4Ssk=gMsEK zX?mDpi4PS;a_SeuVCeh7XyV3_L=-;14XxQ`iMP4)1M!7#1)nUq?LnN17MYwA=!pxy z?(j`-`GY6_;ojzq!OD}F==-CYa@z$g)WRm0`o%M-jSCVRL9y8%ey{x}Awkm9Jb5rO z#sXweFpYqpUmpNX`n~nDo>2FIC_q~j?QaIvtLmXH1x8=tH<^|@h^mxOM~=$h(~o=U z+EW4pi?8ki@4y2?dI-~R%*FH(2Hj%FR@D)ZUU2ML){pw?E7bQqf!ld$^tP08k}Y*x zn`T60erKF`0Pe~GPSd;+!$${Zrzx+{V%HA;i!{P<#;%hgxSjE0k-&wSnbKxTsjapA zPw1LW_$IhGGRn6I8vJq1Mu)W<4>rr=8nzgr(H-el;WA4tTP*o(uSY_?@*+{gbt`~5V^Xbp-mkjysrM?gZl&FUme)uLW{GvOTzX9F*ZU#aj2+On-%QvPmb-iA zK~#sfpgVY}q|YOYvwZ7-3bY7Q@@a47Q{L~X5P|IS53IBg;9Uxg4=(iaiX@|lW`gR{ zxG-ne0q%Y04+J0|`jZrlUXXh$KZBj@XDA~A8b=y{v+Fs5oW@-}rCAkf^{UYoTj3!B zUKolL)EZtO)fWujS{fR6%(i2=|I!J-W!qxF3I9A%b|L)tfo}y=utgETxamUA_|4nd#`oc|RSaiD->Zx(Y-F z3o8>FZgl*V*{gM>VI4}xy1~$Xg7h1__d(n-j!GD);@k(`r^KW>jtIph5f zrjcw&)juzTuH?YPVB9v2bfD&wCk{#4Ssyyi!=>m&wvAW)^J;hlUkQ1=>*>m!h`QRa%kfk60WAYhJIOA}c+_KG# zRk$bms2p#1OP7kTcMB%Q)^KI!F)4X+mNZkFZ=}<~Q7LICS@)d814ZR091NUf?VAJo zQl1U|$vq4~sZ+}$HW@XE*(0rG1IkA2nRal@Dc`K`jjI?_Fol0VGz%UNN)dDCp8ZD7 z0)m~g;G^F>uQdbN#l9jblI^1>S9o5gte}MUI}Lb`ZJZxoC*KviAU39MkTxswi*3V0 zPU9DprQ5c6kYAt7G3A7}@8d@SvUKXa2nW|ABcdV~k?J8wQ;Q+-1HG~POS4U%oabZu z!bQ5PPSZ`)|AEfmIVz<8#81u4wjSEyCwhpy7o5~9=(-XBmw*`*2yI*S=E@DFu57xR z!XG_BFINy^a!zu^ps(3^oBRIpw#oypwL7FC_A+L2%YqrC^8vT{VpJu%Oq<$op4Ms154YMc%4Zs2-G6^eZ@cA9J@mnaDaop%vK*_t6AUT_eHUI^FYio;xM1Uzt z%9}xyh7@p_>Xuj-^L~n)#?ESs{kH<|XgFX$wc2mi9Tyz&aoxcy5G_F>Kze}yi-3hm zTVTst$>Ja`1QpW_8Vy*N`BNrudXygqRxbrO`_Sukn~%LhLC`1w&(i(EIWMKRH^ipS zN$yHFk;CCOg-s&1b{5wV4QEmJ)lrDOIJg9K<@($cVQ)nKam0fY0{l+Ga)!PFghRQ+ z0#yNSF(p3-%ZAlUmZynBW9^FB%MIgULx6w2D0HlMyj~G7cKnDj_>??%6R{7yeJ8et z$dp${4bed;^98X)wLQU)X^dpMl>muqpo<74`S6i>lTH=_AWKC02bVazLML$d2+|o5 z2gW|yY@W3XdXFGvP3j)&_y?WRvqTcHf7}CP71Ra6DobtpKVa;XgfXhAM+hYwF_GE3 z4mAu{VfYS)Q>3?^EBueH#{y^3+s6(zVRKX&E_;BVjHmj!C865Jf&Pw?Cb24l?T=tO zr$A#aP0W?`R95*BE7dj zQ?H%bpG)b(rqj9Ltt^DdcA%0puusV~mgm5a2Ko5EIM9O>fwT9;!y+U_v?V_Ar9j^{ z{6z+ZDDiG0ej`5AEGGz>|U*+cY}1L2;^0&f*U*FQmDVTC{X5L>cTTBS?HlfrtzhsG{7xZ!}$ zmx4j*$D&JD0ZKP3h$FK3U0Vf4mTICk;@L>elN5pCY+n0z?K{&qwmB(=NjTUwSCTnm zvye#9i`@}f&j}^9O-ITYY>;HB2|#fc9LDSkYYp|0A%3$qoq$fdpBElHO#cdH2gSw` zf7{yY%}VZ;hWicy%!#b$?QST5B~gcwL<{A{b4oguYeBare=LSW-G^SE-xym}w8UnL zQp=>E(x*JY}kVSx^kVjcrlKBoo_&qzxNs%LyGTD^pLHmQgkAKh7T9k*=as zn&Z2283+IX1swp`{|S6d=nS;^cP zHH^DRmc3HI$-)8Gha4w*2{jPV&zu7*BBQ66noOppv*PkfRqfU9K7BSQlKKh`+#m)X z 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

QA> z;3}-iGl#h%JYZTqU+vJ>NB!iwsJ_T3R3&AKKi)g)KYqo$VWvIUatKgsB^QN24tJHt zZVnr~u|;3)Z?16&csvV>i>QnHqNRzvopyDHsMF_1jfqu#%_p8diXf7!ZpnZh6jfw} zpS5@hz9f1$#lFM3YNTJVJc&+|K|-D`o9nx_{&n6uL{_0H2!xF9Z1@YxH<7xzWi%>O zMC+9{2K>ogBx5_?2CmjA;rQg#8_E*eD+WY7MM)sd{l!E0i@A0$t|Y}xSYK5ioe?fQ z8uLBe+C5gfJY95l8kgUywPCTSaSAtKxq-{Wp{cRX35;iM9+#EBLo+J?r3BK>KgD~7?|)O}c&Yvvc2f&w5J%~3Qqz5^ ztDmGf^ZG>(8k}x@#h-JUc8#^cti(#$LCw7h#z(LKT-Yfxp6;&0YNs@>$OYZk#*bu* zsrQxV2_Hve>|5Q0X61%lNaUYlE%b%ya97Ube&2HTvp;bWrNdWN)`&mVl*EYkVvK+?N+AnAl!+Bh@$~bHSu;K>`%M;q0QYZ) zH?VvAY-jG>QDpqFNUC#a5|AZS|rc;7+O4ibKDK)cT}pS)eDd7GxLL za`B3$rRRq9cKk7{)|HF)m;yM;QB^rnm-!bMFh`xLD=UO_bUqX*(rJqaO10_jXj{m+n{N6Ll4odsJ1y>CMWvZ+c(#)b2}wDyqm*C17FM9cUd`dYy4*B%TUck_ z;_CXt(f)6$eM7!92wRE(B^(xA0fT+P#g=8vzRS=$uD-Mw=HPUmfZO`NsSBgH~TD+eB3 zLc;O$qlHw(A_&7|&1GP)6n$Zve_CIBtKKp_kb=JUfJgpQlxF|TfYq(d>}-p7i{hBz zEkkYH?L??>Gsm!_Wy_MTZc9%?dk{?bVoh3|U` zUr)3}d*R~nNTpa<7n_k%$Yyb4;p&efGatP%y)tE$VPh3}WqnNle51vF+w%M$INtAl zVW0Vu*);kxZBM&ig=}+QU7VyEbsno!O&{+&RM!g#9Wz5NCo48SO$azu9JZLO#1CL6 zb!I+9WRWeZig(>o^+e6cf{;By9{i4xWnyeVCRvUa(JvB6+2e_7nN|@O5eCN@&@3VT zs491Lx#uO^3u7^ZhFN+^Ni9f7;;yl%LN9RPc$# zPG~<5lA4{DO{L@cXKF{;C&jIS-e)`f5eHR z7c}WIqv`lF5#CKSG+)vsdEcYWFev*6Tv(IK8s@3J4mF>Z@=G(Hx}joTYSYtTBi_%F zta%)h)gKKa=k9$J?8(8tL%ZeTTx*m+r8lKo{;ZBYpex0Fpq@KSec!=zTKmzu!s*q zcV&ss>)I;SEm4NQkK50}>QywBSBl{$$UZegnC^sAd9|f#`)2FK$izff>pI_ZvG@bS zfO<{MAKy)^jBsckEuhS$X&^4W{!ai}&)`sVz*8F{mF>~eBBTsW2Y-|Ht3PaDsrymx ziDRej5dpiPqa%*I$(_d~aX^VT#+=^yPYxZ}EE9P?WyxfkwT#WmqTeLs%7u*}!$`N` z#A9YT09PI{`ONnxEAQ)3xjLwqtAb5uD-ejFGntUsK;yRncigXh!x zMdNv;3&*hiPi(alk+b)tN5!XFuMX6<)`-YJa8t;8$=f`3lFInlaUUfdEuIQJb2Z`(=N9ijgSX3su!8 z`5Y{;m74TT!Fys8%ZDxIUl~o#kXD?pZXx?1B>{zkyZhT7Hk)%e`tErU+D0nO@fNcE zx+Uo5n^mY0apPM(LTELQi1)Gkj-59^DEk!>&`mNOzrwqB>cQ~{Dt>;rxw$#^D9w)# zfjF^Z zKs<)tsGdTQ)NGnnHtTh2de*6r?_x|y;98peFa|L*^ViwZ&-W`ORh|32V!N$M0b_t- z0sxj0{z6o$`-i`;{Qo>ozv)iazlHr}GcNBg+n<(w#daHdREmMf zqFqLYI&W=k@8bHa?+dKaX5ZrF09gRk+=1FH|E6}&e>zw+L~q4%d&+tLfY0M*rDS1n zKbH1kPcCw4xFOq!OA~Ann|fRvjvMUR#9?qV9&b#rxs2KGd$= zVm>pcB~ZRyFkWtb0LQCX7CjjD?)fXb1+2Hi!E?NPK1`%wz8|~I)%*20>WZZpK8-D7 zaQ6kMRjVVdnqQ7?vI1U0o81x@{_c5SGKdfem~?uoRu65ff`&=ZUW^0dPH`*q^4UtN zPdwKzt}!JS4#8W7kf7am{%lO6BaGClNx#z@DsnfQ?6&&{7C*sTtO-AD6-`tHj6XlK z-k1OM>DZ=O&#M&NG4q;NwaPuf2Whsp%6z_~pZG*`NLfnw&Yc&NVLIROIQ-8qo?Kz0 zp~CgNrpMBroy!cQqHQq$szr}w$-&YSdH3%7BaHt&ps%ICQ(Rn?>d)M?M8rc@YzEhty(Y8E(1#DC$<#I%~lbp7$v7`HK*er>J0YLX%{iM7G}Zek=8-k$|JXWPf0Oh7IHCme_0y9*G;9(JLu^wo#oBZJmCk;+ zsAAehXSYg=!_-i3Y3qE@zB+`SBxf_Td*-|A=xi9D^yLZnYhmRP48TDkaXgh)Gc#K9 z)z^e&Zx0UuGm`3dZ(S#DS>M(Asxnq%KmfElH8(%kpdO)+@zcl9uTzvS*=&Biz5AX} z8;4t+xjPv3eQt|-@Rlw~1KW5V@b<}ARo!Awv52zj=U)zPkTkW<|1&d|q|MCXncuFx zGh{vfy)?pqujy#-aCp;s-;6`9FyURlVUu|Y8l1PU(T;sTGgr23&G9X-dk!nsjs`_% z>4zJ6AGOEEsrtJUZSmYSePi#3U`m$A$*b2F>9oeNxjhV$lw@XQ37en{v#{Gp`%;ix z>(T=P>x%*85BbPwJDRB0e0P z*-bOD{n7b5;wy>qFd-wtfg|$*gBzubGpU>dm0743MISmh`h#F#QA*}6QF8Kgvn^6= ztc;Pl)d?8XLr?&B+~PU^$YqVFW~Ro2pl%-}^U|yOP>QlrY0%<>4lJfY=dW4>0=0cL zuXZ&)LA*Y>&>&R8+T8dAla*OBgLnS9l3n8rUiLTbtkUy+olKJ%2k^;%G`%P1^)}CQ ztrz%}iBn2>SB5-XRG$=&j>I5L*=oZ7D12m;g5}1j)l#>HAfVnvs3oA?$W?g1+D~Z=I(fXq?xJ2`bzW$qI!`}{>iC5rScR=V zSz|_>^#X4*-bxZqX}^dWOUH#p#@BB?Cy-T9ke=pUo&#$9*N$zgq9&b=WI&9n1iJXR zdgocX{{x5hBw0vF*7WmMWZECUn_mX$uS@thF=436nQ{D%9Mju5K_cFs=N8y9H zUYN2V`s7i(=!r&XKjlBhWw`l$O`$RQVdkkoPpC<{YK?bloC$ zr?cO5OkSvPBe_(U9!o?@ECU2>rMOkoN%@w~@436ItRf4Pq+O?O86yvAmyG*bJpX84GZI@{vQrT9$~Tra1OrE_-T> zr=paeVA77ut^^dR1bh&+S3+MjK`r*?%rP<5-J>!wbJoVDm*>dl6k&p2M#~P+t)&{;fUuXzpGEaEltgXMb!Ns05 zH5T{VeE?}+1}sDpa#GnhH7R*rkT2#`I*Kpwm&)12M)Jt{EN-#*7#EG1HK|bF^&ph6 zB4ezyTMYWj`xGSBhRm;&%3`Qm1Hb7@et>j4?(N@yW%KcI!lY1jGrtpvkfx)haY}=Q zVB+w>4-+1Zl)ru2Z~ z{1~9jA%;a6dKe`1kl*sd61@j(WgP2UG=?EiyZP0c8skI15u?${V@Hil@ zqGWp}?oBk83guW!xC(e8GwgK;d|Ga)6Z2!u2M^>v*4=*47{a+LkX1g z{%`|Fvj6`&9;w#(!y>Igec(2e%h}QMLB;vz8c=LhpWKut$d|I%(Xl71_F7a3P(l94 zjPo(?M`x6dkY)+lZ{mbHM;;LWSWSO)<*kmOV;b-q{0sY)%9N8OI!Aeu1qcIhpAkUg zr3$H+dTKpl7cvAvA{7x8*E4LUhe*|yZf7e^OVcHMJlQh|dLWH`gdSL&x~O~^|Lr^y zgZ_OL8FI9ho4WK0ZzPx?pmjYxXZ0?Kj#wV`&w*QgGj!hGAz`roh0*)bf>v-q`c2>Z$0^7E<07*%0ilVerAs~4_1wrqYOrJ9&zpeazP?6~40R_|e_;@&P zoYfg?#)5KRKuJImnY90-AW_BHr+&z4qM$gGL8z(o@3+H^DPe=WJ1ukDc1s_$Knif2 zIP(9P74&XEo@M~b2bdZVLQLc>VR05%6Q$w}o(uP`o*RF zSw%Cq^R)$>kMzzO?zZK`eB&a-ax%jT&^kF4h2DWhQs$Jv(4?sP@;Ra(od;aYp&)aL zMYvxHty+qSjmW6Wc(og9&z&%2WZKT|7k>FXThi_9R((5r=_w0oTtZ<=TPtp@rqh^| zC+Y!bHf%a=UF`79ajBUO`Ld6&WcwqEU{TVGbZ)nVsT>yx4?XR34yqvU2mOv!E9O&e$64JZBsK;?^4fHUagH zH|7$P{X1MW(N)e4s!#OTk`hn&yi32+mim;XIJacu)-}z3MzF{^ex zgdo#mz(j;F&4s5%-cZ*>%0w7v4CZ0&8Dp8-~Z&E41iWplbnf0E4e9Vi7O;?D>v z$W{jL`9G5oLPu{6A*3lo*wor2PuX2^Ook5}XnjEs$mu#7W2 zI@^I710eU7{G$ZZ(fk(Ha)kynZO~|-nfC8us2#6J0G-=%Vqry z<%BkV?KPWcBf0%ykRAKh?yNOux%mLJY48U)n|fzSHbB)xy6(YD2A1nmkY=t^6n<>b zWoEFtz#5RP+BsEF8QmGvujQM1Cub8HAKukHBE2w=r12RP2l?R~lLkFxVl*U7ZjS34 zs6GvdwHn8ue#}HebUb94afOu>5rOr+4u${QFU9~Rm4B@4KT~X{n=sPhzql~% z|69xrk?u*uz#jp-9MMoDEK(g`EFB^c3hkD!(qIX z`Rgb0I>pbHBvVkP$1^gViO}=;fBha~(G3Ru`Yt7@|K7Byn5g(KL$2`R*u`bVFc!0k zGD(Es-2a}UqYeSx;JKJz+QozjrQbvTe^#l%&6||4r-(ovhdoBIpc};fBm&%s4N_{Ah84 z_5?B_DLUd%`F8|B*bWo9OL+Ib$8C@Lcc^!gP~S&qM8Xli`Wv}Z15|Sv?SC!|HvVQ zDIzj_I3sc;BNBS7e0#dr?CWGpc8aTi+QoNc6BDHhYf>tOf`$CAQ`)l)Uf|@04l~X} z{->FMdI4#8qW&ZI3sr3Pdz7T$On@PCN1@xL3Gd~B9LD1E(x5b-UPmM)NNvGud1b|< zz^4C^U2l}TJec{vE~z_hvaniA1*To7x*QHER3H5?0a6hp0@ndTe@pCwilP5~BpMX| zpNpPy+UEJu8A&OJ#bry~+}yl!v^n0|5`-JaWPp5rv?(Ctns+?q6e1cy2J{R-5B7f( z)QX=^OoB?qKQi2+?lRgfx^q#x(HrTY8<7Zw+v{SF46+(+E>WR-M7@2G>e19n@6!_V&(0hHd_XUfBGCx% z{*L?vHdM2;Ga^MO!(^>l{R0A&>Ktj7tK~fGHV9Tv`8f)^&SA`#xhxONq$uYEwkY)L znQYqH+Mdg}BK`$qlL)0zKqT6Ks|8p#kTE&XGErt6Qk)>7%mq_w#n11))#Unao#U&O zPK6*QK4-p&Bf!{Ay%)n|vm~M4ljyg%P0qFKY+iAxp2x~%Fn^vdY`?i^kTapwvI-p3^0lkU#c zd9Jau@q5x3vQO{VVhsId0p4zf>@2uX(q?{&BjzH9jEp!r;Q0A0iK z{{Dho@7@aWal{z0M>WEIa7rI#fS5Sx8h+dDRfmGcWspWNZop6F@2DdJh6il>r^vos zL{lhsqjI|U#3tOS$4e}-L~`*tSTxPZ%49k(S~lulHk;X6jW{Kg&kdTs*kZtU(Y$Z4 z-k_n{@R5pq#a&&)!(SbR~Xtm3A!@)d`JN}vmEsxdsNWtdI zGrQ=p*vPK9P+6!O68$d=wzdr>u$Zam2-)wFvw^?+e!_APf|C#*%jj@Nd|I`hNxEBa z81FbdA)#@lNFlvyd5S|K8N!l7srHLd3`R-Q+X)y!s`%{;DP?xc2A?Twy1~N+e_7;G zCToh>3x2+k|G1_2tAkTBtUb57KP^EIk6kY)Hd%jIdMeF(NX=y%iH?aW+`rfu=Ww3S zebltA^rrIuW`o*v6*+K-Wl%-yBNlv3qtA8z_{V zbp2PHx+q1wz=T>V^z2t6d`xGvHxe#81>T8z@_Uynb~JMWZ#@ZujAMi=@(CUnMmy=0 zuE{}io8yCaZtHDGk01rcf}hXUQ!*BsBrkEX6TzUODj+AsY>K)>RF=+e0|GzaWa6dGnz`=kR2@Qb$X^B4)I;R&q=42q26|ws}5!A8Q z?4;W?^Mi*8Z9Hr~$#K{uq@;63J39$+UsDr!&7RG>9TJr4v$pO6oVDA$FzJg8@4Aie zAszFbP}og#ctp~w*=x;qf_zAai?^z6r4N( z@bzkT64|8(#JuO7F%$Nex{QpKgJmYq3Tw`Yz=@0F2`--{baKUmff8`Cb1Q`hzz1rd zkae~zFCY91yl(OYhl8SQ4s5@jl)<1#p$>(z+aw872rPoUb3RDSROktM8I(FUR9yD_O{FEOzEn&1~F+& zdnthKTI`5}Cl0b{|D)JOfwcaWug2w`n61L5R{z{c-=)xlY3RkHoTizD!Y zu0W+@@R`L^Lj8a^y4nk>vE>8ZfwK9Z%xE#&APp((V+O;71p7o6idBWG>F8&7Qu3a~ z$+wa#19|e+T)I1*=47-X>)s29Z>tc%_rmQ%10a5g57Xj4>yFM#B61EDTOagL)6;d0 zSFVXFWFc8);6G$bnIqyHDu7tMG~!!lb8ZnwST6YlLDCVz42vJQr{?Hr=co%2US69K z9HGglsw_~G18ImLIP^0xAWK>4%&u0I^{RMJza)u5$I~W>=vt5rh6q#;&=wIt&9jOuP!Gp%sN@q`YGJh_u(s+kFqpZP$2>8 zu0dt%!-FF(y!>#gQshvFl{1H0m0^a!l1%TjG=NYgePPR2(NzzoXQ<_hIeBgtBJN|NIARZB})%S?lfn;T>%m4bkjq6zjYaZd99so9`aWA zxB!pZWzm5fX?ov=@F&S22MIuA2_~rNJGy{QsP?rxj`!6ZKSWln_H50h0E^>2;FQTc z=PSecRKg)Hy(pkwmR6#azrqQDq?Y_Z>q|1_^_#|g&UR6(4FOPrlX9sER_EQ^+{cv3 zj%OSY9;c(3IERov;2^-U<@axL`Lw%*3Bc(qU!n9BDIuaJ$jS`AK?x};0f96>KQ}ml z-q0S=-2_JnlnNZX_3l~TAAGC;i^(9B9U(&E-;4Pa3lZuxypeJJXrCYh+AS<0W{fFC z&cfn_71JrI^YI}i>9m_$Vsadhuc(-qpxwdBaiU-3ciNvcc5h!RTy{Vf)bto+#a!C} zcRhKd#ALS1()fdus*ZEnVPWe0sdMPHPrp0+vP1Mn&Aauf9Ko+M0gj0=8T5c_;XAGn zUSSKbn<$;nIt3=!6Yt~KMs(#(9q^8H3mi{~skmN`ZpS+Gs&2(u;brfix4O1=IW1ZD z2Cf@g?83`ub;oyi*iC7kv$^RVxT!XJYD~8TAS|WfS~b7HQ0I(ii>ZD-IqRXy>`Y?E zLmx6`HyKDv+Y?yjhL4c!e5-E6qhKaHOx3peO?8DX;aeY+fiji*3evT4ci42Orp3~>_dNyupETmzXlj3HeWAz0$oURWNeAnCi{^F)P+i7l-3 z>ok2I!h12o!Qtm}j`9J)i+?2;|2%mZ@vn@uqu0mJEmRiGW=|KSx3Xfu7x6Vk#rLGv zc^h^jEyuP=J+PXre~sUD86Jo}9tykUzAb*mCj>iJ9mRd1M`Xi0DJaQ2jpRaaFi;0Nc@}c_91K~ie#VU)LeMdv_r!&IK`>d9n?(Yba zddHtTm5gn~c0a!Qa?0d9bw9dx4^|ssMl5s%YjLs4(Q$Rf2HCDTe9*cUf|{cF5;Q^- zkJb=Qs*zSa=`EW_bK|a0YQ7KQ3m#t<5w8?Hxi(>SEyf{4RAo3aUgC45ds_zE@8Y%r zS4q`+9UqjECu9IVH+|gAXH{DJc(TJT#=0@z&iV_Kj}lmSF7zh`L6a?z#(^9rG$i#KPIAj#JIW+#-jZP=;(yzAJ;#MS4!Ac7 zIJkdT7ouyPI4!N_Wb69rzAyn3h!c-#T!X+{AGS`qnO#|CIb|y$`fHMMA`%-SiIm7y zjltk^4mLB-krH^%-&>YrC-pARuh*F^2GCvkn=kh~=!r&s&sY7~_hF2>#OfLgP_@LV z8pF=i#K2nU`jZ7t1sy_bUr=oN};-QJGL(_b!l;7PyJ5uT~awX2}YV14&GP)0$u zg-Sen_s6Y@ou*=zO*Cj>A^Al>vCrIxc4ZM`N>~;FcquOY&&NsKbajtV{I{hWii6FW6OJSYn zcr;zkrxRo~pWvj=T+GkxHlKo&sYO~oy<`k;1!#yiCmGo+ter$G~15VWF$XZJf;U~HLd04pWm$xW);s(qD?7Z(JrUh(V~+F3c%G*+V#wg zEvxIU(J+aM_!_MAAyiydn)C-@t2N0NzVag!pw(Rb#8+Z6Vve71e#>rp z|EUduOT3CjMRJK2GtO#MQnL$6SM%a`d;1W#auyWv zNL$yimc$n71C>lKsJl$Yjg11BrrJQ=f4w-yovI=iH>HQ{_6!F zhH>M}Cjb3K_7=yMkp=Gdi%#ELAM zXYZ-P<6?l#`f+nDM+VsP2kxrzn;Jo^+c7jQ17Oh;H`_^<}QxpC^6S=@|}Ic zZXb%NadD5&-ufQGc6re5d;#%DwzNg`2u=F;gM=*EQvE|Q|AOj0>#NJmGK@Eqi~*li z^krVFu_Ua08iO1ag#<5dNAtD#Nz`be$yvk-_7l5ZqGn#@%qsb3O*2QOS#U!LGOzS5 z!S@^}2`whZz3z!?Ulp;ucCqVMnii*>O|Giz^2<}I371nUaU{)5E>IDYad5yc(@=cA ziTSusDL#wa@_K~w4TsbHmnQOsB%Y}Ja6ADr4U<-Ol{)R=q^dTC2w&Bs2I>%poh#X3 zo!A&G#`UH2U1rUe%*F+~g?%g;%Tlm}TwHjT`IVG7N^*I7`7SQe57vf!c0pM_W8yZU z-xjC6CSS!B3~i?2sBaiVLCJxGc$zPMb6j0(F(i56eER(HJRe8c#MD<6_Rn=e)0dYh zawL9Wxc0YiFB01mQi#tcu8>teD<#3HaiYkLWm!svMtS zXk?z2SpjcHE-FXMycJx9&d<&ToX}EHrufQ8A)VW9W>p-!17``9h2CZ*X1UGmW*~tb z9wBtcvlAkdNkn$pRS&%GJbLtqTD>|pKj5)Ui5a@|=2-J8Gd^$A8F4o^$3Dz=3(C(| zL#)aucA*L9tsWItiU^Oyqx;^#+((|X@2MTWQvuJ`HM7!Boaa>Qo~2f3Gu}YApL>0x za8&kqCC^NQYQ}j%x}`{GoT~Kk(ZHL0ubR^83Lm96Dd}264}?^YT(hLEsV7n`t0R!z^un^4)8IIvYvcb+V#3^C=d?L#1?B?iZ|EueE{QgBW}XQ3)wcLlRT z-O1>BHc|3=m^!1n8k0+7vS+5gF_y{hwhDMFKwLHf>Ho|1MgI9Mj0&!Bc5FLgG(yko zLg|#L30D)AuhYtsa8Ru5k29(pCpqN2_}zc}P<~9K+$85@+y5rW2SvuES)Z5Jk%0Kz z?R^a_UApB*k72xPp_k3f09Cca?rqxa*U@h+g}a%(5pnB1`4W2U2x?=%jt;5nw9mgN z#L76}@+qFtCzX|z$sVuR#j=+8yI$VkiE&eJE=m_(ePFljx9t+q5$`qOay{KwZUf8b zutd2;cZM^$`=bXh@2o%K_3*?CfP%x@R{3P`q?ddqCk#r_3vpYTERc!m=E>6kqO$bzR0Ujl?Qvj;ma`G-0!toXwxE-tcgo)6fUmIkBVU{ z0P;a`V53%KQ39mzv}ID@(Rx=Nn{Wnf=FmB~P$H5hMyx?!@tIPelS zq;G0U%E^ff{BGVKB`_^_0x$nsxYBqs)?{q_b@SGjpoNYI_X+1kH%7z0rwz>pe>s*} z2{6?+FjFE(2b?IVsE8%y2AG?>=$S|e;hEA!(2_)npqdM2oT6&sDAj7bf1iv}jU{XP zmhD%wj1@y+s?EhhbXqBkxTK7)HLh-j7JTSY$Zo^Yg}k@GKCxtW>%S)F zmyRCrBOw7s{wY6SZ~yv&O*}kuIM`MybE#M+lKgT-gV+B{x`pLwX2j|DDDgNR9sJq3 z($yHcBJgL|JlvCnq(82T*iX*tCzd|}Tuu<369g_5BAohaW{S?$?@*^uM>E`-F$J43 z(E;{H99LB|gK~Y+Na;YK5idI5TjImRL-2*(B(qzIz9c^};fG5l7pEkI7hNC;!{vlf z(9qC8-l(bJRkXAc=(HCkeP^kuHxWh-J5R5L`3Sy(K_}r@Rz~JU>Z=c+ZjIVA+?OiQ zE=oiWFA7=O68Rfw$M{IO&?X<=16c4i@E^F=*47m3TzLX8>GTC&-UBTNvadi2Yqhp0 z`)g}(v#OP6Yq?=f6*K?+fS&9~I}xaB{VBt}=-#~s1l*MZ@KHx&V`I2k=Z^&iEnT6W zBw+9VOiFvV1wIyA-Q7%pW+Xk6bvQlK zL=uVU$qa0vc4y&rz?)q8z5Nuv-lgsY`;v+{)G35U9~6U)(IRlNISdqB!b#vo!!`9| zv0v)9#QkLme+f^#174(|tF0K3DLpSQ(AIBt(170TN04sv?3S*QP!H59607F77ABHp zx1>_k^#H<&z-cFHgtMcEvp*D#1Ug|I-WG@b^A9~!I0>OB8dUW|J>v-d>SG%_j^I>&)TI6H25 zsGvbO02%0PA`~Wlp>UE|Y_NTQ602uh!T;z_w#X@sXgOEK9{9F>8cu^8SmZPi=6uGtk6U?QN3J?b(vJsLE7jB$3Z4x%0su15|2dV^FJ8eh?WeAV3C3gjMTO~MgV50i z#o{L_KmP(t?*Xf?vf%`aW*3=*Kv!!J-Ud|TFMNRYbOk24Cp66p-Iq~YvB$AdU}WT zqyk`CEiOO9Q-1s|Fnob6spDq~%J0kt1{}zs%b}Av#MwRP^Z}E@3m+k{!+?gl01Y|; z%$uD-aP#JAO>Nm7tilPbvMhnv6gTe~DWM4fgZ#`~ literal 129876 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%0Dfqf4XY$R&T$X9lYXwikfXYWzUaoo+jm1F^pO7Y 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 MboFyt=akR{0EeZ<7XSbN diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5724ad7ebc783..241bb838d7a01 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5994,9 +5994,6 @@ "visTypeVislib.vislib.legend.toggleOptionsButtonAriaLabel": "{legendDataLabel}、トグルオプション", "visTypeVislib.vislib.tooltip.fieldLabel": "フィールド", "visTypeVislib.vislib.tooltip.valueLabel": "値", - "visTypeXy.advancedSettings.visualization.legacyChartsLibrary.deprecation": "Visualizeのエリアグラフ、折れ線グラフ、棒グラフのレガシーグラフライブラリは廃止予定であり、8.0以降ではサポートされません。", - "visTypeXy.advancedSettings.visualization.legacyChartsLibrary.description": "Visualizeでエリア、折れ線、棒グラフのレガシーグラフライブラリを有効にします。", - "visTypeXy.advancedSettings.visualization.legacyChartsLibrary.name": "XY軸レガシーグラフライブラリ", "visTypeXy.aggResponse.allDocsTitle": "すべてのドキュメント", "visTypeXy.area.areaDescription": "軸と線の間のデータを強調します。", "visTypeXy.area.areaTitle": "エリア", @@ -6029,7 +6026,6 @@ "visTypeXy.controls.pointSeries.gridAxis.dontShowLabel": "非表示", "visTypeXy.controls.pointSeries.gridAxis.gridText": "グリッド", "visTypeXy.controls.pointSeries.gridAxis.xAxisLinesLabel": "X 軸線を表示", - "visTypeXy.controls.pointSeries.gridAxis.yAxisLinesDisabledTooltip": "ヒストグラムに X 軸線は表示できません。", "visTypeXy.controls.pointSeries.gridAxis.yAxisLinesLabel": "Y 軸線を表示", "visTypeXy.controls.pointSeries.series.chartTypeLabel": "チャートタイプ", "visTypeXy.controls.pointSeries.series.circlesRadius": "点のサイズ", @@ -6255,10 +6251,6 @@ "visualize.editor.defaultEditBreadcrumbText": "ビジュアライゼーションを編集", "visualize.experimentalVisInfoText": "このビジュアライゼーションはまだ実験段階であり、オフィシャルGA機能のサポートSLAが適用されません。フィードバックがある場合は、{githubLink}で問題を報告してください。", "visualize.helpMenu.appName": "Visualizeライブラリ", - "visualize.legacyCharts.conditionalMessage.advanced settings link": "高度な設定", - "visualize.legacyCharts.conditionalMessage.newLibrary": "{link}で新しいライブラリに切り替える", - "visualize.legacyCharts.conditionalMessage.noPermissions": "新しいライブラリに切り替えるには、システム管理者に連絡してください。", - "visualize.legacyCharts.notificationMessage": "レガシーグラフライブラリを使用しています。これは7.16で削除されます。{conditionalMessage}", "visualize.linkedToSearch.unlinkSuccessNotificationText": "保存された検索「{searchTitle}」からリンクが解除されました", "visualize.listing.betaTitle": "ベータ", "visualize.listing.betaTooltip": "このビジュアライゼーションはベータ段階で、変更される可能性があります。デザインとコードはオフィシャルGA機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャルGA機能のSLAが適用されません", @@ -17427,7 +17419,7 @@ "xpack.ml.ruleEditor.scopeSection.noPermissionToViewFilterListsTitle": "フィルターリストを表示するパーミッションがありません", "xpack.ml.ruleEditor.scopeSection.scopeTitle": "範囲", "xpack.ml.ruleEditor.selectRuleAction.createRuleLinkText": "ルールを作成", - "xpack.ml.ruleEditor.selectRuleAction.orText": "OR ", + "xpack.ml.ruleEditor.selectRuleAction.orText": "OR ", "xpack.ml.ruleEditor.typicalAppliesTypeText": "通常", "xpack.ml.sampleDataLinkLabel": "ML ジョブ", "xpack.ml.settings.anomalyDetection.anomalyDetectionTitle": "異常検知", @@ -23214,9 +23206,9 @@ "xpack.securitySolution.open.timeline.showingLabel": "表示中:", "xpack.securitySolution.open.timeline.singleTemplateLabel": "テンプレート", "xpack.securitySolution.open.timeline.singleTimelineLabel": "タイムライン", - "xpack.securitySolution.open.timeline.successfullyDeletedTimelinesTitle": "{totalTimelines, plural, =0 {すべてのタイムライン} other {{totalTimelines} 個のタイムライン}}の削除が正常に完了しました", + "xpack.securitySolution.open.timeline.successfullyDeletedTimelinesTitle": "{totalTimelines, plural, =0 {すべてのタイムライン} other {{totalTimelines} 個のタイムライン}}の削除が正常に完了しました", "xpack.securitySolution.open.timeline.successfullyDeletedTimelineTemplatesTitle": "{totalTimelineTemplates, plural, =0 {すべてのタイムライン} other {{totalTimelineTemplates}個のタイムラインテンプレート}}が正常に削除されました", - "xpack.securitySolution.open.timeline.successfullyExportedTimelinesTitle": "{totalTimelines, plural, =0 {すべてのタイムライン} other {{totalTimelines} 個のタイムライン}}のエクスポートが正常に完了しました", + "xpack.securitySolution.open.timeline.successfullyExportedTimelinesTitle": "{totalTimelines, plural, =0 {すべてのタイムライン} other {{totalTimelines} 個のタイムライン}}のエクスポートが正常に完了しました", "xpack.securitySolution.open.timeline.successfullyExportedTimelineTemplatesTitle": "{totalTimelineTemplates, plural, =0 {すべてのタイムライン} other {{totalTimelineTemplates} タイムラインテンプレート}}が正常にエクスポートされました", "xpack.securitySolution.open.timeline.timelineNameTableHeader": "タイムライン名", "xpack.securitySolution.open.timeline.timelineTemplateNameTableHeader": "テンプレート名", @@ -27190,4 +27182,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。", "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f54e290cf8bb4..1f7f7a18939ac 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6041,9 +6041,6 @@ "visTypeVislib.vislib.legend.toggleOptionsButtonAriaLabel": "{legendDataLabel}, 切换选项", "visTypeVislib.vislib.tooltip.fieldLabel": "字段", "visTypeVislib.vislib.tooltip.valueLabel": "值", - "visTypeXy.advancedSettings.visualization.legacyChartsLibrary.deprecation": "在 Visualize 中面积图、折线图和条形图的旧版图表库已弃用,自 7.16 起将不受支持。", - "visTypeXy.advancedSettings.visualization.legacyChartsLibrary.description": "在 Visualize 中启用面积图、折线图和条形图的旧版图表库。", - "visTypeXy.advancedSettings.visualization.legacyChartsLibrary.name": "XY 轴旧版图表库", "visTypeXy.aggResponse.allDocsTitle": "所有文档", "visTypeXy.area.areaDescription": "突出轴与线之间的数据。", "visTypeXy.area.areaTitle": "面积图", @@ -6076,7 +6073,6 @@ "visTypeXy.controls.pointSeries.gridAxis.dontShowLabel": "不显示", "visTypeXy.controls.pointSeries.gridAxis.gridText": "网格", "visTypeXy.controls.pointSeries.gridAxis.xAxisLinesLabel": "显示 X 轴线", - "visTypeXy.controls.pointSeries.gridAxis.yAxisLinesDisabledTooltip": "直方图的 X 轴线无法显示。", "visTypeXy.controls.pointSeries.gridAxis.yAxisLinesLabel": "Y 轴线", "visTypeXy.controls.pointSeries.series.chartTypeLabel": "图表类型", "visTypeXy.controls.pointSeries.series.circlesRadius": "点大小", @@ -6303,10 +6299,6 @@ "visualize.editor.defaultEditBreadcrumbText": "编辑可视化", "visualize.experimentalVisInfoText": "此可视化为试验性功能,不受正式发行版功能支持 SLA 的约束。如欲提供反馈,请在 {githubLink} 中创建问题。", "visualize.helpMenu.appName": "Visualize 库", - "visualize.legacyCharts.conditionalMessage.advanced settings link": "免费的 API 密钥。", - "visualize.legacyCharts.conditionalMessage.newLibrary": "切换到{link}中的新库", - "visualize.legacyCharts.conditionalMessage.noPermissions": "请联系您的系统管理员以切换到新库。", - "visualize.legacyCharts.notificationMessage": "您正在使用旧版图表库,7.16 将会移除该库。{conditionalMessage}", "visualize.linkedToSearch.unlinkSuccessNotificationText": "已取消与已保存搜索“{searchTitle}”的链接", "visualize.listing.betaTitle": "公测版", "visualize.listing.betaTooltip": "此可视化为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束", @@ -17687,7 +17679,7 @@ "xpack.ml.ruleEditor.scopeSection.noPermissionToViewFilterListsTitle": "您无权查看筛选列表", "xpack.ml.ruleEditor.scopeSection.scopeTitle": "范围", "xpack.ml.ruleEditor.selectRuleAction.createRuleLinkText": "创建规则", - "xpack.ml.ruleEditor.selectRuleAction.orText": "或 ", + "xpack.ml.ruleEditor.selectRuleAction.orText": "或 ", "xpack.ml.ruleEditor.typicalAppliesTypeText": "典型", "xpack.ml.sampleDataLinkLabel": "ML 作业", "xpack.ml.settings.anomalyDetection.anomalyDetectionTitle": "异常检测", @@ -27636,4 +27628,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。", "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。" } -} +} \ No newline at end of file diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 9698ab5ee5137..174870b374e82 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -105,7 +105,6 @@ export default async function ({ readConfigFile }) { defaults: { 'accessibility:disableAnimations': true, 'dateFormat:tz': 'UTC', - 'visualization:visualize:legacyChartsLibrary': true, 'visualization:visualize:legacyPieChartsLibrary': true, }, }, diff --git a/x-pack/test/functional/fixtures/kbn_archiver/rollup/rollup.json b/x-pack/test/functional/fixtures/kbn_archiver/rollup/rollup.json index 00596d865f03d..deb14fe74b187 100644 --- a/x-pack/test/functional/fixtures/kbn_archiver/rollup/rollup.json +++ b/x-pack/test/functional/fixtures/kbn_archiver/rollup/rollup.json @@ -4,7 +4,6 @@ "buildNum": 9007199254740991, "dateFormat:tz": "UTC", "defaultIndex": "rollup", - "visualization:visualize:legacyChartsLibrary": true, "visualization:visualize:legacyPieChartsLibrary": true }, "coreMigrationVersion": "7.15.0", diff --git a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/async_search.ts b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/async_search.ts index 00656c4aa8f6a..3428071684900 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/async_search.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/async_search.ts @@ -15,6 +15,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'header', 'dashboard', 'visChart']); const dashboardPanelActions = getService('dashboardPanelActions'); const queryBar = getService('queryBar'); + const elasticChart = getService('elasticChart'); + const xyChartSelector = 'visTypeXyChart'; + + const enableNewChartLibraryDebug = async () => { + await elasticChart.setNewChartUiDebugFlag(); + await queryBar.submitQuery(); + }; describe('dashboard with async search', () => { before(async function () { @@ -30,7 +37,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.loadSavedDashboard('Not Delayed'); await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.missingOrFail('embeddableErrorLabel'); - const data = await PageObjects.visChart.getBarChartData('Sum of bytes'); + await enableNewChartLibraryDebug(); + const data = await PageObjects.visChart.getBarChartData(xyChartSelector, 'Sum of bytes'); expect(data.length).to.be(5); }); @@ -39,7 +47,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.loadSavedDashboard('Delayed 5s'); await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.missingOrFail('embeddableErrorLabel'); - const data = await PageObjects.visChart.getBarChartData(''); + await enableNewChartLibraryDebug(); + const data = await PageObjects.visChart.getBarChartData(xyChartSelector, 'Sum of bytes'); expect(data.length).to.be(5); }); diff --git a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/save_search_session.ts b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/save_search_session.ts index f7ea80de57bda..bab93ad0483d3 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/save_search_session.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/dashboard/async_search/save_search_session.ts @@ -17,6 +17,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const searchSessions = getService('searchSessions'); const queryBar = getService('queryBar'); + const elasticChart = getService('elasticChart'); + + const enableNewChartLibraryDebug = async () => { + await elasticChart.setNewChartUiDebugFlag(); + await queryBar.submitQuery(); + }; describe('save a search sessions', () => { before(async function () { @@ -92,14 +98,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Check that session is restored await searchSessions.expectState('restored'); await testSubjects.missingOrFail('embeddableErrorLabel'); - const data = await PageObjects.visChart.getBarChartData('Sum of bytes'); - expect(data.length).to.be(5); // switching dashboard to edit mode (or any other non-fetch required) state change // should leave session state untouched await PageObjects.dashboard.switchToEditMode(); await searchSessions.expectState('restored'); + const xyChartSelector = 'visTypeXyChart'; + await enableNewChartLibraryDebug(); + const data = await PageObjects.visChart.getBarChartData(xyChartSelector, 'Sum of bytes'); + expect(data.length).to.be(5); + // navigating to a listing page clears the session await PageObjects.dashboard.gotoDashboardLandingPage(); await searchSessions.missingOrFail(); diff --git a/x-pack/test/upgrade/apps/dashboard/dashboard_smoke_tests.ts b/x-pack/test/upgrade/apps/dashboard/dashboard_smoke_tests.ts index 0bc3cd7c2610e..2c79191f6269d 100644 --- a/x-pack/test/upgrade/apps/dashboard/dashboard_smoke_tests.ts +++ b/x-pack/test/upgrade/apps/dashboard/dashboard_smoke_tests.ts @@ -40,7 +40,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); await kibanaServer.uiSettings.update( { - 'visualization:visualize:legacyChartsLibrary': true, 'visualization:visualize:legacyPieChartsLibrary': true, }, { space } From aab533826fd979bf66d40fbf1755d082a31e7bcb Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Sep 2021 14:55:10 -0400 Subject: [PATCH 010/139] [APM] Updates failed transaction correlations help (#111222) (#111574) Co-authored-by: Lisa Cawley --- .../failed_transactions_correlations_help_popover.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations_help_popover.tsx b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations_help_popover.tsx index e66101d619224..65f6f54ecf89e 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations_help_popover.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations_help_popover.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { EuiCode } from '@elastic/eui'; import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -36,7 +37,11 @@ export function FailedTransactionsCorrelationsHelpPopover() {

event.outcome, + value: failure, + }} />

From bf40a85f9afef7708df90b9a01c6b9a8d011db12 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Sep 2021 15:04:52 -0400 Subject: [PATCH 011/139] Ensure name columns are not cut off on APM tables (#111046) (#111578) * Switch the overview to single column when we're at < 1200px * Hide sparkplots on transactions, dependencies, and instances tables at widths where they could be easily cut off * Right-align sparkplot columns * Make chart heights match latency chart height when showing a single column * Make all impact bars fixed 96px width * Make values be shown before sparklines * Make SparkPlots `overflow: 'hidden'` * Remove fixed widths from columns * Return breakpoint and width from `useBreakpoints` * Rename "breakPoint" to "breakpoint" everywhere to match what EUI does * Replace `ServiceListMetric` (on used on service list) with `ListMetric` (used when sparkplots are conditionally shown) Fixes #110165. Co-authored-by: Nathan L Smith --- .../app/RumDashboard/LocalUIFilters/index.tsx | 4 +- .../app/RumDashboard/RumDashboard.tsx | 4 +- .../components/app/RumDashboard/RumHome.tsx | 6 +-- .../agent_configurations/List/index.tsx | 3 +- .../Settings/anomaly_detection/jobs_list.tsx | 3 +- .../custom_link/custom_link_table.tsx | 3 +- .../app/backend_detail_overview/index.tsx | 4 +- .../failed_transactions_correlations.tsx | 3 +- .../app/error_group_overview/List/index.tsx | 11 ++-- .../service_inventory/service_list/index.tsx | 42 +++++++-------- .../service_list/service_list.test.tsx | 52 +++++++++---------- .../components/app/service_overview/index.tsx | 24 ++++++--- .../index.tsx | 3 ++ .../{get_column.tsx => get_columns.tsx} | 14 ++--- .../service_overview_errors_table/index.tsx | 2 +- .../get_columns.tsx | 32 +++++++----- .../index.tsx | 6 +++ .../app/trace_overview/trace_list.tsx | 7 ++- .../__snapshots__/ImpactBar.test.js.snap | 10 ++++ .../components/shared/ImpactBar/index.tsx | 12 ++++- .../shared/charts/spark_plot/index.tsx | 11 ++-- .../shared/dependencies_table/index.tsx | 42 ++++++++++----- .../list_metric.tsx} | 33 ++++-------- .../shared/overview_table_container/index.tsx | 4 +- .../public/components/shared/search_bar.tsx | 4 +- .../shared/time_comparison/index.tsx | 4 +- .../shared/transaction_type_select.tsx | 4 +- .../shared/transactions_table/get_columns.tsx | 31 +++++++---- .../shared/transactions_table/index.tsx | 8 +++ ...points.test.ts => use_breakpoints.test.ts} | 4 +- ...use_break_points.ts => use_breakpoints.ts} | 14 +++-- 31 files changed, 239 insertions(+), 165 deletions(-) rename x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/{get_column.tsx => get_columns.tsx} (91%) rename x-pack/plugins/apm/public/components/{app/service_inventory/service_list/ServiceListMetric.tsx => shared/list_metric.tsx} (50%) rename x-pack/plugins/apm/public/hooks/{use_break_points.test.ts => use_breakpoints.test.ts} (97%) rename x-pack/plugins/apm/public/hooks/{use_break_points.ts => use_breakpoints.ts} (75%) diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx index 3453970376dc0..d6897599f5288 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx @@ -22,7 +22,7 @@ import { UxLocalUIFilterName, uxLocalUIFilterNames, } from '../../../../../common/ux_ui_filter'; -import { useBreakPoints } from '../../../../hooks/use_break_points'; +import { useBreakpoints } from '../../../../hooks/use_breakpoints'; import { FieldValueSuggestions } from '../../../../../../observability/public'; import { URLFilter } from '../URLFilter'; import { SelectedFilters } from './SelectedFilters'; @@ -84,7 +84,7 @@ function LocalUIFilters() { return dataFilters; }, [environment, serviceName]); - const { isSmall } = useBreakPoints(); + const { isSmall } = useBreakpoints(); const title = ( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx index a182de8540f58..4ed011441c81b 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx @@ -11,11 +11,11 @@ import { UXMetrics } from './UXMetrics'; import { ImpactfulMetrics } from './ImpactfulMetrics'; import { PageLoadAndViews } from './Panels/PageLoadAndViews'; import { VisitorBreakdownsPanel } from './Panels/VisitorBreakdowns'; -import { useBreakPoints } from '../../../hooks/use_break_points'; +import { useBreakpoints } from '../../../hooks/use_breakpoints'; import { ClientMetrics } from './ClientMetrics'; export function RumDashboard() { - const { isSmall } = useBreakPoints(); + const { isSmall } = useBreakpoints(); return ( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx index 487d477485ce1..cb80698adeaa7 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx @@ -15,7 +15,7 @@ import { DatePicker } from '../../shared/DatePicker'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { UxEnvironmentFilter } from '../../shared/EnvironmentFilter'; import { UserPercentile } from './UserPercentile'; -import { useBreakPoints } from '../../../hooks/use_break_points'; +import { useBreakpoints } from '../../../hooks/use_breakpoints'; export const UX_LABEL = i18n.translate('xpack.apm.ux.title', { defaultMessage: 'Dashboard', @@ -25,7 +25,7 @@ export function RumHome() { const { observability } = useApmPluginContext(); const PageTemplateComponent = observability.navigation.PageTemplate; - const { isSmall, isXXL } = useBreakPoints(); + const { isSmall, isXXL } = useBreakpoints(); const envStyle = isSmall ? {} : { maxWidth: 500 }; @@ -57,7 +57,7 @@ export function RumHome() { } function PageHeader() { - const { isSmall } = useBreakPoints(); + const { isSmall } = useBreakpoints(); const envStyle = isSmall ? {} : { maxWidth: 400 }; diff --git a/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/List/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/List/index.tsx index 140584a625b90..3ab5c25ed3dc9 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/List/index.tsx @@ -12,6 +12,7 @@ import { EuiEmptyPrompt, EuiHealth, EuiToolTip, + RIGHT_ALIGNMENT, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; @@ -180,7 +181,7 @@ export function AgentConfigurationList({ render: (_, { service }) => getOptionLabel(service.environment), }, { - align: 'right', + align: RIGHT_ALIGNMENT, field: '@timestamp', name: i18n.translate( 'xpack.apm.agentConfig.configTable.lastUpdatedColumnLabel', diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx index 81a0da9792830..7aafb27aa18f3 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx @@ -12,6 +12,7 @@ import { EuiSpacer, EuiText, EuiTitle, + RIGHT_ALIGNMENT, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -38,7 +39,7 @@ const columns: Array> = [ }, { field: 'job_id', - align: 'right', + align: RIGHT_ALIGNMENT, name: i18n.translate( 'xpack.apm.settings.anomalyDetection.jobList.actionColumnLabel', { defaultMessage: 'Action' } diff --git a/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/custom_link_table.tsx b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/custom_link_table.tsx index 86a7a8742eaea..a5134058a349d 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/custom_link_table.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/custom_link_table.tsx @@ -11,6 +11,7 @@ import { EuiFlexItem, EuiSpacer, EuiText, + RIGHT_ALIGNMENT, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; @@ -50,7 +51,7 @@ export function CustomLinkTable({ items = [], onCustomLinkSelected }: Props) { }, { width: '160px', - align: 'right', + align: RIGHT_ALIGNMENT, field: '@timestamp', name: i18n.translate( 'xpack.apm.settings.customizeUI.customLink.table.lastUpdated', diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx index 2c9ec0a232974..1060e20e9c595 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx @@ -27,7 +27,7 @@ import { getKueryBarBoolFilter, kueryBarPlaceholder, } from '../../../../common/backends'; -import { useBreakPoints } from '../../../hooks/use_break_points'; +import { useBreakpoints } from '../../../hooks/use_breakpoints'; export function BackendDetailOverview() { const { @@ -63,7 +63,7 @@ export function BackendDetailOverview() { backendName, }); - const largeScreenOrSmaller = useBreakPoints().isLarge; + const largeScreenOrSmaller = useBreakpoints().isLarge; return ( diff --git a/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx index fc1d9a3324b24..c32f4815160f9 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/failed_transactions_correlations.tsx @@ -20,6 +20,7 @@ import { EuiBetaBadge, EuiBadge, EuiToolTip, + RIGHT_ALIGNMENT, } from '@elastic/eui'; import type { EuiTableSortingType } from '@elastic/eui/src/components/basic_table/table_types'; import type { Direction } from '@elastic/eui/src/services/sort/sort_direction'; @@ -156,7 +157,6 @@ export function FailedTransactionsCorrelations({ : []; return [ { - width: '80px', field: 'normalizedScore', name: ( <> @@ -168,6 +168,7 @@ export function FailedTransactionsCorrelations({ )} ), + align: RIGHT_ALIGNMENT, render: (_, { normalizedScore }) => { return ( <> diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/List/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/List/index.tsx index 73eb9c72416af..81d1208e6cbf5 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/List/index.tsx @@ -5,7 +5,12 @@ * 2.0. */ -import { EuiBadge, EuiIconTip, EuiToolTip } from '@elastic/eui'; +import { + EuiBadge, + EuiIconTip, + EuiToolTip, + RIGHT_ALIGNMENT, +} from '@elastic/eui'; import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; @@ -150,7 +155,7 @@ function ErrorGroupList({ items, serviceName }: Props) { name: '', field: 'handled', sortable: false, - align: 'right', + align: RIGHT_ALIGNMENT, render: (_, { handled }) => handled === false && ( @@ -181,7 +186,7 @@ function ErrorGroupList({ items, serviceName }: Props) { defaultMessage: 'Latest occurrence', } ), - align: 'right', + align: RIGHT_ALIGNMENT, render: (_, { latestOccurrenceAt }) => latestOccurrenceAt ? ( diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx index a3820622f8c9d..e085c5794f80a 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx @@ -11,16 +11,13 @@ import { EuiIcon, EuiText, EuiToolTip, + RIGHT_ALIGNMENT, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { TypeOf } from '@kbn/typed-react-router-config'; import { orderBy } from 'lodash'; import React, { useMemo } from 'react'; import { ValuesType } from 'utility-types'; -import { - BreakPoints, - useBreakPoints, -} from '../../../../hooks/use_break_points'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { ServiceHealthStatus } from '../../../../../common/service_health_status'; import { @@ -33,17 +30,18 @@ import { asTransactionRate, } from '../../../../../common/utils/formatters'; import { useApmParams } from '../../../../hooks/use_apm_params'; +import { Breakpoints, useBreakpoints } from '../../../../hooks/use_breakpoints'; +import { useFallbackToTransactionsFetcher } from '../../../../hooks/use_fallback_to_transactions_fetcher'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { unit } from '../../../../utils/style'; import { ApmRoutes } from '../../../routing/apm_route_config'; +import { AggregatedTransactionsBadge } from '../../../shared/aggregated_transactions_badge'; import { EnvironmentBadge } from '../../../shared/EnvironmentBadge'; +import { ListMetric } from '../../../shared/list_metric'; import { ITableColumn, ManagedTable } from '../../../shared/managed_table'; import { ServiceLink } from '../../../shared/service_link'; -import { HealthBadge } from './HealthBadge'; -import { ServiceListMetric } from './ServiceListMetric'; import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; -import { useFallbackToTransactionsFetcher } from '../../../../hooks/use_fallback_to_transactions_fetcher'; -import { AggregatedTransactionsBadge } from '../../../shared/aggregated_transactions_badge'; +import { HealthBadge } from './HealthBadge'; type ServiceListAPIResponse = APIReturnType<'GET /api/apm/services'>; type Items = ServiceListAPIResponse['items']; @@ -66,14 +64,14 @@ export function getServiceColumns({ query, showTransactionTypeColumn, comparisonData, - breakPoints, + breakpoints, }: { query: TypeOf['query']; showTransactionTypeColumn: boolean; - breakPoints: BreakPoints; + breakpoints: Breakpoints; comparisonData?: ServicesDetailedStatisticsAPIResponse; }): Array> { - const { isSmall, isLarge, isXl } = breakPoints; + const { isSmall, isLarge, isXl } = breakpoints; const showWhenSmallOrGreaterThanLarge = isSmall || !isLarge; const showWhenSmallOrGreaterThanXL = isSmall || !isXl; return [ @@ -97,7 +95,6 @@ export function getServiceColumns({ name: i18n.translate('xpack.apm.servicesTable.nameColumnLabel', { defaultMessage: 'Name', }), - width: '40%', sortable: true, render: (_, { serviceName, agentName, transactionType }) => ( ( - ), - align: 'left', - width: showWhenSmallOrGreaterThanLarge ? `${unit * 11}px` : 'auto', + align: RIGHT_ALIGNMENT, }, { field: 'throughput', @@ -173,7 +169,7 @@ export function getServiceColumns({ sortable: true, dataType: 'number', render: (_, { serviceName, throughput }) => ( - ), - align: 'left', - width: showWhenSmallOrGreaterThanLarge ? `${unit * 11}px` : 'auto', + align: RIGHT_ALIGNMENT, }, { field: 'transactionErrorRate', @@ -196,7 +191,7 @@ export function getServiceColumns({ render: (_, { serviceName, transactionErrorRate }) => { const valueLabel = asPercent(transactionErrorRate, 1); return ( - ); }, - align: 'left', - width: showWhenSmallOrGreaterThanLarge ? `${unit * 10}px` : 'auto', + align: RIGHT_ALIGNMENT, }, ]; } @@ -228,7 +222,7 @@ export function ServiceList({ comparisonData, isLoading, }: Props) { - const breakPoints = useBreakPoints(); + const breakpoints = useBreakpoints(); const displayHealthStatus = items.some((item) => 'healthStatus' in item); const showTransactionTypeColumn = items.some( @@ -250,9 +244,9 @@ export function ServiceList({ query, showTransactionTypeColumn, comparisonData, - breakPoints, + breakpoints, }), - [query, showTransactionTypeColumn, comparisonData, breakPoints] + [query, showTransactionTypeColumn, comparisonData, breakpoints] ); const columns = displayHealthStatus diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/service_list.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/service_list.test.tsx index 9859c629f973b..75aad2283de0b 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/service_list.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/service_list.test.tsx @@ -7,7 +7,7 @@ import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { BreakPoints } from '../../../../hooks/use_break_points'; +import { Breakpoints } from '../../../../hooks/use_breakpoints'; import { ServiceHealthStatus } from '../../../../../common/service_health_status'; import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { mockMoment, renderWithTheme } from '../../../../utils/testHelpers'; @@ -90,11 +90,11 @@ describe('ServiceList', () => { const renderedColumns = getServiceColumns({ query, showTransactionTypeColumn: true, - breakPoints: { + breakpoints: { isSmall: true, isLarge: true, isXl: true, - } as BreakPoints, + } as Breakpoints, }).map((c) => c.render ? c.render!(service[c.field!], service) : service[c.field!] ); @@ -110,7 +110,7 @@ describe('ServiceList', () => { `); expect(renderedColumns[3]).toMatchInlineSnapshot(`"request"`); expect(renderedColumns[4]).toMatchInlineSnapshot(` - { const renderedColumns = getServiceColumns({ query, showTransactionTypeColumn: true, - breakPoints: { + breakpoints: { isSmall: false, isLarge: true, isXl: true, - } as BreakPoints, + } as Breakpoints, }).map((c) => c.render ? c.render!(service[c.field!], service) : service[c.field!] ); expect(renderedColumns.length).toEqual(5); expect(renderedColumns[2]).toMatchInlineSnapshot(` - { const renderedColumns = getServiceColumns({ query, showTransactionTypeColumn: true, - breakPoints: { + breakpoints: { isSmall: false, isLarge: false, isXl: true, - } as BreakPoints, + } as Breakpoints, }).map((c) => c.render ? c.render!(service[c.field!], service) : service[c.field!] ); @@ -166,7 +166,7 @@ describe('ServiceList', () => { /> `); expect(renderedColumns[3]).toMatchInlineSnapshot(` - { const renderedColumns = getServiceColumns({ query, showTransactionTypeColumn: true, - breakPoints: { + breakpoints: { isSmall: false, isLarge: false, isXl: false, - } as BreakPoints, + } as Breakpoints, }).map((c) => c.render ? c.render!(service[c.field!], service) : service[c.field!] ); expect(renderedColumns.length).toEqual(7); expect(renderedColumns[2]).toMatchInlineSnapshot(` - - `); + + `); expect(renderedColumns[3]).toMatchInlineSnapshot(`"request"`); expect(renderedColumns[4]).toMatchInlineSnapshot(` - - `); + + `); }); }); }); diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index 45372188994c7..9b930c3f08f3b 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -13,7 +13,7 @@ import { isRumAgentName, isIosAgentName } from '../../../../common/agent_name'; import { AnnotationsContextProvider } from '../../../context/annotations/annotations_context'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; -import { useBreakPoints } from '../../../hooks/use_break_points'; +import { useBreakpoints } from '../../../hooks/use_breakpoints'; import { LatencyChart } from '../../shared/charts/latency_chart'; import { TransactionBreakdownChart } from '../../shared/charts/transaction_breakdown_chart'; import { TransactionErrorRateChart } from '../../shared/charts/transaction_error_rate_chart'; @@ -59,10 +59,16 @@ export function ServiceOverview() { replace(history, { query: { transactionType } }); } - // The default EuiFlexGroup breaks at 768, but we want to break at 992, so we + const latencyChartHeight = 200; + + // The default EuiFlexGroup breaks at 768, but we want to break at 1200, so we // observe the window width and set the flex directions of rows accordingly - const { isMedium } = useBreakPoints(); - const rowDirection = isMedium ? 'column' : 'row'; + const { isLarge } = useBreakpoints(); + const isSingleColumn = isLarge; + const nonLatencyChartHeight = isSingleColumn + ? latencyChartHeight + : chartHeight; + const rowDirection = isSingleColumn ? 'column' : 'row'; const isRumAgent = isRumAgentName(agentName); const isIosAgent = isIosAgentName(agentName); @@ -91,7 +97,7 @@ export function ServiceOverview() { @@ -105,7 +111,7 @@ export function ServiceOverview() { > @@ -116,6 +122,7 @@ export function ServiceOverview() { kuery={kuery} environment={environment} fixedHeight={true} + isSingleColumn={isSingleColumn} start={start} end={end} /> @@ -132,7 +139,7 @@ export function ServiceOverview() { {!isRumAgent && ( {i18n.translate( @@ -186,7 +194,7 @@ export function ServiceOverview() { responsive={false} > diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx index 08f29d7727cda..109fdfee1ed77 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx @@ -22,11 +22,13 @@ import { getTimeRangeComparison } from '../../../shared/time_comparison/get_time interface ServiceOverviewDependenciesTableProps { fixedHeight?: boolean; + isSingleColumn?: boolean; link?: ReactNode; } export function ServiceOverviewDependenciesTable({ fixedHeight, + isSingleColumn = true, link, }: ServiceOverviewDependenciesTableProps) { const { @@ -122,6 +124,7 @@ export function ServiceOverviewDependenciesTable({ { - return ; + return ( + + + + ); }, - width: `${unit * 9}px`, - align: 'right', }, { field: 'occurrences', @@ -72,7 +74,7 @@ export function getColumns({ defaultMessage: 'Occurrences', } ), - width: `${unit * 12}px`, + align: RIGHT_ALIGNMENT, render: (_, { occurrences, group_id: errorGroupId }) => { const currentPeriodTimeseries = errorGroupDetailedStatistics?.currentPeriod?.[errorGroupId] diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx index cae9cbae69794..7222b1f4dbf68 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx @@ -23,7 +23,7 @@ import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink'; import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper'; import { getTimeRangeComparison } from '../../../shared/time_comparison/get_time_range_comparison'; import { OverviewTableContainer } from '../../../shared/overview_table_container'; -import { getColumns } from './get_column'; +import { getColumns } from './get_columns'; import { useApmParams } from '../../../../hooks/use_apm_params'; import { useTimeRange } from '../../../../hooks/use_time_range'; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx index 07081069039cf..97a92cb9e0576 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx @@ -25,12 +25,11 @@ import { asTransactionRate, } from '../../../../../common/utils/formatters'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; -import { unit } from '../../../../utils/style'; -import { SparkPlot } from '../../../shared/charts/spark_plot'; import { MetricOverviewLink } from '../../../shared/Links/apm/MetricOverviewLink'; import { ServiceNodeMetricOverviewLink } from '../../../shared/Links/apm/ServiceNodeMetricOverviewLink'; -import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; +import { ListMetric } from '../../../shared/list_metric'; import { getLatencyColumnLabel } from '../../../shared/transactions_table/get_latency_column_label'; +import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; import { InstanceActionsMenu } from './instance_actions_menu'; type ServiceInstanceMainStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/main_statistics'>; @@ -48,6 +47,7 @@ export function getColumns({ itemIdToExpandedRowMap, toggleRowActionMenu, itemIdToOpenActionMenuRowMap, + shouldShowSparkPlots = true, }: { serviceName: string; kuery: string; @@ -59,6 +59,7 @@ export function getColumns({ itemIdToExpandedRowMap: Record; toggleRowActionMenu: (selectedServiceNodeName: string) => void; itemIdToOpenActionMenuRowMap: Record; + shouldShowSparkPlots?: boolean; }): Array> { return [ { @@ -101,16 +102,17 @@ export function getColumns({ { field: 'latency', name: getLatencyColumnLabel(latencyAggregationType), - width: `${unit * 11}px`, + align: RIGHT_ALIGNMENT, render: (_, { serviceNodeName, latency }) => { const currentPeriodTimestamp = detailedStatsData?.currentPeriod?.[serviceNodeName]?.latency; const previousPeriodTimestamp = detailedStatsData?.previousPeriod?.[serviceNodeName]?.latency; return ( - { const currentPeriodTimestamp = detailedStatsData?.currentPeriod?.[serviceNodeName]?.throughput; const previousPeriodTimestamp = detailedStatsData?.previousPeriod?.[serviceNodeName]?.throughput; return ( - { const currentPeriodTimestamp = detailedStatsData?.currentPeriod?.[serviceNodeName]?.errorRate; const previousPeriodTimestamp = detailedStatsData?.previousPeriod?.[serviceNodeName]?.errorRate; return ( - { const currentPeriodTimestamp = detailedStatsData?.currentPeriod?.[serviceNodeName]?.cpuUsage; const previousPeriodTimestamp = detailedStatsData?.previousPeriod?.[serviceNodeName]?.cpuUsage; return ( - { const currentPeriodTimestamp = detailedStatsData?.currentPeriod?.[serviceNodeName]?.memoryUsage; const previousPeriodTimestamp = detailedStatsData?.previousPeriod?.[serviceNodeName]?.memoryUsage; return ( - ; type MainStatsServiceInstanceItem = ServiceInstanceMainStatistics['currentPeriod'][0]; @@ -118,6 +119,10 @@ export function ServiceOverviewInstancesTable({ setItemIdToExpandedRowMap(expandedRowMapValues); }; + // Hide the spark plots if we're below 1600 px + const { isXl } = useBreakpoints(); + const shouldShowSparkPlots = !isXl; + const columns = getColumns({ agentName, serviceName, @@ -129,6 +134,7 @@ export function ServiceOverviewInstancesTable({ itemIdToExpandedRowMap, toggleRowActionMenu, itemIdToOpenActionMenuRowMap, + shouldShowSparkPlots, }); const pagination = { diff --git a/x-pack/plugins/apm/public/components/app/trace_overview/trace_list.tsx b/x-pack/plugins/apm/public/components/app/trace_overview/trace_list.tsx index 341661a108565..765a4b1aeae8c 100644 --- a/x-pack/plugins/apm/public/components/app/trace_overview/trace_list.tsx +++ b/x-pack/plugins/apm/public/components/app/trace_overview/trace_list.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiIcon, EuiToolTip } from '@elastic/eui'; +import { EuiIcon, EuiToolTip, RIGHT_ALIGNMENT } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; @@ -14,7 +14,7 @@ import { asTransactionRate, } from '../../../../common/utils/formatters'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; -import { truncate, unit } from '../../../utils/style'; +import { truncate } from '../../../utils/style'; import { EmptyMessage } from '../../shared/EmptyMessage'; import { ImpactBar } from '../../shared/ImpactBar'; import { TransactionDetailLink } from '../../shared/Links/apm/transaction_detail_link'; @@ -110,8 +110,7 @@ const traceListColumns: Array> = [ ), - width: `${unit * 6}px`, - align: 'left', + align: RIGHT_ALIGNMENT, sortable: true, render: (_, { impact }) => , }, diff --git a/x-pack/plugins/apm/public/components/shared/ImpactBar/__snapshots__/ImpactBar.test.js.snap b/x-pack/plugins/apm/public/components/shared/ImpactBar/__snapshots__/ImpactBar.test.js.snap index 87b5b68e26026..1c7b9c7662776 100644 --- a/x-pack/plugins/apm/public/components/shared/ImpactBar/__snapshots__/ImpactBar.test.js.snap +++ b/x-pack/plugins/apm/public/components/shared/ImpactBar/__snapshots__/ImpactBar.test.js.snap @@ -5,6 +5,11 @@ exports[`ImpactBar component should render with default values 1`] = ` color="primary" max={100} size="m" + style={ + Object { + "width": "96px", + } + } value={25} /> `; @@ -14,6 +19,11 @@ exports[`ImpactBar component should render with overridden values 1`] = ` color="danger" max={5} size="s" + style={ + Object { + "width": "96px", + } + } value={2} /> `; diff --git a/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx b/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx index 87b3c669e993c..5c7c4edec1850 100644 --- a/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx @@ -7,6 +7,7 @@ import { EuiProgress } from '@elastic/eui'; import React from 'react'; +import { unit } from '../../../utils/style'; // TODO: extend from EUI's EuiProgress prop interface export interface ImpactBarProps extends Record { @@ -16,6 +17,8 @@ export interface ImpactBarProps extends Record { color?: string; } +const style = { width: `${unit * 6}px` }; + export function ImpactBar({ value, size = 'm', @@ -24,6 +27,13 @@ export function ImpactBar({ ...rest }: ImpactBarProps) { return ( - + ); } diff --git a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx index 2743e957cd8ee..2f38ab9cdeb4b 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx @@ -40,6 +40,8 @@ function hasValidTimeseries( return !!series?.some((point) => point.y !== null); } +const flexGroupStyle = { overflow: 'hidden' }; + export function SparkPlot({ color, series, @@ -80,11 +82,15 @@ export function SparkPlot({ return ( + + {valueLabel} + {hasValidTimeseries(series) ? ( @@ -129,9 +135,6 @@ export function SparkPlot({

)} - - {valueLabel} - ); } diff --git a/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx index 63d78ab4d4e40..a034eeb3d284a 100644 --- a/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx @@ -5,7 +5,12 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + RIGHT_ALIGNMENT, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { ConnectionStatsItemWithComparisonData } from '../../../../common/connections'; @@ -14,15 +19,15 @@ import { asPercent, asTransactionRate, } from '../../../../common/utils/formatters'; +import { useBreakpoints } from '../../../hooks/use_breakpoints'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { unit } from '../../../utils/style'; -import { SparkPlot } from '../charts/spark_plot'; +import { EmptyMessage } from '../EmptyMessage'; import { ImpactBar } from '../ImpactBar'; -import { TruncateWithTooltip } from '../truncate_with_tooltip'; +import { ListMetric } from '../list_metric'; import { ITableColumn, ManagedTable } from '../managed_table'; -import { EmptyMessage } from '../EmptyMessage'; -import { TableFetchWrapper } from '../table_fetch_wrapper'; import { OverviewTableContainer } from '../overview_table_container'; +import { TableFetchWrapper } from '../table_fetch_wrapper'; +import { TruncateWithTooltip } from '../truncate_with_tooltip'; export type DependenciesItem = Omit< ConnectionStatsItemWithComparisonData, @@ -35,6 +40,7 @@ export type DependenciesItem = Omit< interface Props { dependencies: DependenciesItem[]; fixedHeight?: boolean; + isSingleColumn?: boolean; link?: React.ReactNode; title: React.ReactNode; nameColumnTitle: React.ReactNode; @@ -46,6 +52,7 @@ export function DependenciesTable(props: Props) { const { dependencies, fixedHeight, + isSingleColumn = true, link, title, nameColumnTitle, @@ -53,6 +60,10 @@ export function DependenciesTable(props: Props) { compact = true, } = props; + // SparkPlots should be hidden if we're in two-column view and size XL (1200px) + const { isXl } = useBreakpoints(); + const shouldShowSparkPlots = isSingleColumn || !isXl; + const columns: Array> = [ { field: 'name', @@ -68,12 +79,13 @@ export function DependenciesTable(props: Props) { name: i18n.translate('xpack.apm.dependenciesTable.columnLatency', { defaultMessage: 'Latency (avg.)', }), - width: `${unit * 11}px`, + align: RIGHT_ALIGNMENT, render: (_, { currentStats, previousStats }) => { return ( - { return ( - { return ( - { return ( - + diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/ServiceListMetric.tsx b/x-pack/plugins/apm/public/components/shared/list_metric.tsx similarity index 50% rename from x-pack/plugins/apm/public/components/app/service_inventory/service_list/ServiceListMetric.tsx rename to x-pack/plugins/apm/public/components/shared/list_metric.tsx index 7a4721407e69b..df70ced1daa53 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/ServiceListMetric.tsx +++ b/x-pack/plugins/apm/public/components/shared/list_metric.tsx @@ -6,32 +6,19 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import React from 'react'; -import { Coordinate } from '../../../../../typings/timeseries'; -import { SparkPlot } from '../../../shared/charts/spark_plot'; +import React, { ComponentProps } from 'react'; +import { SparkPlot } from './charts/spark_plot'; -export function ServiceListMetric({ - color, - series, - valueLabel, - comparisonSeries, - hideSeries = false, -}: { - color: 'euiColorVis1' | 'euiColorVis0' | 'euiColorVis7'; - series?: Coordinate[]; - comparisonSeries?: Coordinate[]; - valueLabel: React.ReactNode; +interface ListMetricProps extends ComponentProps { hideSeries?: boolean; -}) { +} + +export function ListMetric(props: ListMetricProps) { + const { hideSeries, ...sparkPlotProps } = props; + const { valueLabel } = sparkPlotProps; + if (!hideSeries) { - return ( - - ); + return ; } return ( diff --git a/x-pack/plugins/apm/public/components/shared/overview_table_container/index.tsx b/x-pack/plugins/apm/public/components/shared/overview_table_container/index.tsx index 6caf6aca02733..15246d5375abe 100644 --- a/x-pack/plugins/apm/public/components/shared/overview_table_container/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/overview_table_container/index.tsx @@ -7,7 +7,7 @@ import React, { ReactNode } from 'react'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; -import { useBreakPoints } from '../../../hooks/use_break_points'; +import { useBreakpoints } from '../../../hooks/use_breakpoints'; /** * The height for a table on a overview page. Is the height of a 5-row basic @@ -62,7 +62,7 @@ export function OverviewTableContainer({ fixedHeight?: boolean; isEmptyAndNotInitiated: boolean; }) { - const { isMedium } = useBreakPoints(); + const { isMedium } = useBreakpoints(); return ( > { return [ { @@ -71,16 +77,17 @@ export function getColumns({ field: 'latency', sortable: true, name: getLatencyColumnLabel(latencyAggregationType), - width: `${unit * 11}px`, + align: RIGHT_ALIGNMENT, render: (_, { latency, name }) => { const currentTimeseries = transactionGroupDetailedStatistics?.currentPeriod?.[name]?.latency; const previousTimeseries = transactionGroupDetailedStatistics?.previousPeriod?.[name]?.latency; return ( - { const currentTimeseries = transactionGroupDetailedStatistics?.currentPeriod?.[name]?.throughput; @@ -105,9 +112,10 @@ export function getColumns({ transactionGroupDetailedStatistics?.previousPeriod?.[name] ?.throughput; return ( - { const currentTimeseries = transactionGroupDetailedStatistics?.currentPeriod?.[name]?.errorRate; const previousTimeseries = transactionGroupDetailedStatistics?.previousPeriod?.[name]?.errorRate; return ( - { const currentImpact = transactionGroupDetailedStatistics?.currentPeriod?.[name]?.impact ?? @@ -158,7 +167,7 @@ export function getColumns({ const previousImpact = transactionGroupDetailedStatistics?.previousPeriod?.[name]?.impact; return ( - + diff --git a/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx b/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx index 5d9f96500f101..08fc9e54c1444 100644 --- a/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx @@ -28,6 +28,7 @@ import { getTimeRangeComparison } from '../time_comparison/get_time_range_compar import { OverviewTableContainer } from '../overview_table_container'; import { getColumns } from './get_columns'; import { ElasticDocsLink } from '../Links/ElasticDocsLink'; +import { useBreakpoints } from '../../../hooks/use_breakpoints'; type ApiResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups/main_statistics'>; @@ -57,6 +58,7 @@ const DEFAULT_SORT = { interface Props { hideViewTransactionsLink?: boolean; + isSingleColumn?: boolean; numberOfTransactionsPerPage?: number; showAggregationAccurateCallout?: boolean; environment: string; @@ -69,6 +71,7 @@ interface Props { export function TransactionsTable({ fixedHeight = false, hideViewTransactionsLink = false, + isSingleColumn = true, numberOfTransactionsPerPage = 5, showAggregationAccurateCallout = false, environment, @@ -87,6 +90,10 @@ export function TransactionsTable({ sort: DEFAULT_SORT, }); + // SparkPlots should be hidden if we're in two-column view and size XL (1200px) + const { isXl } = useBreakpoints(); + const shouldShowSparkPlots = isSingleColumn || !isXl; + const { pageIndex, sort } = tableOptions; const { direction, field } = sort; @@ -214,6 +221,7 @@ export function TransactionsTable({ latencyAggregationType, transactionGroupDetailedStatistics, comparisonEnabled, + shouldShowSparkPlots, }); const isLoading = status === FETCH_STATUS.LOADING; diff --git a/x-pack/plugins/apm/public/hooks/use_break_points.test.ts b/x-pack/plugins/apm/public/hooks/use_breakpoints.test.ts similarity index 97% rename from x-pack/plugins/apm/public/hooks/use_break_points.test.ts rename to x-pack/plugins/apm/public/hooks/use_breakpoints.test.ts index 66cfd3119a26b..93b738a1e3e50 100644 --- a/x-pack/plugins/apm/public/hooks/use_break_points.test.ts +++ b/x-pack/plugins/apm/public/hooks/use_breakpoints.test.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { getScreenSizes } from './use_break_points'; +import { getScreenSizes } from './use_breakpoints'; -describe('use_break_points', () => { +describe('use_breakpoints', () => { describe('getScreenSizes', () => { it('return xs when within 0px - 5740x', () => { expect(getScreenSizes(0)).toEqual({ diff --git a/x-pack/plugins/apm/public/hooks/use_break_points.ts b/x-pack/plugins/apm/public/hooks/use_breakpoints.ts similarity index 75% rename from x-pack/plugins/apm/public/hooks/use_break_points.ts rename to x-pack/plugins/apm/public/hooks/use_breakpoints.ts index 3b428a02dc97b..f87ddb929b66a 100644 --- a/x-pack/plugins/apm/public/hooks/use_break_points.ts +++ b/x-pack/plugins/apm/public/hooks/use_breakpoints.ts @@ -8,9 +8,13 @@ import { useState } from 'react'; import useWindowSize from 'react-use/lib/useWindowSize'; import useDebounce from 'react-use/lib/useDebounce'; -import { isWithinMaxBreakpoint, isWithinMinBreakpoint } from '@elastic/eui'; +import { + getBreakpoint, + isWithinMaxBreakpoint, + isWithinMinBreakpoint, +} from '@elastic/eui'; -export type BreakPoints = ReturnType; +export type Breakpoints = ReturnType; export function getScreenSizes(windowWidth: number) { return { @@ -24,17 +28,19 @@ export function getScreenSizes(windowWidth: number) { }; } -export function useBreakPoints() { +export function useBreakpoints() { const { width } = useWindowSize(); + const [breakpoint, setBreakpoint] = useState(getBreakpoint(width)); const [screenSizes, setScreenSizes] = useState(getScreenSizes(width)); useDebounce( () => { + setBreakpoint(getBreakpoint(width)); setScreenSizes(getScreenSizes(width)); }, 50, [width] ); - return screenSizes; + return { ...screenSizes, breakpoint, width }; } From 788f18070bb96e0efc75874c8946ad8a6b663b0f Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Sep 2021 15:17:35 -0400 Subject: [PATCH 012/139] Share history package via shared deps (#111537) (#111579) Co-authored-by: Tim Roes --- packages/kbn-ui-shared-deps/src/entry.js | 1 + packages/kbn-ui-shared-deps/src/index.js | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/kbn-ui-shared-deps/src/entry.js b/packages/kbn-ui-shared-deps/src/entry.js index 20e26ca6a2864..013d5f894c013 100644 --- a/packages/kbn-ui-shared-deps/src/entry.js +++ b/packages/kbn-ui-shared-deps/src/entry.js @@ -55,3 +55,4 @@ export const KbnAnalytics = require('@kbn/analytics'); export const KbnStd = require('@kbn/std'); export const SaferLodashSet = require('@elastic/safer-lodash-set'); export const RisonNode = require('rison-node'); +export const History = require('history'); diff --git a/packages/kbn-ui-shared-deps/src/index.js b/packages/kbn-ui-shared-deps/src/index.js index 291c7c471d27c..3d3553ba23546 100644 --- a/packages/kbn-ui-shared-deps/src/index.js +++ b/packages/kbn-ui-shared-deps/src/index.js @@ -100,6 +100,7 @@ exports.externals = { '@kbn/std': '__kbnSharedDeps__.KbnStd', '@elastic/safer-lodash-set': '__kbnSharedDeps__.SaferLodashSet', 'rison-node': '__kbnSharedDeps__.RisonNode', + history: '__kbnSharedDeps__.History', }; /** From d39a53f27486a534fb87ff81cdcd1c72a350a5cf Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Wed, 8 Sep 2021 14:35:31 -0500 Subject: [PATCH 013/139] [7.x] [docker] Replace deprecated server.host value in default config (#111467) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/setup/docker.asciidoc | 2 +- .../docker_generator/templates/kibana_yml.template.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/setup/docker.asciidoc b/docs/setup/docker.asciidoc index 9b285103793bb..ac55b3b98ff68 100644 --- a/docs/setup/docker.asciidoc +++ b/docs/setup/docker.asciidoc @@ -144,7 +144,7 @@ The following settings have different default values when using the Docker images: [horizontal] -`server.host`:: `"0"` +`server.host`:: `"0.0.0.0"` `elasticsearch.hosts`:: `http://elasticsearch:9200` `monitoring.ui.container.elasticsearch.enabled`:: `true` diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts index 395f69fd9df23..e22d8ecdd4fd8 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts @@ -17,7 +17,7 @@ function generator({ imageFlavor }: TemplateContext) { # # Default Kibana configuration for docker target - server.host: "0" + server.host: "0.0.0.0" server.shutdownTimeout: "5s" elasticsearch.hosts: [ "http://elasticsearch:9200" ] ${!imageFlavor ? 'monitoring.ui.container.elasticsearch.enabled: true' : ''} From 2db1a6178f741c96d06f2578327f4ecb44a28f5e Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Sep 2021 15:53:51 -0400 Subject: [PATCH 014/139] [Uptime] Added loading for certificates page on initial load (#111526) (#111604) Co-authored-by: Shahzad --- .../components/certificates/certificates_list.tsx | 10 ++++++++-- .../public/components/certificates/translations.ts | 4 ++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/certificates/certificates_list.tsx b/x-pack/plugins/uptime/public/components/certificates/certificates_list.tsx index ec505472f584a..f01cdc5dfb9c8 100644 --- a/x-pack/plugins/uptime/public/components/certificates/certificates_list.tsx +++ b/x-pack/plugins/uptime/public/components/certificates/certificates_list.tsx @@ -15,7 +15,7 @@ import { CertMonitors } from './cert_monitors'; import * as labels from './translations'; import { Cert, CertMonitor } from '../../../common/runtime_types'; import { FingerprintCol } from './fingerprint_col'; -import { NO_CERTS_AVAILABLE } from './translations'; +import { LOADING_CERTIFICATES, NO_CERTS_AVAILABLE } from './translations'; interface Page { index: number; @@ -111,7 +111,13 @@ export const CertificateList: React.FC = ({ page, sort, onChange }) => { direction: sort.direction, }, }} - noItemsMessage={{NO_CERTS_AVAILABLE}} + noItemsMessage={ + loading ? ( + LOADING_CERTIFICATES + ) : ( + {NO_CERTS_AVAILABLE} + ) + } /> ); }; diff --git a/x-pack/plugins/uptime/public/components/certificates/translations.ts b/x-pack/plugins/uptime/public/components/certificates/translations.ts index 9e60bc79fe3a4..45ea2bf9c47cf 100644 --- a/x-pack/plugins/uptime/public/components/certificates/translations.ts +++ b/x-pack/plugins/uptime/public/components/certificates/translations.ts @@ -70,3 +70,7 @@ export const COPY_FINGERPRINT = i18n.translate('xpack.uptime.certs.list.copyFing export const NO_CERTS_AVAILABLE = i18n.translate('xpack.uptime.certs.list.empty', { defaultMessage: 'No Certificates found. Note: Certificates are only visible for Heartbeat 7.8+', }); + +export const LOADING_CERTIFICATES = i18n.translate('xpack.uptime.certificates.loading', { + defaultMessage: 'Loading certificates ...', +}); From f15cc547ecc6048daeac73f7eb09a6488d04eff8 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 8 Sep 2021 12:53:37 -0700 Subject: [PATCH 015/139] skip flaky suite (#108352) (cherry picked from commit b328fb39819167f95f0ae57f757ab3ff2d5f4d6d) --- x-pack/test/functional/apps/lens/drag_and_drop.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/lens/drag_and_drop.ts b/x-pack/test/functional/apps/lens/drag_and_drop.ts index e7b7ba18d62fb..b53d4370d561e 100644 --- a/x-pack/test/functional/apps/lens/drag_and_drop.ts +++ b/x-pack/test/functional/apps/lens/drag_and_drop.ts @@ -11,7 +11,8 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); - describe('lens drag and drop tests', () => { + // FLAKY: https://github.com/elastic/kibana/issues/108352 + describe.skip('lens drag and drop tests', () => { describe('basic drag and drop', () => { it('should construct the basic split xy chart', async () => { await PageObjects.visualize.navigateToNewVisualization(); From b2481a65a556f407615673b6e34ba08f9bfbac25 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 8 Sep 2021 12:56:46 -0700 Subject: [PATCH 016/139] skip flaky suite (#111628) (cherry picked from commit 32e98730b50b87238aafb2e291063d6eaae36699) --- x-pack/test/functional/apps/lens/add_to_dashboard.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/lens/add_to_dashboard.ts b/x-pack/test/functional/apps/lens/add_to_dashboard.ts index 5e51573e32503..6a04415fab26e 100644 --- a/x-pack/test/functional/apps/lens/add_to_dashboard.ts +++ b/x-pack/test/functional/apps/lens/add_to_dashboard.ts @@ -62,7 +62,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.assertMetric('Maximum of bytes', '19,986'); }; - describe('lens add-to-dashboards tests', () => { + // FLAKY: https://github.com/elastic/kibana/issues/111628 + describe.skip('lens add-to-dashboards tests', () => { it('should allow new lens to be added by value to a new dashboard', async () => { await createNewLens(); await PageObjects.lens.save('New Lens from Modal', false, false, false, 'new'); From 56cb2fdd36e33d40381f93786e8d4b4e68a168f6 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Wed, 8 Sep 2021 12:57:33 -0700 Subject: [PATCH 017/139] [7.x] Replace hard-coded doc links in uptime #111075 (#111576) --- .../kibana-plugin-core-public.doclinksstart.links.md | 8 +++++++- .../kibana-plugin-core-public.doclinksstart.md | 2 +- src/core/public/doc_links/doc_links_service.ts | 12 +++++++++++- src/core/public/public.api.md | 8 +++++++- x-pack/plugins/uptime/public/apps/render_app.tsx | 4 ++-- .../public/lib/alert_types/duration_anomaly.tsx | 2 +- .../uptime/public/lib/alert_types/monitor_status.tsx | 2 +- x-pack/plugins/uptime/public/lib/alert_types/tls.tsx | 2 +- 8 files changed, 31 insertions(+), 9 deletions(-) diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index c556d58421737..7e4ed7ec2dd1b 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -188,7 +188,13 @@ readonly links: { timeUnits: string; updateTransform: string; }>; - readonly observability: Record; + readonly observability: Readonly<{ + guide: string; + monitorStatus: string; + monitorUptime: string; + tlsCertificate: string; + uptimeDurationAnomaly: string; + }>; readonly alerting: Record; readonly maps: Record; readonly monitoring: Record; diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index 7d862f50bba18..89156501d476e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly apm: {
readonly kibanaSettings: string;
readonly supportedServiceMaps: string;
readonly customLinks: string;
readonly droppedTransactionSpans: string;
readonly upgrading: string;
readonly metaData: string;
};
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
readonly suricataModule: string;
readonly zeekModule: string;
};
readonly auditbeat: {
readonly base: string;
readonly auditdModule: string;
readonly systemModule: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly libbeat: {
readonly getStarted: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly privileges: string;
readonly guide: string;
readonly gettingStarted: string;
readonly ml: string;
readonly ruleChangeLog: string;
readonly detectionsReq: string;
readonly networkMap: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
readonly autocompleteChanges: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
}>;
readonly ecs: {
readonly guide: string;
};
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly apm: {
readonly kibanaSettings: string;
readonly supportedServiceMaps: string;
readonly customLinks: string;
readonly droppedTransactionSpans: string;
readonly upgrading: string;
readonly metaData: string;
};
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
readonly suricataModule: string;
readonly zeekModule: string;
};
readonly auditbeat: {
readonly base: string;
readonly auditdModule: string;
readonly systemModule: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly libbeat: {
readonly getStarted: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly privileges: string;
readonly guide: string;
readonly gettingStarted: string;
readonly ml: string;
readonly ruleChangeLog: string;
readonly detectionsReq: string;
readonly networkMap: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
readonly autocompleteChanges: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Readonly<{
guide: string;
monitorStatus: string;
monitorUptime: string;
tlsCertificate: string;
uptimeDurationAnomaly: string;
}>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
}>;
readonly ecs: {
readonly guide: string;
};
} | | diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index e4d440b2d8c83..afa74f8eaa6ab 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -284,6 +284,10 @@ export class DocLinksService { }, observability: { guide: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/index.html`, + monitorStatus: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/monitor-status-alert.html`, + monitorUptime: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/monitor-uptime.html`, + tlsCertificate: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/tls-certificate-alert.html`, + uptimeDurationAnomaly: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/duration-anomaly-alert.html`, }, alerting: { guide: `${KIBANA_DOCS}create-and-manage-rules.html`, @@ -641,7 +645,13 @@ export interface DocLinksStart { timeUnits: string; updateTransform: string; }>; - readonly observability: Record; + readonly observability: Readonly<{ + guide: string; + monitorStatus: string; + monitorUptime: string; + tlsCertificate: string; + uptimeDurationAnomaly: string; + }>; readonly alerting: Record; readonly maps: Record; readonly monitoring: Record; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 0038c193c510e..f0d9ad2a40cb9 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -654,7 +654,13 @@ export interface DocLinksStart { timeUnits: string; updateTransform: string; }>; - readonly observability: Record; + readonly observability: Readonly<{ + guide: string; + monitorStatus: string; + monitorUptime: string; + tlsCertificate: string; + uptimeDurationAnomaly: string; + }>; readonly alerting: Record; readonly maps: Record; readonly monitoring: Record; diff --git a/x-pack/plugins/uptime/public/apps/render_app.tsx b/x-pack/plugins/uptime/public/apps/render_app.tsx index 07119f985e738..376fb4c8364c0 100644 --- a/x-pack/plugins/uptime/public/apps/render_app.tsx +++ b/x-pack/plugins/uptime/public/apps/render_app.tsx @@ -27,7 +27,7 @@ export function renderApp( const { application: { capabilities }, chrome: { setBadge, setHelpExtension }, - docLinks: { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL }, + docLinks, http: { basePath }, i18n, } = core; @@ -59,7 +59,7 @@ export function renderApp( links: [ { linkType: 'documentation', - href: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/monitor-uptime.html`, + href: `${docLinks.links.observability.monitorUptime}`, }, { linkType: 'discuss', diff --git a/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx b/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx index 9e0b0b7931efa..3a97eaa2d8402 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx +++ b/x-pack/plugins/uptime/public/lib/alert_types/duration_anomaly.tsx @@ -26,7 +26,7 @@ export const initDurationAnomalyAlertType: AlertTypeInitializer = ({ id: CLIENT_ALERT_TYPES.DURATION_ANOMALY, iconClass: 'uptimeApp', documentationUrl(docLinks) { - return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/observability/${docLinks.DOC_LINK_VERSION}/duration-anomaly-alert.html`; + return `${docLinks.links.observability.uptimeDurationAnomaly}`; }, alertParamsExpression: (params: unknown) => ( diff --git a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx index 58dbfdfbf26db..5b0c2f77b9f49 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx +++ b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx @@ -37,7 +37,7 @@ export const initMonitorStatusAlertType: AlertTypeInitializer = ({ description, iconClass: 'uptimeApp', documentationUrl(docLinks) { - return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/observability/${docLinks.DOC_LINK_VERSION}/monitor-status-alert.html`; + return `${docLinks.links.observability.monitorStatus}`; }, alertParamsExpression: (params: any) => ( diff --git a/x-pack/plugins/uptime/public/lib/alert_types/tls.tsx b/x-pack/plugins/uptime/public/lib/alert_types/tls.tsx index f8abfb6b6dd00..c83743c2373f7 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/tls.tsx +++ b/x-pack/plugins/uptime/public/lib/alert_types/tls.tsx @@ -23,7 +23,7 @@ export const initTlsAlertType: AlertTypeInitializer = ({ id: CLIENT_ALERT_TYPES.TLS, iconClass: 'uptimeApp', documentationUrl(docLinks) { - return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/observability/${docLinks.DOC_LINK_VERSION}/tls-certificate-alert.html`; + return `${docLinks.links.observability.tlsCertificate}`; }, alertParamsExpression: (params: any) => ( From 5739942a2a4ad71a39602617fb147d372096a1cc Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Sep 2021 16:15:54 -0400 Subject: [PATCH 018/139] [docs/docker] Include server.shutdownTimeout in default configuration (#111468) (#111647) Co-authored-by: Jonathan Budzenski --- docs/setup/docker.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/setup/docker.asciidoc b/docs/setup/docker.asciidoc index ac55b3b98ff68..ed8f8e17c9e53 100644 --- a/docs/setup/docker.asciidoc +++ b/docs/setup/docker.asciidoc @@ -145,6 +145,7 @@ images: [horizontal] `server.host`:: `"0.0.0.0"` +`server.shutdownTimeout`:: `"5s"` `elasticsearch.hosts`:: `http://elasticsearch:9200` `monitoring.ui.container.elasticsearch.enabled`:: `true` From 9df2914200fc933ba65b2414f298dc10e914d760 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Sep 2021 16:17:06 -0400 Subject: [PATCH 019/139] [Discover] Cleanup legacy code fragments (#110646) (#111608) Co-authored-by: Matthias Wilhelm --- .../public/application/application.ts | 39 --- .../components/action_bar/action_bar.tsx | 148 ++++++----- .../context_error_message.tsx | 40 ++- .../application/apps/context/context_app.tsx | 6 +- .../services/context.predecessors.test.ts | 3 +- .../services/context.successors.test.ts | 4 +- .../services/utils/get_es_query_sort.ts | 2 +- .../apps/context/services/utils/sorting.ts | 2 +- .../application/apps/doc/components/doc.tsx | 142 +++++------ .../components/table_header/helpers.tsx | 2 +- .../table_header/table_header.test.tsx | 2 +- .../components/table_header/table_header.tsx | 2 +- .../doc_table/doc_table_wrapper.tsx | 3 +- .../doc_table/lib/get_default_sort.ts | 2 +- .../main/components/doc_table/lib/get_sort.ts | 4 +- .../lib/get_sort_for_search_source.ts | 2 +- .../doc_table/lib/row_formatter.tsx | 3 +- .../components/layout/discover_layout.tsx | 239 +++++++++--------- .../sidebar/discover_index_pattern.tsx | 37 ++- .../sidebar/lib/field_calculator.js | 14 +- .../sidebar/lib/field_calculator.test.ts | 20 +- .../components/top_nav/get_top_nav_links.ts | 2 +- .../uninitialized/uninitialized.tsx | 53 ++-- .../apps/main/utils/calc_field_counts.ts | 2 +- .../get_switch_index_pattern_app_state.ts | 2 +- .../apps/main/utils/resolve_index_pattern.ts | 6 +- .../discover_grid/discover_grid.tsx | 69 +++-- .../discover_grid/discover_grid_context.tsx | 2 +- .../discover_grid/discover_grid_flyout.tsx | 2 +- .../discover_grid/get_render_cell_value.tsx | 2 +- .../components/doc_viewer/doc_viewer_tab.tsx | 9 +- .../source_viewer/source_viewer.test.tsx | 3 +- .../components/table/table.test.tsx | 9 +- .../public/application/discover_router.tsx | 8 +- .../helpers/use_data_grid_columns.ts | 2 +- .../discover/public/application/index.tsx | 11 +- .../discover/public/kibana_services.ts | 24 +- src/plugins/discover/public/plugin.tsx | 5 +- test/functional/page_objects/discover_page.ts | 2 +- 39 files changed, 421 insertions(+), 508 deletions(-) delete mode 100644 src/plugins/discover/public/application/application.ts diff --git a/src/plugins/discover/public/application/application.ts b/src/plugins/discover/public/application/application.ts deleted file mode 100644 index c0294ca043895..0000000000000 --- a/src/plugins/discover/public/application/application.ts +++ /dev/null @@ -1,39 +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 { renderApp as renderReactApp } from './index'; - -/** - * Here's where Discover is mounted and rendered - */ -export async function renderApp(moduleName: string, element: HTMLElement) { - const app = mountDiscoverApp(moduleName, element); - return () => { - app(); - }; -} - -function buildDiscoverElement(mountpoint: HTMLElement) { - // due to legacy angular tags, we need some manual DOM intervention here - const appWrapper = document.createElement('div'); - const discoverApp = document.createElement('discover-app'); - const discover = document.createElement('discover'); - appWrapper.appendChild(discoverApp); - discoverApp.append(discover); - mountpoint.appendChild(appWrapper); - return discover; -} - -function mountDiscoverApp(moduleName: string, element: HTMLElement) { - const mountpoint = document.createElement('div'); - const discoverElement = buildDiscoverElement(mountpoint); - // @ts-expect-error - const app = renderReactApp({ element: discoverElement }); - element.appendChild(mountpoint); - return app; -} diff --git a/src/plugins/discover/public/application/apps/context/components/action_bar/action_bar.tsx b/src/plugins/discover/public/application/apps/context/components/action_bar/action_bar.tsx index 634e6d2c90a91..d9d56964358f8 100644 --- a/src/plugins/discover/public/application/apps/context/components/action_bar/action_bar.tsx +++ b/src/plugins/discover/public/application/apps/context/components/action_bar/action_bar.tsx @@ -9,7 +9,7 @@ import './_action_bar.scss'; import React, { useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty, EuiFieldNumber, @@ -84,84 +84,78 @@ export function ActionBar({ } }, [docCount, newDocCount]); return ( - -
- {isSuccessor && } - {isSuccessor && showWarning && ( - - )} - {isSuccessor && showWarning && } - - - { - const value = newDocCount + defaultStepSize; - if (isValid(value)) { - setNewDocCount(value); - onChangeCount(type, value); - } + + {isSuccessor && } + {isSuccessor && showWarning && } + {isSuccessor && showWarning && } + + + { + const value = newDocCount + defaultStepSize; + if (isValid(value)) { + setNewDocCount(value); + onChangeCount(type, value); + } + }} + flush="right" + > + + + + + + { + setNewDocCount(ev.target.valueAsNumber); }} - flush="right" - > - - - - - - { + if (newDocCount !== docCount && isValid(newDocCount)) { + onChangeCount(type, newDocCount); } - compressed - className="cxtSizePicker" - data-test-subj={`${type}CountPicker`} - disabled={isDisabled} - min={MIN_CONTEXT_SIZE} - max={MAX_CONTEXT_SIZE} - onChange={(ev) => { - setNewDocCount(ev.target.valueAsNumber); - }} - onBlur={() => { - if (newDocCount !== docCount && isValid(newDocCount)) { - onChangeCount(type, newDocCount); - } - }} - type="number" - value={newDocCount >= 0 ? newDocCount : ''} + }} + type="number" + value={newDocCount >= 0 ? newDocCount : ''} + /> + + + + + {isSuccessor ? ( + + ) : ( + - - - - - {isSuccessor ? ( - - ) : ( - - )} - - - - {!isSuccessor && showWarning && ( - - )} - {!isSuccessor && } - -
+ )} + + +
+ {!isSuccessor && showWarning && } + {!isSuccessor && } + ); } diff --git a/src/plugins/discover/public/application/apps/context/components/context_error_message/context_error_message.tsx b/src/plugins/discover/public/application/apps/context/components/context_error_message/context_error_message.tsx index fac948d0f7040..fc05deeae7e51 100644 --- a/src/plugins/discover/public/application/apps/context/components/context_error_message/context_error_message.tsx +++ b/src/plugins/discover/public/application/apps/context/components/context_error_message/context_error_message.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiCallOut, EuiText } from '@elastic/eui'; -import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { FailureReason, LoadingStatus, @@ -27,27 +27,25 @@ export function ContextErrorMessage({ status }: ContextErrorMessageProps) { return null; } return ( - - + } + color="danger" + iconType="alert" + data-test-subj="contextErrorMessageTitle" + > + + {status.reason === FailureReason.UNKNOWN && ( - } - color="danger" - iconType="alert" - data-test-subj="contextErrorMessageTitle" - > - - {status.reason === FailureReason.UNKNOWN && ( - - )} - - - + )} + +
); } diff --git a/src/plugins/discover/public/application/apps/context/context_app.tsx b/src/plugins/discover/public/application/apps/context/context_app.tsx index 6198ced1550bb..070391edae71c 100644 --- a/src/plugins/discover/public/application/apps/context/context_app.tsx +++ b/src/plugins/discover/public/application/apps/context/context_app.tsx @@ -9,7 +9,7 @@ import React, { Fragment, memo, useEffect, useRef, useMemo, useCallback } from 'react'; import './context_app.scss'; import classNames from 'classnames'; -import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiText, EuiPageContent, EuiPage, EuiSpacer } from '@elastic/eui'; import { cloneDeep } from 'lodash'; import { esFilters, SortDirection } from '../../../../../data/public'; @@ -138,7 +138,7 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp }; return ( - + {fetchedState.anchorStatus.value === LoadingStatus.FAILED ? ( ) : ( @@ -182,6 +182,6 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp )} - + ); }; diff --git a/src/plugins/discover/public/application/apps/context/services/context.predecessors.test.ts b/src/plugins/discover/public/application/apps/context/services/context.predecessors.test.ts index a503715f4b5e2..6fad858488c4e 100644 --- a/src/plugins/discover/public/application/apps/context/services/context.predecessors.test.ts +++ b/src/plugins/discover/public/application/apps/context/services/context.predecessors.test.ts @@ -8,9 +8,10 @@ import moment from 'moment'; import { get, last } from 'lodash'; +import { SortDirection } from 'src/plugins/data/common'; import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; import { fetchContextProvider, SurrDocType } from './context'; -import { setServices, SortDirection } from '../../../../kibana_services'; +import { setServices } from '../../../../kibana_services'; import { Query } from '../../../../../../data/public'; import { DiscoverServices } from '../../../../build_services'; import { EsHitRecord, EsHitRecordList } from '../../../types'; diff --git a/src/plugins/discover/public/application/apps/context/services/context.successors.test.ts b/src/plugins/discover/public/application/apps/context/services/context.successors.test.ts index fcd1bad487c4e..6c44f0aa3f7b5 100644 --- a/src/plugins/discover/public/application/apps/context/services/context.successors.test.ts +++ b/src/plugins/discover/public/application/apps/context/services/context.successors.test.ts @@ -8,9 +8,9 @@ import moment from 'moment'; import { get, last } from 'lodash'; - +import { SortDirection } from 'src/plugins/data/common'; import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; -import { setServices, SortDirection } from '../../../../kibana_services'; +import { setServices } from '../../../../kibana_services'; import { Query } from '../../../../../../data/public'; import { fetchContextProvider, SurrDocType } from './context'; import { DiscoverServices } from '../../../../build_services'; diff --git a/src/plugins/discover/public/application/apps/context/services/utils/get_es_query_sort.ts b/src/plugins/discover/public/application/apps/context/services/utils/get_es_query_sort.ts index 2144d2f1cd7fd..955ae983d2caa 100644 --- a/src/plugins/discover/public/application/apps/context/services/utils/get_es_query_sort.ts +++ b/src/plugins/discover/public/application/apps/context/services/utils/get_es_query_sort.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { EsQuerySortValue, SortDirection } from '../../../../../kibana_services'; +import type { EsQuerySortValue, SortDirection } from 'src/plugins/data/common'; /** * Returns `EsQuerySort` which is used to sort records in the ES query diff --git a/src/plugins/discover/public/application/apps/context/services/utils/sorting.ts b/src/plugins/discover/public/application/apps/context/services/utils/sorting.ts index c6f389ddca46a..c8ce787707cab 100644 --- a/src/plugins/discover/public/application/apps/context/services/utils/sorting.ts +++ b/src/plugins/discover/public/application/apps/context/services/utils/sorting.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { IndexPattern } from '../../../../../kibana_services'; +import type { IndexPattern } from 'src/plugins/data/common'; export enum SortDirection { asc = 'asc', diff --git a/src/plugins/discover/public/application/apps/doc/components/doc.tsx b/src/plugins/discover/public/application/apps/doc/components/doc.tsx index b184a1cfd238c..f33ffe561e490 100644 --- a/src/plugins/discover/public/application/apps/doc/components/doc.tsx +++ b/src/plugins/discover/public/application/apps/doc/components/doc.tsx @@ -7,7 +7,7 @@ */ import React from 'react'; -import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCallOut, EuiLink, EuiLoadingSpinner, EuiPageContent, EuiPage } from '@elastic/eui'; import { IndexPatternsContract } from 'src/plugins/data/public'; import { getServices } from '../../../../kibana_services'; @@ -43,82 +43,80 @@ export function Doc(props: DocProps) { const [reqState, hit, indexPattern] = useEsDocSearch(props); const indexExistsLink = getServices().docLinks.links.apis.indexExists; return ( - - - - {reqState === ElasticRequestState.NotFoundIndexPattern && ( - - } - /> - )} - {reqState === ElasticRequestState.NotFound && ( - - } - > + + + {reqState === ElasticRequestState.NotFoundIndexPattern && ( + + } + /> + )} + {reqState === ElasticRequestState.NotFound && ( + - - )} + } + > + + + )} - {reqState === ElasticRequestState.Error && ( - - } - > + {reqState === ElasticRequestState.Error && ( + {' '} - - - - - )} + id="discover.doc.failedToExecuteQueryDescription" + defaultMessage="Cannot run search" + /> + } + > + {' '} + + + + + )} - {reqState === ElasticRequestState.Loading && ( - - {' '} - - - )} + {reqState === ElasticRequestState.Loading && ( + + {' '} + + + )} - {reqState === ElasticRequestState.Found && hit !== null && indexPattern && ( -
- -
- )} -
-
-
+ {reqState === ElasticRequestState.Found && hit !== null && indexPattern && ( +
+ +
+ )} + + ); } diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/helpers.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/helpers.tsx index 345d7ed8480bf..6f369e04676c4 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/helpers.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/helpers.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ import { i18n } from '@kbn/i18n'; -import { IndexPattern } from '../../../../../../../kibana_services'; +import type { IndexPattern } from 'src/plugins/data/common'; export type SortOrder = [string, string]; export interface ColumnProps { diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.test.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.test.tsx index 7b72e94169cfe..83320c1b6d3da 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.test.tsx @@ -8,10 +8,10 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; +import type { IndexPattern, IndexPatternField } from 'src/plugins/data/common'; import { TableHeader } from './table_header'; import { findTestSubject } from '@elastic/eui/lib/test'; import { SortOrder } from './helpers'; -import { IndexPattern, IndexPatternField } from '../../../../../../../kibana_services'; function getMockIndexPattern() { return ({ diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.tsx index cb8198f1d6d6a..f891e809ee702 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.tsx @@ -7,7 +7,7 @@ */ import React from 'react'; -import { IndexPattern } from '../../../../../../../kibana_services'; +import type { IndexPattern } from 'src/plugins/data/common'; import { TableHeaderColumn } from './table_header_column'; import { SortOrder, getDisplayedColumns } from './helpers'; import { getDefaultSort } from '../../lib/get_default_sort'; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.tsx index 086750ed4d359..08e7dcfb66695 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.tsx @@ -8,6 +8,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; +import type { IndexPattern, IndexPatternField } from 'src/plugins/data/common'; import { FormattedMessage } from '@kbn/i18n/react'; import { TableHeader } from './components/table_header/table_header'; import { FORMATS_UI_SETTINGS } from '../../../../../../../field_formats/common'; @@ -17,7 +18,7 @@ import { SHOW_MULTIFIELDS, SORT_DEFAULT_ORDER_SETTING, } from '../../../../../../common'; -import { getServices, IndexPattern, IndexPatternField } from '../../../../../kibana_services'; +import { getServices } from '../../../../../kibana_services'; import { SortOrder } from './components/table_header/helpers'; import { DocTableRow, TableRow } from './components/table_row'; import { DocViewFilterFn } from '../../../../doc_views/doc_views_types'; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.ts index e01ff0b00e2b0..2705140988148 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { IndexPattern } from '../../../../../../kibana_services'; +import type { IndexPattern } from 'src/plugins/data/common'; import { isSortable } from './get_sort'; import { SortOrder } from '../components/table_header/helpers'; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.ts index 2c687a59ea291..1e597f85666fc 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import _ from 'lodash'; +import { isPlainObject } from 'lodash'; import { IndexPattern } from '../../../../../../../../data/public'; export type SortPairObj = Record; @@ -30,7 +30,7 @@ function createSortObject( ) { const [field, direction] = sortPair as SortPairArr; return { [field]: direction }; - } else if (_.isPlainObject(sortPair) && isSortable(Object.keys(sortPair)[0], indexPattern)) { + } else if (isPlainObject(sortPair) && isSortable(Object.keys(sortPair)[0], indexPattern)) { return sortPair as SortPairObj; } } diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.ts index 2bc8a71301df9..de862fdcd29f2 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { EsQuerySortValue, IndexPattern } from '../../../../../../kibana_services'; +import type { EsQuerySortValue, IndexPattern } from 'src/plugins/data/common'; import { SortOrder } from '../components/table_header/helpers'; import { getSort } from './get_sort'; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.tsx index b85544bd84cde..ae3f1cd0057cc 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.tsx @@ -7,8 +7,9 @@ */ import React, { Fragment } from 'react'; +import type { IndexPattern } from 'src/plugins/data/common'; import { MAX_DOC_FIELDS_DISPLAYED } from '../../../../../../../common'; -import { getServices, IndexPattern } from '../../../../../../kibana_services'; +import { getServices } from '../../../../../../kibana_services'; interface Props { defPairs: Array<[string, unknown]>; 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 ce745fecbbfad..7e3d7ff10b3a6 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 @@ -20,7 +20,6 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { METRIC_TYPE } from '@kbn/analytics'; -import { I18nProvider } from '@kbn/i18n/react'; import classNames from 'classnames'; import { DiscoverNoResults } from '../no_results'; import { LoadingSpinner } from '../loading_spinner/loading_spinner'; @@ -152,131 +151,129 @@ export function DiscoverLayout({ const contentCentered = resultState === 'uninitialized' || resultState === 'none'; return ( - - - - -

- {savedSearch.title} -

- + + + +

+ {savedSearch.title} +

+ + + + + - +
+ + setIsSidebarClosed(!isSidebarClosed)} + data-test-subj="collapseSideBarButton" + aria-controls="discover-sidebar" + aria-expanded={isSidebarClosed ? 'false' : 'true'} + aria-label={i18n.translate('discover.toggleSidebarAriaLabel', { + defaultMessage: 'Toggle sidebar', + })} + /> +
- - -
- - setIsSidebarClosed(!isSidebarClosed)} - data-test-subj="collapseSideBarButton" - aria-controls="discover-sidebar" - aria-expanded={isSidebarClosed ? 'false' : 'true'} - aria-label={i18n.translate('discover.toggleSidebarAriaLabel', { - defaultMessage: 'Toggle sidebar', - })} - /> -
-
-
- - - {resultState === 'none' && ( - !f.meta.disabled).length > 0 - } - onDisableFilters={onDisableFilters} - /> - )} - {resultState === 'uninitialized' && ( - savedSearchRefetch$.next()} /> - )} - {resultState === 'loading' && } - {resultState === 'ready' && ( - - - - - - - + + + {resultState === 'none' && ( + !f.meta.disabled).length > 0 + } + onDisableFilters={onDisableFilters} + /> + )} + {resultState === 'uninitialized' && ( + savedSearchRefetch$.next()} /> + )} + {resultState === 'loading' && } + {resultState === 'ready' && ( + + + - - )} - - - -
-
-
+ + + + + + )} + + + + + ); } diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern.tsx index f22d88f2b2150..62e74ec2c3523 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_index_pattern.tsx @@ -7,12 +7,11 @@ */ import React, { useState, useEffect } from 'react'; -import { I18nProvider } from '@kbn/i18n/react'; import { SavedObject } from 'kibana/public'; import { IndexPattern, IndexPatternAttributes } from 'src/plugins/data/public'; - import { IndexPatternRef } from './types'; import { ChangeIndexPattern } from './change_indexpattern'; + export interface DiscoverIndexPatternProps { /** * list of available index patterns, if length > 1, component offers a "change" link @@ -55,23 +54,21 @@ export function DiscoverIndexPattern({ } return ( - - { - const indexPattern = options.find((pattern) => pattern.id === id); - if (indexPattern) { - onChangeIndexPattern(id); - setSelected(indexPattern); - } - }} - /> - + { + const indexPattern = options.find((pattern) => pattern.id === id); + if (indexPattern) { + onChangeIndexPattern(id); + setSelected(indexPattern); + } + }} + /> ); } diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.js b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.js index 02adc6b68300a..8f86cdad82cf7 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.js +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.js @@ -6,19 +6,19 @@ * Side Public License, v 1. */ -import _ from 'lodash'; +import { map, sortBy, without, each, defaults, isObject } from 'lodash'; import { i18n } from '@kbn/i18n'; function getFieldValues(hits, field, indexPattern) { const name = field.name; const flattenHit = indexPattern.flattenHit; - return _.map(hits, function (hit) { + return map(hits, function (hit) { return flattenHit(hit)[name]; }); } function getFieldValueCounts(params) { - params = _.defaults(params, { + params = defaults(params, { count: 5, grouped: false, }); @@ -44,7 +44,7 @@ function getFieldValueCounts(params) { try { const groups = _groupValues(allValues, params); - counts = _.map(_.sortBy(groups, 'count').reverse().slice(0, params.count), function (bucket) { + counts = map(sortBy(groups, 'count').reverse().slice(0, params.count), function (bucket) { return { value: bucket.value, count: bucket.count, @@ -80,7 +80,7 @@ function getFieldValueCounts(params) { // returns a count of fields in the array that are undefined or null function _countMissing(array) { - return array.length - _.without(array, undefined, null).length; + return array.length - without(array, undefined, null).length; } function _groupValues(allValues, params) { @@ -88,7 +88,7 @@ function _groupValues(allValues, params) { let k; allValues.forEach(function (value) { - if (_.isObject(value) && !Array.isArray(value)) { + if (isObject(value) && !Array.isArray(value)) { throw new Error( i18n.translate( 'discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForObjectFieldsErrorMessage', @@ -105,7 +105,7 @@ function _groupValues(allValues, params) { k = value == null ? undefined : [value]; } - _.each(k, function (key) { + each(k, function (key) { if (groups.hasOwnProperty(key)) { groups[key].count++; } else { diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.test.ts b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.test.ts index 49cdb83256599..c3ff7970c5aac 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.test.ts +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.test.ts @@ -8,7 +8,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import _ from 'lodash'; +import { keys, each, cloneDeep, clone, uniq, filter, map } from 'lodash'; // @ts-expect-error import realHits from '../../../../../../__fixtures__/real_hits.js'; @@ -80,7 +80,7 @@ describe('fieldCalculator', function () { }); it('should have a a key for value in the array when not grouping array terms', function () { - expect(_.keys(groups).length).toBe(3); + expect(keys(groups).length).toBe(3); expect(groups.foo).toBeInstanceOf(Object); expect(groups.bar).toBeInstanceOf(Object); expect(groups.baz).toBeInstanceOf(Object); @@ -100,7 +100,7 @@ describe('fieldCalculator', function () { }); it('should group array terms when passed params.grouped', function () { - expect(_.keys(groups).length).toBe(4); + expect(keys(groups).length).toBe(4); expect(groups['foo,bar']).toBeInstanceOf(Object); }); @@ -120,7 +120,7 @@ describe('fieldCalculator', function () { let hits: any; beforeEach(function () { - hits = _.each(_.cloneDeep(realHits), (hit) => indexPattern.flattenHit(hit)); + hits = each(cloneDeep(realHits), (hit) => indexPattern.flattenHit(hit)); }); it('Should return an array of values for _source fields', function () { @@ -131,11 +131,11 @@ describe('fieldCalculator', function () { ); expect(extensions).toBeInstanceOf(Array); expect( - _.filter(extensions, function (v) { + filter(extensions, function (v) { return v === 'html'; }).length ).toBe(8); - expect(_.uniq(_.clone(extensions)).sort()).toEqual(['gif', 'html', 'php', 'png']); + expect(uniq(clone(extensions)).sort()).toEqual(['gif', 'html', 'php', 'png']); }); it('Should return an array of values for core meta fields', function () { @@ -146,11 +146,11 @@ describe('fieldCalculator', function () { ); expect(types).toBeInstanceOf(Array); expect( - _.filter(types, function (v) { + filter(types, function (v) { return v === 'apache'; }).length ).toBe(18); - expect(_.uniq(_.clone(types)).sort()).toEqual(['apache', 'nginx']); + expect(uniq(clone(types)).sort()).toEqual(['apache', 'nginx']); }); }); @@ -158,7 +158,7 @@ describe('fieldCalculator', function () { let params: { hits: any; field: any; count: number; indexPattern: IndexPattern }; beforeEach(function () { params = { - hits: _.cloneDeep(realHits), + hits: cloneDeep(realHits), field: indexPattern.fields.getByName('extension'), count: 3, indexPattern, @@ -170,7 +170,7 @@ describe('fieldCalculator', function () { expect(extensions).toBeInstanceOf(Object); expect(extensions.buckets).toBeInstanceOf(Array); expect(extensions.buckets.length).toBe(3); - expect(_.map(extensions.buckets, 'value')).toEqual(['html', 'php', 'gif']); + expect(map(extensions.buckets, 'value')).toEqual(['html', 'php', 'gif']); expect(extensions.error).toBe(undefined); }); 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 f7b4a35b4cf8b..a692dacd5e9f4 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 @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; +import type { IndexPattern, ISearchSource } from 'src/plugins/data/common'; import { showOpenSearchPanel } from './show_open_search_panel'; import { getSharingData, showPublicUrlSwitch } from '../../utils/get_sharing_data'; import { unhashUrl } from '../../../../../../../kibana_utils/public'; @@ -15,7 +16,6 @@ import { DiscoverServices } from '../../../../../build_services'; import { SavedSearch } from '../../../../../saved_searches'; import { onSaveSearch } from './on_save_search'; import { GetStateReturn } from '../../services/discover_state'; -import { IndexPattern, ISearchSource } from '../../../../../kibana_services'; import { openOptionsPopover } from './open_options_popover'; /** diff --git a/src/plugins/discover/public/application/apps/main/components/uninitialized/uninitialized.tsx b/src/plugins/discover/public/application/apps/main/components/uninitialized/uninitialized.tsx index c6be5e6028bdd..c9e0c43900ba1 100644 --- a/src/plugins/discover/public/application/apps/main/components/uninitialized/uninitialized.tsx +++ b/src/plugins/discover/public/application/apps/main/components/uninitialized/uninitialized.tsx @@ -7,8 +7,7 @@ */ import React from 'react'; -import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; - +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; interface Props { @@ -17,31 +16,29 @@ interface Props { export const DiscoverUninitialized = ({ onRefresh }: Props) => { return ( - - - -

- } - body={ -

- -

- } - actions={ - - - - } - /> - + + + + } + body={ +

+ +

+ } + actions={ + + + + } + /> ); }; diff --git a/src/plugins/discover/public/application/apps/main/utils/calc_field_counts.ts b/src/plugins/discover/public/application/apps/main/utils/calc_field_counts.ts index 57178776a97d4..1ce7023539be4 100644 --- a/src/plugins/discover/public/application/apps/main/utils/calc_field_counts.ts +++ b/src/plugins/discover/public/application/apps/main/utils/calc_field_counts.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { IndexPattern } from '../../../../kibana_services'; +import type { IndexPattern } from 'src/plugins/data/common'; import { ElasticSearchHit } from '../../../doc_views/doc_views_types'; /** diff --git a/src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.ts b/src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.ts index 00f194662e410..ff082587172a0 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { IndexPattern } from '../../../../kibana_services'; +import type { IndexPattern } from 'src/plugins/data/common'; import { getSortArray, SortPairArr } from '../components/doc_table/lib/get_sort'; /** diff --git a/src/plugins/discover/public/application/apps/main/utils/resolve_index_pattern.ts b/src/plugins/discover/public/application/apps/main/utils/resolve_index_pattern.ts index a5149ea2b3dd7..226db12114de8 100644 --- a/src/plugins/discover/public/application/apps/main/utils/resolve_index_pattern.ts +++ b/src/plugins/discover/public/application/apps/main/utils/resolve_index_pattern.ts @@ -7,10 +7,8 @@ */ import { i18n } from '@kbn/i18n'; -import { IUiSettingsClient, SavedObject, ToastsStart } from 'kibana/public'; -import { IndexPattern } from '../../../../kibana_services'; -import { IndexPatternsContract, SearchSource } from '../../../../../../data/common'; - +import type { IndexPattern, IndexPatternsContract, SearchSource } from 'src/plugins/data/common'; +import type { IUiSettingsClient, SavedObject, ToastsStart } from 'kibana/public'; export type IndexPatternSavedObject = SavedObject & { title: string }; interface IndexPatternData { diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx index e33d25c8693a6..ca0692a8c9039 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx @@ -21,7 +21,7 @@ import { EuiLoadingSpinner, EuiIcon, } from '@elastic/eui'; -import { IndexPattern } from '../../../kibana_services'; +import type { IndexPattern } from 'src/plugins/data/common'; import { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types'; import { getSchemaDetectors } from './discover_grid_schema'; import { DiscoverGridFlyout } from './discover_grid_flyout'; @@ -36,7 +36,6 @@ import { import { defaultPageSize, gridStyle, pageSizeArr, toolbarVisibility } from './constants'; import { DiscoverServices } from '../../../build_services'; import { getDisplayedColumns } from '../../helpers/columns'; -import { KibanaContextProvider } from '../../../../../kibana_react/public'; import { MAX_DOC_FIELDS_DISPLAYED, SHOW_MULTIFIELDS } from '../../../../common'; import { DiscoverGridDocumentToolbarBtn, getDocId } from './discover_grid_document_selection'; import { SortPairArr } from '../../apps/main/components/doc_table/lib/get_sort'; @@ -385,41 +384,39 @@ export const DiscoverGrid = ({ data-document-number={displayedRows.length} className={className} > - - { - if (onResize) { - onResize(col); - } - }} - pagination={paginationObj} - renderCellValue={renderCellValue} - rowCount={rowCount} - schemaDetectors={schemaDetectors} - sorting={sorting as EuiDataGridSorting} - toolbarVisibility={ - defaultColumns - ? { - ...toolbarVisibility, - showColumnSelector: false, - showSortSelector: isSortEnabled, - additionalControls, - } - : { - ...toolbarVisibility, - showSortSelector: isSortEnabled, - additionalControls, - } + { + if (onResize) { + onResize(col); } - /> - + }} + pagination={paginationObj} + renderCellValue={renderCellValue} + rowCount={rowCount} + schemaDetectors={schemaDetectors} + sorting={sorting as EuiDataGridSorting} + toolbarVisibility={ + defaultColumns + ? { + ...toolbarVisibility, + showColumnSelector: false, + showSortSelector: isSortEnabled, + additionalControls, + } + : { + ...toolbarVisibility, + showSortSelector: isSortEnabled, + additionalControls, + } + } + /> {showDisclaimer && (

diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_context.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_context.tsx index e57d3fb8362ae..0103ad3d98870 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_context.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_context.tsx @@ -7,8 +7,8 @@ */ import React from 'react'; +import type { IndexPattern } from 'src/plugins/data/common'; import { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types'; -import { IndexPattern } from '../../../kibana_services'; export interface GridContext { expanded: ElasticSearchHit | undefined; diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx index 2a97c5d0be819..c5b75dbe85aea 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx @@ -8,6 +8,7 @@ import React, { useMemo, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; +import type { IndexPattern } from 'src/plugins/data/common'; import { EuiFlexGroup, EuiFlexItem, @@ -24,7 +25,6 @@ import { keys, } from '@elastic/eui'; import { DocViewer } from '../doc_viewer/doc_viewer'; -import { IndexPattern } from '../../../kibana_services'; import { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types'; import { DiscoverServices } from '../../../build_services'; import { getContextUrl } from '../../helpers/get_context_url'; diff --git a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx index 8bea977ece554..ccbcc31e154c9 100644 --- a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx @@ -9,6 +9,7 @@ import React, { Fragment, useContext, useEffect } from 'react'; import themeLight from '@elastic/eui/dist/eui_theme_light.json'; import themeDark from '@elastic/eui/dist/eui_theme_dark.json'; +import type { IndexPattern } from 'src/plugins/data/common'; import { EuiDataGridCellValueElementProps, @@ -16,7 +17,6 @@ import { EuiDescriptionListTitle, EuiDescriptionListDescription, } from '@elastic/eui'; -import { IndexPattern } from '../../../kibana_services'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; import { DiscoverGridContext } from './discover_grid_context'; import { JsonCodeEditor } from '../json_code_editor/json_code_editor'; diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx index 52d9b8316bb09..e2af88b91b3ff 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx +++ b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { isEqual } from 'lodash'; -import { I18nProvider } from '@kbn/i18n/react'; import { DocViewRenderTab } from './doc_viewer_render_tab'; import { DocViewerError } from './doc_viewer_render_error'; import { DocViewRenderFn, DocViewRenderProps } from '../../doc_views/doc_views_types'; @@ -68,11 +67,9 @@ export class DocViewerTab extends React.Component { // doc view is provided by a react component if (Component) { return ( - - - - - + + + ); } diff --git a/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx b/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx index 625ac93a335ac..d9e9199e6586a 100644 --- a/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx +++ b/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx @@ -7,6 +7,7 @@ */ import React from 'react'; +import type { IndexPattern } from 'src/plugins/data/common'; import { mountWithIntl } from '@kbn/test/jest'; import { SourceViewer } from './source_viewer'; import * as hooks from '../../services/use_es_doc_search'; @@ -18,7 +19,7 @@ jest.mock('../../../kibana_services', () => ({ getServices: jest.fn(), })); -import { getServices, IndexPattern } from '../../../kibana_services'; +import { getServices } from '../../../kibana_services'; const mockIndexPattern = { getComputedFields: () => [], diff --git a/src/plugins/discover/public/application/components/table/table.test.tsx b/src/plugins/discover/public/application/components/table/table.test.tsx index da6820ba4a70a..589c97b400eb4 100644 --- a/src/plugins/discover/public/application/components/table/table.test.tsx +++ b/src/plugins/discover/public/application/components/table/table.test.tsx @@ -7,9 +7,8 @@ */ import React from 'react'; -import { mount } from 'enzyme'; +import { mountWithIntl } from '@kbn/test/jest'; import { findTestSubject } from '@elastic/eui/lib/test'; -import { I18nProvider } from '@kbn/i18n/react'; import { DocViewerTable, DocViewerTableProps } from './table'; import { indexPatterns, IndexPattern } from '../../../../../data/public'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; @@ -77,11 +76,7 @@ indexPattern.fields.getByName = (name: string) => { indexPattern.flattenHit = indexPatterns.flattenHitWrapper(indexPattern, indexPattern.metaFields); const mountComponent = (props: DocViewerTableProps) => { - return mount( - - - - ); + return mountWithIntl(); }; describe('DocViewTable at Discover', () => { diff --git a/src/plugins/discover/public/application/discover_router.tsx b/src/plugins/discover/public/application/discover_router.tsx index 7c7921935a7fa..320ce3e5f644a 100644 --- a/src/plugins/discover/public/application/discover_router.tsx +++ b/src/plugins/discover/public/application/discover_router.tsx @@ -23,8 +23,8 @@ export const discoverRouter = (services: DiscoverServices, history: History) => history, }; return ( - - + + } /> - - + + ); }; diff --git a/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts b/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts index 66e8889bcb062..4dbe14017dc6e 100644 --- a/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts +++ b/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts @@ -7,9 +7,9 @@ */ import { useMemo } from 'react'; +import type { IndexPattern, IndexPatternsContract } from 'src/plugins/data/common'; import { Capabilities, IUiSettingsClient } from 'kibana/public'; -import { IndexPattern, IndexPatternsContract } from '../../kibana_services'; import { AppState as DiscoverState, GetStateReturn as DiscoverGetStateReturn, diff --git a/src/plugins/discover/public/application/index.tsx b/src/plugins/discover/public/application/index.tsx index 4ac50eecd518a..f6c7d60ed7db8 100644 --- a/src/plugins/discover/public/application/index.tsx +++ b/src/plugins/discover/public/application/index.tsx @@ -5,14 +5,12 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import ReactDOM from 'react-dom'; - -import { AppMountParameters } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { getServices } from '../kibana_services'; import { discoverRouter } from './discover_router'; +import { toMountPoint } from '../../../kibana_react/public'; -export const renderApp = ({ element }: AppMountParameters) => { +export const renderApp = (element: HTMLElement) => { const services = getServices(); const { history: getHistory, capabilities, chrome, data } = services; @@ -28,11 +26,10 @@ export const renderApp = ({ element }: AppMountParameters) => { iconType: 'glasses', }); } - const app = discoverRouter(services, history); - ReactDOM.render(app, element); + const unmount = toMountPoint(discoverRouter(services, history))(element); return () => { + unmount(); data.search.session.clear(); - ReactDOM.unmountComponentAtNode(element); }; }; diff --git a/src/plugins/discover/public/kibana_services.ts b/src/plugins/discover/public/kibana_services.ts index 1e92c0e4c2f1d..72925a1578c30 100644 --- a/src/plugins/discover/public/kibana_services.ts +++ b/src/plugins/discover/public/kibana_services.ts @@ -6,13 +6,12 @@ * Side Public License, v 1. */ -import _ from 'lodash'; +import { once } from 'lodash'; import { createHashHistory } from 'history'; -import { ScopedHistory, AppMountParameters } from 'kibana/public'; -import { UiActionsStart } from 'src/plugins/ui_actions/public'; +import type { ScopedHistory, AppMountParameters } from 'kibana/public'; +import type { UiActionsStart } from 'src/plugins/ui_actions/public'; import { DiscoverServices } from './build_services'; import { createGetterSetter } from '../../kibana_utils/public'; -import { search } from '../../data/public'; import { DocViewsRegistry } from './application/doc_views/doc_views_registry'; let services: DiscoverServices | null = null; @@ -48,7 +47,7 @@ export const [getDocViewsRegistry, setDocViewsRegistry] = createGetterSetter { +export const getHistory = once(() => { const history = createHashHistory(); history.listen(() => { // keep at least one listener so that `history.location` always in sync @@ -72,18 +71,3 @@ export const syncHistoryLocations = () => { export const [getScopedHistory, setScopedHistory] = createGetterSetter( 'scopedHistory' ); - -export const { tabifyAggResponse } = search; -export { unhashUrl, redirectWhenMissing } from '../../kibana_utils/public'; -export { formatMsg, formatStack, subscribeWithScope } from '../../kibana_legacy/public'; - -// EXPORT types -export { - IndexPatternsContract, - IndexPattern, - indexPatterns, - IndexPatternField, - ISearchSource, - EsQuerySortValue, - SortDirection, -} from '../../data/public'; diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index c43c759c5d344..9e5e32111723b 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -320,10 +320,9 @@ export class DiscoverPlugin // make sure the index pattern list is up to date await depsStart.data.indexPatterns.clearCache(); - const { renderApp } = await import('./application/application'); - const unmount = await renderApp('discover', params.element); + const { renderApp } = await import('./application'); + const unmount = renderApp(params.element); return () => { - params.element.classList.remove('dscAppWrapper'); unlistenParentHistory(); unmount(); appUnMounted(); diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index b964566b06927..6a6063467f6af 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -486,7 +486,7 @@ export class DiscoverPageObject extends FtrService { * Check if Discover app is currently rendered on the screen. */ public async isDiscoverAppOnScreen(): Promise { - const result = await this.find.allByCssSelector('discover-app'); + const result = await this.find.allByCssSelector('.dscPage'); return result.length === 1; } From 6931a75b8755a80b14f8792e9cc8a183d85ea4a3 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Sep 2021 16:33:52 -0400 Subject: [PATCH 020/139] Fix logic issues resulting from CrawlerLogic/CrawlerOverviewLogic split (#111564) (#111611) Co-authored-by: Byron Hulcher --- .../crawler/components/delete_domain_panel.tsx | 4 ++-- .../app_search/components/crawler/crawler_logic.ts | 2 +- .../components/crawler/crawler_overview_logic.ts | 2 +- .../crawler/crawler_single_domain_logic.test.ts | 11 +++++++++++ .../components/crawler/crawler_single_domain_logic.ts | 3 +++ 5 files changed, 18 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/delete_domain_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/delete_domain_panel.tsx index 6b8377775021c..5c74bfbe98790 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/delete_domain_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/delete_domain_panel.tsx @@ -15,7 +15,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { CrawlerSingleDomainLogic } from '../crawler_single_domain_logic'; -import { getDeleteDomainSuccessMessage } from '../utils'; +import { getDeleteDomainConfirmationMessage } from '../utils'; export const DeleteDomainPanel: React.FC = ({}) => { const { domain } = useValues(CrawlerSingleDomainLogic); @@ -61,7 +61,7 @@ export const DeleteDomainPanel: React.FC = ({}) => { color="danger" iconType="trash" onClick={() => { - if (confirm(getDeleteDomainSuccessMessage(domain.url))) { + if (confirm(getDeleteDomainConfirmationMessage(domain.url))) { deleteDomain(domain); } }} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts index 902058593fe46..972532597e344 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.ts @@ -45,7 +45,7 @@ interface CrawlerActions { } export const CrawlerLogic = kea>({ - path: ['enterprise_search', 'app_search', 'crawler', 'crawler_overview'], + path: ['enterprise_search', 'app_search', 'crawler_logic'], actions: { clearTimeoutId: true, createNewTimeoutForCrawlRequests: (duration) => ({ duration }), diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts index 5f9dbdd01efc7..c6a26e50a6758 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts @@ -25,7 +25,7 @@ export const CrawlerOverviewLogic = kea ({ domain }), }, - listeners: ({ actions, values }) => ({ + listeners: () => ({ deleteDomain: async ({ domain }) => { const { http } = HttpLogic.values; const { engineName } = EngineLogic.values; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts index 951ef4e31ca03..03e20ea988f98 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts @@ -13,8 +13,17 @@ import { } from '../../../__mocks__/kea_logic'; import '../../__mocks__/engine_logic.mock'; +jest.mock('./crawler_logic', () => ({ + CrawlerLogic: { + actions: { + fetchCrawlerData: jest.fn(), + }, + }, +})); + import { nextTick } from '@kbn/test/jest'; +import { CrawlerLogic } from './crawler_logic'; import { CrawlerSingleDomainLogic, CrawlerSingleDomainValues } from './crawler_single_domain_logic'; import { CrawlerDomain, CrawlerPolicies, CrawlerRules } from './types'; @@ -150,6 +159,7 @@ describe('CrawlerSingleDomainLogic', () => { describe('listeners', () => { describe('deleteDomain', () => { it('flashes a success toast and redirects the user to the crawler overview on success', async () => { + jest.spyOn(CrawlerLogic.actions, 'fetchCrawlerData'); const { navigateToUrl } = mockKibanaValues; http.delete.mockReturnValue(Promise.resolve()); @@ -161,6 +171,7 @@ describe('CrawlerSingleDomainLogic', () => { '/internal/app_search/engines/some-engine/crawler/domains/1234' ); + expect(CrawlerLogic.actions.fetchCrawlerData).toHaveBeenCalled(); expect(flashSuccessToast).toHaveBeenCalled(); expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/crawler'); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts index 1939e418fec79..9452ae1d578ed 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts @@ -14,6 +14,8 @@ import { KibanaLogic } from '../../../shared/kibana'; import { ENGINE_CRAWLER_PATH } from '../../routes'; import { EngineLogic, generateEnginePath } from '../engine'; +import { CrawlerLogic } from './crawler_logic'; + import { CrawlerDomain, EntryPoint, Sitemap, CrawlRule } from './types'; import { crawlerDomainServerToClient, getDeleteDomainSuccessMessage } from './utils'; @@ -78,6 +80,7 @@ export const CrawlerSingleDomainLogic = kea< `/internal/app_search/engines/${engineName}/crawler/domains/${domain.id}` ); + CrawlerLogic.actions.fetchCrawlerData(); flashSuccessToast(getDeleteDomainSuccessMessage(domain.url)); KibanaLogic.values.navigateToUrl(generateEnginePath(ENGINE_CRAWLER_PATH)); } catch (e) { From 5e235d43f1a09b667e6e20898f84cd2a1765cba2 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Sep 2021 16:51:18 -0400 Subject: [PATCH 021/139] [CI] Buildkite ES Snapshots Pipelines / attempt 2 (#111565) (#111622) Co-authored-by: Brian Seeders --- .buildkite/pipelines/es_snapshots/build.yml | 5 + .buildkite/pipelines/es_snapshots/promote.yml | 12 +++ .buildkite/pipelines/es_snapshots/verify.yml | 102 ++++++++++++++++++ .buildkite/scripts/build_kibana_plugins.sh | 0 .../scripts/download_build_artifacts.sh | 4 +- .buildkite/scripts/lifecycle/pre_command.sh | 5 + .buildkite/scripts/steps/build_kibana.sh | 8 ++ .../steps/es_snapshots/bucket_config.js | 4 + .../scripts/steps/es_snapshots/build.sh | 101 +++++++++++++++++ .../steps/es_snapshots/create_manifest.js | 90 ++++++++++++++++ .../scripts/steps/es_snapshots/promote.sh | 13 +++ .../steps/es_snapshots/promote_manifest.js | 46 ++++++++ .../steps/es_snapshots/trigger_promote.sh | 20 ++++ .../scripts/steps/functional/oss_cigroup.sh | 16 +++ .../scripts/steps/functional/xpack_cigroup.sh | 18 ++++ .../scripts/steps/test/api_integration.sh | 11 ++ .buildkite/scripts/steps/test/jest.sh | 8 ++ .../scripts/steps/test/jest_integration.sh | 8 ++ src/dev/precommit_hook/casing_check_config.js | 2 +- 19 files changed, 470 insertions(+), 3 deletions(-) create mode 100644 .buildkite/pipelines/es_snapshots/build.yml create mode 100644 .buildkite/pipelines/es_snapshots/promote.yml create mode 100755 .buildkite/pipelines/es_snapshots/verify.yml mode change 100644 => 100755 .buildkite/scripts/build_kibana_plugins.sh create mode 100755 .buildkite/scripts/steps/build_kibana.sh create mode 100644 .buildkite/scripts/steps/es_snapshots/bucket_config.js create mode 100755 .buildkite/scripts/steps/es_snapshots/build.sh create mode 100644 .buildkite/scripts/steps/es_snapshots/create_manifest.js create mode 100755 .buildkite/scripts/steps/es_snapshots/promote.sh create mode 100644 .buildkite/scripts/steps/es_snapshots/promote_manifest.js create mode 100644 .buildkite/scripts/steps/es_snapshots/trigger_promote.sh create mode 100755 .buildkite/scripts/steps/functional/oss_cigroup.sh create mode 100755 .buildkite/scripts/steps/functional/xpack_cigroup.sh create mode 100755 .buildkite/scripts/steps/test/api_integration.sh create mode 100755 .buildkite/scripts/steps/test/jest.sh create mode 100755 .buildkite/scripts/steps/test/jest_integration.sh diff --git a/.buildkite/pipelines/es_snapshots/build.yml b/.buildkite/pipelines/es_snapshots/build.yml new file mode 100644 index 0000000000000..2bc555de8bf5d --- /dev/null +++ b/.buildkite/pipelines/es_snapshots/build.yml @@ -0,0 +1,5 @@ +steps: + - command: .buildkite/scripts/steps/es_snapshots/build.sh + label: Build ES Snapshot + agents: + queue: c2-8 diff --git a/.buildkite/pipelines/es_snapshots/promote.yml b/.buildkite/pipelines/es_snapshots/promote.yml new file mode 100644 index 0000000000000..5a003321246a1 --- /dev/null +++ b/.buildkite/pipelines/es_snapshots/promote.yml @@ -0,0 +1,12 @@ +steps: + - block: 'Promote' + prompt: "Enter the details for the snapshot you'd like to promote" + if: "build.env('ES_SNAPSHOT_MANIFEST') == null" + # Later, this could be a dropdown dynamically filled with recent builds + fields: + - text: 'ES_SNAPSHOT_MANIFEST' + key: 'ES_SNAPSHOT_MANIFEST' + hint: 'URL pointing to the manifest to promote' + required: true + - label: Promote Snapshot + command: .buildkite/scripts/steps/es_snapshots/promote.sh diff --git a/.buildkite/pipelines/es_snapshots/verify.yml b/.buildkite/pipelines/es_snapshots/verify.yml new file mode 100755 index 0000000000000..f67de4819c23a --- /dev/null +++ b/.buildkite/pipelines/es_snapshots/verify.yml @@ -0,0 +1,102 @@ +env: + IGNORE_SHIP_CI_STATS_ERROR: 'true' +steps: + - block: 'Verify' + prompt: "Enter the details for the snapshot you'd like to verify" + if: "build.env('ES_SNAPSHOT_MANIFEST') == null" + # Later, this could be a dropdown dynamically filled with recent builds + fields: + - text: 'ES_SNAPSHOT_MANIFEST' + key: 'ES_SNAPSHOT_MANIFEST' + hint: 'URL pointing to the manifest to promote' + required: true + + - command: .buildkite/scripts/lifecycle/pre_build.sh + label: Pre-Build + + - wait + + - command: .buildkite/scripts/steps/build_kibana.sh + label: Build Kibana Distribution and Plugins + agents: + queue: c2-8 + key: build + if: "build.env('KIBANA_BUILD_ID') == null || build.env('KIBANA_BUILD_ID') == ''" + + - command: .buildkite/scripts/steps/functional/xpack_cigroup.sh + label: 'Default CI Group' + parallelism: 13 + agents: + queue: ci-group-6 + artifact_paths: target/junit/**/*.xml + depends_on: build + key: default-cigroup + retry: + automatic: + - exit_status: '*' + limit: 1 + + - command: CI_GROUP=Docker .buildkite/scripts/steps/functional/xpack_cigroup.sh + label: 'Docker CI Group' + agents: + queue: ci-group-6 + artifact_paths: target/junit/**/*.xml + depends_on: build + key: default-cigroup-docker + retry: + automatic: + - exit_status: '*' + limit: 1 + + - command: .buildkite/scripts/steps/functional/oss_cigroup.sh + label: 'OSS CI Group' + parallelism: 12 + agents: + queue: ci-group-4d + artifact_paths: target/junit/**/*.xml + depends_on: build + key: oss-cigroup + retry: + automatic: + - exit_status: '*' + limit: 1 + + - command: .buildkite/scripts/steps/test/jest_integration.sh + label: 'Jest Integration Tests' + agents: + queue: jest + artifact_paths: target/junit/**/*.xml + key: jest-integration + retry: + automatic: + - exit_status: '*' + limit: 1 + + - command: .buildkite/scripts/steps/test/api_integration.sh + label: 'API Integration Tests' + agents: + queue: jest + artifact_paths: target/junit/**/*.xml + key: api-integration + + - command: .buildkite/scripts/steps/es_snapshots/trigger_promote.sh + label: Trigger promotion + depends_on: + - default-cigroup + - default-cigroup-docker + - oss-cigroup + - jest-integration + - api-integration + + - wait: ~ + continue_on_failure: true + + - plugins: + - junit-annotate#v1.9.0: + artifacts: target/junit/**/*.xml + + - wait: ~ + continue_on_failure: true + + - command: .buildkite/scripts/lifecycle/post_build.sh + label: Post-Build diff --git a/.buildkite/scripts/build_kibana_plugins.sh b/.buildkite/scripts/build_kibana_plugins.sh old mode 100644 new mode 100755 diff --git a/.buildkite/scripts/download_build_artifacts.sh b/.buildkite/scripts/download_build_artifacts.sh index 6a6b7246753f6..1e7525fff25ea 100755 --- a/.buildkite/scripts/download_build_artifacts.sh +++ b/.buildkite/scripts/download_build_artifacts.sh @@ -7,8 +7,8 @@ if [[ ! -d "$KIBANA_BUILD_LOCATION/bin" ]]; then cd "$WORKSPACE" - buildkite-agent artifact download kibana-default.tar.gz . - buildkite-agent artifact download kibana-default-plugins.tar.gz . + buildkite-agent artifact download kibana-default.tar.gz . --build "${KIBANA_BUILD_ID:-$BUILDKITE_BUILD_ID}" + buildkite-agent artifact download kibana-default-plugins.tar.gz . --build "${KIBANA_BUILD_ID:-$BUILDKITE_BUILD_ID}" mkdir -p "$KIBANA_BUILD_LOCATION" tar -xzf kibana-default.tar.gz -C "$KIBANA_BUILD_LOCATION" --strip=1 diff --git a/.buildkite/scripts/lifecycle/pre_command.sh b/.buildkite/scripts/lifecycle/pre_command.sh index b0113e6b16964..759f1e7b4ff9e 100755 --- a/.buildkite/scripts/lifecycle/pre_command.sh +++ b/.buildkite/scripts/lifecycle/pre_command.sh @@ -72,3 +72,8 @@ if [[ "${SKIP_CI_SETUP:-}" != "true" ]]; then source .buildkite/scripts/common/setup_bazel.sh fi fi + +PIPELINE_PRE_COMMAND=${PIPELINE_PRE_COMMAND:-".buildkite/scripts/lifecycle/pipelines/$BUILDKITE_PIPELINE_SLUG/pre_command.sh"} +if [[ -f "$PIPELINE_PRE_COMMAND" ]]; then + source "$PIPELINE_PRE_COMMAND" +fi diff --git a/.buildkite/scripts/steps/build_kibana.sh b/.buildkite/scripts/steps/build_kibana.sh new file mode 100755 index 0000000000000..5896dcac5d444 --- /dev/null +++ b/.buildkite/scripts/steps/build_kibana.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -euo pipefail + +.buildkite/scripts/bootstrap.sh +.buildkite/scripts/build_kibana.sh +.buildkite/scripts/build_kibana_plugins.sh +.buildkite/scripts/post_build_kibana.sh diff --git a/.buildkite/scripts/steps/es_snapshots/bucket_config.js b/.buildkite/scripts/steps/es_snapshots/bucket_config.js new file mode 100644 index 0000000000000..a18d1182c4a89 --- /dev/null +++ b/.buildkite/scripts/steps/es_snapshots/bucket_config.js @@ -0,0 +1,4 @@ +module.exports = { + BASE_BUCKET_DAILY: 'kibana-ci-es-snapshots-daily', + BASE_BUCKET_PERMANENT: 'kibana-ci-es-snapshots-permanent', +}; diff --git a/.buildkite/scripts/steps/es_snapshots/build.sh b/.buildkite/scripts/steps/es_snapshots/build.sh new file mode 100755 index 0000000000000..91b5004594a6d --- /dev/null +++ b/.buildkite/scripts/steps/es_snapshots/build.sh @@ -0,0 +1,101 @@ +#!/bin/bash + +set -euo pipefail + +source .buildkite/scripts/common/util.sh + +echo "--- Cloning Elasticsearch and preparing workspace" + +cd .. +destination="$(pwd)/es-build" +rm -rf "$destination" +mkdir -p "$destination" + +mkdir -p elasticsearch && cd elasticsearch + +export ELASTICSEARCH_BRANCH="${ELASTICSEARCH_BRANCH:-$BUILDKITE_BRANCH}" + +if [[ ! -d .git ]]; then + git init + git remote add origin https://github.com/elastic/elasticsearch.git +fi +git fetch origin --depth 1 "$ELASTICSEARCH_BRANCH" +git reset --hard FETCH_HEAD + +ELASTICSEARCH_GIT_COMMIT="$(git rev-parse HEAD)" +export ELASTICSEARCH_GIT_COMMIT + +ELASTICSEARCH_GIT_COMMIT_SHORT="$(git rev-parse --short HEAD)" +export ELASTICSEARCH_GIT_COMMIT_SHORT + +# These turn off automation in the Elasticsearch repo +export BUILD_NUMBER="" +export JENKINS_URL="" +export BUILD_URL="" +export JOB_NAME="" +export NODE_NAME="" +export DOCKER_BUILDKIT="" + +# Reads the ES_BUILD_JAVA env var out of .ci/java-versions.properties and exports it +export "$(grep '^ES_BUILD_JAVA' .ci/java-versions.properties | xargs)" + +export PATH="$HOME/.java/$ES_BUILD_JAVA/bin:$PATH" +export JAVA_HOME="$HOME/.java/$ES_BUILD_JAVA" + +# The Elasticsearch Dockerfile needs to be built with root privileges, but Docker on our servers is running using a non-root user +# So, let's use docker-in-docker to temporarily create a privileged docker daemon to run `docker build` on +# We have to do this, because there's no `docker build --privileged` or similar + +echo "--- Setting up Docker-in-Docker for Elasticsearch" + +docker rm -f dind || true # If there's an old daemon running that somehow didn't get cleaned up, lets remove it first +CERTS_DIR="$HOME/dind-certs" +rm -rf "$CERTS_DIR" +docker run -d --rm --privileged --name dind --userns host -p 2377:2376 -e DOCKER_TLS_CERTDIR=/certs -v "$CERTS_DIR":/certs docker:dind + +trap "docker rm -f dind" EXIT + +export DOCKER_TLS_VERIFY=true +export DOCKER_CERT_PATH="$CERTS_DIR/client" +export DOCKER_TLS_CERTDIR="$CERTS_DIR" +export DOCKER_HOST=localhost:2377 + +echo "--- Build Elasticsearch" +./gradlew -Dbuild.docker=true assemble --parallel + +echo "--- Create distribution archives" +find distribution -type f \( -name 'elasticsearch-*-*-*-*.tar.gz' -o -name 'elasticsearch-*-*-*-*.zip' \) -not -path '*no-jdk*' -not -path '*build-context*' -exec cp {} "$destination" \; + +ls -alh "$destination" + +echo "--- Create docker image archives" +docker images "docker.elastic.co/elasticsearch/elasticsearch" +docker images "docker.elastic.co/elasticsearch/elasticsearch" --format "{{.Tag}}" | xargs -n1 echo 'docker save docker.elastic.co/elasticsearch/elasticsearch:${0} | gzip > ../es-build/elasticsearch-${0}-docker-image.tar.gz' +docker images "docker.elastic.co/elasticsearch/elasticsearch" --format "{{.Tag}}" | xargs -n1 bash -c 'docker save docker.elastic.co/elasticsearch/elasticsearch:${0} | gzip > ../es-build/elasticsearch-${0}-docker-image.tar.gz' + +echo "--- Create checksums for snapshot files" +cd "$destination" +find ./* -exec bash -c "shasum -a 512 {} > {}.sha512" \; + +cd "$BUILDKITE_BUILD_CHECKOUT_PATH" +node "$(dirname "${0}")/create_manifest.js" "$destination" + +ES_SNAPSHOT_MANIFEST="$(buildkite-agent meta-data get ES_SNAPSHOT_MANIFEST)" + +cat << EOF | buildkite-agent annotate --style "info" + - \`ELASTICSEARCH_BRANCH\` - \`$ELASTICSEARCH_BRANCH\` + - \`ELASTICSEARCH_GIT_COMMIT\` - \`$ELASTICSEARCH_GIT_COMMIT\` + - \`ES_SNAPSHOT_MANIFEST\` - \`$ES_SNAPSHOT_MANIFEST\` + - \`ES_SNAPSHOT_VERSION\` - \`$(buildkite-agent meta-data get ES_SNAPSHOT_VERSION)\` + - \`ES_SNAPSHOT_ID\` - \`$(buildkite-agent meta-data get ES_SNAPSHOT_ID)\` +EOF + +cat << EOF | buildkite-agent pipeline upload +steps: + - trigger: 'kibana-elasticsearch-snapshot-verify' + async: true + build: + env: + ES_SNAPSHOT_MANIFEST: '$ES_SNAPSHOT_MANIFEST' + branch: '$BUILDKITE_BRANCH' +EOF diff --git a/.buildkite/scripts/steps/es_snapshots/create_manifest.js b/.buildkite/scripts/steps/es_snapshots/create_manifest.js new file mode 100644 index 0000000000000..3173737e984e8 --- /dev/null +++ b/.buildkite/scripts/steps/es_snapshots/create_manifest.js @@ -0,0 +1,90 @@ +const fs = require('fs'); +const { execSync } = require('child_process'); +const { BASE_BUCKET_DAILY } = require('./bucket_config.js'); + +(async () => { + console.log('--- Create ES Snapshot Manifest'); + + const destination = process.argv[2] || __dirname + '/test'; + + const ES_BRANCH = process.env.ELASTICSEARCH_BRANCH; + const GIT_COMMIT = process.env.ELASTICSEARCH_GIT_COMMIT; + const GIT_COMMIT_SHORT = process.env.ELASTICSEARCH_GIT_COMMIT_SHORT; + + let VERSION = ''; + let SNAPSHOT_ID = ''; + let DESTINATION = ''; + + const now = new Date(); + + // format: yyyyMMdd-HHmmss + const date = [ + now.getFullYear(), + (now.getMonth() + 1).toString().padStart(2, '0'), + now.getDate().toString().padStart(2, '0'), + '-', + now.getHours().toString().padStart(2, '0'), + now.getMinutes().toString().padStart(2, '0'), + now.getSeconds().toString().padStart(2, '0'), + ].join(''); + + try { + const files = fs.readdirSync(destination); + const manifestEntries = files + .filter((filename) => !filename.match(/.sha512$/)) + .filter((filename) => !filename.match(/.json$/)) + .map((filename) => { + const parts = filename.replace('elasticsearch-oss', 'oss').split('-'); + + VERSION = VERSION || parts[1]; + SNAPSHOT_ID = SNAPSHOT_ID || `${date}_${GIT_COMMIT_SHORT}`; + DESTINATION = DESTINATION || `${VERSION}/archives/${SNAPSHOT_ID}`; + + return { + filename: filename, + checksum: filename + '.sha512', + url: `https://storage.googleapis.com/${BASE_BUCKET_DAILY}/${DESTINATION}/${filename}`, + version: parts[1], + platform: parts[3], + architecture: parts[4].split('.')[0], + license: parts[0] == 'oss' ? 'oss' : 'default', + }; + }); + + const manifest = { + id: SNAPSHOT_ID, + bucket: `${BASE_BUCKET_DAILY}/${DESTINATION}`.toString(), + branch: ES_BRANCH, + sha: GIT_COMMIT, + sha_short: GIT_COMMIT_SHORT, + version: VERSION, + generated: now.toISOString(), + archives: manifestEntries, + }; + + const manifestJSON = JSON.stringify(manifest, null, 2); + fs.writeFileSync(`${destination}/manifest.json`, manifestJSON); + + console.log('Manifest:', manifestJSON); + + execSync( + ` + set -euo pipefail + + echo '--- Upload files to GCS' + cd "${destination}" + gsutil -m cp -r *.* gs://${BASE_BUCKET_DAILY}/${DESTINATION} + cp manifest.json manifest-latest.json + gsutil cp manifest-latest.json gs://${BASE_BUCKET_DAILY}/${VERSION} + + buildkite-agent meta-data set ES_SNAPSHOT_MANIFEST 'https://storage.googleapis.com/${BASE_BUCKET_DAILY}/${DESTINATION}/manifest.json' + buildkite-agent meta-data set ES_SNAPSHOT_VERSION '${VERSION}' + buildkite-agent meta-data set ES_SNAPSHOT_ID '${SNAPSHOT_ID}' + `, + { shell: '/bin/bash' } + ); + } catch (ex) { + console.error(ex); + process.exit(1); + } +})(); diff --git a/.buildkite/scripts/steps/es_snapshots/promote.sh b/.buildkite/scripts/steps/es_snapshots/promote.sh new file mode 100755 index 0000000000000..20f79d1a4e2e4 --- /dev/null +++ b/.buildkite/scripts/steps/es_snapshots/promote.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -euo pipefail + +export ES_SNAPSHOT_MANIFEST="${ES_SNAPSHOT_MANIFEST:-"$(buildkite-agent meta-data get ES_SNAPSHOT_MANIFEST)"}" + +cat << EOF | buildkite-agent annotate --style "info" + This promotion is for the following snapshot manifest: + + $ES_SNAPSHOT_MANIFEST +EOF + +node "$(dirname "${0}")/promote_manifest.js" "$ES_SNAPSHOT_MANIFEST" diff --git a/.buildkite/scripts/steps/es_snapshots/promote_manifest.js b/.buildkite/scripts/steps/es_snapshots/promote_manifest.js new file mode 100644 index 0000000000000..ce14935dd1b84 --- /dev/null +++ b/.buildkite/scripts/steps/es_snapshots/promote_manifest.js @@ -0,0 +1,46 @@ +const fs = require('fs'); +const { execSync } = require('child_process'); +const { BASE_BUCKET_DAILY, BASE_BUCKET_PERMANENT } = require('./bucket_config.js'); + +(async () => { + try { + const MANIFEST_URL = process.argv[2]; + + if (!MANIFEST_URL) { + throw Error('Manifest URL missing'); + } + + const tempDir = fs.mkdtempSync('snapshot-promotion'); + process.chdir(tempDir); + + execSync(`curl '${MANIFEST_URL}' > manifest.json`); + + const manifestJson = fs.readFileSync('manifest.json').toString(); + const manifest = JSON.parse(manifestJson); + const { id, bucket, version } = manifest; + + const manifestPermanentJson = manifestJson + .split(BASE_BUCKET_DAILY) + .join(BASE_BUCKET_PERMANENT) + .split(`${version}/archives/${id}`) + .join(version); // e.g. replaceAll + + fs.writeFileSync('manifest-permanent.json', manifestPermanentJson); + + execSync( + ` + set -euo pipefail + cp manifest.json manifest-latest-verified.json + gsutil cp manifest-latest-verified.json gs://${BASE_BUCKET_DAILY}/${version}/ + rm manifest.json + cp manifest-permanent.json manifest.json + gsutil -m cp -r gs://${bucket}/* gs://${BASE_BUCKET_PERMANENT}/${version}/ + gsutil cp manifest.json gs://${BASE_BUCKET_PERMANENT}/${version}/ + `, + { shell: '/bin/bash' } + ); + } catch (ex) { + console.error(ex); + process.exit(1); + } +})(); diff --git a/.buildkite/scripts/steps/es_snapshots/trigger_promote.sh b/.buildkite/scripts/steps/es_snapshots/trigger_promote.sh new file mode 100644 index 0000000000000..1e8256d8c6645 --- /dev/null +++ b/.buildkite/scripts/steps/es_snapshots/trigger_promote.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -euo pipefail + +# If ES_SNAPSHOT_MANIFEST is set dynamically during the verify job, rather than provided during the trigger, +# such as if you provide it as input during a manual build, +# the ES_SNAPSHOT_MANIFEST env var will be empty in the context of the pipeline. +# So, we'll trigger with a script instead, so that we can ensure ES_SNAPSHOT_MANIFEST is populated. + +export ES_SNAPSHOT_MANIFEST="${ES_SNAPSHOT_MANIFEST:-"$(buildkite-agent meta-data get ES_SNAPSHOT_MANIFEST)"}" + +cat << EOF | buildkite-agent pipeline upload +steps: + - trigger: 'kibana-elasticsearch-snapshot-promote' + async: true + build: + env: + ES_SNAPSHOT_MANIFEST: '$ES_SNAPSHOT_MANIFEST' + branch: '$BUILDKITE_BRANCH' +EOF diff --git a/.buildkite/scripts/steps/functional/oss_cigroup.sh b/.buildkite/scripts/steps/functional/oss_cigroup.sh new file mode 100755 index 0000000000000..b4c643868ff7d --- /dev/null +++ b/.buildkite/scripts/steps/functional/oss_cigroup.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -euo pipefail + +.buildkite/scripts/bootstrap.sh +.buildkite/scripts/download_build_artifacts.sh + +export CI_GROUP=${CI_GROUP:-$((BUILDKITE_PARALLEL_JOB+1))} +export JOB=kibana-oss-ciGroup${CI_GROUP} + +echo "--- OSS CI Group $CI_GROUP" + +node scripts/functional_tests \ + --bail \ + --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ + --include-tag "ciGroup$CI_GROUP" diff --git a/.buildkite/scripts/steps/functional/xpack_cigroup.sh b/.buildkite/scripts/steps/functional/xpack_cigroup.sh new file mode 100755 index 0000000000000..e6ef0bba87904 --- /dev/null +++ b/.buildkite/scripts/steps/functional/xpack_cigroup.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -euo pipefail + +.buildkite/scripts/bootstrap.sh +.buildkite/scripts/download_build_artifacts.sh + +export CI_GROUP=${CI_GROUP:-$((BUILDKITE_PARALLEL_JOB+1))} +export JOB=kibana-default-ciGroup${CI_GROUP} + +echo "--- Default CI Group $CI_GROUP" + +cd "$XPACK_DIR" + +node scripts/functional_tests \ + --bail \ + --kibana-install-dir "$KIBANA_BUILD_LOCATION" \ + --include-tag "ciGroup$CI_GROUP" diff --git a/.buildkite/scripts/steps/test/api_integration.sh b/.buildkite/scripts/steps/test/api_integration.sh new file mode 100755 index 0000000000000..4bf1ed1406ac5 --- /dev/null +++ b/.buildkite/scripts/steps/test/api_integration.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -euo pipefail + +.buildkite/scripts/bootstrap.sh + +echo '--- API Integration Tests' +node scripts/functional_tests \ + --config test/api_integration/config.js \ + --bail \ + --debug diff --git a/.buildkite/scripts/steps/test/jest.sh b/.buildkite/scripts/steps/test/jest.sh new file mode 100755 index 0000000000000..ab9be759b43a5 --- /dev/null +++ b/.buildkite/scripts/steps/test/jest.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -euo pipefail + +.buildkite/scripts/bootstrap.sh + +echo '--- Jest' +node scripts/jest --ci --verbose --maxWorkers=13 diff --git a/.buildkite/scripts/steps/test/jest_integration.sh b/.buildkite/scripts/steps/test/jest_integration.sh new file mode 100755 index 0000000000000..eb243e55670e3 --- /dev/null +++ b/.buildkite/scripts/steps/test/jest_integration.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -euo pipefail + +.buildkite/scripts/bootstrap.sh + +echo '--- Jest Integration Tests' +node scripts/jest_integration --ci --verbose diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index ef9d0970aef7f..401ff60ee4187 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -68,7 +68,7 @@ export const IGNORE_FILE_GLOBS = [ '**/BUILD.bazel', // Buildkite - '.buildkite/hooks/*', + '.buildkite/*', ]; /** From 66de538c0e9e43db0850433c4261fc01fc964295 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Sep 2021 16:58:44 -0400 Subject: [PATCH 022/139] [Security Solution][Endpoint][TrustedApps][EventFilters] Change add button color (#111218) (#111621) Co-authored-by: Candace Park <56409205+parkiino@users.noreply.github.com> --- .../lists/public/exceptions/components/builder/logic_buttons.tsx | 1 - .../pages/trusted_apps/view/components/condition_group/index.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/logic_buttons.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/logic_buttons.tsx index 30fda556f0df8..3846b844bb55a 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/logic_buttons.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/logic_buttons.tsx @@ -41,7 +41,6 @@ export const BuilderLogicButtons: React.FC = ({ (

Date: Wed, 8 Sep 2021 14:00:27 -0700 Subject: [PATCH 023/139] Use consistent 'issues' and 'critical' vs. 'warning' terminology in UA. (#111221) --- .../es_deprecations/deprecations_list.test.ts | 2 +- .../error_handling.test.ts | 6 ++++-- .../es_deprecations/es_deprecations.tsx | 4 ++-- .../es_deprecations/es_deprecations_table.tsx | 2 +- .../es_deprecations_table_cells.tsx | 8 +++++++- .../kibana_deprecations.tsx | 10 +++++----- .../kibana_deprecations_table.tsx | 19 +++++++++++++++---- 7 files changed, 35 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/deprecations_list.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/deprecations_list.test.ts index 316e1a65f0890..390aeeb6d33e3 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/deprecations_list.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/deprecations_list.test.ts @@ -162,7 +162,7 @@ describe('ES deprecations table', () => { expect(exists('noDeprecationsRow')).toBe(true); expect(find('noDeprecationsRow').text()).toContain( - 'No Elasticsearch deprecation warnings found' + 'No Elasticsearch deprecation issues found' ); }); }); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/error_handling.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/error_handling.test.ts index b45f0b0527ac0..dbe49dfb714b9 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/error_handling.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/error_handling.test.ts @@ -53,7 +53,7 @@ describe('Error handling', () => { expect(exists('kibanaDeprecationErrors')).toBe(true); expect(find('kibanaDeprecationErrors').text()).toContain( - 'Deprecation warnings may be incomplete' + 'List of deprecation issues might be incomplete' ); }); @@ -78,6 +78,8 @@ describe('Error handling', () => { component.update(); expect(exists('kibanaRequestError')).toBe(true); - expect(find('kibanaRequestError').text()).toContain('Could not retrieve Kibana deprecations'); + expect(find('kibanaRequestError').text()).toContain( + 'Could not retrieve Kibana deprecation issues' + ); }); }); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx index b823740433f84..5ce37e2e43815 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx @@ -38,14 +38,14 @@ const getDeprecationCountByLevel = (deprecations: EnrichedDeprecationInfo[]) => const i18nTexts = { pageTitle: i18n.translate('xpack.upgradeAssistant.esDeprecations.pageTitle', { - defaultMessage: 'Elasticsearch deprecation warnings', + defaultMessage: 'Elasticsearch deprecation issues', }), pageDescription: i18n.translate('xpack.upgradeAssistant.esDeprecations.pageDescription', { defaultMessage: 'You must resolve all critical issues before upgrading. Back up recommended. Make sure you have a current snapshot before modifying your configuration or reindexing.', }), isLoading: i18n.translate('xpack.upgradeAssistant.esDeprecations.loadingText', { - defaultMessage: 'Loading deprecations…', + defaultMessage: 'Loading deprecation issues…', }), }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table.tsx index 18375d296aa3d..68e5e8b00d007 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table.tsx @@ -45,7 +45,7 @@ const i18nTexts = { noDeprecationsMessage: i18n.translate( 'xpack.upgradeAssistant.esDeprecations.table.noDeprecationsMessage', { - defaultMessage: 'No Elasticsearch deprecation warnings found', + defaultMessage: 'No Elasticsearch deprecation issues found', } ), typeFilterLabel: i18n.translate('xpack.upgradeAssistant.esDeprecations.table.typeFilterLabel', { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table_cells.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table_cells.tsx index a42ab1baa887f..2e2299ec8b1e0 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table_cells.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations_table_cells.tsx @@ -26,6 +26,12 @@ const i18nTexts = { defaultMessage: 'Critical', } ), + warningBadgeLabel: i18n.translate( + 'xpack.upgradeAssistant.esDeprecations.defaultDeprecation.warningBadgeLabel', + { + defaultMessage: 'Warning', + } + ), manualCellLabel: i18n.translate( 'xpack.upgradeAssistant.esDeprecations.defaultDeprecation.manualCellLabel', { @@ -52,7 +58,7 @@ export const EsDeprecationsTableCells: React.FunctionComponent = ({ return {i18nTexts.criticalBadgeLabel}; } - return <>{''}; + return {i18nTexts.warningBadgeLabel}; } // "Issue" column diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx index a6d7e4fdb674a..eec233b7fda6a 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx @@ -26,7 +26,7 @@ const { useGlobalFlyout } = GlobalFlyout; const i18nTexts = { pageTitle: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.pageTitle', { - defaultMessage: 'Kibana deprecation warnings', + defaultMessage: 'Kibana deprecation issues', }), pageDescription: ( i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.kibanaDeprecationErrorDescription', { defaultMessage: - 'Failed to get deprecation warnings for {pluginCount, plural, one {this plugin} other {these plugins}}: {pluginIds}. Check the Kibana server logs for more details.', + 'Failed to get deprecation issues for {pluginCount, plural, one {this plugin} other {these plugins}}: {pluginIds}. Check the Kibana server logs for more information.', values: { pluginCount: pluginIds.length, pluginIds: pluginIds.join(', '), }, }), requestErrorTitle: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.requestErrorTitle', { - defaultMessage: 'Could not retrieve Kibana deprecations', + defaultMessage: 'Could not retrieve Kibana deprecation issues', }), requestErrorDescription: i18n.translate( 'xpack.upgradeAssistant.kibanaDeprecationErrors.requestErrorDescription', diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations_table.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations_table.tsx index 9117bcdbcaee5..1df9402c686aa 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations_table.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations_table.tsx @@ -86,6 +86,12 @@ const i18nTexts = { defaultMessage: 'critical', } ), + warningBadgeLabel: i18n.translate( + 'xpack.upgradeAssistant.kibanaDeprecations.table.warningBadgeLabel', + { + defaultMessage: 'warning', + } + ), searchPlaceholderLabel: i18n.translate( 'xpack.upgradeAssistant.kibanaDeprecations.table.searchPlaceholderLabel', { @@ -115,11 +121,16 @@ export const KibanaDeprecationsTable: React.FunctionComponent = ({ truncateText: true, sortable: true, render: (level: KibanaDeprecationDetails['level']) => { - if (level === 'critical') { - return {i18nTexts.criticalBadgeLabel}; - } + switch (level) { + case 'critical': + return {i18nTexts.criticalBadgeLabel}; - return <>{''}; + case 'warning': + return {i18nTexts.warningBadgeLabel}; + + default: + return <>{''}; + } }, }, { From af34f8e954bc76967762f05fe107222a1108f269 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Sep 2021 17:17:49 -0400 Subject: [PATCH 024/139] [Security Solution] Enable schema-driven sorting descriptions in column headers (#111232) (#111607) ## Summary This PR resolves issue reported by @snide to enable schema-driven sorting descriptions in column headers. @chandlerprall recommends obtaining a **+1** from the Machine Learning and Observability solutions, because the fix updates an `i18n` constant in Kibana common to all consumers of `EuiDataGrid`. ## Details Thanks @chandlerprall for paring on this! The Alerts table, `Host > Events`, and other `EuiDataGrid`-based views in the Security Solution make use of the default [`EuiDataGrid` schemas](https://elastic.github.io/eui/#/tabular-content/data-grid-schemas-and-popovers). The default schemas enable `EuiDataGrid` to automatically display, for example, `Old-New` and `New-Old` sorting descriptions for datetime fields, as opposed to generic `A-Z` and `Z-A` descriptions. The following (shared) Kibana `i18n` constant in `src/core/public/i18n/i18n_eui_mapping.tsx` is expected to be rendered a `string` at runtime: ```ts 'euiColumnActions.sort': ({ schemaLabel }: EuiValues) => i18n.translate('core.euiColumnActions.sort', { defaultMessage: 'Sort {schemaLabel}', values: { schemaLabel }, }), ``` But the constant was rendered in `EuiDataGrid` column headers as `[object Object]` when schemas were enabled, as shown in the screenshot below: ![column-header-object-object](https://user-images.githubusercontent.com/4459398/132079843-a8b0f5e5-9d47-4816-8baa-e29577511bf1.png) _Above: The `sortTextAsc/Desc` text was rendered as `[object Object]`_ The temporary workaround described by [#110041](https://github.com/elastic/kibana/issues/110041) ensured that `Sort A-Z` and `Sort Z-A` labels were always displayed (in lieu of `[object Object]`), as shown in the screenshot below: ![image](https://user-images.githubusercontent.com/324519/130789326-bfe67cae-e4f7-469a-9b57-320cbf733cc8.png) _Above: `Sort A-Z` and `Sort Z-A` labels were always displayed as a workaround_ The fix in this PR updates the following (shared) Kibana `i18n` constant in `src/core/public/i18n/i18n_eui_mapping.tsx` to use a `FormattedMessage`: ```ts 'euiColumnActions.sort': ({ schemaLabel }: EuiValues) => ( ), ``` , which ensures a schema-specific sorting label is displayed as-expected. It also removes the workaround, as shown in the animated gif below: ![after](https://user-images.githubusercontent.com/4459398/132080352-1ee41a7e-8884-45ad-ae3c-daa9a0127aac.gif) _Above: Schema-specific sorting descriptions are displayed for `datetime`, `text`, and `numeric` column headers_ Co-authored-by: Andrew Goldstein --- src/core/public/i18n/i18n_eui_mapping.tsx | 12 +++++++----- .../t_grid/body/column_headers/helpers.test.tsx | 8 ++------ .../t_grid/body/column_headers/helpers.tsx | 5 ++--- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/core/public/i18n/i18n_eui_mapping.tsx b/src/core/public/i18n/i18n_eui_mapping.tsx index 4175dac712e82..2fe9657bce8c9 100644 --- a/src/core/public/i18n/i18n_eui_mapping.tsx +++ b/src/core/public/i18n/i18n_eui_mapping.tsx @@ -218,11 +218,13 @@ export const getEuiContextMapping = (): EuiTokensObject => { 'euiColumnActions.hideColumn': i18n.translate('core.euiColumnActions.hideColumn', { defaultMessage: 'Hide column', }), - 'euiColumnActions.sort': ({ schemaLabel }: EuiValues) => - i18n.translate('core.euiColumnActions.sort', { - defaultMessage: 'Sort {schemaLabel}', - values: { schemaLabel }, - }), + 'euiColumnActions.sort': ({ schemaLabel }: EuiValues) => ( + + ), 'euiColumnActions.moveLeft': i18n.translate('core.euiColumnActions.moveLeft', { defaultMessage: 'Move left', }), diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx index 42057062d8b54..2e684b9eda989 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx @@ -98,12 +98,8 @@ describe('helpers', () => { describe('getColumnHeaders', () => { // additional properties used by `EuiDataGrid`: const actions = { - showSortAsc: { - label: 'Sort A-Z', - }, - showSortDesc: { - label: 'Sort Z-A', - }, + showSortAsc: true, + showSortDesc: true, }; const defaultSortDirection = 'desc'; const isSortable = true; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx index cd08e880bcb25..c658000e6d331 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx @@ -23,11 +23,10 @@ import { MINIMUM_ACTIONS_COLUMN_WIDTH, } from '../constants'; import { allowSorting } from '../helpers'; -import * as i18n from './translations'; const defaultActions: EuiDataGridColumnActions = { - showSortAsc: { label: i18n.SORT_AZ }, - showSortDesc: { label: i18n.SORT_ZA }, + showSortAsc: true, + showSortDesc: true, }; const getAllBrowserFields = (browserFields: BrowserFields): Array> => From 54358b237880a31ea9ca9900ccab4a9e5fac1096 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Sep 2021 17:42:56 -0400 Subject: [PATCH 025/139] [APM] Bug with Transaction Latency Threshold rule (#111541) (#111633) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cauê Marcondes <55978943+cauemarcondes@users.noreply.github.com> --- x-pack/plugins/apm/public/components/alerting/fields.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/apm/public/components/alerting/fields.tsx b/x-pack/plugins/apm/public/components/alerting/fields.tsx index 8480fd276cb74..e482635152365 100644 --- a/x-pack/plugins/apm/public/components/alerting/fields.tsx +++ b/x-pack/plugins/apm/public/components/alerting/fields.tsx @@ -42,10 +42,13 @@ export function EnvironmentField({ const title = i18n.translate('xpack.apm.alerting.fields.environment', { defaultMessage: 'Environment', }); - - // "1" means "All" is the only option and we should not show a select. if (options.length === 1) { - return ; + return ( + + ); } return ( From 25081dad1d4c8b95bc5f5174ce0fca0cd5e23e67 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Sep 2021 17:48:36 -0400 Subject: [PATCH 026/139] [ML] APM Correlations: Fix API integration tests. (#111532) (#111638) A bugfix for significant terms on the ES side resulted in changes to assertions we have for api integration tests for APM correlations. This PR updates the affected tests. Co-authored-by: Walter Rafelsberger --- .../errors_failed_transactions.ts | 9 +++------ .../tests/correlations/failed_transactions.ts | 20 +++++++++---------- .../correlations/latency_slow_transactions.ts | 7 +------ 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/x-pack/test/apm_api_integration/tests/correlations/errors_failed_transactions.ts b/x-pack/test/apm_api_integration/tests/correlations/errors_failed_transactions.ts index b08ced565ec30..b3c5302ee2c6b 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/errors_failed_transactions.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/errors_failed_transactions.ts @@ -22,7 +22,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { query: { start: range.start, end: range.end, - fieldNames: 'user_agent.name,user_agent.os.name,url.original', + fieldNames: 'http.response.status_code,user_agent.name,user_agent.os.name,url.original', environment: 'ENVIRONMENT_ALL', kuery: '', }, @@ -40,8 +40,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { } ); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/109660 - registry.when.skip( + registry.when( 'correlations errors failed transactions with data and default args', { config: 'trial', archives: ['apm_8.0.0'] }, () => { @@ -66,8 +65,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const sortedFieldNames = significantTerms.map(({ fieldName }) => fieldName).sort(); expectSnapshot(sortedFieldNames).toMatchInline(` Array [ - "user_agent.name", - "user_agent.name", + "http.response.status_code", ] `); }); @@ -77,7 +75,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(significantTerms.map((term) => term.timeseries.length)).toMatchInline(` Array [ 31, - 31, ] `); }); diff --git a/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts b/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts index 4b484502d5826..3c629de2f69aa 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts @@ -40,7 +40,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }; }; - registry.when('on trial license without data', { config: 'trial', archives: [] }, () => { + registry.when('failed transactions without data', { config: 'trial', archives: [] }, () => { it('queries the search strategy and returns results', async () => { const intialResponse = await supertest .post(`/internal/bsearch`) @@ -129,8 +129,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/109703 - registry.when.skip('on trial license with data', { config: 'trial', archives: ['8.0.0'] }, () => { + registry.when('failed transactions with data', { config: 'trial', archives: ['8.0.0'] }, () => { it('queries the search strategy and returns results', async () => { const intialResponse = await supertest .post(`/internal/bsearch`) @@ -215,26 +214,25 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(finalRawResponse?.overallHistogram).to.be(undefined); expect(finalRawResponse?.failedTransactionsCorrelations.length).to.eql( - 43, - `Expected 43 identified correlations, got ${finalRawResponse?.failedTransactionsCorrelations.length}.` + 30, + `Expected 30 identified correlations, got ${finalRawResponse?.failedTransactionsCorrelations.length}.` ); expect(finalRawResponse?.log.map((d: string) => d.split(': ')[1])).to.eql([ 'Identified 68 fieldCandidates.', 'Identified correlations for 68 fields out of 68 candidates.', - 'Identified 43 significant correlations relating to failed transactions.', + 'Identified 30 significant correlations relating to failed transactions.', ]); const sortedCorrelations = finalRawResponse?.failedTransactionsCorrelations.sort(); const correlation = sortedCorrelations[0]; expect(typeof correlation).to.be('object'); - expect(correlation?.key).to.be('HTTP 5xx'); expect(correlation?.doc_count).to.be(31); - expect(correlation?.score).to.be(100.17736139032642); - expect(correlation?.bg_count).to.be(60); - expect(correlation?.fieldName).to.be('transaction.result'); - expect(correlation?.fieldValue).to.be('HTTP 5xx'); + expect(correlation?.score).to.be(83.70467673605746); + expect(correlation?.bg_count).to.be(31); + expect(correlation?.fieldName).to.be('http.response.status_code'); + expect(correlation?.fieldValue).to.be(500); expect(typeof correlation?.pValue).to.be('number'); expect(typeof correlation?.normalizedScore).to.be('number'); expect(typeof correlation?.failurePercentage).to.be('number'); diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency_slow_transactions.ts b/x-pack/test/apm_api_integration/tests/correlations/latency_slow_transactions.ts index 09c092ed1a646..c72753a86f6a6 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/latency_slow_transactions.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/latency_slow_transactions.ts @@ -43,8 +43,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { } ); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/109583 - registry.when.skip( + registry.when( 'correlations latency slow transactions with data and default args', { config: 'trial', archives: ['apm_8.0.0'] }, () => { @@ -74,8 +73,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { "url.original", "user_agent.name", "user_agent.name", - "user_agent.name", - "user_agent.name", "user_agent.os.name", ] `); @@ -91,8 +88,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { 15, 15, 15, - 15, - 15, ] `); }); From 8b11a49dcbd90abe7e174b69aa10050168286805 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Sep 2021 17:57:10 -0400 Subject: [PATCH 027/139] [yarn.lock] Dedupe browserslist (#111584) (#111641) Co-authored-by: Jonathan Budzenski --- yarn.lock | 72 ++++--------------------------------------------------- 1 file changed, 4 insertions(+), 68 deletions(-) diff --git a/yarn.lock b/yarn.lock index 237969e06fa9e..f0dbc754631ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8872,7 +8872,7 @@ browserslist@4.14.2: escalade "^3.0.2" node-releases "^1.1.61" -browserslist@^4.0.0: +browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.16.1, browserslist@^4.6.0, browserslist@^4.8.5: version "4.16.3" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.3.tgz#340aa46940d7db878748567c5dea24a48ddf3717" integrity sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw== @@ -8883,48 +8883,6 @@ browserslist@^4.0.0: escalade "^3.1.1" node-releases "^1.1.70" -browserslist@^4.12.0: - version "4.12.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.12.0.tgz#06c6d5715a1ede6c51fc39ff67fd647f740b656d" - integrity sha512-UH2GkcEDSI0k/lRkuDSzFl9ZZ87skSy9w2XAn1MsZnL+4c4rqbBd3e82UWHbYDpztABrPBhZsTEeuxVfHppqDg== - dependencies: - caniuse-lite "^1.0.30001043" - electron-to-chromium "^1.3.413" - node-releases "^1.1.53" - pkg-up "^2.0.0" - -browserslist@^4.14.5: - version "4.14.7" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.7.tgz#c071c1b3622c1c2e790799a37bb09473a4351cb6" - integrity sha512-BSVRLCeG3Xt/j/1cCGj1019Wbty0H+Yvu2AOuZSuoaUWn3RatbL33Cxk+Q4jRMRAbOm0p7SLravLjpnT6s0vzQ== - dependencies: - caniuse-lite "^1.0.30001157" - colorette "^1.2.1" - electron-to-chromium "^1.3.591" - escalade "^3.1.1" - node-releases "^1.1.66" - -browserslist@^4.16.1: - version "4.16.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.1.tgz#bf757a2da376b3447b800a16f0f1c96358138766" - integrity sha512-UXhDrwqsNcpTYJBTZsbGATDxZbiVDsx6UjpmRUmtnP10pr8wAYr5LgFoEFw9ixriQH2mv/NX2SfGzE/o8GndLA== - dependencies: - caniuse-lite "^1.0.30001173" - colorette "^1.2.1" - electron-to-chromium "^1.3.634" - escalade "^3.1.1" - node-releases "^1.1.69" - -browserslist@^4.6.0, browserslist@^4.8.5: - version "4.14.5" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.5.tgz#1c751461a102ddc60e40993639b709be7f2c4015" - integrity sha512-Z+vsCZIvCBvqLoYkBFTwEYH3v5MCQbsAjp50ERycpOjnPmolg1Gjy4+KaWWpm8QOJt9GHkhdqAl14NpCX73CWA== - dependencies: - caniuse-lite "^1.0.30001135" - electron-to-chromium "^1.3.571" - escalade "^3.1.0" - node-releases "^1.1.61" - bser@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719" @@ -9281,7 +9239,7 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001043, caniuse-lite@^1.0.30001097, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001135, caniuse-lite@^1.0.30001157, caniuse-lite@^1.0.30001173, caniuse-lite@^1.0.30001181: +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001097, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001181: version "1.0.30001208" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001208.tgz" integrity sha512-OE5UE4+nBOro8Dyvv0lfx+SRtfVIOM9uhKqFmJeUbGriqhhStgp1A0OyBpgy3OUF8AhYCT+PVwPC1gMl2ZcQMA== @@ -12432,7 +12390,7 @@ elasticsearch@^16.4.0: chalk "^1.0.0" lodash "^4.17.10" -electron-to-chromium@^1.3.413, electron-to-chromium@^1.3.564, electron-to-chromium@^1.3.571, electron-to-chromium@^1.3.591, electron-to-chromium@^1.3.634: +electron-to-chromium@^1.3.564: version "1.3.642" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.642.tgz#8b884f50296c2ae2a9997f024d0e3e57facc2b94" integrity sha512-cev+jOrz/Zm1i+Yh334Hed6lQVOkkemk2wRozfMF4MtTR7pxf3r3L5Rbd7uX1zMcEqVJ7alJBnJL7+JffkC6FQ== @@ -12858,7 +12816,7 @@ es6-weak-map@^2.0.1, es6-weak-map@^2.0.2: es6-iterator "^2.0.1" es6-symbol "^3.1.1" -escalade@^3.0.2, escalade@^3.1.0, escalade@^3.1.1: +escalade@^3.0.2, escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== @@ -20478,26 +20436,11 @@ node-preload@^0.2.1: dependencies: process-on-spawn "^1.0.0" -node-releases@^1.1.53: - version "1.1.60" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.60.tgz#6948bdfce8286f0b5d0e5a88e8384e954dfe7084" - integrity sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA== - node-releases@^1.1.61: version "1.1.61" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.61.tgz#707b0fca9ce4e11783612ba4a2fcba09047af16e" integrity sha512-DD5vebQLg8jLCOzwupn954fbIiZht05DAZs0k2u8NStSe6h9XdsuIQL8hSRKYiU8WUQRznmSDrKGbv3ObOmC7g== -node-releases@^1.1.66: - version "1.1.67" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.67.tgz#28ebfcccd0baa6aad8e8d4d8fe4cbc49ae239c12" - integrity sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg== - -node-releases@^1.1.69: - version "1.1.70" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.70.tgz#66e0ed0273aa65666d7fe78febe7634875426a08" - integrity sha512-Slf2s69+2/uAD79pVVQo8uSiC34+g8GWY8UH2Qtqv34ZfhYrxpYpfzs9Js9d6O0mbDmALuxaTlplnBTnSELcrw== - node-releases@^1.1.70: version "1.1.71" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb" @@ -21855,13 +21798,6 @@ pkg-up@3.1.0, pkg-up@^3.1.0: dependencies: find-up "^3.0.0" -pkg-up@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f" - integrity sha1-yBmscoBZpGHKscOImivjxJoATX8= - dependencies: - find-up "^2.1.0" - platform@^1.3.0: version "1.3.5" resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.5.tgz#fb6958c696e07e2918d2eeda0f0bc9448d733444" From df07fbc414acb88e2afa139ecf5433b890da42a4 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Sep 2021 18:01:35 -0400 Subject: [PATCH 028/139] [ci] Update list of storybooks to build (#111556) (#111640) Co-authored-by: Jonathan Budzenski --- test/scripts/jenkins_storybook.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/scripts/jenkins_storybook.sh b/test/scripts/jenkins_storybook.sh index 73ab43fd02eba..00cc0d78599dd 100755 --- a/test/scripts/jenkins_storybook.sh +++ b/test/scripts/jenkins_storybook.sh @@ -6,13 +6,20 @@ cd "$KIBANA_DIR" yarn storybook --site apm yarn storybook --site canvas +yarn storybook --site codeeditor yarn storybook --site ci_composite yarn storybook --site url_template_editor -yarn storybook --site codeeditor yarn storybook --site dashboard yarn storybook --site dashboard_enhanced yarn storybook --site data_enhanced yarn storybook --site embeddable +yarn storybook --site expression_error +yarn storybook --site expression_image +yarn storybook --site expression_metric +yarn storybook --site expression_repeat_image +yarn storybook --site expression_reveal_image +yarn storybook --site expression_shape +yarn storybook --site expression_tagcloud yarn storybook --site infra yarn storybook --site security_solution yarn storybook --site ui_actions_enhanced From af9d3f5f42c4c26f4e814ab364941fe994500d5a Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Wed, 8 Sep 2021 16:32:52 -0700 Subject: [PATCH 029/139] Refactor UA Overview to support step-completion (#111243) * Refactor UA Overview to store step-completion state at the root and delegate step-completion logic to each step component. * Add completion status to logs and issues steps --- .../client_integration/helpers/index.ts | 1 + .../helpers/time_manipulation.ts | 24 ++++++++ .../overview/backup_step/backup_step.test.tsx | 58 ++++++++++++++----- .../fix_issues_step/fix_issues_step.test.tsx | 31 ++++++++++ .../fix_logs_step/fix_logs_step.test.tsx | 43 ++++++++++++-- .../overview/backup_step/backup_step.tsx | 19 +++--- .../overview/backup_step/cloud_backup.tsx | 35 ++++++++--- .../fix_issues_step/es_stats/es_stats.tsx | 27 +++++++-- .../fix_issues_step/fix_issues_step.tsx | 56 ++++++++++++++---- .../kibana_stats/kibana_stats.tsx | 12 +++- .../deprecations_count_checkpoint.tsx | 31 +++++++--- .../overview/fix_logs_step/fix_logs_step.tsx | 18 ++++-- .../components/overview/overview.tsx | 37 +++++++++--- .../public/application/components/types.ts | 5 ++ 14 files changed, 323 insertions(+), 74 deletions(-) create mode 100644 x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/time_manipulation.ts diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/index.ts index c52ff3290dd06..ce054fc8af84e 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/index.ts @@ -6,4 +6,5 @@ */ export { setupEnvironment, WithAppDependencies } from './setup_environment'; +export { advanceTime } from './time_manipulation'; export { kibanaDeprecationsServiceHelpers } from './kibana_deprecations_service.mock'; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/time_manipulation.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/time_manipulation.ts new file mode 100644 index 0000000000000..65cec19549736 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/time_manipulation.ts @@ -0,0 +1,24 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +/** + * These helpers are intended to be used in conjunction with jest.useFakeTimers(). + */ + +const flushPromiseJobQueue = async () => { + // See https://stackoverflow.com/questions/52177631/jest-timer-and-promise-dont-work-well-settimeout-and-async-function + await Promise.resolve(); +}; + +export const advanceTime = async (ms: number) => { + await act(async () => { + jest.advanceTimersByTime(ms); + await flushPromiseJobQueue(); + }); +}; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/backup_step/backup_step.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/backup_step/backup_step.test.tsx index 39a94ec458be4..3dcc55adbe61d 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/backup_step/backup_step.test.tsx +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/backup_step/backup_step.test.tsx @@ -5,9 +5,8 @@ * 2.0. */ -import { act } from 'react-dom/test-utils'; - -import { setupEnvironment } from '../../helpers'; +import { CLOUD_BACKUP_STATUS_POLL_INTERVAL_MS } from '../../../../common/constants'; +import { setupEnvironment, advanceTime } from '../../helpers'; import { OverviewTestBed, setupOverviewPage } from '../overview.helpers'; describe('Overview - Backup Step', () => { @@ -34,6 +33,11 @@ describe('Overview - Backup Step', () => { expect(exists('snapshotRestoreLink')).toBe(true); expect(find('snapshotRestoreLink').props().href).toBe('snapshotAndRestoreUrl'); }); + + test('renders step as incomplete ', () => { + const { exists } = testBed; + expect(exists('backupStep-incomplete')).toBe(true); + }); }); describe('On Cloud', () => { @@ -104,6 +108,11 @@ describe('Overview - Backup Step', () => { expect(exists('cloudSnapshotsLink')).toBe(true); expect(find('dataBackedUpStatus').text()).toContain('Last snapshot created on'); }); + + test('renders step as complete ', () => { + const { exists } = testBed; + expect(exists('backupStep-complete')).toBe(true); + }); }); describe(`when data isn't backed up`, () => { @@ -121,24 +130,47 @@ describe('Overview - Backup Step', () => { expect(exists('dataNotBackedUpStatus')).toBe(true); expect(exists('cloudSnapshotsLink')).toBe(true); }); + + test('renders step as incomplete ', () => { + const { exists } = testBed; + expect(exists('backupStep-incomplete')).toBe(true); + }); }); + }); - // FLAKY: https://github.com/elastic/kibana/issues/111255 - test.skip('polls for new status', async () => { - // The behavior we're testing involves state changes over time, so we need finer control over - // timing. + describe('poll for new status', () => { + beforeEach(async () => { jest.useFakeTimers(); - testBed = await setupCloudOverviewPage(); - expect(server.requests.length).toBe(4); - // Resolve the polling timeout. - await act(async () => { - jest.runAllTimers(); + // First request will succeed. + httpRequestsMockHelpers.setLoadCloudBackupStatusResponse({ + isBackedUp: true, + lastBackupTime: '2021-08-25T19:59:59.863Z', }); - expect(server.requests.length).toBe(5); + testBed = await setupCloudOverviewPage(); + }); + + afterEach(() => { jest.useRealTimers(); }); + + test('renders step as incomplete when a success state is followed by an error state', async () => { + const { exists } = testBed; + expect(exists('backupStep-complete')).toBe(true); + + // Second request will error. + httpRequestsMockHelpers.setLoadCloudBackupStatusResponse(undefined, { + statusCode: 400, + message: 'error', + }); + + // Resolve the polling timeout. + await advanceTime(CLOUD_BACKUP_STATUS_POLL_INTERVAL_MS); + testBed.component.update(); + + expect(exists('backupStep-incomplete')).toBe(true); + }); }); }); }); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/fix_issues_step.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/fix_issues_step.test.tsx index 77d3862b9e962..0b572ab41deaa 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/fix_issues_step.test.tsx +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_issues_step/fix_issues_step.test.tsx @@ -44,6 +44,37 @@ describe('Overview - Fix deprecation issues step', () => { server.restore(); }); + describe('Step status', () => { + test(`It's complete when there are no critical deprecations`, async () => { + httpRequestsMockHelpers.setLoadEsDeprecationsResponse(esDeprecationsEmpty); + + await act(async () => { + const deprecationService = deprecationsServiceMock.createStartContract(); + deprecationService.getAllDeprecations = jest.fn().mockRejectedValue([]); + + testBed = await setupOverviewPage({ + services: { + core: { + deprecations: deprecationService, + }, + }, + }); + }); + + const { exists, component } = testBed; + + component.update(); + + expect(exists(`fixIssuesStep-complete`)).toBe(true); + }); + + test(`It's incomplete when there are critical deprecations`, async () => { + const { exists } = testBed; + + expect(exists(`fixIssuesStep-incomplete`)).toBe(true); + }); + }); + describe('ES deprecations', () => { test('Shows deprecation warning and critical counts', () => { const { exists, find } = testBed; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_logs_step/fix_logs_step.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_logs_step/fix_logs_step.test.tsx index dec34ba1e3720..acc64e2872642 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_logs_step/fix_logs_step.test.tsx +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_logs_step/fix_logs_step.test.tsx @@ -33,14 +33,49 @@ describe('Overview - Fix deprecation logs step', () => { server.restore(); }); + describe('Step status', () => { + test(`It's complete when there are no deprecation logs since last checkpoint`, async () => { + httpRequestsMockHelpers.setUpdateDeprecationLoggingResponse(getLoggingResponse(true)); + + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ + count: 0, + }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { exists, component } = testBed; + + component.update(); + + expect(exists(`fixLogsStep-complete`)).toBe(true); + }); + + test(`It's incomplete when there are deprecation logs since last checkpoint`, async () => { + httpRequestsMockHelpers.setUpdateDeprecationLoggingResponse(getLoggingResponse(true)); + + httpRequestsMockHelpers.setLoadDeprecationLogsCountResponse({ + count: 5, + }); + + await act(async () => { + testBed = await setupOverviewPage(); + }); + + const { exists, component } = testBed; + + component.update(); + + expect(exists(`fixLogsStep-incomplete`)).toBe(true); + }); + }); + describe('Step 1 - Toggle log writing and collecting', () => { test('toggles deprecation logging', async () => { const { find, actions } = testBed; - httpRequestsMockHelpers.setUpdateDeprecationLoggingResponse({ - isDeprecationLogIndexingEnabled: false, - isDeprecationLoggingEnabled: false, - }); + httpRequestsMockHelpers.setUpdateDeprecationLoggingResponse(getLoggingResponse(false)); expect(find('deprecationLoggingToggle').props()['aria-checked']).toBe(true); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/backup_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/backup_step.tsx index b85f166e413f5..46b11aee15b33 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/backup_step.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/backup_step.tsx @@ -11,33 +11,34 @@ import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; import type { CloudSetup } from '../../../../../../cloud/public'; import { OnPremBackup } from './on_prem_backup'; -import { CloudBackup, CloudBackupStatusResponse } from './cloud_backup'; +import { CloudBackup } from './cloud_backup'; +import type { OverviewStepProps } from '../../types'; const title = i18n.translate('xpack.upgradeAssistant.overview.backupStepTitle', { defaultMessage: 'Back up your data', }); -interface Props { +interface Props extends OverviewStepProps { cloud?: CloudSetup; - cloudBackupStatusResponse?: CloudBackupStatusResponse; } -export const getBackupStep = ({ cloud, cloudBackupStatusResponse }: Props): EuiStepProps => { +export const getBackupStep = ({ cloud, isComplete, setIsComplete }: Props): EuiStepProps => { + const status = isComplete ? 'complete' : 'incomplete'; + if (cloud?.isCloudEnabled) { return { + status, title, - status: cloudBackupStatusResponse!.data?.isBackedUp ? 'complete' : 'incomplete', + 'data-test-subj': `backupStep-${status}`, children: ( - + ), }; } return { title, + 'data-test-subj': 'backupStep-incomplete', status: 'incomplete', children: , }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/cloud_backup.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/cloud_backup.tsx index e73cfaab8b2b6..abef34a27f30f 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/cloud_backup.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/cloud_backup.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import moment from 'moment-timezone'; import { FormattedDate, FormattedTime, FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -20,22 +20,39 @@ import { EuiCallOut, } from '@elastic/eui'; -import { CloudBackupStatus } from '../../../../../common/types'; -import { UseRequestResponse } from '../../../../shared_imports'; -import { ResponseError } from '../../../lib/api'; - -export type CloudBackupStatusResponse = UseRequestResponse; +import { useAppContext } from '../../../app_context'; interface Props { - cloudBackupStatusResponse: UseRequestResponse; cloudSnapshotsUrl: string; + setIsComplete: (isComplete: boolean) => void; } export const CloudBackup: React.FunctionComponent = ({ - cloudBackupStatusResponse, cloudSnapshotsUrl, + setIsComplete, }) => { - const { isInitialRequest, isLoading, error, data, resendRequest } = cloudBackupStatusResponse; + const { + services: { api }, + } = useAppContext(); + + const { + isInitialRequest, + isLoading, + error, + data, + resendRequest, + } = api.useLoadCloudBackupStatus(); + + // Tell overview whether the step is complete or not. + useEffect(() => { + // Loading shouldn't invalidate the previous state. + if (!isLoading) { + // An error should invalidate the previous state. + setIsComplete((!error && data?.isBackedUp) ?? false); + } + // Depending upon setIsComplete would create an infinite loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [error, isLoading, data]); if (isInitialRequest && isLoading) { return ; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/es_stats/es_stats.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/es_stats/es_stats.tsx index a55f22c8dfda4..7d0dcacfaa207 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/es_stats/es_stats.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/es_stats/es_stats.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FunctionComponent } from 'react'; +import React, { FunctionComponent, useEffect, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; import { EuiStat, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiCard } from '@elastic/eui'; @@ -35,7 +35,11 @@ const i18nTexts = { ), }; -export const ESDeprecationStats: FunctionComponent = () => { +interface Props { + setIsFixed: (isFixed: boolean) => void; +} + +export const ESDeprecationStats: FunctionComponent = ({ setIsFixed }) => { const history = useHistory(); const { services: { api }, @@ -43,10 +47,21 @@ export const ESDeprecationStats: FunctionComponent = () => { const { data: esDeprecations, isLoading, error } = api.useLoadEsDeprecations(); - const warningDeprecations = - esDeprecations?.deprecations?.filter((deprecation) => deprecation.isCritical === false) || []; - const criticalDeprecations = - esDeprecations?.deprecations?.filter((deprecation) => deprecation.isCritical) || []; + const warningDeprecations = useMemo( + () => + esDeprecations?.deprecations?.filter((deprecation) => deprecation.isCritical === false) || [], + [esDeprecations] + ); + const criticalDeprecations = useMemo( + () => esDeprecations?.deprecations?.filter((deprecation) => deprecation.isCritical) || [], + [esDeprecations] + ); + + useEffect(() => { + if (!isLoading && !error) { + setIsFixed(criticalDeprecations.length === 0); + } + }, [setIsFixed, criticalDeprecations, isLoading, error]); const hasWarnings = warningDeprecations.length > 0; const hasCritical = criticalDeprecations.length > 0; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/fix_issues_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/fix_issues_step.tsx index f235075350e66..df9bf58198d9d 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/fix_issues_step.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/fix_issues_step.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { FunctionComponent, useState, useEffect } from 'react'; import { EuiText, EuiFlexItem, EuiFlexGroup, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -13,6 +13,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; import { ESDeprecationStats } from './es_stats'; import { KibanaDeprecationStats } from './kibana_stats'; +import type { OverviewStepProps } from '../../types'; import './_fix_issues_step.scss'; @@ -22,10 +23,49 @@ const i18nTexts = { }), }; -export const getFixIssuesStep = ({ nextMajor }: { nextMajor: number }): EuiStepProps => { +interface Props { + setIsComplete: OverviewStepProps['setIsComplete']; +} + +interface StepProps extends OverviewStepProps { + nextMajor: number; +} + +const FixIssuesStep: FunctionComponent = ({ setIsComplete }) => { + // We consider ES and Kibana issues to be fixed when there are 0 critical issues. + const [isEsFixed, setIsEsFixed] = useState(false); + const [isKibanaFixed, setIsKibanaFixed] = useState(false); + + useEffect(() => { + setIsComplete(isEsFixed && isKibanaFixed); + // Depending upon setIsComplete would create an infinite loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isEsFixed, isKibanaFixed]); + + return ( + + + + + + + + + + ); +}; + +export const getFixIssuesStep = ({ + nextMajor, + isComplete, + setIsComplete, +}: StepProps): EuiStepProps => { + const status = isComplete ? 'complete' : 'incomplete'; + return { title: i18nTexts.reviewStepTitle, - status: 'incomplete', + status, + 'data-test-subj': `fixIssuesStep-${status}`, children: ( <> @@ -40,15 +80,7 @@ export const getFixIssuesStep = ({ nextMajor }: { nextMajor: number }): EuiStepP - - - - - - - - - + ), }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/kibana_stats/kibana_stats.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/kibana_stats/kibana_stats.tsx index 28e60e7e735fb..2cfc555116ba6 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/kibana_stats/kibana_stats.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/kibana_stats/kibana_stats.tsx @@ -41,7 +41,11 @@ const i18nTexts = { ), }; -export const KibanaDeprecationStats: FunctionComponent = () => { +interface Props { + setIsFixed: (isFixed: boolean) => void; +} + +export const KibanaDeprecationStats: FunctionComponent = ({ setIsFixed }) => { const history = useHistory(); const { services: { @@ -77,6 +81,12 @@ export const KibanaDeprecationStats: FunctionComponent = () => { const criticalDeprecationsCount = kibanaDeprecations?.filter((deprecation) => deprecation.level === 'critical')?.length ?? 0; + useEffect(() => { + if (!isLoading && !error) { + setIsFixed(criticalDeprecationsCount === 0); + } + }, [setIsFixed, criticalDeprecationsCount, isLoading, error]); + const hasCritical = criticalDeprecationsCount > 0; const hasWarnings = warningDeprecationsCount > 0; const hasNoDeprecations = !isLoading && !error && !hasWarnings && !hasCritical; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecations_count_checkpoint/deprecations_count_checkpoint.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecations_count_checkpoint/deprecations_count_checkpoint.tsx index 58b7b7ca11ebd..f0a4096687f6c 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecations_count_checkpoint/deprecations_count_checkpoint.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecations_count_checkpoint/deprecations_count_checkpoint.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FunctionComponent, useState } from 'react'; +import React, { FunctionComponent, useState, useEffect } from 'react'; import moment from 'moment-timezone'; import { FormattedDate, FormattedTime, FormattedMessage } from '@kbn/i18n/react'; @@ -64,7 +64,13 @@ const getPreviousCheckpointDate = () => { return now; }; -export const DeprecationsCountCheckpoint: FunctionComponent = () => { +interface Props { + setHasNoDeprecationLogs: (hasNoLogs: boolean) => void; +} + +export const DeprecationsCountCheckpoint: FunctionComponent = ({ + setHasNoDeprecationLogs, +}) => { const { services: { api }, } = useAppContext(); @@ -73,10 +79,11 @@ export const DeprecationsCountCheckpoint: FunctionComponent = () => { previousCheck ); - const warningsCount = data?.count || 0; - const calloutTint = warningsCount > 0 ? 'warning' : 'success'; - const calloutIcon = warningsCount > 0 ? 'alert' : 'check'; - const calloutTestId = warningsCount > 0 ? 'hasWarningsCallout' : 'noWarningsCallout'; + const logsCount = data?.count || 0; + const hasLogs = logsCount > 0; + const calloutTint = hasLogs ? 'warning' : 'success'; + const calloutIcon = hasLogs ? 'alert' : 'check'; + const calloutTestId = hasLogs ? 'hasWarningsCallout' : 'noWarningsCallout'; const onResetClick = () => { const now = moment().toISOString(); @@ -85,6 +92,16 @@ export const DeprecationsCountCheckpoint: FunctionComponent = () => { localStorage.set(LS_SETTING_ID, now); }; + useEffect(() => { + // Loading shouldn't invalidate the previous state. + if (!isLoading) { + // An error should invalidate the previous state. + setHasNoDeprecationLogs(!error && !hasLogs); + } + // Depending upon setHasNoDeprecationLogs would create an infinite loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [error, isLoading, hasLogs]); + if (isInitialRequest && isLoading) { return ; } @@ -109,7 +126,7 @@ export const DeprecationsCountCheckpoint: FunctionComponent = () => { return ( { +interface Props { + setIsComplete: OverviewStepProps['setIsComplete']; +} + +const FixLogsStep: FunctionComponent = ({ setIsComplete }) => { const state = useDeprecationLogging(); return ( @@ -88,17 +93,20 @@ const FixLogsStep: FunctionComponent = () => {

{i18nTexts.deprecationsCountCheckpointTitle}

- + )} ); }; -export const getFixLogsStep = (): EuiStepProps => { +export const getFixLogsStep = ({ isComplete, setIsComplete }: OverviewStepProps): EuiStepProps => { + const status = isComplete ? 'complete' : 'incomplete'; + return { + status, title: i18nTexts.identifyStepTitle, - status: 'incomplete', - children: , + 'data-test-subj': `fixLogsStep-${status}`, + children: , }; }; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx index 1c11bc165d931..010c9b7367158 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FunctionComponent, useEffect } from 'react'; +import React, { FunctionComponent, useEffect, useState } from 'react'; import { EuiSteps, @@ -26,6 +26,8 @@ import { getFixIssuesStep } from './fix_issues_step'; import { getFixLogsStep } from './fix_logs_step'; import { getUpgradeStep } from './upgrade_step'; +type OverviewStep = 'backup' | 'fix_issues' | 'fix_logs'; + export const Overview: FunctionComponent = () => { const { kibanaVersionInfo: { nextMajor }, @@ -51,11 +53,19 @@ export const Overview: FunctionComponent = () => { breadcrumbs.setBreadcrumbs('overview'); }, [breadcrumbs]); - let cloudBackupStatusResponse; + const [completedStepsMap, setCompletedStepsMap] = useState({ + backup: false, + fix_issues: false, + fix_logs: false, + }); - if (cloud?.isCloudEnabled) { - cloudBackupStatusResponse = api.useLoadCloudBackupStatus(); - } + const isStepComplete = (step: OverviewStep) => completedStepsMap[step]; + const setCompletedStep = (step: OverviewStep, isCompleted: boolean) => { + setCompletedStepsMap({ + ...completedStepsMap, + [step]: isCompleted, + }); + }; return ( @@ -97,9 +107,20 @@ export const Overview: FunctionComponent = () => { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/types.ts b/x-pack/plugins/upgrade_assistant/public/application/components/types.ts index 81495c45420bf..6b52f7cece514 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/types.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/types.ts @@ -31,3 +31,8 @@ export interface DeprecationLoggingPreviewProps { resendRequest: () => void; toggleLogging: () => void; } + +export interface OverviewStepProps { + isComplete: boolean; + setIsComplete: (isComplete: boolean) => void; +} From 8d611b7867aac55f9cec5343578f1db5573b04b2 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Sep 2021 19:47:12 -0400 Subject: [PATCH 030/139] [APM] Show badge for failed spans in waterfall (#109812) (#111652) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Casper Hübertz Co-authored-by: Søren Louv-Jansen Co-authored-by: Casper Hübertz --- .../distribution/index.tsx | 7 +- .../use_waterfall_fetcher.ts | 8 +- .../ErrorCount.test.tsx | 38 - .../waterfall_with_summary/ErrorCount.tsx | 35 - .../TransactionTabs.tsx | 19 +- .../waterfall_with_summary/index.tsx | 5 +- .../Waterfall/accordion_waterfall.tsx | 25 +- .../Waterfall/failure_badge.tsx | 37 + .../waterfall_container/Waterfall/index.tsx | 53 +- .../Waterfall/span_flyout/index.tsx | 46 +- .../{database_context.tsx => span_db.tsx} | 12 +- .../Waterfall/sync_badge.tsx | 14 +- .../Waterfall/waterfall_flyout.tsx | 2 +- .../waterfall_helpers.test.ts.snap | 12 +- .../waterfall_helpers.test.ts | 59 +- .../waterfall_helpers/waterfall_helpers.ts | 49 +- .../Waterfall/waterfall_item.tsx | 79 +- .../WaterfallContainer.stories.tsx | 52 +- .../waterfall_container/index.tsx | 8 +- .../waterfallContainer.stories.data.ts | 4166 ++++++++--------- .../shared/Links/apm/ErrorOverviewLink.tsx | 9 +- .../MetadataTable/SpanMetadata/sections.ts | 2 + .../TransactionMetadata/sections.ts | 2 + .../shared/MetadataTable/sections.ts | 8 + .../public/services/rest/createCallApmApi.ts | 13 +- .../create_apm_users_and_roles.ts | 30 + x-pack/plugins/apm/server/index.ts | 5 +- .../traces/__snapshots__/queries.test.ts.snap | 9 - .../apm/server/lib/traces/get_trace.ts | 21 - .../apm/server/lib/traces/get_trace_items.ts | 49 +- .../get_global_apm_server_route_repository.ts | 4 +- x-pack/plugins/apm/server/routes/traces.ts | 4 +- .../apm/typings/es_schemas/raw/span_raw.ts | 1 + .../typings/es_schemas/raw/transaction_raw.ts | 1 + .../translations/translations/zh-CN.json | 1 - .../common/apm_api_supertest.ts | 6 +- .../test/apm_api_integration/common/config.ts | 4 +- .../test/apm_api_integration/tests/index.ts | 3 + .../service_overview/instance_details.ts | 4 +- .../instances_detailed_statistics.ts | 4 +- .../error_groups_detailed_statistics.ts | 4 +- .../tests/traces/trace_by_id.tsx | 92 + 42 files changed, 2478 insertions(+), 2524 deletions(-) delete mode 100644 x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/ErrorCount.test.tsx delete mode 100644 x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/ErrorCount.tsx create mode 100644 x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/failure_badge.tsx rename x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/{database_context.tsx => span_db.tsx} (89%) delete mode 100644 x-pack/plugins/apm/server/lib/traces/get_trace.ts create mode 100644 x-pack/test/apm_api_integration/tests/traces/trace_by_id.tsx diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx index acd8c5f4d57d3..daebcdd8078cd 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx @@ -73,11 +73,7 @@ export function TransactionDistribution({ const { urlParams } = useUrlParams(); - const { - waterfall, - exceedsMax, - status: waterfallStatus, - } = useWaterfallFetcher(); + const { waterfall, status: waterfallStatus } = useWaterfallFetcher(); const markerCurrentTransaction = waterfall.entryWaterfallTransaction?.doc.transaction.duration.us; @@ -215,7 +211,6 @@ export function TransactionDistribution({ urlParams={urlParams} waterfall={waterfall} isLoading={waterfallStatus === FETCH_STATUS.LOADING} - exceedsMax={exceedsMax} traceSamples={traceSamples} /> diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/use_waterfall_fetcher.ts b/x-pack/plugins/apm/public/components/app/transaction_details/use_waterfall_fetcher.ts index 6bde8edcc250a..c015051b3c955 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/use_waterfall_fetcher.ts +++ b/x-pack/plugins/apm/public/components/app/transaction_details/use_waterfall_fetcher.ts @@ -13,9 +13,9 @@ import { useTimeRange } from '../../../hooks/use_time_range'; import { getWaterfall } from './waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers'; const INITIAL_DATA = { - root: undefined, - trace: { items: [], exceedsMax: false, errorDocs: [] }, - errorsPerTransaction: {}, + errorDocs: [], + traceDocs: [], + exceedsMax: false, }; export function useWaterfallFetcher() { @@ -51,5 +51,5 @@ export function useWaterfallFetcher() { transactionId, ]); - return { waterfall, status, error, exceedsMax: data.trace.exceedsMax }; + return { waterfall, status, error }; } diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/ErrorCount.test.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/ErrorCount.test.tsx deleted file mode 100644 index b8476200abfe3..0000000000000 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/ErrorCount.test.tsx +++ /dev/null @@ -1,38 +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 { fireEvent, render } from '@testing-library/react'; -import React from 'react'; -import { expectTextsInDocument } from '../../../../utils/testHelpers'; -import { ErrorCount } from './ErrorCount'; - -describe('ErrorCount', () => { - it('shows singular error message', () => { - const component = render(); - expectTextsInDocument(component, ['1 Error']); - }); - it('shows plural error message', () => { - const component = render(); - expectTextsInDocument(component, ['2 Errors']); - }); - it('prevents click propagation', () => { - const mock = jest.fn(); - const { getByText } = render( - - ); - fireEvent( - getByText('1 Error'), - new MouseEvent('click', { - bubbles: true, - cancelable: true, - }) - ); - expect(mock).not.toHaveBeenCalled(); - }); -}); diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/ErrorCount.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/ErrorCount.tsx deleted file mode 100644 index c66cff89eb0c1..0000000000000 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/ErrorCount.tsx +++ /dev/null @@ -1,35 +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 { EuiText, EuiTextColor } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; - -interface Props { - count: number; -} - -export function ErrorCount({ count }: Props) { - return ( - -

- { - e.stopPropagation(); - }} - > - {i18n.translate('xpack.apm.transactionDetails.errorCount', { - defaultMessage: - '{errorCount, number} {errorCount, plural, one {Error} other {Errors}}', - values: { errorCount: count }, - })} - -

-
- ); -} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/TransactionTabs.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/TransactionTabs.tsx index d402a2b19b5a9..0e01c44b3fb5a 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/TransactionTabs.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/TransactionTabs.tsx @@ -21,15 +21,9 @@ interface Props { transaction: Transaction; urlParams: ApmUrlParams; waterfall: IWaterfall; - exceedsMax: boolean; } -export function TransactionTabs({ - transaction, - urlParams, - waterfall, - exceedsMax, -}: Props) { +export function TransactionTabs({ transaction, urlParams, waterfall }: Props) { const history = useHistory(); const tabs = [timelineTab, metadataTab, logsTab]; const currentTab = @@ -65,7 +59,6 @@ export function TransactionTabs({ @@ -99,19 +92,11 @@ const logsTab = { function TimelineTabContent({ urlParams, waterfall, - exceedsMax, }: { urlParams: ApmUrlParams; waterfall: IWaterfall; - exceedsMax: boolean; }) { - return ( - - ); + return ; } function MetadataTabContent({ transaction }: { transaction: Transaction }) { diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx index b7feb917d2184..df3d7750a8dda 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx @@ -30,7 +30,6 @@ import { useApmParams } from '../../../../hooks/use_apm_params'; interface Props { urlParams: ApmUrlParams; waterfall: IWaterfall; - exceedsMax: boolean; isLoading: boolean; traceSamples: TraceSample[]; } @@ -38,7 +37,6 @@ interface Props { export function WaterfallWithSummary({ urlParams, waterfall, - exceedsMax, isLoading, traceSamples, }: Props) { @@ -125,7 +123,7 @@ export function WaterfallWithSummary({ @@ -135,7 +133,6 @@ export function WaterfallWithSummary({ transaction={entryTransaction} urlParams={urlParams} waterfall={waterfall} - exceedsMax={exceedsMax} /> ); diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/accordion_waterfall.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/accordion_waterfall.tsx index 1935d373caf79..e4a851b890a7c 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/accordion_waterfall.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/accordion_waterfall.tsx @@ -22,8 +22,7 @@ interface AccordionWaterfallProps { level: number; duration: IWaterfall['duration']; waterfallItemId?: string; - errorsPerTransaction: IWaterfall['errorsPerTransaction']; - childrenByParentId: Record; + waterfall: IWaterfall; onToggleEntryTransaction?: () => void; timelineMargins: Margins; onClickWaterfallItem: (item: IWaterfallSpanOrTransaction) => void; @@ -96,9 +95,8 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) { item, level, duration, - childrenByParentId, + waterfall, waterfallItemId, - errorsPerTransaction, timelineMargins, onClickWaterfallItem, onToggleEntryTransaction, @@ -106,12 +104,8 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) { const nextLevel = level + 1; - const errorCount = - item.docType === 'transaction' - ? errorsPerTransaction[item.doc.transaction.id] - : 0; - - const children = childrenByParentId[item.id] || []; + const children = waterfall.childrenByParentId[item.id] || []; + const errorCount = waterfall.getErrorCount(item.id); // To indent the items creating the parent/child tree const marginLeftLevel = 8 * level; @@ -121,7 +115,7 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) { buttonClassName={`button_${item.id}`} key={item.id} id={item.id} - hasError={errorCount > 0} + hasError={item.doc.event?.outcome === 'failure'} marginLeftLevel={marginLeftLevel} childrenCount={children.length} buttonContent={ @@ -152,16 +146,11 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) { > {children.map((child) => ( ))} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/failure_badge.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/failure_badge.tsx new file mode 100644 index 0000000000000..0aaf4b4a10a68 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/failure_badge.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiBadge, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useTheme } from '../../../../../../hooks/use_theme'; + +import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; + +const ResetLineHeight = euiStyled.span` + line-height: initial; +`; + +export function FailureBadge({ outcome }: { outcome?: 'success' | 'failure' }) { + const theme = useTheme(); + + if (outcome !== 'failure') { + return null; + } + + return ( + + + failure + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/index.tsx index d31af783e08c2..3932a02c9d974 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/index.tsx @@ -21,7 +21,6 @@ import { WaterfallFlyout } from './waterfall_flyout'; import { IWaterfall, IWaterfallItem, - IWaterfallSpanOrTransaction, } from './waterfall_helpers/waterfall_helpers'; const Container = euiStyled.div` @@ -61,9 +60,8 @@ const WaterfallItemsContainer = euiStyled.div` interface Props { waterfallItemId?: string; waterfall: IWaterfall; - exceedsMax: boolean; } -export function Waterfall({ waterfall, exceedsMax, waterfallItemId }: Props) { +export function Waterfall({ waterfall, waterfallItemId }: Props) { const history = useHistory(); const [isAccordionOpen, setIsAccordionOpen] = useState(true); const itemContainerHeight = 58; // TODO: This is a nasty way to calculate the height of the svg element. A better approach should be found @@ -74,37 +72,10 @@ export function Waterfall({ waterfall, exceedsMax, waterfallItemId }: Props) { const agentMarks = getAgentMarks(waterfall.entryWaterfallTransaction?.doc); const errorMarks = getErrorMarks(waterfall.errorItems); - function renderItems( - childrenByParentId: Record - ) { - const { entryWaterfallTransaction } = waterfall; - if (!entryWaterfallTransaction) { - return null; - } - return ( - - toggleFlyout({ history, item }) - } - onToggleEntryTransaction={() => setIsAccordionOpen((isOpen) => !isOpen)} - /> - ); - } - return ( - {exceedsMax && ( + {waterfall.apiResponse.exceedsMax && (
- {renderItems(waterfall.childrenByParentId)} + {!waterfall.entryWaterfallTransaction ? null : ( + + toggleFlyout({ history, item }) + } + onToggleEntryTransaction={() => + setIsAccordionOpen((isOpen) => !isOpen) + } + /> + )} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/index.tsx index 50fc56dff7f85..4921dfe0606c3 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/index.tsx @@ -35,8 +35,9 @@ import { HttpInfoSummaryItem } from '../../../../../../shared/Summary/http_info_ import { TimestampTooltip } from '../../../../../../shared/TimestampTooltip'; import { ResponsiveFlyout } from '../ResponsiveFlyout'; import { SyncBadge } from '../sync_badge'; -import { DatabaseContext } from './database_context'; +import { SpanDatabase } from './span_db'; import { StickySpanProperties } from './sticky_span_properties'; +import { FailureBadge } from '../failure_badge'; function formatType(type: string) { switch (type) { @@ -73,13 +74,11 @@ function getSpanTypes(span: Span) { }; } -const SpanBadge = euiStyled(EuiBadge)` - display: inline-block; - margin-right: ${({ theme }) => theme.eui.euiSizeXS}; -`; - -const HttpInfoContainer = euiStyled('div')` - margin-right: ${({ theme }) => theme.eui.euiSizeXS}; +const ContainerWithMarginRight = euiStyled.div` + /* add margin to all direct descendants */ + & > * { + margin-right: ${({ theme }) => theme.eui.euiSizeXS}; + } `; interface Props { @@ -101,7 +100,7 @@ export function SpanFlyout({ const stackframes = span.span.stacktrace; const codeLanguage = parentTransaction?.service.language?.name; - const dbContext = span.span.db; + const spanDb = span.span.db; const httpContext = span.span.http; const spanTypes = getSpanTypes(span); const spanHttpStatusCode = httpContext?.response?.status_code; @@ -173,15 +172,13 @@ export function SpanFlyout({ /> )} , - <> + {spanHttpUrl && ( - - - + )} - {spanTypes.spanType} + {spanTypes.spanType} {spanTypes.spanSubtype && ( - - {spanTypes.spanSubtype} - + {spanTypes.spanSubtype} )} {spanTypes.spanAction && ( @@ -210,15 +205,18 @@ export function SpanFlyout({ { defaultMessage: 'Action' } )} > - {spanTypes.spanAction} + {spanTypes.spanAction} )} + + + - , + , ]} /> - + ['db']; + spanDb?: NonNullable['db']; } -export function DatabaseContext({ dbContext }: Props) { +export function SpanDatabase({ spanDb }: Props) { const theme = useTheme(); const dbSyntaxLineHeight = theme.eui.euiSizeL; const previewHeight = 240; // 10 * dbSyntaxLineHeight - if (!dbContext || !dbContext.statement) { + if (!spanDb || !spanDb.statement) { return null; } - if (dbContext.type !== 'sql') { - return {dbContext.statement}; + if (spanDb.type !== 'sql') { + return {spanDb.statement}; } return ( @@ -73,7 +73,7 @@ export function DatabaseContext({ dbContext }: Props) { overflowX: 'scroll', }} > - {dbContext.statement} + {spanDb.statement} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/sync_badge.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/sync_badge.tsx index cfc369fa12a26..fe74f3d51c8bc 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/sync_badge.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/sync_badge.tsx @@ -8,12 +8,6 @@ import { EuiBadge } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; - -const SpanBadge = euiStyled(EuiBadge)` - display: inline-block; - margin-right: ${({ theme }) => theme.eui.euiSizeXS}; -`; export interface SyncBadgeProps { /** @@ -26,19 +20,19 @@ export function SyncBadge({ sync }: SyncBadgeProps) { switch (sync) { case true: return ( - + {i18n.translate('xpack.apm.transactionDetails.syncBadgeBlocking', { defaultMessage: 'blocking', })} - + ); case false: return ( - + {i18n.translate('xpack.apm.transactionDetails.syncBadgeAsync', { defaultMessage: 'async', })} - + ); default: return null; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_flyout.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_flyout.tsx index 4163388db1ec0..948f790848e8f 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_flyout.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_flyout.tsx @@ -55,7 +55,7 @@ export function WaterfallFlyout({ rootTransactionDuration={ waterfall.rootTransaction?.transaction.duration.us } - errorCount={waterfall.errorsPerTransaction[currentItem.id]} + errorCount={waterfall.getErrorCount(currentItem.id)} /> ); default: diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap index 8905162daada2..b1ea74c3eb0c0 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap @@ -912,11 +912,7 @@ Object { "skew": 0, }, ], - "errorsCount": 1, - "errorsPerTransaction": Object { - "myTransactionId1": 2, - "myTransactionId2": 3, - }, + "getErrorCount": [Function], "items": Array [ Object { "color": "", @@ -2188,11 +2184,7 @@ Object { "skew": 0, }, "errorItems": Array [], - "errorsCount": 0, - "errorsPerTransaction": Object { - "myTransactionId1": 2, - "myTransactionId2": 3, - }, + "getErrorCount": [Function], "items": Array [ Object { "color": "", diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers.test.ts b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers.test.ts index 34933a3a6f8ec..3e0c5034f37a2 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers.test.ts +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers.test.ts @@ -129,44 +129,39 @@ describe('waterfall_helpers', () => { it('should return full waterfall', () => { const entryTransactionId = 'myTransactionId1'; - const errorsPerTransaction = { - myTransactionId1: 2, - myTransactionId2: 3, + const apiResp = { + traceDocs: hits, + errorDocs, + exceedsMax: false, }; - const waterfall = getWaterfall( - { - trace: { items: hits, errorDocs, exceedsMax: false }, - errorsPerTransaction, - }, - entryTransactionId - ); + const waterfall = getWaterfall(apiResp, entryTransactionId); + const { apiResponse, ...waterfallRest } = waterfall; expect(waterfall.items.length).toBe(6); expect(waterfall.items[0].id).toBe('myTransactionId1'); expect(waterfall.errorItems.length).toBe(1); - expect(waterfall.errorsCount).toEqual(1); - expect(waterfall).toMatchSnapshot(); + expect(waterfall.getErrorCount('myTransactionId1')).toEqual(1); + expect(waterfallRest).toMatchSnapshot(); + expect(apiResponse).toEqual(apiResp); }); it('should return partial waterfall', () => { const entryTransactionId = 'myTransactionId2'; - const errorsPerTransaction = { - myTransactionId1: 2, - myTransactionId2: 3, + const apiResp = { + traceDocs: hits, + errorDocs, + exceedsMax: false, }; - const waterfall = getWaterfall( - { - trace: { items: hits, errorDocs, exceedsMax: false }, - errorsPerTransaction, - }, - entryTransactionId - ); + const waterfall = getWaterfall(apiResp, entryTransactionId); + + const { apiResponse, ...waterfallRest } = waterfall; expect(waterfall.items.length).toBe(4); expect(waterfall.items[0].id).toBe('myTransactionId2'); expect(waterfall.errorItems.length).toBe(0); - expect(waterfall.errorsCount).toEqual(0); - expect(waterfall).toMatchSnapshot(); + expect(waterfall.getErrorCount('myTransactionId2')).toEqual(0); + expect(waterfallRest).toMatchSnapshot(); + expect(apiResponse).toEqual(apiResp); }); it('should reparent spans', () => { const traceItems = [ @@ -238,8 +233,9 @@ describe('waterfall_helpers', () => { const entryTransactionId = 'myTransactionId1'; const waterfall = getWaterfall( { - trace: { items: traceItems, errorDocs: [], exceedsMax: false }, - errorsPerTransaction: {}, + traceDocs: traceItems, + errorDocs: [], + exceedsMax: false, }, entryTransactionId ); @@ -247,6 +243,7 @@ describe('waterfall_helpers', () => { id: item.id, parentId: item.parent?.id, }); + expect(waterfall.items.length).toBe(5); expect(getIdAndParentId(waterfall.items[0])).toEqual({ id: 'myTransactionId1', @@ -269,8 +266,9 @@ describe('waterfall_helpers', () => { parentId: 'mySpanIdB', }); expect(waterfall.errorItems.length).toBe(0); - expect(waterfall.errorsCount).toEqual(0); + expect(waterfall.getErrorCount('myTransactionId1')).toEqual(0); }); + it("shouldn't reparent spans when child id isn't found", () => { const traceItems = [ { @@ -341,8 +339,9 @@ describe('waterfall_helpers', () => { const entryTransactionId = 'myTransactionId1'; const waterfall = getWaterfall( { - trace: { items: traceItems, errorDocs: [], exceedsMax: false }, - errorsPerTransaction: {}, + traceDocs: traceItems, + errorDocs: [], + exceedsMax: false, }, entryTransactionId ); @@ -372,7 +371,7 @@ describe('waterfall_helpers', () => { parentId: 'mySpanIdB', }); expect(waterfall.errorItems.length).toBe(0); - expect(waterfall.errorsCount).toEqual(0); + expect(waterfall.getErrorCount('myTransactionId1')).toEqual(0); }); }); diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers.ts b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers.ts index b1b03318dd40f..f438de49baaec 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers.ts +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers.ts @@ -6,7 +6,7 @@ */ import { euiPaletteColorBlind } from '@elastic/eui'; -import { first, flatten, groupBy, isEmpty, sortBy, sum, uniq } from 'lodash'; +import { first, flatten, groupBy, isEmpty, sortBy, uniq } from 'lodash'; import { APIReturnType } from '../../../../../../../services/rest/createCallApmApi'; import { APMError } from '../../../../../../../../typings/es_schemas/ui/apm_error'; import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; @@ -35,10 +35,10 @@ export interface IWaterfall { duration: number; items: IWaterfallItem[]; childrenByParentId: Record; - errorsPerTransaction: TraceAPIResponse['errorsPerTransaction']; - errorsCount: number; + getErrorCount: (parentId: string) => number; legends: IWaterfallLegend[]; errorItems: IWaterfallError[]; + apiResponse: TraceAPIResponse; } interface IWaterfallSpanItemBase @@ -80,7 +80,8 @@ export type IWaterfallSpanOrTransaction = | IWaterfallTransaction | IWaterfallSpan; -export type IWaterfallItem = IWaterfallSpanOrTransaction | IWaterfallError; +// export type IWaterfallItem = IWaterfallSpanOrTransaction | IWaterfallError; +export type IWaterfallItem = IWaterfallSpanOrTransaction; export interface IWaterfallLegend { type: WaterfallLegendType; @@ -268,7 +269,7 @@ const getWaterfallDuration = (waterfallItems: IWaterfallItem[]) => 0 ); -const getWaterfallItems = (items: TraceAPIResponse['trace']['items']) => +const getWaterfallItems = (items: TraceAPIResponse['traceDocs']) => items.map((item) => { const docType = item.processor.event; switch (docType) { @@ -336,7 +337,7 @@ function isInEntryTransaction( } function getWaterfallErrors( - errorDocs: TraceAPIResponse['trace']['errorDocs'], + errorDocs: TraceAPIResponse['errorDocs'], items: IWaterfallItem[], entryWaterfallTransaction?: IWaterfallTransaction ) { @@ -362,24 +363,44 @@ function getWaterfallErrors( ); } +// map parent.id to the number of errors +/* + { 'parentId': 2 } + */ +function getErrorCountByParentId(errorDocs: TraceAPIResponse['errorDocs']) { + return errorDocs.reduce>((acc, doc) => { + const parentId = doc.parent?.id; + + if (!parentId) { + return acc; + } + + acc[parentId] = (acc[parentId] ?? 0) + 1; + + return acc; + }, {}); +} + export function getWaterfall( - { trace, errorsPerTransaction }: TraceAPIResponse, + apiResponse: TraceAPIResponse, entryTransactionId?: Transaction['transaction']['id'] ): IWaterfall { - if (isEmpty(trace.items) || !entryTransactionId) { + if (isEmpty(apiResponse.traceDocs) || !entryTransactionId) { return { + apiResponse, duration: 0, items: [], - errorsPerTransaction, - errorsCount: sum(Object.values(errorsPerTransaction)), legends: [], errorItems: [], childrenByParentId: {}, + getErrorCount: () => 0, }; } + const errorCountByParentId = getErrorCountByParentId(apiResponse.errorDocs); + const waterfallItems: IWaterfallSpanOrTransaction[] = getWaterfallItems( - trace.items + apiResponse.traceDocs ); const childrenByParentId = getChildrenGroupedByParentId( @@ -396,7 +417,7 @@ export function getWaterfall( entryWaterfallTransaction ); const errorItems = getWaterfallErrors( - trace.errorDocs, + apiResponse.errorDocs, items, entryWaterfallTransaction ); @@ -406,14 +427,14 @@ export function getWaterfall( const legends = getLegends(items); return { + apiResponse, entryWaterfallTransaction, rootTransaction, duration, items, - errorsPerTransaction, - errorsCount: errorItems.length, legends, errorItems, childrenByParentId: getChildrenGroupedByParentId(items), + getErrorCount: (parentId: string) => errorCountByParentId[parentId] ?? 0, }; } diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_item.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_item.tsx index 3af010fb30b86..1a4fa4f5fe836 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_item.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_item.tsx @@ -5,18 +5,23 @@ * 2.0. */ -import { EuiIcon, EuiText, EuiTitle, EuiToolTip } from '@elastic/eui'; +import { EuiBadge, EuiIcon, EuiText, EuiTitle, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { ReactNode } from 'react'; +import { useTheme } from '../../../../../../hooks/use_theme'; import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; import { isRumAgentName } from '../../../../../../../common/agent_name'; -import { TRACE_ID } from '../../../../../../../common/elasticsearch_fieldnames'; +import { + TRACE_ID, + TRANSACTION_ID, +} from '../../../../../../../common/elasticsearch_fieldnames'; import { asDuration } from '../../../../../../../common/utils/formatters'; import { Margins } from '../../../../../shared/charts/Timeline'; -import { ErrorOverviewLink } from '../../../../../shared/Links/apm/ErrorOverviewLink'; -import { ErrorCount } from '../../ErrorCount'; import { SyncBadge } from './sync_badge'; import { IWaterfallSpanOrTransaction } from './waterfall_helpers/waterfall_helpers'; +import { FailureBadge } from './failure_badge'; +import { useApmRouter } from '../../../../../../hooks/use_apm_router'; +import { useApmParams } from '../../../../../../hooks/use_apm_params'; type ItemType = 'transaction' | 'span' | 'error'; @@ -181,15 +186,6 @@ export function WaterfallItem({ const width = (item.duration / totalDuration) * 100; const left = ((item.offset + item.skew) / totalDuration) * 100; - const tooltipContent = i18n.translate( - 'xpack.apm.transactionDetails.errorsOverviewLinkTooltip', - { - values: { errorCount }, - defaultMessage: - '{errorCount, plural, one {View 1 related error} other {View # related errors}}', - } - ); - const isCompositeSpan = item.docType === 'span' && item.doc.span.composite; const itemBarStyle = getItemBarStyle(item, color, width, left); @@ -216,27 +212,56 @@ export function WaterfallItem({ - {errorCount > 0 && item.docType === 'transaction' ? ( - - - - - - ) : null} + + {item.docType === 'span' && } ); } +function RelatedErrors({ + item, + errorCount, +}: { + item: IWaterfallSpanOrTransaction; + errorCount: number; +}) { + const apmRouter = useApmRouter(); + const theme = useTheme(); + const { query } = useApmParams('/services/:serviceName/transactions/view'); + + const href = apmRouter.link(`/services/:serviceName/errors`, { + path: { serviceName: item.doc.service.name }, + query: { + ...query, + kuery: `${TRACE_ID} : "${item.doc.trace.id}" and ${TRANSACTION_ID} : "${item.doc.transaction?.id}"`, + }, + }); + + if (errorCount > 0) { + return ( + // eslint-disable-next-line jsx-a11y/click-events-have-key-events +
e.stopPropagation()}> + + {i18n.translate('xpack.apm.waterfall.errorCount', { + defaultMessage: + '{errorCount, plural, one {View related error} other {View # related errors}}', + values: { errorCount }, + })} + +
+ ); + } + + return ; +} + function getItemBarStyle( item: IWaterfallSpanOrTransaction, color: string, diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/WaterfallContainer.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/WaterfallContainer.stories.tsx index f8abff2c9609c..a03b7b29f9666 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/WaterfallContainer.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/WaterfallContainer.stories.tsx @@ -8,7 +8,6 @@ import React, { ComponentType } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; -import { APIReturnType } from '../../../../../services/rest/createCallApmApi'; import { WaterfallContainer } from './index'; import { getWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers'; import { @@ -19,8 +18,6 @@ import { urlParams, } from './waterfallContainer.stories.data'; -type TraceAPIResponse = APIReturnType<'GET /api/apm/traces/{traceId}'>; - export default { title: 'app/TransactionDetails/Waterfall', component: WaterfallContainer, @@ -36,57 +33,24 @@ export default { }; export function Example() { - const waterfall = getWaterfall( - simpleTrace as TraceAPIResponse, - '975c8d5bfd1dd20b' - ); - return ( - - ); + const waterfall = getWaterfall(simpleTrace, '975c8d5bfd1dd20b'); + return ; } export function WithErrors() { - const waterfall = getWaterfall( - (traceWithErrors as unknown) as TraceAPIResponse, - '975c8d5bfd1dd20b' - ); - return ( - - ); + const waterfall = getWaterfall(traceWithErrors, '975c8d5bfd1dd20b'); + return ; } export function ChildStartsBeforeParent() { const waterfall = getWaterfall( - traceChildStartBeforeParent as TraceAPIResponse, + traceChildStartBeforeParent, '975c8d5bfd1dd20b' ); - return ( - - ); + return ; } export function InferredSpans() { - const waterfall = getWaterfall( - inferredSpans as TraceAPIResponse, - 'f2387d37260d00bd' - ); - return ( - - ); + const waterfall = getWaterfall(inferredSpans, 'f2387d37260d00bd'); + return ; } diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/index.tsx index f3949fcfb03d5..6ef7651a1e404 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/index.tsx @@ -19,14 +19,9 @@ import { useApmServiceContext } from '../../../../../context/apm_service/use_apm interface Props { urlParams: ApmUrlParams; waterfall: IWaterfall; - exceedsMax: boolean; } -export function WaterfallContainer({ - urlParams, - waterfall, - exceedsMax, -}: Props) { +export function WaterfallContainer({ urlParams, waterfall }: Props) { const { serviceName } = useApmServiceContext(); if (!waterfall) { @@ -83,7 +78,6 @@ export function WaterfallContainer({ ); diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfallContainer.stories.data.ts b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfallContainer.stories.data.ts index 80ae2978498b3..1e58c1bd00a28 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfallContainer.stories.data.ts +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfallContainer.stories.data.ts @@ -7,6 +7,7 @@ import type { Location } from 'history'; import type { ApmUrlParams } from '../../../../../context/url_params_context/types'; +import { APIReturnType } from '../../../../../services/rest/createCallApmApi'; export const location = { pathname: '/services/opbeans-go/transactions/view', @@ -15,6 +16,8 @@ export const location = { hash: '', } as Location; +type TraceAPIResponse = APIReturnType<'GET /api/apm/traces/{traceId}'>; + export const urlParams = { start: '2020-03-22T15:16:38.742Z', end: '2020-03-23T15:16:38.742Z', @@ -32,2296 +35,2265 @@ export const urlParams = { } as ApmUrlParams; export const simpleTrace = { - trace: { - items: [ - { - container: { - id: + traceDocs: [ + { + container: { + id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', + }, + process: { + pid: 6, + title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', + ppid: 1, + }, + agent: { + name: 'java', + ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', + version: '1.14.1-SNAPSHOT', + }, + internal: { + sampler: { + value: 46, + }, + }, + source: { + ip: '172.19.0.13', + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: '172.19.0.9', + full: 'http://172.19.0.9:3000/api/orders', + }, + observer: { + hostname: 'f37f48d8b60b', + id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', + ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', + type: 'apm-server', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.785Z', + ecs: { + version: '1.4.0', + }, + service: { + node: { + name: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', }, - process: { - pid: 6, - title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', - ppid: 1, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '10.0.2', }, - agent: { - name: 'java', - ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', - version: '1.14.1-SNAPSHOT', + language: { + name: 'Java', + version: '10.0.2', }, - internal: { - sampler: { - value: 46, - }, + version: 'None', + }, + host: { + hostname: '4cf84d094553', + os: { + platform: 'Linux', + }, + ip: '172.19.0.9', + name: '4cf84d094553', + architecture: 'amd64', + }, + http: { + request: { + headers: { + Accept: ['*/*'], + 'User-Agent': ['Python/3.7 aiohttp/3.3.2'], + Host: ['172.19.0.9:3000'], + 'Accept-Encoding': ['gzip, deflate'], + }, + method: 'get', + socket: { + encrypted: false, + remote_address: '172.19.0.13', + }, + body: { + original: '[REDACTED]', + }, + }, + response: { + headers: { + 'Transfer-Encoding': ['chunked'], + Date: ['Mon, 23 Mar 2020 15:04:28 GMT'], + 'Content-Type': ['application/json;charset=ISO-8859-1'], + }, + status_code: 200, + finished: true, + headers_sent: true, + }, + version: '1.1', + }, + client: { + ip: '172.19.0.13', + }, + transaction: { + duration: { + us: 18842, + }, + result: 'HTTP 2xx', + name: 'DispatcherServlet#doGet', + id: '49809ad3c26adf74', + span_count: { + dropped: 0, + started: 1, + }, + type: 'request', + sampled: true, + }, + user_agent: { + original: 'Python/3.7 aiohttp/3.3.2', + name: 'Other', + device: { + name: 'Other', }, - source: { - ip: '172.19.0.13', - }, - processor: { - name: 'transaction', - event: 'transaction', - }, - url: { - path: '/api/orders', - scheme: 'http', - port: 3000, - domain: '172.19.0.9', - full: 'http://172.19.0.9:3000/api/orders', - }, - observer: { - hostname: 'f37f48d8b60b', - id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', - ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', - type: 'apm-server', - version: '8.0.0', - version_major: 8, - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.785Z', - ecs: { - version: '1.4.0', - }, - service: { - node: { - name: - '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', - }, - environment: 'production', - name: 'opbeans-java', - runtime: { - name: 'Java', - version: '10.0.2', - }, - language: { - name: 'Java', - version: '10.0.2', - }, - version: 'None', + }, + timestamp: { + us: 1584975868785000, + }, + }, + { + parent: { + id: 'fc107f7b556eb49b', + }, + agent: { + name: 'go', + version: '1.7.2', + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: 'opbeans-go', + full: 'http://opbeans-go:3000/api/orders', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.787Z', + service: { + node: { + name: + 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', + }, + environment: 'production', + framework: { + name: 'gin', + version: 'v1.4.0', + }, + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1', + }, + language: { + name: 'go', + version: 'go1.14.1', }, - host: { - hostname: '4cf84d094553', - os: { - platform: 'Linux', - }, - ip: '172.19.0.9', - name: '4cf84d094553', - architecture: 'amd64', + version: 'None', + }, + transaction: { + duration: { + us: 16597, + }, + result: 'HTTP 2xx', + name: 'GET /api/orders', + id: '975c8d5bfd1dd20b', + span_count: { + dropped: 0, + started: 1, + }, + type: 'request', + sampled: true, + }, + timestamp: { + us: 1584975868787052, + }, + }, + { + parent: { + id: 'daae24d83c269918', + }, + agent: { + name: 'python', + version: '5.5.2', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + timestamp: { + us: 1584975868788603, + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: 'opbeans-go', + full: 'http://opbeans-go:3000/api/orders', + }, + '@timestamp': '2020-03-23T15:04:28.788Z', + service: { + node: { + name: + 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', }, - http: { - request: { - headers: { - Accept: ['*/*'], - 'User-Agent': ['Python/3.7 aiohttp/3.3.2'], - Host: ['172.19.0.9:3000'], - 'Accept-Encoding': ['gzip, deflate'], - }, - method: 'get', - socket: { - encrypted: false, - remote_address: '172.19.0.13', - }, - body: { - original: '[REDACTED]', - }, - }, - response: { - headers: { - 'Transfer-Encoding': ['chunked'], - Date: ['Mon, 23 Mar 2020 15:04:28 GMT'], - 'Content-Type': ['application/json;charset=ISO-8859-1'], - }, - status_code: 200, - finished: true, - headers_sent: true, - }, - version: '1.1', + environment: 'production', + framework: { + name: 'django', + version: '2.1.13', }, - client: { - ip: '172.19.0.13', + name: 'opbeans-python', + runtime: { + name: 'CPython', + version: '3.6.10', }, - transaction: { - duration: { - us: 18842, - }, - result: 'HTTP 2xx', - name: 'DispatcherServlet#doGet', - id: '49809ad3c26adf74', - span_count: { - dropped: 0, - started: 1, - }, - type: 'request', - sampled: true, + language: { + name: 'python', + version: '3.6.10', }, - user_agent: { - original: 'Python/3.7 aiohttp/3.3.2', - name: 'Other', - device: { - name: 'Other', - }, + version: 'None', + }, + transaction: { + result: 'HTTP 2xx', + duration: { + us: 14648, + }, + name: 'GET opbeans.views.orders', + span_count: { + dropped: 0, + started: 1, + }, + id: '6fb0ff7365b87298', + type: 'request', + sampled: true, + }, + }, + { + container: { + id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', + }, + parent: { + id: '49809ad3c26adf74', + }, + process: { + pid: 6, + title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', + ppid: 1, + }, + agent: { + name: 'java', + ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', + version: '1.14.1-SNAPSHOT', + }, + internal: { + sampler: { + value: 44, + }, + }, + destination: { + address: 'opbeans-go', + port: 3000, + }, + processor: { + name: 'transaction', + event: 'span', + }, + observer: { + hostname: 'f37f48d8b60b', + id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', + type: 'apm-server', + ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.785Z', + ecs: { + version: '1.4.0', + }, + service: { + node: { + name: + '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '10.0.2', }, - timestamp: { - us: 1584975868785000, + language: { + name: 'Java', + version: '10.0.2', }, + version: 'None', + }, + host: { + hostname: '4cf84d094553', + os: { + platform: 'Linux', + }, + ip: '172.19.0.9', + name: '4cf84d094553', + architecture: 'amd64', + }, + connection: { + hash: + "{service.environment:'production'}/{service.name:'opbeans-java'}/{span.subtype:'http'}/{destination.address:'opbeans-go'}/{span.type:'external'}", }, - { - parent: { - id: 'fc107f7b556eb49b', + transaction: { + id: '49809ad3c26adf74', + }, + timestamp: { + us: 1584975868785273, + }, + span: { + duration: { + us: 17530, }, - agent: { - name: 'go', - version: '1.7.2', - }, - processor: { - name: 'transaction', - event: 'transaction', - }, - url: { - path: '/api/orders', - scheme: 'http', - port: 3000, - domain: 'opbeans-go', - full: 'http://opbeans-go:3000/api/orders', - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.787Z', - service: { - node: { - name: - 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', - }, - environment: 'production', - framework: { - name: 'gin', - version: 'v1.4.0', - }, - name: 'opbeans-go', - runtime: { - name: 'gc', - version: 'go1.14.1', - }, - language: { - name: 'go', - version: 'go1.14.1', + subtype: 'http', + name: 'GET opbeans-go', + destination: { + service: { + resource: 'opbeans-go:3000', + name: 'http://opbeans-go:3000', + type: 'external', }, - version: 'None', }, - transaction: { - duration: { - us: 16597, + http: { + response: { + status_code: 200, }, - result: 'HTTP 2xx', - name: 'GET /api/orders', - id: '975c8d5bfd1dd20b', - span_count: { - dropped: 0, - started: 1, + url: { + original: 'http://opbeans-go:3000/api/orders', }, - type: 'request', - sampled: true, }, - timestamp: { - us: 1584975868787052, + id: 'fc107f7b556eb49b', + type: 'external', + }, + }, + { + parent: { + id: '975c8d5bfd1dd20b', + }, + agent: { + name: 'go', + version: '1.7.2', + }, + processor: { + name: 'transaction', + event: 'span', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.787Z', + service: { + node: { + name: + 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', + }, + environment: 'production', + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1', + }, + language: { + name: 'go', + version: 'go1.14.1', }, + version: 'None', }, - { - parent: { - id: 'daae24d83c269918', + transaction: { + id: '975c8d5bfd1dd20b', + }, + timestamp: { + us: 1584975868787174, + }, + span: { + duration: { + us: 16250, }, - agent: { - name: 'python', - version: '5.5.2', - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - timestamp: { - us: 1584975868788603, - }, - processor: { - name: 'transaction', - event: 'transaction', - }, - url: { - path: '/api/orders', - scheme: 'http', - port: 3000, - domain: 'opbeans-go', - full: 'http://opbeans-go:3000/api/orders', - }, - '@timestamp': '2020-03-23T15:04:28.788Z', - service: { - node: { - name: - 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', - }, - environment: 'production', - framework: { - name: 'django', - version: '2.1.13', - }, - name: 'opbeans-python', - runtime: { - name: 'CPython', - version: '3.6.10', - }, - language: { - name: 'python', - version: '3.6.10', + subtype: 'http', + destination: { + service: { + resource: 'opbeans-python:3000', + name: 'http://opbeans-python:3000', + type: 'external', }, - version: 'None', }, - transaction: { - result: 'HTTP 2xx', - duration: { - us: 14648, + name: 'GET opbeans-python:3000', + http: { + response: { + status_code: 200, }, - name: 'GET opbeans.views.orders', - span_count: { - dropped: 0, - started: 1, + url: { + original: 'http://opbeans-python:3000/api/orders', }, - id: '6fb0ff7365b87298', - type: 'request', - sampled: true, }, + id: 'daae24d83c269918', + type: 'external', }, - { - container: { - id: - '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', + }, + { + container: { + id: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', + }, + parent: { + id: '6fb0ff7365b87298', + }, + agent: { + name: 'python', + version: '5.5.2', + }, + processor: { + name: 'transaction', + event: 'span', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.790Z', + service: { + node: { + name: + 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', }, - parent: { - id: '49809ad3c26adf74', + environment: 'production', + framework: { + name: 'django', + version: '2.1.13', }, - process: { - pid: 6, - title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', - ppid: 1, + name: 'opbeans-python', + runtime: { + name: 'CPython', + version: '3.6.10', }, - agent: { - name: 'java', - ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', - version: '1.14.1-SNAPSHOT', + language: { + name: 'python', + version: '3.6.10', }, - internal: { - sampler: { - value: 44, - }, + version: 'None', + }, + transaction: { + id: '6fb0ff7365b87298', + }, + timestamp: { + us: 1584975868790080, + }, + span: { + duration: { + us: 2519, }, + subtype: 'postgresql', + name: 'SELECT FROM opbeans_order', destination: { - address: 'opbeans-go', - port: 3000, - }, - processor: { - name: 'transaction', - event: 'span', - }, - observer: { - hostname: 'f37f48d8b60b', - id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', - type: 'apm-server', - ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', - version: '8.0.0', - version_major: 8, - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.785Z', - ecs: { - version: '1.4.0', - }, - service: { - node: { - name: - '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', - }, - environment: 'production', - name: 'opbeans-java', - runtime: { - name: 'Java', - version: '10.0.2', - }, - language: { - name: 'Java', - version: '10.0.2', + service: { + resource: 'postgresql', + name: 'postgresql', + type: 'db', }, - version: 'None', }, - host: { - hostname: '4cf84d094553', - os: { - platform: 'Linux', - }, - ip: '172.19.0.9', - name: '4cf84d094553', - architecture: 'amd64', + action: 'query', + id: 'c9407abb4d08ead1', + type: 'db', + sync: true, + db: { + statement: + 'SELECT "opbeans_order"."id", "opbeans_order"."customer_id", "opbeans_customer"."full_name", "opbeans_order"."created_at" FROM "opbeans_order" INNER JOIN "opbeans_customer" ON ("opbeans_order"."customer_id" = "opbeans_customer"."id") LIMIT 1000', + type: 'sql', }, - connection: { - hash: - "{service.environment:'production'}/{service.name:'opbeans-java'}/{span.subtype:'http'}/{destination.address:'opbeans-go'}/{span.type:'external'}", + }, + }, + ], + exceedsMax: false, + errorDocs: [], +} as TraceAPIResponse; + +export const traceWithErrors = ({ + traceDocs: [ + { + container: { + id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', + }, + process: { + pid: 6, + title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', + ppid: 1, + }, + agent: { + name: 'java', + ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', + version: '1.14.1-SNAPSHOT', + }, + internal: { + sampler: { + value: 46, }, - transaction: { - id: '49809ad3c26adf74', + }, + source: { + ip: '172.19.0.13', + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: '172.19.0.9', + full: 'http://172.19.0.9:3000/api/orders', + }, + observer: { + hostname: 'f37f48d8b60b', + id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', + ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', + type: 'apm-server', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.785Z', + ecs: { + version: '1.4.0', + }, + service: { + node: { + name: + '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', }, - timestamp: { - us: 1584975868785273, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '10.0.2', }, - span: { - duration: { - us: 17530, - }, - subtype: 'http', - name: 'GET opbeans-go', - destination: { - service: { - resource: 'opbeans-go:3000', - name: 'http://opbeans-go:3000', - type: 'external', - }, - }, - http: { - response: { - status_code: 200, - }, - url: { - original: 'http://opbeans-go:3000/api/orders', - }, - }, - id: 'fc107f7b556eb49b', - type: 'external', + language: { + name: 'Java', + version: '10.0.2', }, + version: 'None', + }, + host: { + hostname: '4cf84d094553', + os: { + platform: 'Linux', + }, + ip: '172.19.0.9', + name: '4cf84d094553', + architecture: 'amd64', + }, + http: { + request: { + headers: { + Accept: ['*/*'], + 'User-Agent': ['Python/3.7 aiohttp/3.3.2'], + Host: ['172.19.0.9:3000'], + 'Accept-Encoding': ['gzip, deflate'], + }, + method: 'get', + socket: { + encrypted: false, + remote_address: '172.19.0.13', + }, + body: { + original: '[REDACTED]', + }, + }, + response: { + headers: { + 'Transfer-Encoding': ['chunked'], + Date: ['Mon, 23 Mar 2020 15:04:28 GMT'], + 'Content-Type': ['application/json;charset=ISO-8859-1'], + }, + status_code: 200, + finished: true, + headers_sent: true, + }, + version: '1.1', + }, + client: { + ip: '172.19.0.13', + }, + transaction: { + duration: { + us: 18842, + }, + result: 'HTTP 2xx', + name: 'DispatcherServlet#doGet', + id: '49809ad3c26adf74', + span_count: { + dropped: 0, + started: 1, + }, + type: 'request', + sampled: true, }, - { - parent: { - id: '975c8d5bfd1dd20b', + user_agent: { + original: 'Python/3.7 aiohttp/3.3.2', + name: 'Other', + device: { + name: 'Other', }, - agent: { + }, + timestamp: { + us: 1584975868785000, + }, + }, + { + parent: { + id: 'fc107f7b556eb49b', + }, + agent: { + name: 'go', + version: '1.7.2', + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: 'opbeans-go', + full: 'http://opbeans-go:3000/api/orders', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.787Z', + service: { + node: { + name: + 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', + }, + environment: 'production', + framework: { + name: 'gin', + version: 'v1.4.0', + }, + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1', + }, + language: { name: 'go', - version: '1.7.2', - }, - processor: { - name: 'transaction', - event: 'span', - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.787Z', - service: { - node: { - name: - 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', - }, - environment: 'production', - name: 'opbeans-go', - runtime: { - name: 'gc', - version: 'go1.14.1', - }, - language: { - name: 'go', - version: 'go1.14.1', - }, - version: 'None', - }, - transaction: { - id: '975c8d5bfd1dd20b', - }, - timestamp: { - us: 1584975868787174, - }, - span: { - duration: { - us: 16250, - }, - subtype: 'http', - destination: { - service: { - resource: 'opbeans-python:3000', - name: 'http://opbeans-python:3000', - type: 'external', - }, - }, - name: 'GET opbeans-python:3000', - http: { - response: { - status_code: 200, - }, - url: { - original: 'http://opbeans-python:3000/api/orders', - }, - }, - id: 'daae24d83c269918', - type: 'external', + version: 'go1.14.1', }, + version: 'None', + }, + transaction: { + duration: { + us: 16597, + }, + result: 'HTTP 2xx', + name: 'GET /api/orders', + id: '975c8d5bfd1dd20b', + span_count: { + dropped: 0, + started: 1, + }, + type: 'request', + sampled: true, }, - { - container: { - id: + timestamp: { + us: 1584975868787052, + }, + }, + { + parent: { + id: 'daae24d83c269918', + }, + agent: { + name: 'python', + version: '5.5.2', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + timestamp: { + us: 1584975868788603, + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: 'opbeans-go', + full: 'http://opbeans-go:3000/api/orders', + }, + '@timestamp': '2020-03-23T15:04:28.788Z', + service: { + node: { + name: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', }, - parent: { - id: '6fb0ff7365b87298', - }, - agent: { - name: 'python', - version: '5.5.2', - }, - processor: { - name: 'transaction', - event: 'span', - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.790Z', - service: { - node: { - name: - 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', - }, - environment: 'production', - framework: { - name: 'django', - version: '2.1.13', - }, - name: 'opbeans-python', - runtime: { - name: 'CPython', - version: '3.6.10', - }, - language: { - name: 'python', - version: '3.6.10', - }, - version: 'None', + environment: 'production', + framework: { + name: 'django', + version: '2.1.13', }, - transaction: { - id: '6fb0ff7365b87298', + name: 'opbeans-python', + runtime: { + name: 'CPython', + version: '3.6.10', }, - timestamp: { - us: 1584975868790080, + language: { + name: 'python', + version: '3.6.10', }, - span: { - duration: { - us: 2519, - }, - subtype: 'postgresql', - name: 'SELECT FROM opbeans_order', - destination: { - service: { - resource: 'postgresql', - name: 'postgresql', - type: 'db', - }, - }, - action: 'query', - id: 'c9407abb4d08ead1', - type: 'db', - sync: true, - db: { - statement: - 'SELECT "opbeans_order"."id", "opbeans_order"."customer_id", "opbeans_customer"."full_name", "opbeans_order"."created_at" FROM "opbeans_order" INNER JOIN "opbeans_customer" ON ("opbeans_order"."customer_id" = "opbeans_customer"."id") LIMIT 1000', - type: 'sql', - }, + version: 'None', + }, + transaction: { + result: 'HTTP 2xx', + duration: { + us: 14648, + }, + name: 'GET opbeans.views.orders', + span_count: { + dropped: 0, + started: 1, + }, + id: '6fb0ff7365b87298', + type: 'request', + sampled: true, + }, + }, + { + container: { + id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', + }, + parent: { + id: '49809ad3c26adf74', + }, + process: { + pid: 6, + title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', + ppid: 1, + }, + agent: { + name: 'java', + ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', + version: '1.14.1-SNAPSHOT', + }, + internal: { + sampler: { + value: 44, }, }, - ], - exceedsMax: false, - errorDocs: [], - }, - errorsPerTransaction: {}, -}; - -export const traceWithErrors = { - trace: { - items: [ - { - container: { - id: + destination: { + address: 'opbeans-go', + port: 3000, + }, + processor: { + name: 'transaction', + event: 'span', + }, + observer: { + hostname: 'f37f48d8b60b', + id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', + type: 'apm-server', + ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.785Z', + ecs: { + version: '1.4.0', + }, + service: { + node: { + name: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', }, - process: { - pid: 6, - title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', - ppid: 1, - }, - agent: { - name: 'java', - ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', - version: '1.14.1-SNAPSHOT', + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '10.0.2', }, - internal: { - sampler: { - value: 46, - }, + language: { + name: 'Java', + version: '10.0.2', }, - source: { - ip: '172.19.0.13', - }, - processor: { - name: 'transaction', - event: 'transaction', - }, - url: { - path: '/api/orders', - scheme: 'http', - port: 3000, - domain: '172.19.0.9', - full: 'http://172.19.0.9:3000/api/orders', - }, - observer: { - hostname: 'f37f48d8b60b', - id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', - ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', - type: 'apm-server', - version: '8.0.0', - version_major: 8, - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.785Z', - ecs: { - version: '1.4.0', - }, - service: { - node: { - name: - '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', - }, - environment: 'production', - name: 'opbeans-java', - runtime: { - name: 'Java', - version: '10.0.2', - }, - language: { - name: 'Java', - version: '10.0.2', - }, - version: 'None', + version: 'None', + }, + host: { + hostname: '4cf84d094553', + os: { + platform: 'Linux', + }, + ip: '172.19.0.9', + name: '4cf84d094553', + architecture: 'amd64', + }, + connection: { + hash: + "{service.environment:'production'}/{service.name:'opbeans-java'}/{span.subtype:'http'}/{destination.address:'opbeans-go'}/{span.type:'external'}", + }, + transaction: { + id: '49809ad3c26adf74', + }, + timestamp: { + us: 1584975868785273, + }, + span: { + duration: { + us: 17530, }, - host: { - hostname: '4cf84d094553', - os: { - platform: 'Linux', + subtype: 'http', + name: 'GET opbeans-go', + destination: { + service: { + resource: 'opbeans-go:3000', + name: 'http://opbeans-go:3000', + type: 'external', }, - ip: '172.19.0.9', - name: '4cf84d094553', - architecture: 'amd64', }, http: { - request: { - headers: { - Accept: ['*/*'], - 'User-Agent': ['Python/3.7 aiohttp/3.3.2'], - Host: ['172.19.0.9:3000'], - 'Accept-Encoding': ['gzip, deflate'], - }, - method: 'get', - socket: { - encrypted: false, - remote_address: '172.19.0.13', - }, - body: { - original: '[REDACTED]', - }, - }, response: { - headers: { - 'Transfer-Encoding': ['chunked'], - Date: ['Mon, 23 Mar 2020 15:04:28 GMT'], - 'Content-Type': ['application/json;charset=ISO-8859-1'], - }, status_code: 200, - finished: true, - headers_sent: true, - }, - version: '1.1', - }, - client: { - ip: '172.19.0.13', - }, - transaction: { - duration: { - us: 18842, }, - result: 'HTTP 2xx', - name: 'DispatcherServlet#doGet', - id: '49809ad3c26adf74', - span_count: { - dropped: 0, - started: 1, + url: { + original: 'http://opbeans-go:3000/api/orders', }, - type: 'request', - sampled: true, - }, - user_agent: { - original: 'Python/3.7 aiohttp/3.3.2', - name: 'Other', - device: { - name: 'Other', - }, - }, - timestamp: { - us: 1584975868785000, }, + id: 'fc107f7b556eb49b', + type: 'external', }, - { - parent: { - id: 'fc107f7b556eb49b', - }, - agent: { + }, + { + parent: { + id: '975c8d5bfd1dd20b', + }, + agent: { + name: 'go', + version: '1.7.2', + }, + processor: { + name: 'transaction', + event: 'span', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.787Z', + service: { + node: { + name: + 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', + }, + environment: 'production', + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1', + }, + language: { name: 'go', - version: '1.7.2', - }, - processor: { - name: 'transaction', - event: 'transaction', - }, - url: { - path: '/api/orders', - scheme: 'http', - port: 3000, - domain: 'opbeans-go', - full: 'http://opbeans-go:3000/api/orders', - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.787Z', - service: { - node: { - name: - 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', - }, - environment: 'production', - framework: { - name: 'gin', - version: 'v1.4.0', - }, - name: 'opbeans-go', - runtime: { - name: 'gc', - version: 'go1.14.1', - }, - language: { - name: 'go', - version: 'go1.14.1', - }, - version: 'None', - }, - transaction: { - duration: { - us: 16597, - }, - result: 'HTTP 2xx', - name: 'GET /api/orders', - id: '975c8d5bfd1dd20b', - span_count: { - dropped: 0, - started: 1, - }, - type: 'request', - sampled: true, - }, - timestamp: { - us: 1584975868787052, + version: 'go1.14.1', }, + version: 'None', + }, + transaction: { + id: '975c8d5bfd1dd20b', }, - { - parent: { - id: 'daae24d83c269918', + timestamp: { + us: 1584975868787174, + }, + span: { + duration: { + us: 16250, }, - agent: { - name: 'python', - version: '5.5.2', - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - timestamp: { - us: 1584975868788603, - }, - processor: { - name: 'transaction', - event: 'transaction', - }, - url: { - path: '/api/orders', - scheme: 'http', - port: 3000, - domain: 'opbeans-go', - full: 'http://opbeans-go:3000/api/orders', - }, - '@timestamp': '2020-03-23T15:04:28.788Z', - service: { - node: { - name: - 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', - }, - environment: 'production', - framework: { - name: 'django', - version: '2.1.13', - }, - name: 'opbeans-python', - runtime: { - name: 'CPython', - version: '3.6.10', - }, - language: { - name: 'python', - version: '3.6.10', + subtype: 'http', + destination: { + service: { + resource: 'opbeans-python:3000', + name: 'http://opbeans-python:3000', + type: 'external', }, - version: 'None', }, - transaction: { - result: 'HTTP 2xx', - duration: { - us: 14648, + name: 'GET opbeans-python:3000', + http: { + response: { + status_code: 200, }, - name: 'GET opbeans.views.orders', - span_count: { - dropped: 0, - started: 1, + url: { + original: 'http://opbeans-python:3000/api/orders', }, - id: '6fb0ff7365b87298', - type: 'request', - sampled: true, }, + id: 'daae24d83c269918', + type: 'external', }, - { - container: { - id: - '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', + }, + { + container: { + id: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', + }, + parent: { + id: '6fb0ff7365b87298', + }, + agent: { + name: 'python', + version: '5.5.2', + }, + processor: { + name: 'transaction', + event: 'span', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.790Z', + service: { + node: { + name: + 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', }, - parent: { - id: '49809ad3c26adf74', + environment: 'production', + framework: { + name: 'django', + version: '2.1.13', }, - process: { - pid: 6, - title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', - ppid: 1, + name: 'opbeans-python', + runtime: { + name: 'CPython', + version: '3.6.10', }, - agent: { - name: 'java', - ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', - version: '1.14.1-SNAPSHOT', + language: { + name: 'python', + version: '3.6.10', }, - internal: { - sampler: { - value: 44, - }, + version: 'None', + }, + transaction: { + id: '6fb0ff7365b87298', + }, + timestamp: { + us: 1584975868790080, + }, + span: { + duration: { + us: 2519, }, + subtype: 'postgresql', + name: 'SELECT FROM opbeans_order', destination: { - address: 'opbeans-go', - port: 3000, - }, - processor: { - name: 'transaction', - event: 'span', - }, - observer: { - hostname: 'f37f48d8b60b', - id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', - type: 'apm-server', - ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', - version: '8.0.0', - version_major: 8, - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.785Z', - ecs: { - version: '1.4.0', - }, - service: { - node: { - name: - '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', - }, - environment: 'production', - name: 'opbeans-java', - runtime: { - name: 'Java', - version: '10.0.2', + service: { + resource: 'postgresql', + name: 'postgresql', + type: 'db', }, - language: { - name: 'Java', - version: '10.0.2', - }, - version: 'None', - }, - host: { - hostname: '4cf84d094553', - os: { - platform: 'Linux', - }, - ip: '172.19.0.9', - name: '4cf84d094553', - architecture: 'amd64', - }, - connection: { - hash: - "{service.environment:'production'}/{service.name:'opbeans-java'}/{span.subtype:'http'}/{destination.address:'opbeans-go'}/{span.type:'external'}", }, - transaction: { - id: '49809ad3c26adf74', - }, - timestamp: { - us: 1584975868785273, - }, - span: { - duration: { - us: 17530, - }, - subtype: 'http', - name: 'GET opbeans-go', - destination: { - service: { - resource: 'opbeans-go:3000', - name: 'http://opbeans-go:3000', - type: 'external', - }, - }, - http: { - response: { - status_code: 200, - }, - url: { - original: 'http://opbeans-go:3000/api/orders', - }, - }, - id: 'fc107f7b556eb49b', - type: 'external', + action: 'query', + id: 'c9407abb4d08ead1', + type: 'db', + sync: true, + db: { + statement: + 'SELECT "opbeans_order"."id", "opbeans_order"."customer_id", "opbeans_customer"."full_name", "opbeans_order"."created_at" FROM "opbeans_order" INNER JOIN "opbeans_customer" ON ("opbeans_order"."customer_id" = "opbeans_customer"."id") LIMIT 1000', + type: 'sql', }, }, - { - parent: { - id: '975c8d5bfd1dd20b', - }, - agent: { - name: 'go', - version: '1.7.2', - }, - processor: { - name: 'transaction', - event: 'span', - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.787Z', - service: { - node: { - name: - 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', - }, - environment: 'production', - name: 'opbeans-go', - runtime: { - name: 'gc', - version: 'go1.14.1', - }, - language: { - name: 'go', - version: 'go1.14.1', - }, - version: 'None', - }, - transaction: { - id: '975c8d5bfd1dd20b', - }, - timestamp: { - us: 1584975868787174, - }, - span: { - duration: { - us: 16250, - }, - subtype: 'http', - destination: { - service: { - resource: 'opbeans-python:3000', - name: 'http://opbeans-python:3000', - type: 'external', - }, - }, - name: 'GET opbeans-python:3000', - http: { - response: { - status_code: 200, - }, - url: { - original: 'http://opbeans-python:3000/api/orders', - }, - }, - id: 'daae24d83c269918', - type: 'external', - }, + }, + ], + exceedsMax: false, + errorDocs: [ + { + parent: { + id: '975c8d5bfd1dd20b', }, - { - container: { - id: - 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', - }, - parent: { - id: '6fb0ff7365b87298', - }, - agent: { - name: 'python', - version: '5.5.2', - }, - processor: { - name: 'transaction', - event: 'span', - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.790Z', - service: { - node: { - name: - 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', - }, - environment: 'production', - framework: { - name: 'django', - version: '2.1.13', - }, - name: 'opbeans-python', - runtime: { - name: 'CPython', - version: '3.6.10', - }, - language: { - name: 'python', - version: '3.6.10', - }, - version: 'None', - }, - transaction: { - id: '6fb0ff7365b87298', - }, - timestamp: { - us: 1584975868790080, - }, - span: { - duration: { - us: 2519, - }, - subtype: 'postgresql', - name: 'SELECT FROM opbeans_order', - destination: { - service: { - resource: 'postgresql', - name: 'postgresql', - type: 'db', - }, - }, - action: 'query', - id: 'c9407abb4d08ead1', - type: 'db', - sync: true, - db: { - statement: - 'SELECT "opbeans_order"."id", "opbeans_order"."customer_id", "opbeans_customer"."full_name", "opbeans_order"."created_at" FROM "opbeans_order" INNER JOIN "opbeans_customer" ON ("opbeans_order"."customer_id" = "opbeans_customer"."id") LIMIT 1000', - type: 'sql', - }, - }, + agent: { + name: 'go', + version: '1.7.2', }, - ], - exceedsMax: false, - errorDocs: [ - { - parent: { - id: '975c8d5bfd1dd20b', - }, - agent: { + error: { + culprit: 'logrusMiddleware', + log: { + level: 'error', + message: 'GET //api/products (502)', + }, + id: '1f3cb98206b5c54225cb7c8908a658da', + grouping_key: '4dba2ff58fe6c036a5dee2ce411e512a', + }, + processor: { + name: 'error', + event: 'error', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T16:04:28.787Z', + service: { + node: { + name: + 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', + }, + environment: 'production', + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1', + }, + language: { name: 'go', - version: '1.7.2', - }, - error: { - culprit: 'logrusMiddleware', - log: { - level: 'error', - message: 'GET //api/products (502)', - }, - id: '1f3cb98206b5c54225cb7c8908a658da', - grouping_key: '4dba2ff58fe6c036a5dee2ce411e512a', - }, - processor: { - name: 'error', - event: 'error', - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T16:04:28.787Z', - service: { - node: { - name: - 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', - }, - environment: 'production', - name: 'opbeans-go', - runtime: { - name: 'gc', - version: 'go1.14.1', - }, - language: { - name: 'go', - version: 'go1.14.1', - }, - version: 'None', - }, - transaction: { - id: '975c8d5bfd1dd20b', - sampled: false, - }, - timestamp: { - us: 1584975868787052, + version: 'go1.14.1', }, + version: 'None', }, - { - parent: { - id: '6fb0ff7365b87298', - }, - agent: { - name: 'python', - version: '5.5.2', - }, - error: { - culprit: 'logrusMiddleware', - log: { - level: 'error', - message: 'GET //api/products (502)', - }, - id: '1f3cb98206b5c54225cb7c8908a658d2', - grouping_key: '4dba2ff58fe6c036a5dee2ce411e512a', - }, - processor: { - name: 'error', - event: 'error', - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T16:04:28.790Z', - service: { - node: { - name: - 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', - }, - environment: 'production', - name: 'opbeans-python', - runtime: { - name: 'gc', - version: 'go1.14.1', - }, - version: 'None', - }, - transaction: { - id: '6fb0ff7365b87298', - sampled: false, - }, - timestamp: { - us: 1584975868790000, - }, + transaction: { + id: '975c8d5bfd1dd20b', + sampled: false, + }, + timestamp: { + us: 1584975868787052, }, - ], - }, - errorsPerTransaction: { - '975c8d5bfd1dd20b': 1, - '6fb0ff7365b87298': 1, - }, -}; + }, + { + parent: { + id: '6fb0ff7365b87298', + }, + agent: { + name: 'python', + version: '5.5.2', + }, + error: { + culprit: 'logrusMiddleware', + log: { + level: 'error', + message: 'GET //api/products (502)', + }, + id: '1f3cb98206b5c54225cb7c8908a658d2', + grouping_key: '4dba2ff58fe6c036a5dee2ce411e512a', + }, + processor: { + name: 'error', + event: 'error', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T16:04:28.790Z', + service: { + node: { + name: + 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', + }, + environment: 'production', + name: 'opbeans-python', + runtime: { + name: 'gc', + version: 'go1.14.1', + }, + version: 'None', + }, + transaction: { + id: '6fb0ff7365b87298', + sampled: false, + }, + timestamp: { + us: 1584975868790000, + }, + }, + ], +} as unknown) as TraceAPIResponse; export const traceChildStartBeforeParent = { - trace: { - items: [ - { - container: { - id: - '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', - }, - process: { - pid: 6, - title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', - ppid: 1, - }, - agent: { - name: 'java', - ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', - version: '1.14.1-SNAPSHOT', - }, - internal: { - sampler: { - value: 46, - }, - }, - source: { - ip: '172.19.0.13', - }, - processor: { - name: 'transaction', - event: 'transaction', - }, - url: { - path: '/api/orders', - scheme: 'http', - port: 3000, - domain: '172.19.0.9', - full: 'http://172.19.0.9:3000/api/orders', - }, - observer: { - hostname: 'f37f48d8b60b', - id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', - ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', - type: 'apm-server', - version: '8.0.0', - version_major: 8, - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.785Z', - ecs: { - version: '1.4.0', - }, - service: { - node: { - name: - '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', - }, - environment: 'production', - name: 'opbeans-java', - runtime: { - name: 'Java', - version: '10.0.2', - }, - language: { - name: 'Java', - version: '10.0.2', - }, - version: 'None', - }, - host: { - hostname: '4cf84d094553', - os: { - platform: 'Linux', - }, - ip: '172.19.0.9', - name: '4cf84d094553', - architecture: 'amd64', + traceDocs: [ + { + container: { + id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', + }, + process: { + pid: 6, + title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', + ppid: 1, + }, + agent: { + name: 'java', + ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', + version: '1.14.1-SNAPSHOT', + }, + internal: { + sampler: { + value: 46, }, - http: { - request: { - headers: { - Accept: ['*/*'], - 'User-Agent': ['Python/3.7 aiohttp/3.3.2'], - Host: ['172.19.0.9:3000'], - 'Accept-Encoding': ['gzip, deflate'], - }, - method: 'get', - socket: { - encrypted: false, - remote_address: '172.19.0.13', - }, - body: { - original: '[REDACTED]', - }, - }, - response: { - headers: { - 'Transfer-Encoding': ['chunked'], - Date: ['Mon, 23 Mar 2020 15:04:28 GMT'], - 'Content-Type': ['application/json;charset=ISO-8859-1'], - }, - status_code: 200, - finished: true, - headers_sent: true, - }, - version: '1.1', + }, + source: { + ip: '172.19.0.13', + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: '172.19.0.9', + full: 'http://172.19.0.9:3000/api/orders', + }, + observer: { + hostname: 'f37f48d8b60b', + id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', + ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', + type: 'apm-server', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.785Z', + ecs: { + version: '1.4.0', + }, + service: { + node: { + name: + '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', }, - client: { - ip: '172.19.0.13', + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '10.0.2', }, - transaction: { - duration: { - us: 18842, - }, - result: 'HTTP 2xx', - name: 'DispatcherServlet#doGet', - id: '49809ad3c26adf74', - span_count: { - dropped: 0, - started: 1, - }, - type: 'request', - sampled: true, + language: { + name: 'Java', + version: '10.0.2', }, - user_agent: { - original: 'Python/3.7 aiohttp/3.3.2', + version: 'None', + }, + host: { + hostname: '4cf84d094553', + os: { + platform: 'Linux', + }, + ip: '172.19.0.9', + name: '4cf84d094553', + architecture: 'amd64', + }, + http: { + request: { + headers: { + Accept: ['*/*'], + 'User-Agent': ['Python/3.7 aiohttp/3.3.2'], + Host: ['172.19.0.9:3000'], + 'Accept-Encoding': ['gzip, deflate'], + }, + method: 'get', + socket: { + encrypted: false, + remote_address: '172.19.0.13', + }, + body: { + original: '[REDACTED]', + }, + }, + response: { + headers: { + 'Transfer-Encoding': ['chunked'], + Date: ['Mon, 23 Mar 2020 15:04:28 GMT'], + 'Content-Type': ['application/json;charset=ISO-8859-1'], + }, + status_code: 200, + finished: true, + headers_sent: true, + }, + version: '1.1', + }, + client: { + ip: '172.19.0.13', + }, + transaction: { + duration: { + us: 18842, + }, + result: 'HTTP 2xx', + name: 'DispatcherServlet#doGet', + id: '49809ad3c26adf74', + span_count: { + dropped: 0, + started: 1, + }, + type: 'request', + sampled: true, + }, + user_agent: { + original: 'Python/3.7 aiohttp/3.3.2', + name: 'Other', + device: { name: 'Other', - device: { - name: 'Other', - }, - }, - timestamp: { - us: 1584975868785000, }, }, - { - parent: { - id: 'fc107f7b556eb49b', - }, - agent: { + timestamp: { + us: 1584975868785000, + }, + }, + { + parent: { + id: 'fc107f7b556eb49b', + }, + agent: { + name: 'go', + version: '1.7.2', + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: 'opbeans-go', + full: 'http://opbeans-go:3000/api/orders', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.787Z', + service: { + node: { + name: + 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', + }, + environment: 'production', + framework: { + name: 'gin', + version: 'v1.4.0', + }, + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1', + }, + language: { name: 'go', - version: '1.7.2', - }, - processor: { - name: 'transaction', - event: 'transaction', - }, - url: { - path: '/api/orders', - scheme: 'http', - port: 3000, - domain: 'opbeans-go', - full: 'http://opbeans-go:3000/api/orders', - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.787Z', - service: { - node: { - name: - 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', - }, - environment: 'production', - framework: { - name: 'gin', - version: 'v1.4.0', - }, - name: 'opbeans-go', - runtime: { - name: 'gc', - version: 'go1.14.1', - }, - language: { - name: 'go', - version: 'go1.14.1', - }, - version: 'None', + version: 'go1.14.1', }, - transaction: { - duration: { - us: 16597, - }, - result: 'HTTP 2xx', - name: 'GET /api/orders', - id: '975c8d5bfd1dd20b', - span_count: { - dropped: 0, - started: 1, - }, - type: 'request', - sampled: true, + version: 'None', + }, + transaction: { + duration: { + us: 16597, + }, + result: 'HTTP 2xx', + name: 'GET /api/orders', + id: '975c8d5bfd1dd20b', + span_count: { + dropped: 0, + started: 1, + }, + type: 'request', + sampled: true, + }, + timestamp: { + us: 1584975868787052, + }, + }, + { + parent: { + id: 'daae24d83c269918', + }, + agent: { + name: 'python', + version: '5.5.2', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + timestamp: { + us: 1584975868780000, + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: 'opbeans-go', + full: 'http://opbeans-go:3000/api/orders', + }, + '@timestamp': '2020-03-23T15:04:28.788Z', + service: { + node: { + name: + 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', }, - timestamp: { - us: 1584975868787052, + environment: 'production', + framework: { + name: 'django', + version: '2.1.13', }, - }, - { - parent: { - id: 'daae24d83c269918', + name: 'opbeans-python', + runtime: { + name: 'CPython', + version: '3.6.10', }, - agent: { + language: { name: 'python', - version: '5.5.2', - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - timestamp: { - us: 1584975868780000, - }, - processor: { - name: 'transaction', - event: 'transaction', - }, - url: { - path: '/api/orders', - scheme: 'http', - port: 3000, - domain: 'opbeans-go', - full: 'http://opbeans-go:3000/api/orders', - }, - '@timestamp': '2020-03-23T15:04:28.788Z', - service: { - node: { - name: - 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', - }, - environment: 'production', - framework: { - name: 'django', - version: '2.1.13', - }, - name: 'opbeans-python', - runtime: { - name: 'CPython', - version: '3.6.10', - }, - language: { - name: 'python', - version: '3.6.10', - }, - version: 'None', + version: '3.6.10', }, - transaction: { - result: 'HTTP 2xx', - duration: { - us: 1464, - }, - name: 'I started before my parent 😰', - span_count: { - dropped: 0, - started: 1, - }, - id: '6fb0ff7365b87298', - type: 'request', - sampled: true, + version: 'None', + }, + transaction: { + result: 'HTTP 2xx', + duration: { + us: 1464, + }, + name: 'I started before my parent 😰', + span_count: { + dropped: 0, + started: 1, + }, + id: '6fb0ff7365b87298', + type: 'request', + sampled: true, + }, + }, + { + container: { + id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', + }, + parent: { + id: '49809ad3c26adf74', + }, + process: { + pid: 6, + title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', + ppid: 1, + }, + agent: { + name: 'java', + ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', + version: '1.14.1-SNAPSHOT', + }, + internal: { + sampler: { + value: 44, }, }, - { - container: { - id: + destination: { + address: 'opbeans-go', + port: 3000, + }, + processor: { + name: 'transaction', + event: 'span', + }, + observer: { + hostname: 'f37f48d8b60b', + id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', + type: 'apm-server', + ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.785Z', + ecs: { + version: '1.4.0', + }, + service: { + node: { + name: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', }, - parent: { - id: '49809ad3c26adf74', - }, - process: { - pid: 6, - title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', - ppid: 1, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '10.0.2', }, - agent: { - name: 'java', - ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', - version: '1.14.1-SNAPSHOT', + language: { + name: 'Java', + version: '10.0.2', }, - internal: { - sampler: { - value: 44, - }, + version: 'None', + }, + host: { + hostname: '4cf84d094553', + os: { + platform: 'Linux', + }, + ip: '172.19.0.9', + name: '4cf84d094553', + architecture: 'amd64', + }, + connection: { + hash: + "{service.environment:'production'}/{service.name:'opbeans-java'}/{span.subtype:'http'}/{destination.address:'opbeans-go'}/{span.type:'external'}", + }, + transaction: { + id: '49809ad3c26adf74', + }, + timestamp: { + us: 1584975868785273, + }, + span: { + duration: { + us: 17530, }, + subtype: 'http', + name: 'GET opbeans-go', destination: { - address: 'opbeans-go', - port: 3000, - }, - processor: { - name: 'transaction', - event: 'span', - }, - observer: { - hostname: 'f37f48d8b60b', - id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', - type: 'apm-server', - ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', - version: '8.0.0', - version_major: 8, - }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', - }, - '@timestamp': '2020-03-23T15:04:28.785Z', - ecs: { - version: '1.4.0', - }, - service: { - node: { - name: - '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e', + service: { + resource: 'opbeans-go:3000', + name: 'http://opbeans-go:3000', + type: 'external', }, - environment: 'production', - name: 'opbeans-java', - runtime: { - name: 'Java', - version: '10.0.2', - }, - language: { - name: 'Java', - version: '10.0.2', - }, - version: 'None', - }, - host: { - hostname: '4cf84d094553', - os: { - platform: 'Linux', - }, - ip: '172.19.0.9', - name: '4cf84d094553', - architecture: 'amd64', - }, - connection: { - hash: - "{service.environment:'production'}/{service.name:'opbeans-java'}/{span.subtype:'http'}/{destination.address:'opbeans-go'}/{span.type:'external'}", - }, - transaction: { - id: '49809ad3c26adf74', }, - timestamp: { - us: 1584975868785273, - }, - span: { - duration: { - us: 17530, - }, - subtype: 'http', - name: 'GET opbeans-go', - destination: { - service: { - resource: 'opbeans-go:3000', - name: 'http://opbeans-go:3000', - type: 'external', - }, + http: { + response: { + status_code: 200, }, - http: { - response: { - status_code: 200, - }, - url: { - original: 'http://opbeans-go:3000/api/orders', - }, + url: { + original: 'http://opbeans-go:3000/api/orders', }, - id: 'fc107f7b556eb49b', - type: 'external', }, + id: 'fc107f7b556eb49b', + type: 'external', }, - { - parent: { - id: '975c8d5bfd1dd20b', - }, - agent: { + }, + { + parent: { + id: '975c8d5bfd1dd20b', + }, + agent: { + name: 'go', + version: '1.7.2', + }, + processor: { + name: 'transaction', + event: 'span', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.787Z', + service: { + node: { + name: + 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', + }, + environment: 'production', + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1', + }, + language: { name: 'go', - version: '1.7.2', - }, - processor: { - name: 'transaction', - event: 'span', + version: 'go1.14.1', }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', + version: 'None', + }, + transaction: { + id: '975c8d5bfd1dd20b', + }, + timestamp: { + us: 1584975868787174, + }, + span: { + duration: { + us: 16250, }, - '@timestamp': '2020-03-23T15:04:28.787Z', - service: { - node: { - name: - 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29', - }, - environment: 'production', - name: 'opbeans-go', - runtime: { - name: 'gc', - version: 'go1.14.1', - }, - language: { - name: 'go', - version: 'go1.14.1', + subtype: 'http', + destination: { + service: { + resource: 'opbeans-python:3000', + name: 'http://opbeans-python:3000', + type: 'external', }, - version: 'None', - }, - transaction: { - id: '975c8d5bfd1dd20b', }, - timestamp: { - us: 1584975868787174, - }, - span: { - duration: { - us: 16250, - }, - subtype: 'http', - destination: { - service: { - resource: 'opbeans-python:3000', - name: 'http://opbeans-python:3000', - type: 'external', - }, + name: 'I am his 👇🏻 parent 😡', + http: { + response: { + status_code: 200, }, - name: 'I am his 👇🏻 parent 😡', - http: { - response: { - status_code: 200, - }, - url: { - original: 'http://opbeans-python:3000/api/orders', - }, + url: { + original: 'http://opbeans-python:3000/api/orders', }, - id: 'daae24d83c269918', - type: 'external', }, + id: 'daae24d83c269918', + type: 'external', + }, + }, + { + container: { + id: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', }, - { - container: { - id: + parent: { + id: '6fb0ff7365b87298', + }, + agent: { + name: 'python', + version: '5.5.2', + }, + processor: { + name: 'transaction', + event: 'span', + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322', + }, + '@timestamp': '2020-03-23T15:04:28.790Z', + service: { + node: { + name: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', }, - parent: { - id: '6fb0ff7365b87298', + environment: 'production', + framework: { + name: 'django', + version: '2.1.13', }, - agent: { - name: 'python', - version: '5.5.2', + name: 'opbeans-python', + runtime: { + name: 'CPython', + version: '3.6.10', }, - processor: { - name: 'transaction', - event: 'span', + language: { + name: 'python', + version: '3.6.10', }, - trace: { - id: '513d33fafe99bbe6134749310c9b5322', + version: 'None', + }, + transaction: { + id: '6fb0ff7365b87298', + }, + timestamp: { + us: 1584975868781000, + }, + span: { + duration: { + us: 2519, }, - '@timestamp': '2020-03-23T15:04:28.790Z', - service: { - node: { - name: - 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51', - }, - environment: 'production', - framework: { - name: 'django', - version: '2.1.13', - }, - name: 'opbeans-python', - runtime: { - name: 'CPython', - version: '3.6.10', - }, - language: { - name: 'python', - version: '3.6.10', + subtype: 'postgresql', + name: 'I am using my parents skew 😇', + destination: { + service: { + resource: 'postgresql', + name: 'postgresql', + type: 'db', }, - version: 'None', - }, - transaction: { - id: '6fb0ff7365b87298', - }, - timestamp: { - us: 1584975868781000, }, - span: { - duration: { - us: 2519, - }, - subtype: 'postgresql', - name: 'I am using my parents skew 😇', - destination: { - service: { - resource: 'postgresql', - name: 'postgresql', - type: 'db', - }, - }, - action: 'query', - id: 'c9407abb4d08ead1', - type: 'db', - sync: true, - db: { - statement: - 'SELECT "opbeans_order"."id", "opbeans_order"."customer_id", "opbeans_customer"."full_name", "opbeans_order"."created_at" FROM "opbeans_order" INNER JOIN "opbeans_customer" ON ("opbeans_order"."customer_id" = "opbeans_customer"."id") LIMIT 1000', - type: 'sql', - }, + action: 'query', + id: 'c9407abb4d08ead1', + type: 'db', + sync: true, + db: { + statement: + 'SELECT "opbeans_order"."id", "opbeans_order"."customer_id", "opbeans_customer"."full_name", "opbeans_order"."created_at" FROM "opbeans_order" INNER JOIN "opbeans_customer" ON ("opbeans_order"."customer_id" = "opbeans_customer"."id") LIMIT 1000', + type: 'sql', }, }, - ], - exceedsMax: false, - errorDocs: [], - }, - errorsPerTransaction: {}, -}; + }, + ], + exceedsMax: false, + errorDocs: [], +} as TraceAPIResponse; export const inferredSpans = { - trace: { - items: [ - { - container: { - id: + traceDocs: [ + { + container: { + id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', + }, + agent: { + name: 'java', + ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', + version: '1.15.1-SNAPSHOT', + }, + process: { + pid: 6, + title: '/opt/java/openjdk/bin/java', + ppid: 1, + }, + source: { + ip: '172.18.0.8', + }, + processor: { + name: 'transaction', + event: 'transaction', + }, + url: { + path: '/api/products/2', + scheme: 'http', + port: 3000, + domain: '172.18.0.7', + full: 'http://172.18.0.7:3000/api/products/2', + }, + observer: { + hostname: '7189f754b5a3', + id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', + ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', + type: 'apm-server', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '3b0dc77f3754e5bcb9da0e4c15e0db97', + }, + '@timestamp': '2020-04-09T11:36:00.786Z', + ecs: { + version: '1.5.0', + }, + service: { + node: { + name: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', }, - agent: { - name: 'java', - ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', - version: '1.15.1-SNAPSHOT', - }, - process: { - pid: 6, - title: '/opt/java/openjdk/bin/java', - ppid: 1, - }, - source: { - ip: '172.18.0.8', - }, - processor: { - name: 'transaction', - event: 'transaction', - }, - url: { - path: '/api/products/2', - scheme: 'http', - port: 3000, - domain: '172.18.0.7', - full: 'http://172.18.0.7:3000/api/products/2', - }, - observer: { - hostname: '7189f754b5a3', - id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', - ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', - type: 'apm-server', - version: '8.0.0', - version_major: 8, - }, - trace: { - id: '3b0dc77f3754e5bcb9da0e4c15e0db97', - }, - '@timestamp': '2020-04-09T11:36:00.786Z', - ecs: { - version: '1.5.0', - }, - service: { - node: { - name: - 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', - }, - environment: 'production', - name: 'opbeans-java', - runtime: { - name: 'Java', - version: '11.0.6', - }, - language: { - name: 'Java', - version: '11.0.6', - }, - version: 'None', - }, - host: { - hostname: 'fc2ae281f56f', - os: { - platform: 'Linux', - }, - ip: '172.18.0.7', - name: 'fc2ae281f56f', - architecture: 'amd64', - }, - client: { - ip: '172.18.0.8', + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '11.0.6', }, - http: { - request: { - headers: { - Accept: ['*/*'], - 'User-Agent': ['Python/3.7 aiohttp/3.3.2'], - Host: ['172.18.0.7:3000'], - 'Accept-Encoding': ['gzip, deflate'], - }, - method: 'get', - socket: { - encrypted: false, - remote_address: '172.18.0.8', - }, - }, - response: { - headers: { - 'Transfer-Encoding': ['chunked'], - Date: ['Thu, 09 Apr 2020 11:36:01 GMT'], - 'Content-Type': ['application/json;charset=UTF-8'], - }, - status_code: 200, - finished: true, - headers_sent: true, - }, - version: '1.1', + language: { + name: 'Java', + version: '11.0.6', }, - user_agent: { - original: 'Python/3.7 aiohttp/3.3.2', + version: 'None', + }, + host: { + hostname: 'fc2ae281f56f', + os: { + platform: 'Linux', + }, + ip: '172.18.0.7', + name: 'fc2ae281f56f', + architecture: 'amd64', + }, + client: { + ip: '172.18.0.8', + }, + http: { + request: { + headers: { + Accept: ['*/*'], + 'User-Agent': ['Python/3.7 aiohttp/3.3.2'], + Host: ['172.18.0.7:3000'], + 'Accept-Encoding': ['gzip, deflate'], + }, + method: 'get', + socket: { + encrypted: false, + remote_address: '172.18.0.8', + }, + }, + response: { + headers: { + 'Transfer-Encoding': ['chunked'], + Date: ['Thu, 09 Apr 2020 11:36:01 GMT'], + 'Content-Type': ['application/json;charset=UTF-8'], + }, + status_code: 200, + finished: true, + headers_sent: true, + }, + version: '1.1', + }, + user_agent: { + original: 'Python/3.7 aiohttp/3.3.2', + name: 'Other', + device: { name: 'Other', - device: { - name: 'Other', - }, - }, - transaction: { - duration: { - us: 237537, - }, - result: 'HTTP 2xx', - name: 'APIRestController#product', - span_count: { - dropped: 0, - started: 3, - }, - id: 'f2387d37260d00bd', - type: 'request', - sampled: true, - }, - timestamp: { - us: 1586432160786001, }, }, - { - container: { - id: + transaction: { + duration: { + us: 237537, + }, + result: 'HTTP 2xx', + name: 'APIRestController#product', + span_count: { + dropped: 0, + started: 3, + }, + id: 'f2387d37260d00bd', + type: 'request', + sampled: true, + }, + timestamp: { + us: 1586432160786001, + }, + }, + { + container: { + id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', + }, + parent: { + id: 'f2387d37260d00bd', + }, + agent: { + name: 'java', + ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', + version: '1.15.1-SNAPSHOT', + }, + process: { + pid: 6, + title: '/opt/java/openjdk/bin/java', + ppid: 1, + }, + processor: { + name: 'transaction', + event: 'span', + }, + observer: { + hostname: '7189f754b5a3', + id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', + ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', + type: 'apm-server', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '3b0dc77f3754e5bcb9da0e4c15e0db97', + }, + '@timestamp': '2020-04-09T11:36:00.810Z', + ecs: { + version: '1.5.0', + }, + service: { + node: { + name: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', }, - parent: { - id: 'f2387d37260d00bd', - }, - agent: { - name: 'java', - ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', - version: '1.15.1-SNAPSHOT', - }, - process: { - pid: 6, - title: '/opt/java/openjdk/bin/java', - ppid: 1, - }, - processor: { - name: 'transaction', - event: 'span', - }, - observer: { - hostname: '7189f754b5a3', - id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', - ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', - type: 'apm-server', - version: '8.0.0', - version_major: 8, - }, - trace: { - id: '3b0dc77f3754e5bcb9da0e4c15e0db97', - }, - '@timestamp': '2020-04-09T11:36:00.810Z', - ecs: { - version: '1.5.0', - }, - service: { - node: { - name: - 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', - }, - environment: 'production', - name: 'opbeans-java', - runtime: { - name: 'Java', - version: '11.0.6', - }, - language: { - name: 'Java', - version: '11.0.6', - }, - version: 'None', - }, - host: { - hostname: 'fc2ae281f56f', - os: { - platform: 'Linux', - }, - ip: '172.18.0.7', - name: 'fc2ae281f56f', - architecture: 'amd64', - }, - transaction: { - id: 'f2387d37260d00bd', - }, - span: { - duration: { - us: 204574, - }, - subtype: 'inferred', - name: 'ServletInvocableHandlerMethod#invokeAndHandle', - id: 'a5df600bd7bd5e38', - type: 'app', + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '11.0.6', }, - timestamp: { - us: 1586432160810441, + language: { + name: 'Java', + version: '11.0.6', }, + version: 'None', + }, + host: { + hostname: 'fc2ae281f56f', + os: { + platform: 'Linux', + }, + ip: '172.18.0.7', + name: 'fc2ae281f56f', + architecture: 'amd64', + }, + transaction: { + id: 'f2387d37260d00bd', + }, + span: { + duration: { + us: 204574, + }, + subtype: 'inferred', + name: 'ServletInvocableHandlerMethod#invokeAndHandle', + id: 'a5df600bd7bd5e38', + type: 'app', }, - { - container: { - id: + timestamp: { + us: 1586432160810441, + }, + }, + { + container: { + id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', + }, + parent: { + id: 'a5df600bd7bd5e38', + }, + agent: { + name: 'java', + ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', + version: '1.15.1-SNAPSHOT', + }, + process: { + pid: 6, + title: '/opt/java/openjdk/bin/java', + ppid: 1, + }, + processor: { + name: 'transaction', + event: 'span', + }, + observer: { + hostname: '7189f754b5a3', + id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', + type: 'apm-server', + ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '3b0dc77f3754e5bcb9da0e4c15e0db97', + }, + '@timestamp': '2020-04-09T11:36:00.810Z', + ecs: { + version: '1.5.0', + }, + service: { + node: { + name: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', }, - parent: { - id: 'a5df600bd7bd5e38', - }, - agent: { - name: 'java', - ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', - version: '1.15.1-SNAPSHOT', - }, - process: { - pid: 6, - title: '/opt/java/openjdk/bin/java', - ppid: 1, - }, - processor: { - name: 'transaction', - event: 'span', - }, - observer: { - hostname: '7189f754b5a3', - id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', - type: 'apm-server', - ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', - version: '8.0.0', - version_major: 8, - }, - trace: { - id: '3b0dc77f3754e5bcb9da0e4c15e0db97', - }, - '@timestamp': '2020-04-09T11:36:00.810Z', - ecs: { - version: '1.5.0', - }, - service: { - node: { - name: - 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', - }, - environment: 'production', - name: 'opbeans-java', - runtime: { - name: 'Java', - version: '11.0.6', - }, - language: { - name: 'Java', - version: '11.0.6', - }, - version: 'None', - }, - host: { - hostname: 'fc2ae281f56f', - os: { - platform: 'Linux', - }, - ip: '172.18.0.7', - name: 'fc2ae281f56f', - architecture: 'amd64', - }, - transaction: { - id: 'f2387d37260d00bd', + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '11.0.6', }, - timestamp: { - us: 1586432160810441, + language: { + name: 'Java', + version: '11.0.6', }, - span: { - duration: { - us: 102993, - }, - stacktrace: [ - { - library_frame: true, - exclude_from_grouping: false, - filename: 'InvocableHandlerMethod.java', - line: { - number: -1, - }, - function: 'doInvoke', + version: 'None', + }, + host: { + hostname: 'fc2ae281f56f', + os: { + platform: 'Linux', + }, + ip: '172.18.0.7', + name: 'fc2ae281f56f', + architecture: 'amd64', + }, + transaction: { + id: 'f2387d37260d00bd', + }, + timestamp: { + us: 1586432160810441, + }, + span: { + duration: { + us: 102993, + }, + stacktrace: [ + { + library_frame: true, + exclude_from_grouping: false, + filename: 'InvocableHandlerMethod.java', + line: { + number: -1, }, - { - exclude_from_grouping: false, - library_frame: true, - filename: 'InvocableHandlerMethod.java', - line: { - number: -1, - }, - function: 'invokeForRequest', + function: 'doInvoke', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'InvocableHandlerMethod.java', + line: { + number: -1, }, - ], - subtype: 'inferred', - name: 'APIRestController#product', - id: '808dc34fc41ce522', - type: 'app', - }, + function: 'invokeForRequest', + }, + ], + subtype: 'inferred', + name: 'APIRestController#product', + id: '808dc34fc41ce522', + type: 'app', + }, + }, + { + container: { + id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', + }, + parent: { + id: 'f2387d37260d00bd', }, - { - container: { - id: + agent: { + name: 'java', + ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', + version: '1.15.1-SNAPSHOT', + }, + process: { + pid: 6, + title: '/opt/java/openjdk/bin/java', + ppid: 1, + }, + processor: { + name: 'transaction', + event: 'span', + }, + labels: { + productId: '2', + }, + observer: { + hostname: '7189f754b5a3', + id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', + ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', + type: 'apm-server', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '3b0dc77f3754e5bcb9da0e4c15e0db97', + }, + '@timestamp': '2020-04-09T11:36:00.832Z', + ecs: { + version: '1.5.0', + }, + service: { + node: { + name: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', }, - parent: { - id: 'f2387d37260d00bd', - }, - agent: { - name: 'java', - ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', - version: '1.15.1-SNAPSHOT', - }, - process: { - pid: 6, - title: '/opt/java/openjdk/bin/java', - ppid: 1, - }, - processor: { - name: 'transaction', - event: 'span', - }, - labels: { - productId: '2', - }, - observer: { - hostname: '7189f754b5a3', - id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', - ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', - type: 'apm-server', - version: '8.0.0', - version_major: 8, - }, - trace: { - id: '3b0dc77f3754e5bcb9da0e4c15e0db97', - }, - '@timestamp': '2020-04-09T11:36:00.832Z', - ecs: { - version: '1.5.0', - }, - service: { - node: { - name: - 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', - }, - environment: 'production', - name: 'opbeans-java', - runtime: { - name: 'Java', - version: '11.0.6', - }, - language: { - name: 'Java', - version: '11.0.6', - }, - version: 'None', - }, - host: { - hostname: 'fc2ae281f56f', - os: { - platform: 'Linux', - }, - ip: '172.18.0.7', - name: 'fc2ae281f56f', - architecture: 'amd64', - }, - transaction: { - id: 'f2387d37260d00bd', + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '11.0.6', }, - timestamp: { - us: 1586432160832300, + language: { + name: 'Java', + version: '11.0.6', }, - span: { - duration: { - us: 99295, - }, - name: 'OpenTracing product span', - id: '41226ae63af4f235', - type: 'unknown', + version: 'None', + }, + host: { + hostname: 'fc2ae281f56f', + os: { + platform: 'Linux', + }, + ip: '172.18.0.7', + name: 'fc2ae281f56f', + architecture: 'amd64', + }, + transaction: { + id: 'f2387d37260d00bd', + }, + timestamp: { + us: 1586432160832300, + }, + span: { + duration: { + us: 99295, }, - child: { id: ['8d80de06aa11a6fc'] }, + name: 'OpenTracing product span', + id: '41226ae63af4f235', + type: 'unknown', + }, + child: { id: ['8d80de06aa11a6fc'] }, + }, + { + container: { + id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', + }, + parent: { + id: '808dc34fc41ce522', + }, + process: { + pid: 6, + title: '/opt/java/openjdk/bin/java', + ppid: 1, + }, + agent: { + name: 'java', + ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', + version: '1.15.1-SNAPSHOT', + }, + processor: { + name: 'transaction', + event: 'span', + }, + observer: { + hostname: '7189f754b5a3', + id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', + ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', + type: 'apm-server', + version: '8.0.0', + version_major: 8, }, - { - container: { - id: + trace: { + id: '3b0dc77f3754e5bcb9da0e4c15e0db97', + }, + '@timestamp': '2020-04-09T11:36:00.859Z', + ecs: { + version: '1.5.0', + }, + service: { + node: { + name: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', }, - parent: { - id: '808dc34fc41ce522', - }, - process: { - pid: 6, - title: '/opt/java/openjdk/bin/java', - ppid: 1, - }, - agent: { - name: 'java', - ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', - version: '1.15.1-SNAPSHOT', - }, - processor: { - name: 'transaction', - event: 'span', - }, - observer: { - hostname: '7189f754b5a3', - id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', - ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', - type: 'apm-server', - version: '8.0.0', - version_major: 8, - }, - trace: { - id: '3b0dc77f3754e5bcb9da0e4c15e0db97', - }, - '@timestamp': '2020-04-09T11:36:00.859Z', - ecs: { - version: '1.5.0', - }, - service: { - node: { - name: - 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', - }, - environment: 'production', - name: 'opbeans-java', - runtime: { - name: 'Java', - version: '11.0.6', - }, - language: { - name: 'Java', - version: '11.0.6', - }, - version: 'None', - }, - host: { - hostname: 'fc2ae281f56f', - os: { - platform: 'Linux', - }, - ip: '172.18.0.7', - name: 'fc2ae281f56f', - architecture: 'amd64', - }, - transaction: { - id: 'f2387d37260d00bd', - }, - timestamp: { - us: 1586432160859600, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '11.0.6', }, - span: { - duration: { - us: 53835, - }, - subtype: 'inferred', - name: 'Loader#executeQueryStatement', - id: '8d80de06aa11a6fc', - type: 'app', + language: { + name: 'Java', + version: '11.0.6', }, + version: 'None', + }, + host: { + hostname: 'fc2ae281f56f', + os: { + platform: 'Linux', + }, + ip: '172.18.0.7', + name: 'fc2ae281f56f', + architecture: 'amd64', + }, + transaction: { + id: 'f2387d37260d00bd', }, - { - container: { - id: + timestamp: { + us: 1586432160859600, + }, + span: { + duration: { + us: 53835, + }, + subtype: 'inferred', + name: 'Loader#executeQueryStatement', + id: '8d80de06aa11a6fc', + type: 'app', + }, + }, + { + container: { + id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', + }, + parent: { + id: '41226ae63af4f235', + }, + agent: { + name: 'java', + ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', + version: '1.15.1-SNAPSHOT', + }, + process: { + pid: 6, + title: '/opt/java/openjdk/bin/java', + ppid: 1, + }, + destination: { + address: 'postgres', + port: 5432, + }, + processor: { + name: 'transaction', + event: 'span', + }, + observer: { + hostname: '7189f754b5a3', + id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', + ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', + type: 'apm-server', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '3b0dc77f3754e5bcb9da0e4c15e0db97', + }, + '@timestamp': '2020-04-09T11:36:00.903Z', + ecs: { + version: '1.5.0', + }, + service: { + node: { + name: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', }, - parent: { - id: '41226ae63af4f235', + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '11.0.6', }, - agent: { - name: 'java', - ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', - version: '1.15.1-SNAPSHOT', + language: { + name: 'Java', + version: '11.0.6', }, - process: { - pid: 6, - title: '/opt/java/openjdk/bin/java', - ppid: 1, + version: 'None', + }, + host: { + hostname: 'fc2ae281f56f', + os: { + platform: 'Linux', + }, + ip: '172.18.0.7', + name: 'fc2ae281f56f', + architecture: 'amd64', + }, + transaction: { + id: 'f2387d37260d00bd', + }, + timestamp: { + us: 1586432160903236, + }, + span: { + duration: { + us: 10211, }, + subtype: 'postgresql', destination: { - address: 'postgres', - port: 5432, - }, - processor: { - name: 'transaction', - event: 'span', - }, - observer: { - hostname: '7189f754b5a3', - id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', - ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', - type: 'apm-server', - version: '8.0.0', - version_major: 8, - }, - trace: { - id: '3b0dc77f3754e5bcb9da0e4c15e0db97', - }, - '@timestamp': '2020-04-09T11:36:00.903Z', - ecs: { - version: '1.5.0', - }, - service: { - node: { - name: - 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', - }, - environment: 'production', - name: 'opbeans-java', - runtime: { - name: 'Java', - version: '11.0.6', - }, - language: { - name: 'Java', - version: '11.0.6', - }, - version: 'None', - }, - host: { - hostname: 'fc2ae281f56f', - os: { - platform: 'Linux', + service: { + resource: 'postgresql', + name: 'postgresql', + type: 'db', }, - ip: '172.18.0.7', - name: 'fc2ae281f56f', - architecture: 'amd64', - }, - transaction: { - id: 'f2387d37260d00bd', - }, - timestamp: { - us: 1586432160903236, }, - span: { - duration: { - us: 10211, - }, - subtype: 'postgresql', - destination: { - service: { - resource: 'postgresql', - name: 'postgresql', - type: 'db', - }, - }, - name: 'SELECT FROM products', - action: 'query', - id: '3708d5623658182f', - type: 'db', - db: { - statement: - 'select product0_.id as col_0_0_, product0_.sku as col_1_0_, product0_.name as col_2_0_, product0_.description as col_3_0_, product0_.cost as col_4_0_, product0_.selling_price as col_5_0_, product0_.stock as col_6_0_, producttyp1_.id as col_7_0_, producttyp1_.name as col_8_0_, (select sum(orderline2_.amount) from order_lines orderline2_ where orderline2_.product_id=product0_.id) as col_9_0_ from products product0_ left outer join product_types producttyp1_ on product0_.type_id=producttyp1_.id where product0_.id=?', - type: 'sql', - user: { - name: 'postgres', - }, + name: 'SELECT FROM products', + action: 'query', + id: '3708d5623658182f', + type: 'db', + db: { + statement: + 'select product0_.id as col_0_0_, product0_.sku as col_1_0_, product0_.name as col_2_0_, product0_.description as col_3_0_, product0_.cost as col_4_0_, product0_.selling_price as col_5_0_, product0_.stock as col_6_0_, producttyp1_.id as col_7_0_, producttyp1_.name as col_8_0_, (select sum(orderline2_.amount) from order_lines orderline2_ where orderline2_.product_id=product0_.id) as col_9_0_ from products product0_ left outer join product_types producttyp1_ on product0_.type_id=producttyp1_.id where product0_.id=?', + type: 'sql', + user: { + name: 'postgres', }, }, }, - { - container: { - id: + }, + { + container: { + id: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', + }, + parent: { + id: '41226ae63af4f235', + }, + process: { + pid: 6, + title: '/opt/java/openjdk/bin/java', + ppid: 1, + }, + agent: { + name: 'java', + ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', + version: '1.15.1-SNAPSHOT', + }, + destination: { + address: 'postgres', + port: 5432, + }, + processor: { + name: 'transaction', + event: 'span', + }, + observer: { + hostname: '7189f754b5a3', + id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', + ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', + type: 'apm-server', + version: '8.0.0', + version_major: 8, + }, + trace: { + id: '3b0dc77f3754e5bcb9da0e4c15e0db97', + }, + '@timestamp': '2020-04-09T11:36:00.859Z', + ecs: { + version: '1.5.0', + }, + service: { + node: { + name: 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', }, - parent: { - id: '41226ae63af4f235', + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '11.0.6', }, - process: { - pid: 6, - title: '/opt/java/openjdk/bin/java', - ppid: 1, + language: { + name: 'Java', + version: '11.0.6', }, - agent: { - name: 'java', - ephemeral_id: '1cb5c830-c677-4b13-b340-ab1502f527c3', - version: '1.15.1-SNAPSHOT', + version: 'None', + }, + host: { + hostname: 'fc2ae281f56f', + os: { + platform: 'Linux', + }, + ip: '172.18.0.7', + name: 'fc2ae281f56f', + architecture: 'amd64', + }, + transaction: { + id: 'f2387d37260d00bd', + }, + timestamp: { + us: 1586432160859508, + }, + span: { + duration: { + us: 4503, }, + subtype: 'postgresql', destination: { - address: 'postgres', - port: 5432, - }, - processor: { - name: 'transaction', - event: 'span', - }, - observer: { - hostname: '7189f754b5a3', - id: 'f32d8d9f-a9f9-4355-8370-548dfd8024dc', - ephemeral_id: 'bff20764-0195-4f78-aa84-d799fc47b954', - type: 'apm-server', - version: '8.0.0', - version_major: 8, - }, - trace: { - id: '3b0dc77f3754e5bcb9da0e4c15e0db97', - }, - '@timestamp': '2020-04-09T11:36:00.859Z', - ecs: { - version: '1.5.0', - }, - service: { - node: { - name: - 'fc2ae281f56fb84728bc9b5e6c17f3d13bbb7f4efd461158558e5c38e655abad', - }, - environment: 'production', - name: 'opbeans-java', - runtime: { - name: 'Java', - version: '11.0.6', - }, - language: { - name: 'Java', - version: '11.0.6', - }, - version: 'None', - }, - host: { - hostname: 'fc2ae281f56f', - os: { - platform: 'Linux', + service: { + resource: 'postgresql', + name: 'postgresql', + type: 'db', }, - ip: '172.18.0.7', - name: 'fc2ae281f56f', - architecture: 'amd64', }, - transaction: { - id: 'f2387d37260d00bd', - }, - timestamp: { - us: 1586432160859508, - }, - span: { - duration: { - us: 4503, - }, - subtype: 'postgresql', - destination: { - service: { - resource: 'postgresql', - name: 'postgresql', - type: 'db', - }, - }, - name: 'empty query', - action: 'query', - id: '9871cfd612368932', - type: 'db', - db: { - rows_affected: 0, - statement: '(empty query)', - type: 'sql', - user: { - name: 'postgres', - }, + name: 'empty query', + action: 'query', + id: '9871cfd612368932', + type: 'db', + db: { + rows_affected: 0, + statement: '(empty query)', + type: 'sql', + user: { + name: 'postgres', }, }, }, - ], - exceedsMax: false, - errorDocs: [], - }, - errorsPerTransaction: {}, -}; + }, + ], + exceedsMax: false, + errorDocs: [], +} as TraceAPIResponse; diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx index 562cd255843bb..b06de47472a11 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { pickKeys } from '../../../../../common/utils/pick_keys'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APMQueryParams } from '../url_helpers'; -import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; +import { APMLink, APMLinkExtendProps } from './APMLink'; const persistedFilters: Array = [ 'host', @@ -18,13 +18,6 @@ const persistedFilters: Array = [ 'serviceVersion', ]; -export function useErrorOverviewHref(serviceName: string) { - return useAPMHref({ - path: `/services/${serviceName}/errors`, - persistedFilters, - }); -} - interface Props extends APMLinkExtendProps { serviceName: string; query?: APMQueryParams; diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts index 141a054a311c3..f19aef8e0bd8a 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts @@ -11,6 +11,7 @@ import { SERVICE, SPAN, LABELS, + EVENT, TRANSACTION, TRACE, MESSAGE_SPAN, @@ -20,6 +21,7 @@ export const SPAN_METADATA_SECTIONS: Section[] = [ LABELS, TRACE, TRANSACTION, + EVENT, SPAN, SERVICE, MESSAGE_SPAN, diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts index 59a2c88809ccc..2f4a3d3229857 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts @@ -9,6 +9,7 @@ import { Section, TRANSACTION, LABELS, + EVENT, HTTP, HOST, CLIENT, @@ -29,6 +30,7 @@ export const TRANSACTION_METADATA_SECTIONS: Section[] = [ { ...LABELS, required: true }, TRACE, TRANSACTION, + EVENT, HTTP, HOST, CLIENT, diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/sections.ts index 3faccce8ea955..efc2ef8bde66b 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/sections.ts +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/sections.ts @@ -21,6 +21,14 @@ export const LABELS: Section = { }), }; +export const EVENT: Section = { + key: 'event', + label: i18n.translate('xpack.apm.metadataTable.section.eventLabel', { + defaultMessage: 'event', + }), + properties: ['outcome'], +}; + export const HTTP: Section = { key: 'http', label: i18n.translate('xpack.apm.metadataTable.section.httpLabel', { diff --git a/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts b/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts index 35dbca1b0c955..40713f93d3ee5 100644 --- a/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts +++ b/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts @@ -9,7 +9,6 @@ import { CoreSetup, CoreStart } from 'kibana/public'; import * as t from 'io-ts'; import type { ClientRequestParamsOf, - EndpointOf, formatRequest as formatRequestType, ReturnOf, RouteRepositoryClient, @@ -26,6 +25,7 @@ import { callApi } from './callApi'; import type { APMServerRouteRepository, APMRouteHandlerResources, + APIEndpoint, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../server'; import { InspectResponse } from '../../../typings/common'; @@ -47,16 +47,15 @@ export type AutoAbortedAPMClient = RouteRepositoryClient< Omit >; -export type APIReturnType< - TEndpoint extends EndpointOf -> = ReturnOf & { +export type APIReturnType = ReturnOf< + APMServerRouteRepository, + TEndpoint +> & { _inspect?: InspectResponse; }; -export type APIEndpoint = EndpointOf; - export type APIClientRequestParamsOf< - TEndpoint extends EndpointOf + TEndpoint extends APIEndpoint > = ClientRequestParamsOf; export type AbstractAPMRepository = ServerRouteRepository< diff --git a/x-pack/plugins/apm/scripts/create-apm-users-and-roles/create_apm_users_and_roles.ts b/x-pack/plugins/apm/scripts/create-apm-users-and-roles/create_apm_users_and_roles.ts index 6b67d8d80e798..708a8b62287be 100644 --- a/x-pack/plugins/apm/scripts/create-apm-users-and-roles/create_apm_users_and_roles.ts +++ b/x-pack/plugins/apm/scripts/create-apm-users-and-roles/create_apm_users_and_roles.ts @@ -28,6 +28,14 @@ export async function createApmUsersAndRoles({ kibana: Kibana; elasticsearch: Elasticsearch; }) { + const isCredentialsValid = await getIsCredentialsValid({ + elasticsearch, + kibana, + }); + if (!isCredentialsValid) { + throw new AbortError('Invalid username/password'); + } + const isSecurityEnabled = await getIsSecurityEnabled({ elasticsearch, kibana, @@ -86,3 +94,25 @@ async function getIsSecurityEnabled({ return false; } } + +async function getIsCredentialsValid({ + elasticsearch, + kibana, +}: { + elasticsearch: Elasticsearch; + kibana: Kibana; +}) { + try { + await callKibana({ + elasticsearch, + kibana, + options: { + validateStatus: (status) => status >= 200 && status < 400, + url: `/`, + }, + }); + return true; + } catch (err) { + return false; + } +} diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index db62cc7adae2b..6ba412bd22029 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -125,7 +125,10 @@ export const plugin = (initContext: PluginInitializerContext) => export { APM_SERVER_FEATURE_ID } from '../common/alert_types'; export { APMPlugin } from './plugin'; export { APMPluginSetup } from './types'; -export { APMServerRouteRepository } from './routes/get_global_apm_server_route_repository'; +export { + APMServerRouteRepository, + APIEndpoint, +} from './routes/get_global_apm_server_route_repository'; export { APMRouteHandlerResources } from './routes/typings'; export type { ProcessorEvent } from '../common/processor_event'; diff --git a/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap index 3c521839b587e..7691373ada815 100644 --- a/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap @@ -8,15 +8,6 @@ Object { ], }, "body": Object { - "aggs": Object { - "by_transaction_id": Object { - "terms": Object { - "execution_hint": "map", - "field": "transaction.id", - "size": "myIndex", - }, - }, - }, "query": Object { "bool": Object { "filter": Array [ diff --git a/x-pack/plugins/apm/server/lib/traces/get_trace.ts b/x-pack/plugins/apm/server/lib/traces/get_trace.ts deleted file mode 100644 index a0cc6b7241d4e..0000000000000 --- a/x-pack/plugins/apm/server/lib/traces/get_trace.ts +++ /dev/null @@ -1,21 +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 { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { getTraceItems } from './get_trace_items'; - -export async function getTrace(traceId: string, setup: Setup & SetupTimeRange) { - const { errorsPerTransaction, ...trace } = await getTraceItems( - traceId, - setup - ); - - return { - trace, - errorsPerTransaction, - }; -} diff --git a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts index 6c957df313866..6cc6713e156bc 100644 --- a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts +++ b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts @@ -9,15 +9,13 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; import { ProcessorEvent } from '../../../common/processor_event'; import { TRACE_ID, - PARENT_ID, TRANSACTION_DURATION, SPAN_DURATION, - TRANSACTION_ID, + PARENT_ID, ERROR_LOG_LEVEL, } from '../../../common/elasticsearch_fieldnames'; import { rangeQuery } from '../../../../observability/server'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { PromiseValueType } from '../../../typings/common'; export async function getTraceItems( traceId: string, @@ -27,7 +25,7 @@ export async function getTraceItems( const maxTraceItems = config['xpack.apm.ui.maxTraceItems']; const excludedLogLevels = ['debug', 'info', 'warning']; - const errorResponsePromise = apmEventClient.search('get_trace_items', { + const errorResponsePromise = apmEventClient.search('get_errors_docs', { apm: { events: [ProcessorEvent.error], }, @@ -42,20 +40,10 @@ export async function getTraceItems( must_not: { terms: { [ERROR_LOG_LEVEL]: excludedLogLevels } }, }, }, - aggs: { - by_transaction_id: { - terms: { - field: TRANSACTION_ID, - size: maxTraceItems, - // high cardinality - execution_hint: 'map' as const, - }, - }, - }, }, }); - const traceResponsePromise = apmEventClient.search('get_trace_span_items', { + const traceResponsePromise = apmEventClient.search('get_trace_docs', { apm: { events: [ProcessorEvent.span, ProcessorEvent.transaction], }, @@ -81,33 +69,18 @@ export async function getTraceItems( }, }); - const [errorResponse, traceResponse]: [ - // explicit intermediary types to avoid TS "excessively deep" error - PromiseValueType, - PromiseValueType - ] = (await Promise.all([errorResponsePromise, traceResponsePromise])) as any; + const [errorResponse, traceResponse] = await Promise.all([ + errorResponsePromise, + traceResponsePromise, + ]); const exceedsMax = traceResponse.hits.total.value > maxTraceItems; - - const items = traceResponse.hits.hits.map((hit) => hit._source); - - const errorFrequencies = { - errorDocs: errorResponse.hits.hits.map(({ _source }) => _source), - errorsPerTransaction: - errorResponse.aggregations?.by_transaction_id.buckets.reduce( - (acc, current) => { - return { - ...acc, - [current.key]: current.doc_count, - }; - }, - {} as Record - ) ?? {}, - }; + const traceDocs = traceResponse.hits.hits.map((hit) => hit._source); + const errorDocs = errorResponse.hits.hits.map((hit) => hit._source); return { - items, exceedsMax, - ...errorFrequencies, + traceDocs, + errorDocs, }; } diff --git a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts index b66daf80bd763..941eb796d3ab3 100644 --- a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts +++ b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts @@ -72,10 +72,10 @@ export type APMServerRouteRepository = ReturnType< // Ensure no APIs return arrays (or, by proxy, the any type), // to guarantee compatibility with _inspect. -type CompositeEndpoint = EndpointOf; +export type APIEndpoint = EndpointOf; type EndpointReturnTypes = { - [Endpoint in CompositeEndpoint]: ReturnOf; + [Endpoint in APIEndpoint]: ReturnOf; }; type ArrayLikeReturnTypes = PickByValue; diff --git a/x-pack/plugins/apm/server/routes/traces.ts b/x-pack/plugins/apm/server/routes/traces.ts index 11747c847fcbd..c5273b7650e56 100644 --- a/x-pack/plugins/apm/server/routes/traces.ts +++ b/x-pack/plugins/apm/server/routes/traces.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; import { setupRequest } from '../lib/helpers/setup_request'; -import { getTrace } from '../lib/traces/get_trace'; +import { getTraceItems } from '../lib/traces/get_trace_items'; import { getTopTransactionGroupList } from '../lib/transaction_groups'; import { createApmServerRoute } from './create_apm_server_route'; import { environmentRt, kueryRt, rangeRt } from './default_api_types'; @@ -52,7 +52,7 @@ const tracesByIdRoute = createApmServerRoute({ const { params } = resources; const { traceId } = params.path; - return getTrace(traceId, setup); + return getTraceItems(traceId, setup); }, }); diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts index e743abec32dca..fd6f2130022c9 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts @@ -17,6 +17,7 @@ interface Processor { export interface SpanRaw extends APMBaseDoc { processor: Processor; trace: { id: string }; // trace is required + event?: { outcome?: 'success' | 'failure' }; service: { name: string; environment?: string; diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/transaction_raw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/transaction_raw.ts index bdafc1eb8270d..e628ab59aabc2 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/transaction_raw.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/transaction_raw.ts @@ -28,6 +28,7 @@ export interface TransactionRaw extends APMBaseDoc { processor: Processor; timestamp: TimestampUs; trace: { id: string }; // trace is required + event?: { outcome?: 'success' | 'failure' }; transaction: { duration: { us: number }; id: string; diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 1f7f7a18939ac..54ed2e3764418 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7245,7 +7245,6 @@ "xpack.apm.transactionDetails.distribution.panelTitle": "延迟分布", "xpack.apm.transactionDetails.emptySelectionText": "单击并拖动以选择范围", "xpack.apm.transactionDetails.errorCount": "{errorCount, number} 个 {errorCount, plural, other {错误}}", - "xpack.apm.transactionDetails.errorsOverviewLinkTooltip": "{errorCount, plural, one {查看 1 个相关错误} other {查看 # 个相关错误}}", "xpack.apm.transactionDetails.noTraceParentButtonTooltip": "找不到上级追溯", "xpack.apm.transactionDetails.percentOfTraceLabelExplanation": "{parentType, select, transaction {事务} trace {追溯} }的百分比超过 100%,因为此{childType, select, span {跨度} transaction {事务} }比根事务花费更长的时间。", "xpack.apm.transactionDetails.requestMethodLabel": "请求方法", diff --git a/x-pack/test/apm_api_integration/common/apm_api_supertest.ts b/x-pack/test/apm_api_integration/common/apm_api_supertest.ts index f930cab3b0568..b9470f39b507f 100644 --- a/x-pack/test/apm_api_integration/common/apm_api_supertest.ts +++ b/x-pack/test/apm_api_integration/common/apm_api_supertest.ts @@ -11,11 +11,11 @@ import request from 'superagent'; import { parseEndpoint } from '../../../plugins/apm/common/apm_api/parse_endpoint'; import type { APIReturnType, - APIEndpoint, APIClientRequestParamsOf, } from '../../../plugins/apm/public/services/rest/createCallApmApi'; +import type { APIEndpoint } from '../../../plugins/apm/server'; -export function createSupertestClient(st: supertest.SuperTest) { +export function createApmApiClient(st: supertest.SuperTest) { return async ( options: { endpoint: TEndpoint; @@ -41,7 +41,7 @@ export function createSupertestClient(st: supertest.SuperTest) { }; } -export type ApmApiSupertest = ReturnType; +export type ApmApiSupertest = ReturnType; export class ApmApiError extends Error { res: request.Response; diff --git a/x-pack/test/apm_api_integration/common/config.ts b/x-pack/test/apm_api_integration/common/config.ts index e8d777814402f..a8c5c433df45e 100644 --- a/x-pack/test/apm_api_integration/common/config.ts +++ b/x-pack/test/apm_api_integration/common/config.ts @@ -13,7 +13,7 @@ import { InheritedFtrProviderContext, InheritedServices } from './ftr_provider_c import { PromiseReturnType } from '../../../plugins/observability/typings/common'; import { createApmUser, APM_TEST_PASSWORD, ApmUser } from './authentication'; import { APMFtrConfigName } from '../configs'; -import { createSupertestClient } from './apm_api_supertest'; +import { createApmApiClient } from './apm_api_supertest'; import { registry } from './registry'; interface Config { @@ -52,7 +52,7 @@ async function getApmApiClient( auth: `${apmUser}:${APM_TEST_PASSWORD}`, }); - return createSupertestClient(supertest(url)); + return createApmApiClient(supertest(url)); } export type CreateTestConfig = ReturnType; diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index c8a57bc613a92..c0690fd2f8260 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -157,6 +157,9 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte describe('traces/top_traces', function () { loadTestFile(require.resolve('./traces/top_traces')); }); + describe('/api/apm/traces/{traceId}', function () { + loadTestFile(require.resolve('./traces/trace_by_id')); + }); // transactions describe('transactions/breakdown', function () { diff --git a/x-pack/test/apm_api_integration/tests/service_overview/instance_details.ts b/x-pack/test/apm_api_integration/tests/service_overview/instance_details.ts index 40bfbbb699e65..0fa64f25520d4 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/instance_details.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/instance_details.ts @@ -11,13 +11,13 @@ import archives from '../../common/fixtures/es_archiver/archives_metadata'; import { registry } from '../../common/registry'; import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; import { getServiceNodeIds } from './get_service_node_ids'; -import { createSupertestClient } from '../../common/apm_api_supertest'; +import { createApmApiClient } from '../../common/apm_api_supertest'; type ServiceOverviewInstanceDetails = APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}'>; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('legacySupertestAsApmReadUser'); - const apmApiSupertest = createSupertestClient(supertest); + const apmApiSupertest = createApmApiClient(supertest); const archiveName = 'apm_8.0.0'; const { start, end } = archives[archiveName]; diff --git a/x-pack/test/apm_api_integration/tests/service_overview/instances_detailed_statistics.ts b/x-pack/test/apm_api_integration/tests/service_overview/instances_detailed_statistics.ts index ffadb7fcf7801..1ad272bafaa80 100644 --- a/x-pack/test/apm_api_integration/tests/service_overview/instances_detailed_statistics.ts +++ b/x-pack/test/apm_api_integration/tests/service_overview/instances_detailed_statistics.ts @@ -14,12 +14,12 @@ import { APIReturnType } from '../../../../plugins/apm/public/services/rest/crea import { FtrProviderContext } from '../../common/ftr_provider_context'; import archives from '../../common/fixtures/es_archiver/archives_metadata'; import { registry } from '../../common/registry'; -import { createSupertestClient } from '../../common/apm_api_supertest'; +import { createApmApiClient } from '../../common/apm_api_supertest'; import { getServiceNodeIds } from './get_service_node_ids'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('legacySupertestAsApmReadUser'); - const apmApiSupertest = createSupertestClient(supertest); + const apmApiSupertest = createApmApiClient(supertest); const archiveName = 'apm_8.0.0'; const { start, end } = archives[archiveName]; diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups_detailed_statistics.ts b/x-pack/test/apm_api_integration/tests/services/error_groups_detailed_statistics.ts index 24507c1e42708..959e77397c37d 100644 --- a/x-pack/test/apm_api_integration/tests/services/error_groups_detailed_statistics.ts +++ b/x-pack/test/apm_api_integration/tests/services/error_groups_detailed_statistics.ts @@ -12,14 +12,14 @@ import archives_metadata from '../../common/fixtures/es_archiver/archives_metada import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; -import { createSupertestClient } from '../../common/apm_api_supertest'; +import { createApmApiClient } from '../../common/apm_api_supertest'; import { getErrorGroupIds } from './get_error_group_ids'; type ErrorGroupsDetailedStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/error_groups/detailed_statistics'>; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('legacySupertestAsApmReadUser'); - const apmApiSupertest = createSupertestClient(supertest); + const apmApiSupertest = createApmApiClient(supertest); const archiveName = 'apm_8.0.0'; const metadata = archives_metadata[archiveName]; diff --git a/x-pack/test/apm_api_integration/tests/traces/trace_by_id.tsx b/x-pack/test/apm_api_integration/tests/traces/trace_by_id.tsx new file mode 100644 index 0000000000000..5b4ab5f45da49 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/traces/trace_by_id.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 expect from '@kbn/expect'; +import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; +import { createApmApiClient, SupertestReturnType } from '../../common/apm_api_supertest'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const apmApiSupertest = createApmApiClient(supertest); + + const archiveName = 'apm_8.0.0'; + const metadata = archives_metadata[archiveName]; + const { start, end } = metadata; + + registry.when('Trace does not exist', { config: 'basic', archives: [] }, () => { + it('handles empty state', async () => { + const response = await apmApiSupertest({ + endpoint: `GET /api/apm/traces/{traceId}`, + params: { + path: { traceId: 'foo' }, + query: { start, end }, + }, + }); + + expect(response.status).to.be(200); + expect(response.body).to.eql({ exceedsMax: false, traceDocs: [], errorDocs: [] }); + }); + }); + + registry.when('Trace exists', { config: 'basic', archives: [archiveName] }, () => { + let response: SupertestReturnType<`GET /api/apm/traces/{traceId}`>; + before(async () => { + response = await apmApiSupertest({ + endpoint: `GET /api/apm/traces/{traceId}`, + params: { + path: { traceId: '64d0014f7530df24e549dd17cc0a8895' }, + query: { start, end }, + }, + }); + }); + + it('returns the correct status code', async () => { + expect(response.status).to.be(200); + }); + + it('returns the correct number of buckets', async () => { + expectSnapshot(response.body.errorDocs.map((doc) => doc.error?.exception?.[0]?.message)) + .toMatchInline(` + Array [ + "Test CaptureError", + "Uncaught Error: Test Error in dashboard", + ] + `); + expectSnapshot( + response.body.traceDocs.map((doc) => + doc.processor.event === 'transaction' + ? // @ts-expect-error + `${doc.transaction.name} (transaction)` + : // @ts-expect-error + `${doc.span.name} (span)` + ) + ).toMatchInline(` + Array [ + "/dashboard (transaction)", + "GET /api/stats (transaction)", + "APIRestController#topProducts (transaction)", + "Parsing the document, executing sync. scripts (span)", + "GET /api/products/top (span)", + "GET /api/stats (span)", + "Requesting and receiving the document (span)", + "SELECT FROM customers (span)", + "SELECT FROM order_lines (span)", + "http://opbeans-frontend:3000/static/css/main.7bd7c5e8.css (span)", + "SELECT FROM products (span)", + "SELECT FROM orders (span)", + "SELECT FROM order_lines (span)", + "Making a connection to the server (span)", + "Fire \\"load\\" event (span)", + "empty query (span)", + ] + `); + expectSnapshot(response.body.exceedsMax).toMatchInline(`false`); + }); + }); +} From 10c671bf9a24ec89f5b9202c5ea5bf4362ad8e24 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 8 Sep 2021 20:40:36 -0400 Subject: [PATCH 031/139] Bump geckodriver to 2.0.4 (#111605) (#111661) Co-authored-by: Jonathan Budzenski --- package.json | 2 +- yarn.lock | 100 +++++++++++++++------------------------------------ 2 files changed, 29 insertions(+), 73 deletions(-) diff --git a/package.json b/package.json index a8f44c5e8bcdb..f36792370bdec 100644 --- a/package.json +++ b/package.json @@ -736,7 +736,7 @@ "fetch-mock": "^7.3.9", "file-loader": "^4.2.0", "form-data": "^4.0.0", - "geckodriver": "^1.22.2", + "geckodriver": "^2.0.4", "glob-watcher": "5.0.3", "grunt": "1.3.0", "grunt-available-tasks": "^0.6.3", diff --git a/yarn.lock b/yarn.lock index f0dbc754631ea..44227cd7f3f05 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6805,10 +6805,10 @@ address@1.1.2, address@^1.0.1: resolved "https://registry.yarnpkg.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6" integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA== -adm-zip@0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.3.tgz#f5ac6b2507f4418e2691c0feb6f130b4b90d7643" - integrity sha512-zsoTXEwRNCxBzRHLENFLuecCcwzzXiEhWo1r3GP68iwi8Q/hW2RrqgeY1nfJ/AhNQNWnZq/4v0TbfMsUkI+TYw== +adm-zip@0.5.5: + version "0.5.5" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.5.tgz#b6549dbea741e4050309f1bb4d47c47397ce2c4f" + integrity sha512-IWwXKnCbirdbyXSfUDvCCrmYrOHANRZcc8NcRrvTlIApdl7PwE9oGcsYvNeJPAVY1M+70b4PxXGKIf8AEuiQ6w== after-all-results@^2.0.0: version "2.0.0" @@ -9251,11 +9251,6 @@ capture-exit@^2.0.0: dependencies: rsvp "^4.8.4" -capture-stack-trace@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d" - integrity sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0= - cardinal@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/cardinal/-/cardinal-2.1.1.tgz#7cc1055d822d212954d07b085dea251cc7bc5505" @@ -10435,13 +10430,6 @@ create-emotion@^9.2.12: stylis "^3.5.0" stylis-rule-sheet "^0.0.10" -create-error-class@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" - integrity sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y= - dependencies: - capture-stack-trace "^1.0.0" - create-hash@^1.1.0, create-hash@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd" @@ -12221,7 +12209,7 @@ dtrace-provider@~0.8: dependencies: nan "^2.14.0" -duplexer2@^0.1.2, duplexer2@^0.1.4, duplexer2@~0.1.0, duplexer2@~0.1.2, duplexer2@~0.1.4: +duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2, duplexer2@~0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" integrity sha1-ixLauHjA1p4+eJEFFmKjL8a93ME= @@ -14428,16 +14416,16 @@ gaze@^1.0.0: dependencies: globule "^1.0.0" -geckodriver@^1.22.2: - version "1.22.2" - resolved "https://registry.yarnpkg.com/geckodriver/-/geckodriver-1.22.2.tgz#e0904bed50a1d2abaa24597d4ae43eb6662f9d72" - integrity sha512-xcf1OLfHqNX4+wQhj4weu2gtiwtPnV8yEEKvLkC8GuFtUc5WjOGodV/2pHiYJjCSJRQfsmIgY5Xs1zaJf/OGFA== +geckodriver@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/geckodriver/-/geckodriver-2.0.4.tgz#2f644ede43ce7bea10336d57838179da0f7374d9" + integrity sha512-3Fu75v6Ov8h5Vt25+djJU56MJA2gRctgjhvG5xGzLFTQjltPz7nojQdBHbmgWznUt3CHl8VaiDn8MaepY7B0dA== dependencies: - adm-zip "0.5.3" + adm-zip "0.5.5" bluebird "3.7.2" - got "5.6.0" + got "11.8.2" https-proxy-agent "5.0.0" - tar "6.0.2" + tar "6.1.9" generic-pool@^3.7.1: version "3.7.1" @@ -14928,29 +14916,7 @@ gonzales-pe@^4.3.0: dependencies: minimist "^1.2.5" -got@5.6.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/got/-/got-5.6.0.tgz#bb1d7ee163b78082bbc8eb836f3f395004ea6fbf" - integrity sha1-ux1+4WO3gIK7yOuDbz85UATqb78= - dependencies: - create-error-class "^3.0.1" - duplexer2 "^0.1.4" - is-plain-obj "^1.0.0" - is-redirect "^1.0.0" - is-retry-allowed "^1.0.0" - is-stream "^1.0.0" - lowercase-keys "^1.0.0" - node-status-codes "^1.0.0" - object-assign "^4.0.1" - parse-json "^2.1.0" - pinkie-promise "^2.0.0" - read-all-stream "^3.0.0" - readable-stream "^2.0.5" - timed-out "^2.0.0" - unzip-response "^1.0.0" - url-parse-lax "^1.0.0" - -got@^11.8.2: +got@11.8.2, got@^11.8.2: version "11.8.2" resolved "https://registry.yarnpkg.com/got/-/got-11.8.2.tgz#7abb3959ea28c31f3576f1576c1effce23f33599" integrity sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ== @@ -16821,11 +16787,6 @@ is-resolvable@^1.0.0: resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== -is-retry-allowed@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34" - integrity sha1-EaBgVotnM5REAz0BJaYaINVk+zQ= - is-root@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c" @@ -20474,11 +20435,6 @@ node-sql-parser@^3.6.1: dependencies: big-integer "^1.6.48" -node-status-codes@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/node-status-codes/-/node-status-codes-1.0.0.tgz#5ae5541d024645d32a58fcddc9ceecea7ae3ac2f" - integrity sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8= - nodemailer@^6.6.2: version "6.6.2" resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.6.2.tgz#e184c9ed5bee245a3e0bcabc7255866385757114" @@ -21395,7 +21351,7 @@ parse-headers@^2.0.0: for-each "^0.3.2" trim "0.0.1" -parse-json@^2.1.0, parse-json@^2.2.0: +parse-json@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= @@ -22318,7 +22274,7 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= -prepend-http@^1.0.0, prepend-http@^1.0.1: +prepend-http@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= @@ -26802,7 +26758,19 @@ tar@4.4.13: safe-buffer "^5.1.2" yallist "^3.0.3" -tar@6.0.2, tar@^6.0.2: +tar@6.1.9: + version "6.1.9" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.9.tgz#5646ef51342ac55456b2466e44da810439978db1" + integrity sha512-XjLaMNl76o07zqZC/aW4lwegdY07baOH1T8w3AEfrHAdyg/oYO4ctjzEBq9Gy9fEP9oHqLIgvx6zuGDGe+bc8Q== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +tar@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.2.tgz#5df17813468a6264ff14f766886c622b84ae2f39" integrity sha512-Glo3jkRtPcvpDlAs/0+hozav78yoXKFr+c4wgw62NNMO3oo4AaJdCo21Uu7lcwr55h39W2XD1LMERc64wtbItg== @@ -28132,11 +28100,6 @@ untildify@^4.0.0: resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== -unzip-response@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-1.0.2.tgz#b984f0877fc0a89c2c773cc1ef7b5b232b5b06fe" - integrity sha1-uYTwh3/AqJwsdzzB73tbIytbBv4= - upath@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" @@ -28229,13 +28192,6 @@ url-loader@^4.0.0: mime-types "^2.1.26" schema-utils "^2.6.5" -url-parse-lax@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" - integrity sha1-evjzA2Rem9eaJy56FKxovAYJ2nM= - dependencies: - prepend-http "^1.0.1" - url-parse-lax@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" From a60dd56aca8c024fe539f7a84d95f630a987ef0d Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Wed, 8 Sep 2021 18:50:23 -0700 Subject: [PATCH 032/139] [Reporting] Remove encryption logic from export types (#111472) (#111664) * [Reporting] change CreateJobFn type to not handle KibanaRequest * fix enqueueJob * fix tests * fix imports * convert enqueue_job into a method of request_handler * Update types.ts * Update report.ts * fix import * update comment and structure Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/reporting/common/types.ts | 2 +- .../server/export_types/csv/create_job.ts | 11 +- .../csv_searchsource/create_job.ts | 19 +-- .../export_types/png/create_job/index.ts | 12 +- .../server/export_types/png_v2/create_job.ts | 12 +- .../create_job/compatibility_shim.test.ts | 40 ++---- .../create_job/compatibility_shim.ts | 7 +- .../printable_pdf/create_job/index.ts | 17 +-- .../printable_pdf_v2/create_job.ts | 12 +- .../reporting/server/lib/enqueue_job.test.ts | 136 ------------------ .../reporting/server/lib/enqueue_job.ts | 66 --------- .../reporting/server/lib/store/report.ts | 4 +- .../server/routes/lib/request_handler.test.ts | 129 +++++++++++++++-- .../server/routes/lib/request_handler.ts | 83 +++++++++-- x-pack/plugins/reporting/server/types.ts | 7 +- 15 files changed, 225 insertions(+), 332 deletions(-) delete mode 100644 x-pack/plugins/reporting/server/lib/enqueue_job.test.ts delete mode 100644 x-pack/plugins/reporting/server/lib/enqueue_job.ts diff --git a/x-pack/plugins/reporting/common/types.ts b/x-pack/plugins/reporting/common/types.ts index 4051c006a5034..fe018feab0f8b 100644 --- a/x-pack/plugins/reporting/common/types.ts +++ b/x-pack/plugins/reporting/common/types.ts @@ -76,7 +76,7 @@ export interface BasePayload extends BaseParams { export interface ReportSource { /* - * Required fields: populated in enqueue_job when the request comes in to + * Required fields: populated in RequestHandler.enqueueJob when the request comes in to * generate the report */ jobtype: string; // refers to `ExportTypeDefinition.jobType` diff --git a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts index 56d6facea9212..b6160daa86f85 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { cryptoFactory } from '../../lib'; import { CreateJobFn, CreateJobFnFactory } from '../../types'; import { IndexPatternSavedObjectDeprecatedCSV, @@ -15,15 +14,11 @@ import { export const createJobFnFactory: CreateJobFnFactory< CreateJobFn -> = function createJobFactoryFn(reporting, logger) { - const config = reporting.getConfig(); - const crypto = cryptoFactory(config.get('encryptionKey')); - - return async function createJob(jobParams, context, request) { +> = function createJobFactoryFn(_reporting, logger) { + return async function createJob(jobParams, context) { logger.warn( `The "/generate/csv" endpoint is deprecated and will be removed in Kibana 8.0. Please recreate the POST URL used to automate this CSV export.` ); - const serializedEncryptedHeaders = await crypto.encrypt(request.headers); const savedObjectsClient = context.core.savedObjects.client; const indexPatternSavedObject = ((await savedObjectsClient.get( @@ -33,8 +28,6 @@ export const createJobFnFactory: CreateJobFnFactory< return { isDeprecated: true, - headers: serializedEncryptedHeaders, - spaceId: reporting.getSpaceId(request, logger), indexPatternSavedObject, ...jobParams, }; diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/create_job.ts index a389f2a3252ca..36569eb865d11 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/create_job.ts @@ -5,26 +5,13 @@ * 2.0. */ -import { CSV_JOB_TYPE } from '../../../common/constants'; -import { cryptoFactory } from '../../lib'; import { CreateJobFn, CreateJobFnFactory } from '../../types'; import { JobParamsCSV, TaskPayloadCSV } from './types'; export const createJobFnFactory: CreateJobFnFactory< CreateJobFn -> = function createJobFactoryFn(reporting, parentLogger) { - const logger = parentLogger.clone([CSV_JOB_TYPE, 'create-job']); - - const config = reporting.getConfig(); - const crypto = cryptoFactory(config.get('encryptionKey')); - - return async function createJob(jobParams, context, request) { - const serializedEncryptedHeaders = await crypto.encrypt(request.headers); - - return { - headers: serializedEncryptedHeaders, - spaceId: reporting.getSpaceId(request, logger), - ...jobParams, - }; +> = function createJobFactoryFn() { + return async function createJob(jobParams) { + return jobParams; }; }; diff --git a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts index aae2e85e823db..68d0d6da5e1d6 100644 --- a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts @@ -5,26 +5,18 @@ * 2.0. */ -import { cryptoFactory } from '../../../lib'; import { CreateJobFn, CreateJobFnFactory } from '../../../types'; import { validateUrls } from '../../common'; import { JobParamsPNG, TaskPayloadPNG } from '../types'; export const createJobFnFactory: CreateJobFnFactory< CreateJobFn -> = function createJobFactoryFn(reporting, logger) { - const config = reporting.getConfig(); - const crypto = cryptoFactory(config.get('encryptionKey')); - - return async function createJob(jobParams, _context, req) { - const serializedEncryptedHeaders = await crypto.encrypt(req.headers); - +> = function createJobFactoryFn() { + return async function createJob(jobParams) { validateUrls([jobParams.relativeUrl]); return { ...jobParams, - headers: serializedEncryptedHeaders, - spaceId: reporting.getSpaceId(req, logger), forceNow: new Date().toISOString(), }; }; diff --git a/x-pack/plugins/reporting/server/export_types/png_v2/create_job.ts b/x-pack/plugins/reporting/server/export_types/png_v2/create_job.ts index d04a5307f22e2..b71f09310681c 100644 --- a/x-pack/plugins/reporting/server/export_types/png_v2/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/png_v2/create_job.ts @@ -5,23 +5,15 @@ * 2.0. */ -import { cryptoFactory } from '../../lib'; import { CreateJobFn, CreateJobFnFactory } from '../../types'; import { JobParamsPNGV2, TaskPayloadPNGV2 } from './types'; export const createJobFnFactory: CreateJobFnFactory< CreateJobFn -> = function createJobFactoryFn(reporting, logger) { - const config = reporting.getConfig(); - const crypto = cryptoFactory(config.get('encryptionKey')); - - return async function createJob({ locatorParams, ...jobParams }, context, req) { - const serializedEncryptedHeaders = await crypto.encrypt(req.headers); - +> = function createJobFactoryFn() { + return async function createJob({ locatorParams, ...jobParams }) { return { ...jobParams, - headers: serializedEncryptedHeaders, - spaceId: reporting.getSpaceId(req, logger), locatorParams: [locatorParams], forceNow: new Date().toISOString(), }; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.test.ts index 4e366f506af2d..e2c3ffdd68818 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.test.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { coreMock, httpServerMock } from 'src/core/server/mocks'; +import { coreMock } from 'src/core/server/mocks'; import { createMockLevelLogger } from '../../../test_helpers'; import { compatibilityShim } from './compatibility_shim'; @@ -15,7 +15,6 @@ const mockRequestHandlerContext = { }; const mockLogger = createMockLevelLogger(); -const mockKibanaRequest = httpServerMock.createKibanaRequest(); const createMockSavedObject = (body: any) => ({ id: 'mockSavedObjectId123', type: 'mockSavedObjectType', @@ -36,8 +35,7 @@ test(`passes title through if provided`, async () => { const createJobMock = jest.fn(); await compatibilityShim(createJobMock, mockLogger)( createMockJobParams({ title, relativeUrls: ['/something'] }), - mockRequestHandlerContext, - mockKibanaRequest + mockRequestHandlerContext ); expect(mockLogger.warn.mock.calls.length).toBe(0); @@ -58,8 +56,7 @@ test(`gets the title from the savedObject`, async () => { await compatibilityShim(createJobMock, mockLogger)( createMockJobParams({ objectType: 'search', savedObjectId: 'abc' }), - mockRequestHandlerContext, - mockKibanaRequest + mockRequestHandlerContext ); expect(mockLogger.warn.mock.calls.length).toBe(2); @@ -83,8 +80,7 @@ test(`passes the objectType and savedObjectId to the savedObjectsClient`, async const savedObjectId = 'abc'; await compatibilityShim(createJobMock, mockLogger)( createMockJobParams({ objectType, savedObjectId }), - context, - mockKibanaRequest + context ); expect(mockLogger.warn.mock.calls.length).toBe(2); @@ -107,8 +103,7 @@ test(`logs no warnings when title and relativeUrls is passed`, async () => { await compatibilityShim(createJobMock, mockLogger)( createMockJobParams({ title: 'Phenomenal Dashboard', relativeUrls: ['/abc', '/def'] }), - mockRequestHandlerContext, - mockKibanaRequest + mockRequestHandlerContext ); expect(mockLogger.warn.mock.calls.length).toBe(0); @@ -119,8 +114,7 @@ test(`logs warning if title can not be provided`, async () => { const createJobMock = jest.fn(); await compatibilityShim(createJobMock, mockLogger)( createMockJobParams({ relativeUrls: ['/abc'] }), - mockRequestHandlerContext, - mockKibanaRequest + mockRequestHandlerContext ); expect(mockLogger.warn.mock.calls.length).toBe(1); @@ -140,8 +134,7 @@ test(`logs deprecations when generating the title/relativeUrl using the savedObj await compatibilityShim(createJobMock, mockLogger)( createMockJobParams({ objectType: 'search', savedObjectId: 'abc' }), - mockRequestHandlerContext, - mockKibanaRequest + mockRequestHandlerContext ); expect(mockLogger.warn.mock.calls.length).toBe(2); @@ -159,8 +152,7 @@ test(`passes objectType through`, async () => { const objectType = 'foo'; await compatibilityShim(createJobMock, mockLogger)( createMockJobParams({ title: 'test', relativeUrls: ['/something'], objectType }), - mockRequestHandlerContext, - mockKibanaRequest + mockRequestHandlerContext ); expect(mockLogger.warn.mock.calls.length).toBe(0); @@ -176,8 +168,7 @@ test(`passes the relativeUrls through`, async () => { const relativeUrls = ['/app/kibana#something', '/app/kibana#something-else']; await compatibilityShim(createJobMock, mockLogger)( createMockJobParams({ title: 'test', relativeUrls }), - mockRequestHandlerContext, - mockKibanaRequest + mockRequestHandlerContext ); expect(mockLogger.warn.mock.calls.length).toBe(0); @@ -193,8 +184,7 @@ const testSavedObjectRelativeUrl = (objectType: string, expectedUrl: string) => await compatibilityShim(createJobMock, mockLogger)( createMockJobParams({ title: 'test', objectType, savedObjectId: 'abc' }), - mockRequestHandlerContext, - mockKibanaRequest + mockRequestHandlerContext ); expect(mockLogger.warn.mock.calls.length).toBe(1); @@ -222,8 +212,7 @@ test(`appends the queryString to the relativeUrl when generating from the savedO savedObjectId: 'abc', queryString: 'foo=bar', }), - mockRequestHandlerContext, - mockKibanaRequest + mockRequestHandlerContext ); expect(mockLogger.warn.mock.calls.length).toBe(1); @@ -248,8 +237,7 @@ test(`throw an Error if the objectType, savedObjectId and relativeUrls are provi relativeUrls: ['/something'], savedObjectId: 'abc', }), - mockRequestHandlerContext, - mockKibanaRequest + mockRequestHandlerContext ); await expect(promise).rejects.toBeDefined(); @@ -260,8 +248,7 @@ test(`passes headers and request through`, async () => { await compatibilityShim(createJobMock, mockLogger)( createMockJobParams({ title: 'test', relativeUrls: ['/something'] }), - mockRequestHandlerContext, - mockKibanaRequest + mockRequestHandlerContext ); expect(mockLogger.warn.mock.calls.length).toBe(0); @@ -269,5 +256,4 @@ test(`passes headers and request through`, async () => { expect(createJobMock.mock.calls.length).toBe(1); expect(createJobMock.mock.calls[0][1]).toBe(mockRequestHandlerContext); - expect(createJobMock.mock.calls[0][2]).toBe(mockKibanaRequest); }); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.ts index 342e1fc7d85de..1d222d61eb07d 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; +import type { SavedObjectsClientContract } from 'kibana/server'; import { url as urlUtils } from '../../../../../../../src/plugins/kibana_utils/server'; import type { LevelLogger } from '../../../lib'; import type { CreateJobFn, ReportingRequestHandlerContext } from '../../../types'; @@ -56,8 +56,7 @@ export function compatibilityShim( ) { return async function ( jobParams: JobParamsPDF | JobParamsPDFLegacy, - context: ReportingRequestHandlerContext, - req: KibanaRequest + context: ReportingRequestHandlerContext ) { let kibanaRelativeUrls = (jobParams as JobParamsPDF).relativeUrls; let reportTitle = jobParams.title; @@ -125,6 +124,6 @@ export function compatibilityShim( isDeprecated, // tack on this flag so it will be saved the TaskPayload }; - return await createJobFn(transformedJobParams, context, req); + return await createJobFn(transformedJobParams, context); }; } diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts index 8411dbcb94d11..1a017726207b4 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts @@ -5,9 +5,7 @@ * 2.0. */ -import { KibanaRequest } from 'src/core/server'; -import { cryptoFactory } from '../../../lib'; -import { CreateJobFn, CreateJobFnFactory, ReportingRequestHandlerContext } from '../../../types'; +import { CreateJobFn, CreateJobFnFactory } from '../../../types'; import { validateUrls } from '../../common'; import { JobParamsPDF, JobParamsPDFLegacy, TaskPayloadPDF } from '../types'; import { compatibilityShim } from './compatibility_shim'; @@ -18,24 +16,15 @@ import { compatibilityShim } from './compatibility_shim'; */ export const createJobFnFactory: CreateJobFnFactory< CreateJobFn -> = function createJobFactoryFn(reporting, logger) { - const config = reporting.getConfig(); - const crypto = cryptoFactory(config.get('encryptionKey')); - +> = function createJobFactoryFn(_reporting, logger) { return compatibilityShim(async function createJobFn( - { relativeUrls, ...jobParams }: JobParamsPDF, // relativeUrls does not belong in the payload - _context: ReportingRequestHandlerContext, - req: KibanaRequest + { relativeUrls, ...jobParams }: JobParamsPDF // relativeUrls does not belong in the payload of PDFV1 ) { validateUrls(relativeUrls); - const serializedEncryptedHeaders = await crypto.encrypt(req.headers); - // return the payload return { ...jobParams, - headers: serializedEncryptedHeaders, - spaceId: reporting.getSpaceId(req, logger), forceNow: new Date().toISOString(), objects: relativeUrls.map((u) => ({ relativeUrl: u })), }; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/create_job.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/create_job.ts index b621759528e80..9effb4a29fe55 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/create_job.ts @@ -5,23 +5,15 @@ * 2.0. */ -import { cryptoFactory } from '../../lib'; import { CreateJobFn, CreateJobFnFactory } from '../../types'; import { JobParamsPDFV2, TaskPayloadPDFV2 } from './types'; export const createJobFnFactory: CreateJobFnFactory< CreateJobFn -> = function createJobFactoryFn(reporting, logger) { - const config = reporting.getConfig(); - const crypto = cryptoFactory(config.get('encryptionKey')); - - return async function createJob(jobParams, context, req) { - const serializedEncryptedHeaders = await crypto.encrypt(req.headers); - +> = function createJobFactoryFn() { + return async function createJob(jobParams) { return { ...jobParams, - headers: serializedEncryptedHeaders, - spaceId: reporting.getSpaceId(req, logger), forceNow: new Date().toISOString(), }; }; diff --git a/x-pack/plugins/reporting/server/lib/enqueue_job.test.ts b/x-pack/plugins/reporting/server/lib/enqueue_job.test.ts deleted file mode 100644 index 50103c8806fe0..0000000000000 --- a/x-pack/plugins/reporting/server/lib/enqueue_job.test.ts +++ /dev/null @@ -1,136 +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 { KibanaRequest } from 'src/core/server'; -import { ReportingCore } from '../'; -import { TaskManagerStartContract } from '../../../task_manager/server'; -import { ReportingInternalStart } from '../core'; -import { - createMockConfigSchema, - createMockLevelLogger, - createMockReportingCore, -} from '../test_helpers'; -import { ReportingRequestHandlerContext } from '../types'; -import { ExportTypesRegistry, ReportingStore } from './'; -import { enqueueJob } from './enqueue_job'; -import { Report } from './store'; - -describe('Enqueue Job', () => { - const logger = createMockLevelLogger(); - let mockReporting: ReportingCore; - let mockExportTypesRegistry: ExportTypesRegistry; - - const mockBaseParams = { - browserTimezone: 'UTC', - headers: 'cool_encrypted_headers', - objectType: 'cool_object_type', - title: 'cool_title', - version: 'unknown' as any, - }; - - beforeEach(() => { - mockBaseParams.version = '7.15.0-test'; - }); - - beforeAll(async () => { - mockExportTypesRegistry = new ExportTypesRegistry(); - mockExportTypesRegistry.register({ - id: 'printablePdf', - name: 'Printable PDFble', - jobType: 'printable_pdf', - jobContentEncoding: 'base64', - jobContentExtension: 'pdf', - validLicenses: ['turquoise'], - createJobFnFactory: () => async () => mockBaseParams, - runTaskFnFactory: jest.fn(), - }); - mockReporting = await createMockReportingCore(createMockConfigSchema()); - mockReporting.getExportTypesRegistry = () => mockExportTypesRegistry; - mockReporting.getStore = () => - Promise.resolve(({ - addReport: jest - .fn() - .mockImplementation( - (report) => new Report({ ...report, _index: '.reporting-foo-index-234' }) - ), - } as unknown) as ReportingStore); - - const scheduleMock = jest.fn().mockImplementation(() => ({ - id: '123-great-id', - })); - - await mockReporting.pluginStart(({ - taskManager: ({ - ensureScheduled: jest.fn(), - schedule: scheduleMock, - } as unknown) as TaskManagerStartContract, - } as unknown) as ReportingInternalStart); - }); - - it('returns a Report object', async () => { - const report = await enqueueJob( - mockReporting, - ({} as unknown) as KibanaRequest, - ({} as unknown) as ReportingRequestHandlerContext, - false, - 'printablePdf', - mockBaseParams, - logger - ); - - const { _id, created_at: _created_at, ...snapObj } = report; - expect(snapObj).toMatchInlineSnapshot(` - Object { - "_index": ".reporting-foo-index-234", - "_primary_term": undefined, - "_seq_no": undefined, - "attempts": 0, - "browser_type": undefined, - "completed_at": undefined, - "created_by": false, - "jobtype": "printable_pdf", - "kibana_id": undefined, - "kibana_name": undefined, - "max_attempts": undefined, - "meta": Object { - "isDeprecated": undefined, - "layout": undefined, - "objectType": "cool_object_type", - }, - "migration_version": "7.14.0", - "output": null, - "payload": Object { - "browserTimezone": "UTC", - "headers": "cool_encrypted_headers", - "objectType": "cool_object_type", - "title": "cool_title", - "version": "7.15.0-test", - }, - "process_expiration": undefined, - "started_at": undefined, - "status": "pending", - "timeout": undefined, - } - `); - }); - - it('provides a default kibana version field for older POST URLs', async () => { - mockBaseParams.version = undefined; - const report = await enqueueJob( - mockReporting, - ({} as unknown) as KibanaRequest, - ({} as unknown) as ReportingRequestHandlerContext, - false, - 'printablePdf', - mockBaseParams, - logger - ); - - const { _id, created_at: _created_at, ...snapObj } = report; - expect(snapObj.payload.version).toBe('7.14.0'); - }); -}); diff --git a/x-pack/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/plugins/reporting/server/lib/enqueue_job.ts deleted file mode 100644 index 129c474fd134a..0000000000000 --- a/x-pack/plugins/reporting/server/lib/enqueue_job.ts +++ /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 { KibanaRequest } from 'src/core/server'; -import { ReportingCore } from '../'; -import type { ReportingRequestHandlerContext } from '../types'; -import { BaseParams, ReportingUser } from '../types'; -import { checkParamsVersion, LevelLogger } from './'; -import { Report } from './store'; - -export async function enqueueJob( - reporting: ReportingCore, - request: KibanaRequest, - context: ReportingRequestHandlerContext, - user: ReportingUser, - exportTypeId: string, - jobParams: BaseParams, - parentLogger: LevelLogger -): Promise { - const logger = parentLogger.clone(['createJob']); - const exportType = reporting.getExportTypesRegistry().getById(exportTypeId); - - if (exportType == null) { - throw new Error(`Export type ${exportTypeId} does not exist in the registry!`); - } - - if (!exportType.createJobFnFactory) { - throw new Error(`Export type ${exportTypeId} is not an async job type!`); - } - - const [createJob, store] = await Promise.all([ - exportType.createJobFnFactory(reporting, logger.clone([exportType.id])), - reporting.getStore(), - ]); - - jobParams.version = checkParamsVersion(jobParams, logger); - const job = await createJob!(jobParams, context, request); - - // 1. Add the report to ReportingStore to show as pending - const report = await store.addReport( - new Report({ - jobtype: exportType.jobType, - created_by: user ? user.username : false, - payload: job, - meta: { - // telemetry fields - objectType: jobParams.objectType, - layout: jobParams.layout?.id, - isDeprecated: job.isDeprecated, - }, - }) - ); - logger.debug(`Successfully stored pending job: ${report._index}/${report._id}`); - - // 2. Schedule the report with Task Manager - const task = await reporting.scheduleTask(report.toReportTaskJSON()); - logger.info( - `Scheduled ${exportType.name} reporting task. Task ID: task:${task.id}. Report ID: ${report._id}` - ); - - return report; -} diff --git a/x-pack/plugins/reporting/server/lib/store/report.ts b/x-pack/plugins/reporting/server/lib/store/report.ts index d45cb7cd39947..2feb162fbf37e 100644 --- a/x-pack/plugins/reporting/server/lib/store/report.ts +++ b/x-pack/plugins/reporting/server/lib/store/report.ts @@ -15,7 +15,7 @@ import { ReportDocumentHead, ReportSource, } from '../../../common/types'; -import { ReportTaskParams } from '../tasks'; +import type { ReportTaskParams } from '../tasks'; export { ReportDocument }; export { ReportApiJSON, ReportSource }; @@ -67,7 +67,7 @@ export class Report implements Partial { this.migration_version = MIGRATION_VERSION; - // see enqueue_job for all the fields that are expected to exist when adding a report + // see RequestHandler.enqueueJob for all the fields that are expected to exist when adding a report if (opts.jobtype == null) { throw new Error(`jobtype is expected!`); } diff --git a/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts b/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts index d730da4803fe9..3e7aae35c07fd 100644 --- a/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts +++ b/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts @@ -8,18 +8,20 @@ import { KibanaRequest, KibanaResponseFactory } from 'kibana/server'; import { coreMock, httpServerMock } from 'src/core/server/mocks'; import { ReportingCore } from '../..'; +import { JobParamsPDF, TaskPayloadPDF } from '../../export_types/printable_pdf/types'; +import { Report, ReportingStore } from '../../lib/store'; +import { ReportApiJSON } from '../../lib/store/report'; import { createMockConfigSchema, createMockLevelLogger, createMockReportingCore, } from '../../test_helpers'; -import { BaseParams, ReportingRequestHandlerContext, ReportingSetup } from '../../types'; +import { ReportingRequestHandlerContext, ReportingSetup } from '../../types'; import { RequestHandler } from './request_handler'; -jest.mock('../../lib/enqueue_job', () => ({ - enqueueJob: () => ({ - _id: 'id-of-this-test-report', - toApiJSON: () => JSON.stringify({ id: 'id-of-this-test-report' }), +jest.mock('../../lib/crypto', () => ({ + cryptoFactory: () => ({ + encrypt: () => `hello mock cypher text`, }), })); @@ -50,10 +52,25 @@ describe('Handle request to generate', () => { let mockResponseFactory: ReturnType; let requestHandler: RequestHandler; - const mockJobParams = {} as BaseParams; + const mockJobParams: JobParamsPDF = { + browserTimezone: 'UTC', + objectType: 'cool_object_type', + title: 'cool_title', + version: 'unknown', + layout: { id: 'preserve_layout' }, + relativeUrls: [], + }; beforeEach(async () => { reportingCore = await createMockReportingCore(createMockConfigSchema({})); + reportingCore.getStore = () => + Promise.resolve(({ + addReport: jest + .fn() + .mockImplementation( + (report) => new Report({ ...report, _index: '.reporting-foo-index-234' }) + ), + } as unknown) as ReportingStore); mockRequest = getMockRequest(); mockResponseFactory = getMockResponseFactory(); @@ -73,6 +90,64 @@ describe('Handle request to generate', () => { ); }); + describe('Enqueue Job', () => { + test('creates a report object to queue', async () => { + const report = await requestHandler.enqueueJob('printablePdf', mockJobParams); + + const { _id, created_at: _created_at, payload, ...snapObj } = report; + expect(snapObj).toMatchInlineSnapshot(` + Object { + "_index": ".reporting-foo-index-234", + "_primary_term": undefined, + "_seq_no": undefined, + "attempts": 0, + "browser_type": undefined, + "completed_at": undefined, + "created_by": "testymcgee", + "jobtype": "printable_pdf", + "kibana_id": undefined, + "kibana_name": undefined, + "max_attempts": undefined, + "meta": Object { + "isDeprecated": false, + "layout": "preserve_layout", + "objectType": "cool_object_type", + }, + "migration_version": "7.14.0", + "output": null, + "process_expiration": undefined, + "started_at": undefined, + "status": "pending", + "timeout": undefined, + } + `); + const { forceNow, ...snapPayload } = payload as TaskPayloadPDF; + expect(snapPayload).toMatchInlineSnapshot(` + Object { + "browserTimezone": "UTC", + "headers": "hello mock cypher text", + "isDeprecated": false, + "layout": Object { + "id": "preserve_layout", + }, + "objectType": "cool_object_type", + "objects": Array [], + "spaceId": undefined, + "title": "cool_title", + "version": "unknown", + } + `); + }); + + test('provides a default kibana version field for older POST URLs', async () => { + ((mockJobParams as unknown) as { version?: string }).version = undefined; + const report = await requestHandler.enqueueJob('printablePdf', mockJobParams); + + const { _id, created_at: _created_at, ...snapObj } = report; + expect(snapObj.payload.version).toBe('7.14.0'); + }); + }); + test('disallows invalid export type', async () => { expect(await requestHandler.handleGenerateRequest('neanderthals', mockJobParams)) .toMatchInlineSnapshot(` @@ -95,15 +170,45 @@ describe('Handle request to generate', () => { }); test('generates the download path', async () => { - expect(await requestHandler.handleGenerateRequest('csv', mockJobParams)).toMatchInlineSnapshot(` + const response = ((await requestHandler.handleGenerateRequest( + 'csv', + mockJobParams + )) as unknown) as { body: { job: ReportApiJSON } }; + const { id, created_at: _created_at, ...snapObj } = response.body.job; + expect(snapObj).toMatchInlineSnapshot(` Object { - "body": Object { - "job": "{\\"id\\":\\"id-of-this-test-report\\"}", - "path": "undefined/api/reporting/jobs/download/id-of-this-test-report", + "attempts": 0, + "browser_type": undefined, + "completed_at": undefined, + "created_by": "testymcgee", + "index": ".reporting-foo-index-234", + "jobtype": "csv", + "kibana_id": undefined, + "kibana_name": undefined, + "max_attempts": undefined, + "meta": Object { + "isDeprecated": true, + "layout": "preserve_layout", + "objectType": "cool_object_type", }, - "headers": Object { - "content-type": "application/json", + "migration_version": "7.14.0", + "output": Object {}, + "payload": Object { + "browserTimezone": "UTC", + "indexPatternSavedObject": undefined, + "isDeprecated": true, + "layout": Object { + "id": "preserve_layout", + }, + "objectType": "cool_object_type", + "relativeUrls": Array [], + "spaceId": undefined, + "title": "cool_title", + "version": "7.14.0", }, + "started_at": undefined, + "status": "pending", + "timeout": undefined, } `); }); diff --git a/x-pack/plugins/reporting/server/routes/lib/request_handler.ts b/x-pack/plugins/reporting/server/routes/lib/request_handler.ts index 8637000f41d95..a87f5c2913031 100644 --- a/x-pack/plugins/reporting/server/routes/lib/request_handler.ts +++ b/x-pack/plugins/reporting/server/routes/lib/request_handler.ts @@ -10,8 +10,8 @@ import { KibanaRequest, KibanaResponseFactory } from 'kibana/server'; import { ReportingCore } from '../..'; import { API_BASE_URL } from '../../../common/constants'; import { JobParamsPDFLegacy } from '../../export_types/printable_pdf/types'; -import { LevelLogger } from '../../lib'; -import { enqueueJob } from '../../lib/enqueue_job'; +import { checkParamsVersion, cryptoFactory, LevelLogger } from '../../lib'; +import { Report } from '../../lib/store'; import { BaseParams, ReportingRequestHandlerContext, ReportingUser } from '../../types'; export const handleUnavailable = (res: KibanaResponseFactory) => { @@ -33,6 +33,75 @@ export class RequestHandler { private logger: LevelLogger ) {} + private async encryptHeaders() { + const encryptionKey = this.reporting.getConfig().get('encryptionKey'); + const crypto = cryptoFactory(encryptionKey); + return await crypto.encrypt(this.req.headers); + } + + public async enqueueJob(exportTypeId: string, jobParams: BaseParams) { + const { reporting, logger, context, req: request, user } = this; + + const exportType = reporting.getExportTypesRegistry().getById(exportTypeId); + + if (exportType == null) { + throw new Error(`Export type ${exportTypeId} does not exist in the registry!`); + } + + if (!exportType.createJobFnFactory) { + throw new Error(`Export type ${exportTypeId} is not an async job type!`); + } + + const [createJob, store] = await Promise.all([ + exportType.createJobFnFactory(reporting, logger.clone([exportType.id])), + reporting.getStore(), + ]); + + if (!createJob) { + throw new Error(`Export type ${exportTypeId} is not an async job type!`); + } + + // 1. ensure the incoming params have a version field + jobParams.version = checkParamsVersion(jobParams, logger); + + // 2. encrypt request headers for the running report job to authenticate itself with Kibana + // 3. call the export type's createJobFn to create the job payload + const [headers, job] = await Promise.all([ + this.encryptHeaders(), + createJob(jobParams, context), + ]); + + const payload = { + ...job, + headers, + spaceId: reporting.getSpaceId(request, logger), + }; + + // 4. Add the report to ReportingStore to show as pending + const report = await store.addReport( + new Report({ + jobtype: exportType.jobType, + created_by: user ? user.username : false, + payload, + meta: { + // telemetry fields + objectType: jobParams.objectType, + layout: jobParams.layout?.id, + isDeprecated: job.isDeprecated, + }, + }) + ); + logger.debug(`Successfully stored pending job: ${report._index}/${report._id}`); + + // 5. Schedule the report with Task Manager + const task = await reporting.scheduleTask(report.toReportTaskJSON()); + logger.info( + `Scheduled ${exportType.name} reporting task. Task ID: task:${task.id}. Report ID: ${report._id}` + ); + + return report; + } + public async handleGenerateRequest( exportTypeId: string, jobParams: BaseParams | JobParamsPDFLegacy @@ -54,15 +123,7 @@ export class RequestHandler { } try { - const report = await enqueueJob( - this.reporting, - this.req, - this.context, - this.user, - exportTypeId, - jobParams, - this.logger - ); + const report = await this.enqueueJob(exportTypeId, jobParams); // return task manager's task information and the download URL const downloadBaseUrl = getDownloadBaseUrl(this.reporting); diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index 406beb2a56b66..84fa6fb5b10d6 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { IRouter, KibanaRequest, RequestHandlerContext } from 'src/core/server'; +import type { IRouter, RequestHandlerContext } from 'src/core/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { DataPluginStart } from 'src/plugins/data/server/plugin'; import { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/server'; @@ -62,9 +62,8 @@ export { BaseParams, BasePayload }; // default fn type for CreateJobFnFactory export type CreateJobFn = ( jobParams: JobParamsType, - context: ReportingRequestHandlerContext, - request: KibanaRequest -) => Promise; + context: ReportingRequestHandlerContext +) => Promise>; // default fn type for RunTaskFnFactory export type RunTaskFn = ( From a3792d6ab71616a0c753bb8733e2e2a046f8d420 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Wed, 8 Sep 2021 18:51:25 -0700 Subject: [PATCH 033/139] [Reporting] Remove `any`, improve telemetry schema for completeness (#111212) (#111665) * [Reporting] Remove `any`, improve telemetry schema for completeness * remove another any * rename file per exported function * test variable name * use variable for DRY * update reporting telemetry contract * added csv_searchsource_immediate to telemetry * fix types * update jest snapshots * remove tests on large literal objects Co-authored-by: Jean-Louis Leysens Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Jean-Louis Leysens Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../generate_csv/generate_csv.ts | 2 +- .../reporting_usage_collector.test.ts.snap | 1285 +++++++++++++++++ .../server/usage/decorate_range_stats.ts | 97 -- ...stats.test.ts => get_export_stats.test.ts} | 83 +- .../server/usage/get_export_stats.ts | 90 ++ .../server/usage/get_export_type_handler.ts | 9 + .../server/usage/get_reporting_usage.ts | 15 +- .../usage/reporting_usage_collector.test.ts | 157 +- .../plugins/reporting/server/usage/schema.ts | 30 +- .../plugins/reporting/server/usage/types.ts | 32 +- .../schema/xpack_plugins.json | 742 +++++++++- 11 files changed, 2212 insertions(+), 330 deletions(-) delete mode 100644 x-pack/plugins/reporting/server/usage/decorate_range_stats.ts rename x-pack/plugins/reporting/server/usage/{decorate_range_stats.test.ts => get_export_stats.test.ts} (64%) create mode 100644 x-pack/plugins/reporting/server/usage/get_export_stats.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts index 7f01f9fcb297c..61dc9881f5bcc 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts @@ -184,7 +184,7 @@ export class CsvGenerator { data: dataTableCell, }: { column: string; - data: any; + data: unknown; }): string => { let cell: string[] | string | object; // check truthiness to guard against _score, _type, etc diff --git a/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap b/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap index 4fab4ca72abab..12debe5c85d5e 100644 --- a/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap +++ b/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap @@ -6,23 +6,73 @@ Object { "isReady": [Function], "schema": Object { "PNG": Object { + "app": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "search": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "available": Object { "type": "boolean", }, "deprecated": Object { "type": "long", }, + "layout": Object { + "canvas": Object { + "type": "long", + }, + "preserve_layout": Object { + "type": "long", + }, + "print": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, }, "PNGV2": Object { + "app": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "search": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "available": Object { "type": "boolean", }, "deprecated": Object { "type": "long", }, + "layout": Object { + "canvas": Object { + "type": "long", + }, + "preserve_layout": Object { + "type": "long", + }, + "print": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -37,23 +87,109 @@ Object { "type": "keyword", }, "csv": Object { + "app": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "search": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "available": Object { "type": "boolean", }, "deprecated": Object { "type": "long", }, + "layout": Object { + "canvas": Object { + "type": "long", + }, + "preserve_layout": Object { + "type": "long", + }, + "print": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, }, "csv_searchsource": Object { + "app": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "search": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "available": Object { "type": "boolean", }, "deprecated": Object { "type": "long", }, + "layout": Object { + "canvas": Object { + "type": "long", + }, + "preserve_layout": Object { + "type": "long", + }, + "print": Object { + "type": "long", + }, + }, + "total": Object { + "type": "long", + }, + }, + "csv_searchsource_immediate": Object { + "app": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "search": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, + "available": Object { + "type": "boolean", + }, + "deprecated": Object { + "type": "long", + }, + "layout": Object { + "canvas": Object { + "type": "long", + }, + "preserve_layout": Object { + "type": "long", + }, + "print": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -63,23 +199,73 @@ Object { }, "last7Days": Object { "PNG": Object { + "app": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "search": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "available": Object { "type": "boolean", }, "deprecated": Object { "type": "long", }, + "layout": Object { + "canvas": Object { + "type": "long", + }, + "preserve_layout": Object { + "type": "long", + }, + "print": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, }, "PNGV2": Object { + "app": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "search": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "available": Object { "type": "boolean", }, "deprecated": Object { "type": "long", }, + "layout": Object { + "canvas": Object { + "type": "long", + }, + "preserve_layout": Object { + "type": "long", + }, + "print": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -88,23 +274,109 @@ Object { "type": "long", }, "csv": Object { + "app": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "search": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "available": Object { "type": "boolean", }, "deprecated": Object { "type": "long", }, + "layout": Object { + "canvas": Object { + "type": "long", + }, + "preserve_layout": Object { + "type": "long", + }, + "print": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, }, "csv_searchsource": Object { + "app": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "search": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, + "available": Object { + "type": "boolean", + }, + "deprecated": Object { + "type": "long", + }, + "layout": Object { + "canvas": Object { + "type": "long", + }, + "preserve_layout": Object { + "type": "long", + }, + "print": Object { + "type": "long", + }, + }, + "total": Object { + "type": "long", + }, + }, + "csv_searchsource_immediate": Object { + "app": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "search": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "available": Object { "type": "boolean", }, "deprecated": Object { "type": "long", }, + "layout": Object { + "canvas": Object { + "type": "long", + }, + "preserve_layout": Object { + "type": "long", + }, + "print": Object { + "type": "long", + }, + }, "total": Object { "type": "long", }, @@ -117,6 +389,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -128,6 +403,9 @@ Object { "type": "long", }, "layout": Object { + "canvas": Object { + "type": "long", + }, "preserve_layout": Object { "type": "long", }, @@ -147,6 +425,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -158,6 +439,9 @@ Object { "type": "long", }, "layout": Object { + "canvas": Object { + "type": "long", + }, "preserve_layout": Object { "type": "long", }, @@ -195,6 +479,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -206,6 +493,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -217,6 +507,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -228,6 +521,23 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, + "csv_searchsource_immediate": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -239,6 +549,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -250,6 +563,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -263,6 +579,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -274,6 +593,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -285,6 +607,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -296,6 +621,23 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, + "csv_searchsource_immediate": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -307,6 +649,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -318,6 +663,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -331,6 +679,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -342,6 +693,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -353,6 +707,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -364,6 +721,23 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, + "csv_searchsource_immediate": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -375,6 +749,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -386,6 +763,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -399,6 +779,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -410,6 +793,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -421,6 +807,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -432,6 +821,23 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, + "csv_searchsource_immediate": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -443,6 +849,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -454,6 +863,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -467,6 +879,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -478,6 +893,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -489,6 +907,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -500,6 +921,23 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, + "csv_searchsource_immediate": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -511,6 +949,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -522,6 +963,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -537,6 +981,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -548,6 +995,9 @@ Object { "type": "long", }, "layout": Object { + "canvas": Object { + "type": "long", + }, "preserve_layout": Object { "type": "long", }, @@ -567,6 +1017,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -578,6 +1031,9 @@ Object { "type": "long", }, "layout": Object { + "canvas": Object { + "type": "long", + }, "preserve_layout": Object { "type": "long", }, @@ -615,6 +1071,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -626,6 +1085,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -637,6 +1099,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -648,6 +1113,23 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, + "csv_searchsource_immediate": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -659,6 +1141,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -670,6 +1155,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -683,6 +1171,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -694,6 +1185,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -705,6 +1199,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -716,6 +1213,23 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, + "csv_searchsource_immediate": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -727,6 +1241,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -738,6 +1255,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -751,6 +1271,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -762,6 +1285,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -773,6 +1299,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -784,6 +1313,23 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, + "csv_searchsource_immediate": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -795,6 +1341,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -806,6 +1355,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -819,6 +1371,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -830,6 +1385,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -841,6 +1399,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -852,6 +1413,23 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, + "csv_searchsource_immediate": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -863,6 +1441,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -874,6 +1455,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -887,6 +1471,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -898,6 +1485,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -909,6 +1499,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -920,6 +1513,23 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, + "csv_searchsource_immediate": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -931,6 +1541,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -942,6 +1555,9 @@ Object { "dashboard": Object { "type": "long", }, + "search": Object { + "type": "long", + }, "visualization": Object { "type": "long", }, @@ -956,49 +1572,198 @@ Object { exports[`data modeling usage data with meta.isDeprecated jobTypes 1`] = ` Object { "PNG": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, + "available": true, + "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "PNGV2": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 0, }, "_all": 9, "available": true, "browser_type": undefined, "csv": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 4, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 4, }, "csv_searchsource": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 5, }, + "csv_searchsource_immediate": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, + "available": true, + "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, "enabled": true, "last7Days": Object { "PNG": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, + "available": true, + "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "PNGV2": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 0, }, "_all": 9, "csv": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 4, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 4, }, "csv_searchsource": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 5, }, + "csv_searchsource_immediate": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, + "available": true, + "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, "printable_pdf": Object { "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, + "available": true, + "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "printable_pdf_v2": Object { + "app": Object { + "canvas workpad": 0, "dashboard": 0, + "search": 0, "visualization": 0, }, "available": true, "deprecated": 0, "layout": Object { + "canvas": 0, "preserve_layout": 0, "print": 0, }, @@ -1021,12 +1786,31 @@ Object { }, "printable_pdf": Object { "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, + "available": true, + "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "printable_pdf_v2": Object { + "app": Object { + "canvas workpad": 0, "dashboard": 0, + "search": 0, "visualization": 0, }, "available": true, "deprecated": 0, "layout": Object { + "canvas": 0, "preserve_layout": 0, "print": 0, }, @@ -1052,49 +1836,198 @@ Object { exports[`data modeling with empty data 1`] = ` Object { "PNG": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, + "available": true, + "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "PNGV2": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 0, }, "_all": 0, "available": true, "browser_type": undefined, "csv": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 0, }, "csv_searchsource": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, + "available": true, + "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "csv_searchsource_immediate": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 0, }, "enabled": true, "last7Days": Object { "PNG": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, + "available": true, + "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "PNGV2": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 0, }, "_all": 0, "csv": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 0, }, "csv_searchsource": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, + "available": true, + "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "csv_searchsource_immediate": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 0, }, "printable_pdf": Object { "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, + "available": true, + "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "printable_pdf_v2": Object { + "app": Object { + "canvas workpad": 0, "dashboard": 0, + "search": 0, "visualization": 0, }, "available": true, "deprecated": 0, "layout": Object { + "canvas": 0, "preserve_layout": 0, "print": 0, }, @@ -1108,12 +2041,31 @@ Object { }, "printable_pdf": Object { "app": Object { + "canvas workpad": 0, "dashboard": 0, + "search": 0, "visualization": 0, }, "available": true, "deprecated": 0, "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "printable_pdf_v2": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, + "available": true, + "deprecated": 0, + "layout": Object { + "canvas": 0, "preserve_layout": 0, "print": 0, }, @@ -1130,49 +2082,198 @@ Object { exports[`data modeling with normal looking usage data 1`] = ` Object { "PNG": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 3, }, + "PNGV2": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, + "available": true, + "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, "_all": 12, "available": true, "browser_type": undefined, "csv": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 0, }, "csv_searchsource": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, + "available": true, + "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "csv_searchsource_immediate": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 0, }, "enabled": true, "last7Days": Object { "PNG": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 1, }, + "PNGV2": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, + "available": true, + "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, "_all": 1, "csv": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 0, }, "csv_searchsource": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, + "available": true, + "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "csv_searchsource_immediate": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 0, }, "printable_pdf": Object { "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, + "available": true, + "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "printable_pdf_v2": Object { + "app": Object { + "canvas workpad": 0, "dashboard": 0, + "search": 0, "visualization": 0, }, "available": true, "deprecated": 0, "layout": Object { + "canvas": 0, "preserve_layout": 0, "print": 0, }, @@ -1195,16 +2296,34 @@ Object { "app": Object { "canvas workpad": 6, "dashboard": 0, + "search": 0, "visualization": 3, }, "available": true, "deprecated": 0, "layout": Object { + "canvas": 0, "preserve_layout": 9, "print": 0, }, "total": 9, }, + "printable_pdf_v2": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, + "available": true, + "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, "status": Object { "completed": 10, "completed_with_warnings": 1, @@ -1237,55 +2356,203 @@ Object { exports[`data modeling with sparse data 1`] = ` Object { "PNG": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 1, }, + "PNGV2": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, + "available": true, + "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, "_all": 4, "available": true, "browser_type": undefined, "csv": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 1, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 1, }, "csv_searchsource": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, + "available": true, + "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "csv_searchsource_immediate": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 0, }, "enabled": true, "last7Days": Object { "PNG": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 1, }, + "PNGV2": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, + "available": true, + "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, "_all": 4, "csv": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 1, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 1, }, "csv_searchsource": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, + "available": true, + "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "csv_searchsource_immediate": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 0, }, "printable_pdf": Object { "app": Object { "canvas workpad": 1, "dashboard": 1, + "search": 0, "visualization": 0, }, "available": true, "deprecated": 0, "layout": Object { + "canvas": 0, "preserve_layout": 2, "print": 0, }, "total": 2, }, + "printable_pdf_v2": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, + "available": true, + "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, "status": Object { "completed": 4, "failed": 0, @@ -1307,16 +2574,34 @@ Object { "app": Object { "canvas workpad": 1, "dashboard": 1, + "search": 0, "visualization": 0, }, "available": true, "deprecated": 0, "layout": Object { + "canvas": 0, "preserve_layout": 2, "print": 0, }, "total": 2, }, + "printable_pdf_v2": Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, + "available": true, + "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, "status": Object { "completed": 4, "failed": 0, diff --git a/x-pack/plugins/reporting/server/usage/decorate_range_stats.ts b/x-pack/plugins/reporting/server/usage/decorate_range_stats.ts deleted file mode 100644 index 99d4b7d934579..0000000000000 --- a/x-pack/plugins/reporting/server/usage/decorate_range_stats.ts +++ /dev/null @@ -1,97 +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 { uniq } from 'lodash'; -import { - CSV_JOB_TYPE, - CSV_JOB_TYPE_DEPRECATED, - DEPRECATED_JOB_TYPES, - PDF_JOB_TYPE, - PNG_JOB_TYPE, -} from '../../common/constants'; -import { AvailableTotal, ExportType, FeatureAvailabilityMap, RangeStats } from './types'; - -const jobTypeIsDeprecated = (jobType: string) => DEPRECATED_JOB_TYPES.includes(jobType); - -function getForFeature( - range: Partial, - typeKey: ExportType, - featureAvailability: FeatureAvailabilityMap, - additional?: any -): AvailableTotal & typeof additional { - const isAvailable = (feature: ExportType) => !!featureAvailability[feature]; - const jobType = range[typeKey] || { total: 0, ...additional, deprecated: 0 }; - - // merge the additional stats for the jobType - type AdditionalType = { [K in keyof typeof additional]: K }; - const filledAdditional: AdditionalType = {}; - if (additional) { - Object.keys(additional).forEach((k) => { - filledAdditional[k] = { ...additional[k], ...jobType[k] }; - }); - } - - // if the type itself is deprecated, all jobs are deprecated, otherwise only some of them might be - const deprecated = jobTypeIsDeprecated(typeKey) ? jobType.total : jobType.deprecated || 0; - - return { - available: isAvailable(typeKey), - total: jobType.total, - deprecated, - ...filledAdditional, - }; -} - -/* - * Decorates range stats (stats for last day, last 7 days, etc) with feature - * availability booleans, and zero-filling for unused features - * - * This function builds the result object for all export types found in the - * Reporting data, even if the type is unknown to this Kibana instance. - */ -export const decorateRangeStats = ( - rangeStats: Partial = {}, - featureAvailability: FeatureAvailabilityMap -): RangeStats => { - const { - _all: rangeAll, - status: rangeStatus, - statuses: rangeStatusByApp, - [PDF_JOB_TYPE]: rangeStatsPdf, - ...rangeStatsBasic - } = rangeStats; - - // combine the known types with any unknown type found in reporting data - const keysBasic = uniq([ - CSV_JOB_TYPE, - CSV_JOB_TYPE_DEPRECATED, - PNG_JOB_TYPE, - ...Object.keys(rangeStatsBasic), - ]) as ExportType[]; - const rangeBasic = keysBasic.reduce((accum, currentKey) => { - return { - ...accum, - [currentKey]: getForFeature(rangeStatsBasic, currentKey, featureAvailability), - }; - }, {}) as Partial; - const rangePdf = { - [PDF_JOB_TYPE]: getForFeature(rangeStats, PDF_JOB_TYPE, featureAvailability, { - app: { dashboard: 0, visualization: 0 }, - layout: { preserve_layout: 0, print: 0 }, - }), - }; - - const resultStats = { - _all: rangeAll || 0, - status: { completed: 0, failed: 0, ...rangeStatus }, - statuses: rangeStatusByApp, - ...rangePdf, - ...rangeBasic, - } as RangeStats; - - return resultStats; -}; diff --git a/x-pack/plugins/reporting/server/usage/decorate_range_stats.test.ts b/x-pack/plugins/reporting/server/usage/get_export_stats.test.ts similarity index 64% rename from x-pack/plugins/reporting/server/usage/decorate_range_stats.test.ts rename to x-pack/plugins/reporting/server/usage/get_export_stats.test.ts index ca1677c2379fc..782f2e910038e 100644 --- a/x-pack/plugins/reporting/server/usage/decorate_range_stats.test.ts +++ b/x-pack/plugins/reporting/server/usage/get_export_stats.test.ts @@ -5,7 +5,9 @@ * 2.0. */ -import { decorateRangeStats } from './decorate_range_stats'; +import { getExportTypesRegistry } from '../lib'; +import { getExportStats } from './get_export_stats'; +import { getExportTypesHandler } from './get_export_type_handler'; import { FeatureAvailabilityMap } from './types'; let featureMap: FeatureAvailabilityMap; @@ -14,8 +16,10 @@ beforeEach(() => { featureMap = { PNG: true, csv: true, csv_searchsource: true, printable_pdf: true }; }); +const exportTypesHandler = getExportTypesHandler(getExportTypesRegistry()); + test('Model of job status and status-by-pdf-app', () => { - const result = decorateRangeStats( + const result = getExportStats( { status: { completed: 0, processing: 1, pending: 2, failed: 3 }, statuses: { @@ -24,7 +28,8 @@ test('Model of job status and status-by-pdf-app', () => { failed: { printable_pdf: { visualization: 2, dashboard: 2, 'canvas workpad': 1 } }, }, }, - featureMap + featureMap, + exportTypesHandler ); expect(result.status).toMatchInlineSnapshot(` @@ -60,7 +65,7 @@ test('Model of job status and status-by-pdf-app', () => { }); test('Model of jobTypes', () => { - const result = decorateRangeStats( + const result = getExportStats( { PNG: { available: true, total: 3 }, printable_pdf: { @@ -71,27 +76,61 @@ test('Model of jobTypes', () => { }, csv_searchsource: { available: true, total: 3 }, }, - featureMap + featureMap, + exportTypesHandler ); expect(result.PNG).toMatchInlineSnapshot(` Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 3, } `); expect(result.csv).toMatchInlineSnapshot(` Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 0, } `); expect(result.csv_searchsource).toMatchInlineSnapshot(` Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 0, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 3, } `); @@ -100,11 +139,13 @@ test('Model of jobTypes', () => { "app": Object { "canvas workpad": 3, "dashboard": 0, + "search": 0, "visualization": 0, }, "available": true, "deprecated": 0, "layout": Object { + "canvas": 0, "preserve_layout": 3, "print": 0, }, @@ -114,28 +155,52 @@ test('Model of jobTypes', () => { }); test('PNG counts, provided count of deprecated jobs explicitly', () => { - const result = decorateRangeStats( + const result = getExportStats( { PNG: { available: true, total: 15, deprecated: 5 } }, - featureMap + featureMap, + exportTypesHandler ); expect(result.PNG).toMatchInlineSnapshot(` Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 5, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 15, } `); }); test('CSV counts, provides all jobs implicitly deprecated due to jobtype', () => { - const result = decorateRangeStats( + const result = getExportStats( { csv: { available: true, total: 15, deprecated: 0 } }, - featureMap + featureMap, + exportTypesHandler ); expect(result.csv).toMatchInlineSnapshot(` Object { + "app": Object { + "canvas workpad": 0, + "dashboard": 0, + "search": 0, + "visualization": 0, + }, "available": true, "deprecated": 15, + "layout": Object { + "canvas": 0, + "preserve_layout": 0, + "print": 0, + }, "total": 15, } `); diff --git a/x-pack/plugins/reporting/server/usage/get_export_stats.ts b/x-pack/plugins/reporting/server/usage/get_export_stats.ts new file mode 100644 index 0000000000000..ffdb6cdc290d7 --- /dev/null +++ b/x-pack/plugins/reporting/server/usage/get_export_stats.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DEPRECATED_JOB_TYPES } from '../../common/constants'; +import { ExportTypesHandler } from './get_export_type_handler'; +import { AvailableTotal, FeatureAvailabilityMap, RangeStats } from './types'; + +const jobTypeIsDeprecated = (jobType: string) => DEPRECATED_JOB_TYPES.includes(jobType); +const defaultTotalsForFeature: Omit = { + total: 0, + deprecated: 0, + app: { 'canvas workpad': 0, search: 0, visualization: 0, dashboard: 0 }, + layout: { canvas: 0, print: 0, preserve_layout: 0 }, +}; + +const isAvailable = (featureAvailability: FeatureAvailabilityMap, feature: string) => + !!featureAvailability[feature]; + +function getAvailableTotalForFeature( + jobType: AvailableTotal, + typeKey: string, + featureAvailability: FeatureAvailabilityMap +): AvailableTotal { + // if the type itself is deprecated, all jobs are deprecated, otherwise only some of them might be + const deprecated = jobTypeIsDeprecated(typeKey) ? jobType.total : jobType.deprecated || 0; + + // merge the additional stats for the jobType + const availableTotal = { + available: isAvailable(featureAvailability, typeKey), + total: jobType.total, + deprecated, + app: { ...defaultTotalsForFeature.app, ...jobType.app }, + layout: { ...defaultTotalsForFeature.layout, ...jobType.layout }, + }; + + return availableTotal as AvailableTotal; +} + +/* + * Decorates range stats (stats for last day, last 7 days, etc) with feature + * availability booleans, and zero-filling for unused features + * + * This function builds the result object for all export types found in the + * Reporting data, even if the type is unknown to this Kibana instance. + */ +export const getExportStats = ( + rangeStatsInput: Partial = {}, + featureAvailability: FeatureAvailabilityMap, + exportTypesHandler: ExportTypesHandler +) => { + const { + _all: rangeAll, + status: rangeStatus, + statuses: rangeStatusByApp, + ...rangeStats + } = rangeStatsInput; + + // combine the known types with any unknown type found in reporting data + const statsForExportType = exportTypesHandler.getJobTypes().reduce((accum, exportType) => { + const availableTotal = rangeStats[exportType as keyof typeof rangeStats]; + + if (!availableTotal) { + return { + ...accum, + [exportType]: { + available: isAvailable(featureAvailability, exportType), + ...defaultTotalsForFeature, + }, + }; + } + + return { + ...accum, + [exportType]: getAvailableTotalForFeature(availableTotal, exportType, featureAvailability), + }; + }, {}); + + const resultStats = { + ...statsForExportType, + _all: rangeAll || 0, + status: { completed: 0, failed: 0, ...rangeStatus }, + statuses: rangeStatusByApp, + } as RangeStats; + + return resultStats; +}; diff --git a/x-pack/plugins/reporting/server/usage/get_export_type_handler.ts b/x-pack/plugins/reporting/server/usage/get_export_type_handler.ts index b741f9dcfbe6b..d44790ccdeede 100644 --- a/x-pack/plugins/reporting/server/usage/get_export_type_handler.ts +++ b/x-pack/plugins/reporting/server/usage/get_export_type_handler.ts @@ -15,6 +15,13 @@ import { FeaturesAvailability } from './'; */ export function getExportTypesHandler(exportTypesRegistry: ExportTypesRegistry) { return { + /* + * Allow usage collection to loop through each registered job type + */ + getJobTypes() { + return exportTypesRegistry.getAll().map(({ jobType }) => jobType); + }, + /* * Based on the X-Pack license and which export types are available, * returns an object where the keys are the export types and the values are @@ -46,3 +53,5 @@ export function getExportTypesHandler(exportTypesRegistry: ExportTypesRegistry) }, }; } + +export type ExportTypesHandler = ReturnType; diff --git a/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts b/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts index e5801b30caff6..8a1b2532cbcaa 100644 --- a/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts +++ b/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts @@ -10,7 +10,7 @@ import { get } from 'lodash'; import type { ReportingConfig } from '../'; import type { ExportTypesRegistry } from '../lib/export_types_registry'; import type { GetLicense } from './'; -import { decorateRangeStats } from './decorate_range_stats'; +import { getExportStats } from './get_export_stats'; import { getExportTypesHandler } from './get_export_type_handler'; import type { AggregationResultBuckets, @@ -108,11 +108,16 @@ type RangeStatSets = Partial & { type ESResponse = Partial; async function handleResponse(response: ESResponse): Promise> { - const buckets = get(response, 'aggregations.ranges.buckets'); + const buckets = get(response, 'aggregations.ranges.buckets') as Record< + 'all' | 'last7Days', + AggregationResultBuckets + >; + if (!buckets) { return {}; } - const { last7Days, all } = buckets as any; + + const { all, last7Days } = buckets; const last7DaysUsage = last7Days ? getAggStats(last7Days) : {}; const allUsage = all ? getAggStats(all) : {}; @@ -196,8 +201,8 @@ export async function getReportingUsage( available: true, browser_type: browserType, enabled: true, - last7Days: decorateRangeStats(last7Days, availability), - ...decorateRangeStats(all, availability), + last7Days: getExportStats(last7Days, availability, exportTypesHandler), + ...getExportStats(all, availability, exportTypesHandler), }; } ); diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts index 31ce6581d7de6..92e85d3532c15 100644 --- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts +++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts @@ -18,7 +18,7 @@ import { getReportingUsageCollector, registerReportingUsageCollector, } from './reporting_usage_collector'; -import { ReportingUsageType, SearchResponse } from './types'; +import { SearchResponse } from './types'; const exportTypesRegistry = getExportTypesRegistry(); @@ -478,161 +478,6 @@ describe('data modeling', () => { const usageStats = await collector.fetch(collectorFetchContext); expect(usageStats).toMatchSnapshot(); }); - - test('Cast various example data to the TypeScript definition', () => { - const check = (obj: ReportingUsageType) => { - return typeof obj; - }; - - // just check that the example objects can be cast to ReportingUsageType - check({ - PNG: { available: true, total: 7 }, - PNGV2: { available: true, total: 7 }, - _all: 21, - available: true, - browser_type: 'chromium', - csv: { available: true, total: 4 }, - csv_searchsource: { available: true, total: 4 }, - enabled: true, - last7Days: { - PNG: { available: true, total: 0 }, - PNGV2: { available: true, total: 0 }, - _all: 0, - csv: { available: true, total: 0 }, - csv_searchsource: { available: true, total: 0 }, - printable_pdf: { - app: { dashboard: 0, visualization: 0 }, - available: true, - layout: { preserve_layout: 0, print: 0 }, - total: 0, - }, - printable_pdf_v2: { - app: { dashboard: 0, visualization: 0 }, - available: true, - layout: { preserve_layout: 0, print: 0 }, - total: 0, - }, - status: { completed: 0, failed: 0 }, - statuses: {}, - }, - printable_pdf: { - app: { 'canvas workpad': 3, dashboard: 3, visualization: 4 }, - available: true, - layout: { preserve_layout: 7, print: 3 }, - total: 10, - }, - printable_pdf_v2: { - app: { 'canvas workpad': 3, dashboard: 3, visualization: 4 }, - available: true, - layout: { preserve_layout: 7, print: 3 }, - total: 10, - }, - status: { completed: 21, failed: 0 }, - statuses: { - completed: { - PNG: { dashboard: 3, visualization: 4 }, - PNGV2: { dashboard: 3, visualization: 4 }, - csv: {}, - printable_pdf: { 'canvas workpad': 3, dashboard: 3, visualization: 4 }, - printable_pdf_v2: { 'canvas workpad': 3, dashboard: 3, visualization: 4 }, - }, - }, - }); - check({ - PNG: { available: true, total: 3 }, - PNGV2: { available: true, total: 3 }, - _all: 4, - available: true, - browser_type: 'chromium', - csv: { available: true, total: 0 }, - csv_searchsource: { available: true, total: 0 }, - enabled: true, - last7Days: { - PNG: { available: true, total: 3 }, - PNGV2: { available: true, total: 3 }, - _all: 4, - csv: { available: true, total: 0 }, - csv_searchsource: { available: true, total: 0 }, - printable_pdf: { - app: { 'canvas workpad': 1, dashboard: 0, visualization: 0 }, - available: true, - layout: { preserve_layout: 1, print: 0 }, - total: 1, - }, - printable_pdf_v2: { - app: { 'canvas workpad': 1, dashboard: 0, visualization: 0 }, - available: true, - layout: { preserve_layout: 1, print: 0 }, - total: 1, - }, - status: { completed: 4, failed: 0 }, - statuses: { - completed: { PNG: { visualization: 3 }, printable_pdf: { 'canvas workpad': 1 } }, - }, - }, - printable_pdf: { - app: { 'canvas workpad': 1, dashboard: 0, visualization: 0 }, - available: true, - layout: { preserve_layout: 1, print: 0 }, - total: 1, - }, - printable_pdf_v2: { - app: { 'canvas workpad': 1, dashboard: 0, visualization: 0 }, - available: true, - layout: { preserve_layout: 1, print: 0 }, - total: 1, - }, - status: { completed: 4, failed: 0 }, - statuses: { - completed: { PNG: { visualization: 3 }, printable_pdf: { 'canvas workpad': 1 } }, - }, - }); - check({ - available: true, - browser_type: 'chromium', - enabled: true, - last7Days: { - _all: 0, - status: { completed: 0, failed: 0 }, - statuses: {}, - printable_pdf: { - available: true, - total: 0, - app: { dashboard: 0, visualization: 0 }, - layout: { preserve_layout: 0, print: 0 }, - }, - printable_pdf_v2: { - available: true, - total: 0, - app: { dashboard: 0, visualization: 0 }, - layout: { preserve_layout: 0, print: 0 }, - }, - csv: { available: true, total: 0 }, - csv_searchsource: { available: true, total: 0 }, - PNG: { available: true, total: 0 }, - PNGV2: { available: true, total: 0 }, - }, - _all: 0, - status: { completed: 0, failed: 0 }, - statuses: {}, - printable_pdf: { - available: true, - total: 0, - app: { dashboard: 0, visualization: 0 }, - layout: { preserve_layout: 0, print: 0 }, - }, - printable_pdf_v2: { - available: true, - total: 0, - app: { dashboard: 0, visualization: 0 }, - layout: { preserve_layout: 0, print: 0 }, - }, - csv: { available: true, total: 0 }, - csv_searchsource: { available: true, total: 0 }, - PNG: { available: true, total: 0 }, - PNGV2: { available: true, total: 0 }, - }); - }); }); describe('Ready for collection observable', () => { diff --git a/x-pack/plugins/reporting/server/usage/schema.ts b/x-pack/plugins/reporting/server/usage/schema.ts index 54545dd23509b..02bf65e7c5e4d 100644 --- a/x-pack/plugins/reporting/server/usage/schema.ts +++ b/x-pack/plugins/reporting/server/usage/schema.ts @@ -11,19 +11,28 @@ import { AvailableTotal, ByAppCounts, JobTypes, + LayoutCounts, RangeStats, ReportingUsageType, } from './types'; const appCountsSchema: MakeSchemaFrom = { + search: { type: 'long' }, 'canvas workpad': { type: 'long' }, dashboard: { type: 'long' }, visualization: { type: 'long' }, }; +const layoutCountsSchema: MakeSchemaFrom = { + canvas: { type: 'long' }, + print: { type: 'long' }, + preserve_layout: { type: 'long' }, +}; + const byAppCountsSchema: MakeSchemaFrom = { csv: appCountsSchema, csv_searchsource: appCountsSchema, + csv_searchsource_immediate: appCountsSchema, PNG: appCountsSchema, PNGV2: appCountsSchema, printable_pdf: appCountsSchema, @@ -34,29 +43,18 @@ const availableTotalSchema: MakeSchemaFrom = { available: { type: 'boolean' }, total: { type: 'long' }, deprecated: { type: 'long' }, + app: appCountsSchema, + layout: layoutCountsSchema, }; const jobTypesSchema: MakeSchemaFrom = { csv: availableTotalSchema, csv_searchsource: availableTotalSchema, + csv_searchsource_immediate: availableTotalSchema, PNG: availableTotalSchema, PNGV2: availableTotalSchema, - printable_pdf: { - ...availableTotalSchema, - app: appCountsSchema, - layout: { - print: { type: 'long' }, - preserve_layout: { type: 'long' }, - }, - }, - printable_pdf_v2: { - ...availableTotalSchema, - app: appCountsSchema, - layout: { - print: { type: 'long' }, - preserve_layout: { type: 'long' }, - }, - }, + printable_pdf: availableTotalSchema, + printable_pdf_v2: availableTotalSchema, }; const rangeStatsSchema: MakeSchemaFrom = { diff --git a/x-pack/plugins/reporting/server/usage/types.ts b/x-pack/plugins/reporting/server/usage/types.ts index 389dc27c46c66..7bd79de090b37 100644 --- a/x-pack/plugins/reporting/server/usage/types.ts +++ b/x-pack/plugins/reporting/server/usage/types.ts @@ -61,37 +61,40 @@ export interface AvailableTotal { available: boolean; total: number; deprecated?: number; + app?: { + search?: number; + dashboard?: number; + visualization?: number; + 'canvas workpad'?: number; + }; + layout?: { + print?: number; + preserve_layout?: number; + canvas?: number; + }; } +// FIXME: find a way to get this from exportTypesHandler or common/constants type BaseJobTypes = | 'csv' | 'csv_searchsource' + | 'csv_searchsource_immediate' | 'PNG' | 'PNGV2' | 'printable_pdf' | 'printable_pdf_v2'; export interface LayoutCounts { + canvas: number; print: number; preserve_layout: number; } -type AppNames = 'canvas workpad' | 'dashboard' | 'visualization'; export type AppCounts = { - [A in AppNames]?: number; + [A in 'canvas workpad' | 'dashboard' | 'visualization' | 'search']?: number; }; -export type JobTypes = { [K in BaseJobTypes]: AvailableTotal } & { - printable_pdf: AvailableTotal & { - app: AppCounts; - layout: LayoutCounts; - }; -} & { - printable_pdf_v2: AvailableTotal & { - app: AppCounts; - layout: LayoutCounts; - }; -}; +export type JobTypes = { [K in BaseJobTypes]: AvailableTotal }; export type ByAppCounts = { [J in BaseJobTypes]?: AppCounts }; @@ -117,8 +120,7 @@ export type ReportingUsageType = RangeStats & { last7Days: RangeStats; }; -export type ExportType = 'csv' | 'csv_searchsource' | 'printable_pdf' | 'PNG'; -export type FeatureAvailabilityMap = { [F in ExportType]: boolean }; +export type FeatureAvailabilityMap = Record; export interface ReportingUsageSearchResponse { aggregations: { diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index eac3434c98c7a..220243870eea7 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -4085,6 +4085,35 @@ }, "deprecated": { "type": "long" + }, + "app": { + "properties": { + "search": { + "type": "long" + }, + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, + "layout": { + "properties": { + "canvas": { + "type": "long" + }, + "print": { + "type": "long" + }, + "preserve_layout": { + "type": "long" + } + } } } }, @@ -4098,6 +4127,77 @@ }, "deprecated": { "type": "long" + }, + "app": { + "properties": { + "search": { + "type": "long" + }, + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, + "layout": { + "properties": { + "canvas": { + "type": "long" + }, + "print": { + "type": "long" + }, + "preserve_layout": { + "type": "long" + } + } + } + } + }, + "csv_searchsource_immediate": { + "properties": { + "available": { + "type": "boolean" + }, + "total": { + "type": "long" + }, + "deprecated": { + "type": "long" + }, + "app": { + "properties": { + "search": { + "type": "long" + }, + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, + "layout": { + "properties": { + "canvas": { + "type": "long" + }, + "print": { + "type": "long" + }, + "preserve_layout": { + "type": "long" + } + } } } }, @@ -4111,6 +4211,35 @@ }, "deprecated": { "type": "long" + }, + "app": { + "properties": { + "search": { + "type": "long" + }, + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, + "layout": { + "properties": { + "canvas": { + "type": "long" + }, + "print": { + "type": "long" + }, + "preserve_layout": { + "type": "long" + } + } } } }, @@ -4124,6 +4253,35 @@ }, "deprecated": { "type": "long" + }, + "app": { + "properties": { + "search": { + "type": "long" + }, + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, + "layout": { + "properties": { + "canvas": { + "type": "long" + }, + "print": { + "type": "long" + }, + "preserve_layout": { + "type": "long" + } + } } } }, @@ -4140,6 +4298,9 @@ }, "app": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4153,6 +4314,9 @@ }, "layout": { "properties": { + "canvas": { + "type": "long" + }, "print": { "type": "long" }, @@ -4176,6 +4340,9 @@ }, "app": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4189,6 +4356,9 @@ }, "layout": { "properties": { + "canvas": { + "type": "long" + }, "print": { "type": "long" }, @@ -4227,6 +4397,9 @@ "properties": { "csv": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4240,6 +4413,25 @@ }, "csv_searchsource": { "properties": { + "search": { + "type": "long" + }, + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, + "csv_searchsource_immediate": { + "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4253,6 +4445,9 @@ }, "PNG": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4266,6 +4461,9 @@ }, "PNGV2": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4279,6 +4477,9 @@ }, "printable_pdf": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4292,6 +4493,9 @@ }, "printable_pdf_v2": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4309,6 +4513,9 @@ "properties": { "csv": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4322,6 +4529,25 @@ }, "csv_searchsource": { "properties": { + "search": { + "type": "long" + }, + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, + "csv_searchsource_immediate": { + "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4335,6 +4561,9 @@ }, "PNG": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4348,6 +4577,9 @@ }, "PNGV2": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4361,6 +4593,9 @@ }, "printable_pdf": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4374,6 +4609,9 @@ }, "printable_pdf_v2": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4391,6 +4629,9 @@ "properties": { "csv": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4404,6 +4645,25 @@ }, "csv_searchsource": { "properties": { + "search": { + "type": "long" + }, + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, + "csv_searchsource_immediate": { + "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4417,6 +4677,9 @@ }, "PNG": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4430,6 +4693,9 @@ }, "PNGV2": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4443,6 +4709,9 @@ }, "printable_pdf": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4456,6 +4725,9 @@ }, "printable_pdf_v2": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4473,6 +4745,9 @@ "properties": { "csv": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4486,6 +4761,25 @@ }, "csv_searchsource": { "properties": { + "search": { + "type": "long" + }, + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, + "csv_searchsource_immediate": { + "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4499,6 +4793,9 @@ }, "PNG": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4512,6 +4809,9 @@ }, "PNGV2": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4525,6 +4825,9 @@ }, "printable_pdf": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4538,6 +4841,9 @@ }, "printable_pdf_v2": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4555,6 +4861,9 @@ "properties": { "csv": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4568,6 +4877,25 @@ }, "csv_searchsource": { "properties": { + "search": { + "type": "long" + }, + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, + "csv_searchsource_immediate": { + "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4581,6 +4909,9 @@ }, "PNG": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4594,6 +4925,83 @@ }, "PNGV2": { "properties": { + "search": { + "type": "long" + }, + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, + "printable_pdf": { + "properties": { + "search": { + "type": "long" + }, + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, + "printable_pdf_v2": { + "properties": { + "search": { + "type": "long" + }, + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + } + } + } + } + }, + "available": { + "type": "boolean" + }, + "browser_type": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "last7Days": { + "properties": { + "csv": { + "properties": { + "available": { + "type": "boolean" + }, + "total": { + "type": "long" + }, + "deprecated": { + "type": "long" + }, + "app": { + "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4605,8 +5013,37 @@ } } }, - "printable_pdf": { + "layout": { + "properties": { + "canvas": { + "type": "long" + }, + "print": { + "type": "long" + }, + "preserve_layout": { + "type": "long" + } + } + } + } + }, + "csv_searchsource": { + "properties": { + "available": { + "type": "boolean" + }, + "total": { + "type": "long" + }, + "deprecated": { + "type": "long" + }, + "app": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4618,35 +5055,22 @@ } } }, - "printable_pdf_v2": { + "layout": { "properties": { - "canvas workpad": { + "canvas": { "type": "long" }, - "dashboard": { + "print": { "type": "long" }, - "visualization": { + "preserve_layout": { "type": "long" } } } } - } - } - }, - "available": { - "type": "boolean" - }, - "browser_type": { - "type": "keyword" - }, - "enabled": { - "type": "boolean" - }, - "last7Days": { - "properties": { - "csv": { + }, + "csv_searchsource_immediate": { "properties": { "available": { "type": "boolean" @@ -4656,19 +5080,35 @@ }, "deprecated": { "type": "long" - } - } - }, - "csv_searchsource": { - "properties": { - "available": { - "type": "boolean" }, - "total": { - "type": "long" + "app": { + "properties": { + "search": { + "type": "long" + }, + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } }, - "deprecated": { - "type": "long" + "layout": { + "properties": { + "canvas": { + "type": "long" + }, + "print": { + "type": "long" + }, + "preserve_layout": { + "type": "long" + } + } } } }, @@ -4682,6 +5122,35 @@ }, "deprecated": { "type": "long" + }, + "app": { + "properties": { + "search": { + "type": "long" + }, + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, + "layout": { + "properties": { + "canvas": { + "type": "long" + }, + "print": { + "type": "long" + }, + "preserve_layout": { + "type": "long" + } + } } } }, @@ -4695,6 +5164,35 @@ }, "deprecated": { "type": "long" + }, + "app": { + "properties": { + "search": { + "type": "long" + }, + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, + "layout": { + "properties": { + "canvas": { + "type": "long" + }, + "print": { + "type": "long" + }, + "preserve_layout": { + "type": "long" + } + } } } }, @@ -4711,6 +5209,9 @@ }, "app": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4724,6 +5225,9 @@ }, "layout": { "properties": { + "canvas": { + "type": "long" + }, "print": { "type": "long" }, @@ -4747,6 +5251,9 @@ }, "app": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4760,6 +5267,9 @@ }, "layout": { "properties": { + "canvas": { + "type": "long" + }, "print": { "type": "long" }, @@ -4798,6 +5308,9 @@ "properties": { "csv": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4811,6 +5324,25 @@ }, "csv_searchsource": { "properties": { + "search": { + "type": "long" + }, + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, + "csv_searchsource_immediate": { + "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4824,6 +5356,9 @@ }, "PNG": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4837,6 +5372,9 @@ }, "PNGV2": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4850,6 +5388,9 @@ }, "printable_pdf": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4863,6 +5404,9 @@ }, "printable_pdf_v2": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4880,6 +5424,9 @@ "properties": { "csv": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4893,6 +5440,25 @@ }, "csv_searchsource": { "properties": { + "search": { + "type": "long" + }, + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, + "csv_searchsource_immediate": { + "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4906,6 +5472,9 @@ }, "PNG": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4919,6 +5488,9 @@ }, "PNGV2": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4932,6 +5504,9 @@ }, "printable_pdf": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4945,6 +5520,9 @@ }, "printable_pdf_v2": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4962,6 +5540,9 @@ "properties": { "csv": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4975,6 +5556,25 @@ }, "csv_searchsource": { "properties": { + "search": { + "type": "long" + }, + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, + "csv_searchsource_immediate": { + "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -4988,6 +5588,9 @@ }, "PNG": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -5001,6 +5604,9 @@ }, "PNGV2": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -5014,6 +5620,9 @@ }, "printable_pdf": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -5027,6 +5636,9 @@ }, "printable_pdf_v2": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -5044,6 +5656,9 @@ "properties": { "csv": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -5057,6 +5672,25 @@ }, "csv_searchsource": { "properties": { + "search": { + "type": "long" + }, + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, + "csv_searchsource_immediate": { + "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -5070,6 +5704,9 @@ }, "PNG": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -5083,6 +5720,9 @@ }, "PNGV2": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -5096,6 +5736,9 @@ }, "printable_pdf": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -5109,6 +5752,9 @@ }, "printable_pdf_v2": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -5126,6 +5772,9 @@ "properties": { "csv": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -5139,6 +5788,25 @@ }, "csv_searchsource": { "properties": { + "search": { + "type": "long" + }, + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, + "csv_searchsource_immediate": { + "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -5152,6 +5820,9 @@ }, "PNG": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -5165,6 +5836,9 @@ }, "PNGV2": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -5178,6 +5852,9 @@ }, "printable_pdf": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, @@ -5191,6 +5868,9 @@ }, "printable_pdf_v2": { "properties": { + "search": { + "type": "long" + }, "canvas workpad": { "type": "long" }, From 8dd4f49d9ff87b7f7153cc253b080f6ff5e863f1 Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Thu, 9 Sep 2021 10:47:28 +0200 Subject: [PATCH 034/139] [jest] update config files to get coverage per plugin (#111299) (#111674) * [jest] update config files to get coverage per plugin * [docs] add details about plugin coverage collection * fix path for newsfeed jest config * fix lint error * update documentation * fix lint errors again * update doc * fix another lint error * Update src/plugins/telemetry_management_section/jest.config.js Co-authored-by: Luke Elmers * Update src/plugins/telemetry_management_section/jest.config.js Co-authored-by: Luke Elmers * [kibana_legacy] fix path Co-authored-by: Luke Elmers Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> # Conflicts: # packages/kbn-ui-framework/README.md # src/plugins/legacy_export/jest.config.js --- dev_docs/key_concepts/anatomy_of_a_plugin.mdx | 24 +++++++++++++++++++ dev_docs/tutorials/testing_plugins.mdx | 11 +++++++++ .../contributing/development-tests.asciidoc | 8 +++++++ packages/kbn-ui-framework/README.md | 3 +++ src/plugins/advanced_settings/jest.config.js | 3 +++ src/plugins/bfetch/jest.config.js | 3 +++ .../expression_tagcloud/jest.config.js | 6 +++++ src/plugins/charts/jest.config.js | 3 +++ src/plugins/console/jest.config.js | 3 +++ src/plugins/dashboard/jest.config.js | 3 +++ src/plugins/data/jest.config.js | 3 +++ src/plugins/discover/jest.config.js | 3 +++ src/plugins/embeddable/jest.config.js | 3 +++ src/plugins/es_ui_shared/jest.config.js | 5 ++++ src/plugins/expression_error/jest.config.js | 3 +++ src/plugins/expression_image/jest.config.js | 5 ++++ src/plugins/expression_metric/jest.config.js | 5 ++++ .../expression_repeat_image/jest.config.js | 5 ++++ .../expression_reveal_image/jest.config.js | 5 ++++ src/plugins/expression_shape/jest.config.js | 5 ++++ src/plugins/expressions/jest.config.js | 3 +++ src/plugins/field_formats/jest.config.js | 3 +++ src/plugins/home/jest.config.js | 3 +++ .../index_pattern_editor/jest.config.js | 3 +++ .../index_pattern_field_editor/jest.config.js | 5 ++++ .../index_pattern_management/jest.config.js | 5 ++++ src/plugins/input_control_vis/jest.config.js | 3 +++ src/plugins/inspector/jest.config.js | 3 +++ src/plugins/kibana_legacy/jest.config.js | 3 +++ src/plugins/kibana_overview/jest.config.js | 3 +++ src/plugins/kibana_react/jest.config.js | 3 +++ .../kibana_usage_collection/jest.config.js | 5 ++++ src/plugins/kibana_utils/jest.config.js | 5 ++++ src/plugins/legacy_export/jest.config.js | 3 +++ src/plugins/management/jest.config.js | 3 +++ src/plugins/maps_ems/jest.config.js | 3 +++ src/plugins/navigation/jest.config.js | 3 +++ src/plugins/newsfeed/jest.config.js | 3 +++ src/plugins/presentation_util/jest.config.js | 5 ++++ src/plugins/saved_objects/jest.config.js | 3 +++ .../saved_objects_management/jest.config.js | 5 ++++ .../saved_objects_tagging_oss/jest.config.js | 5 ++++ src/plugins/screenshot_mode/jest.config.js | 5 ++++ src/plugins/security_oss/jest.config.js | 3 +++ src/plugins/share/jest.config.js | 3 +++ src/plugins/telemetry/jest.config.js | 3 +++ .../jest.config.js | 6 +++++ .../jest.config.js | 4 ++++ src/plugins/ui_actions/jest.config.js | 3 +++ src/plugins/url_forwarding/jest.config.js | 3 +++ src/plugins/usage_collection/jest.config.js | 5 ++++ src/plugins/vis_default_editor/jest.config.js | 3 +++ src/plugins/vis_type_markdown/jest.config.js | 3 +++ src/plugins/vis_type_table/jest.config.js | 6 ++++- src/plugins/vis_type_timelion/jest.config.js | 5 ++++ .../vis_type_timeseries/jest.config.js | 5 ++++ src/plugins/vis_types/metric/jest.config.js | 3 +++ src/plugins/vis_types/pie/jest.config.js | 3 +++ src/plugins/vis_types/tagcloud/jest.config.js | 3 +++ src/plugins/vis_types/vega/jest.config.js | 3 +++ src/plugins/vis_types/vislib/jest.config.js | 5 ++++ src/plugins/vis_types/xy/jest.config.js | 3 +++ src/plugins/visualizations/jest.config.js | 5 ++++ src/plugins/visualize/jest.config.js | 3 +++ x-pack/plugins/actions/jest.config.js | 3 +++ x-pack/plugins/alerting/jest.config.js | 3 +++ x-pack/plugins/apm/jest.config.js | 5 ++++ x-pack/plugins/banners/jest.config.js | 3 +++ x-pack/plugins/canvas/jest.config.js | 5 ++++ x-pack/plugins/cases/jest.config.js | 3 +++ x-pack/plugins/cloud/jest.config.js | 3 +++ .../cross_cluster_replication/jest.config.js | 6 +++++ .../plugins/dashboard_enhanced/jest.config.js | 5 ++++ x-pack/plugins/data_enhanced/jest.config.js | 5 ++++ x-pack/plugins/data_visualizer/jest.config.js | 5 ++++ .../plugins/discover_enhanced/jest.config.js | 5 ++++ x-pack/plugins/drilldowns/jest.config.js | 3 +++ .../embeddable_enhanced/jest.config.js | 3 +++ .../encrypted_saved_objects/jest.config.js | 3 +++ .../plugins/enterprise_search/jest.config.js | 3 ++- x-pack/plugins/event_log/jest.config.js | 3 +++ x-pack/plugins/features/jest.config.js | 3 +++ x-pack/plugins/file_upload/jest.config.js | 5 ++++ x-pack/plugins/fleet/jest.config.js | 3 +++ x-pack/plugins/global_search/jest.config.js | 5 ++++ .../plugins/global_search_bar/jest.config.js | 3 +++ .../global_search_providers/jest.config.js | 5 ++++ x-pack/plugins/graph/jest.config.js | 3 +++ x-pack/plugins/grokdebugger/jest.config.js | 5 ++++ .../index_lifecycle_management/jest.config.js | 6 +++++ .../plugins/index_management/jest.config.js | 5 ++++ x-pack/plugins/infra/jest.config.js | 3 +++ .../plugins/ingest_pipelines/jest.config.js | 5 ++++ x-pack/plugins/lens/jest.config.js | 3 +++ .../plugins/license_api_guard/jest.config.js | 3 +++ .../plugins/license_management/jest.config.js | 5 ++++ x-pack/plugins/licensing/jest.config.js | 3 +++ x-pack/plugins/lists/jest.config.js | 3 +++ x-pack/plugins/logstash/jest.config.js | 5 ++++ x-pack/plugins/maps/jest.config.js | 3 +++ .../plugins/metrics_entities/jest.config.js | 3 +++ x-pack/plugins/ml/jest.config.js | 3 +++ x-pack/plugins/monitoring/jest.config.js | 5 ++++ x-pack/plugins/observability/jest.config.js | 5 ++++ x-pack/plugins/osquery/jest.config.js | 3 +++ x-pack/plugins/painless_lab/jest.config.js | 5 ++++ x-pack/plugins/remote_clusters/jest.config.js | 5 ++++ x-pack/plugins/reporting/jest.config.js | 5 ++++ x-pack/plugins/rollup/jest.config.js | 3 +++ x-pack/plugins/rule_registry/jest.config.js | 3 +++ x-pack/plugins/runtime_fields/jest.config.js | 3 +++ .../saved_objects_tagging/jest.config.js | 5 ++++ x-pack/plugins/searchprofiler/jest.config.js | 5 ++++ x-pack/plugins/security/jest.config.js | 3 +++ .../plugins/security_solution/jest.config.js | 5 ++++ .../plugins/snapshot_restore/jest.config.js | 5 ++++ x-pack/plugins/spaces/jest.config.js | 3 +++ x-pack/plugins/stack_alerts/jest.config.js | 5 ++++ x-pack/plugins/task_manager/jest.config.js | 3 +++ .../telemetry_collection_xpack/jest.config.js | 4 ++++ x-pack/plugins/timelines/jest.config.js | 3 +++ x-pack/plugins/transform/jest.config.js | 3 +++ .../triggers_actions_ui/jest.config.js | 5 ++++ .../ui_actions_enhanced/jest.config.js | 5 ++++ .../plugins/upgrade_assistant/jest.config.js | 5 ++++ x-pack/plugins/uptime/jest.config.js | 3 +++ x-pack/plugins/watcher/jest.config.js | 3 +++ x-pack/plugins/xpack_legacy/jest.config.js | 3 +++ 128 files changed, 525 insertions(+), 2 deletions(-) diff --git a/dev_docs/key_concepts/anatomy_of_a_plugin.mdx b/dev_docs/key_concepts/anatomy_of_a_plugin.mdx index fa0aae2299bb0..6a6f4f8b500a8 100644 --- a/dev_docs/key_concepts/anatomy_of_a_plugin.mdx +++ b/dev_docs/key_concepts/anatomy_of_a_plugin.mdx @@ -32,6 +32,7 @@ plugins/ plugin.ts common index.ts + jest.config.js ``` ### kibana.json @@ -209,6 +210,29 @@ considerations related to how plugins integrate with core APIs and APIs exposed `common/index.ts` is the entry-point into code that can be used both server-side or client side. +### jest.config.js + +If you are adding unit tests (which we recommend), you will need to add a `jest.config.js` file. Here is an example file that you would use if adding a plugin into the `examples` directory. + +```js +module.exports = { + // Default Jest settings, defined in kbn-test package + preset: '@kbn/test', + // The root of the directory containing package.json + rootDir: '../../..', + // The directory which Jest should use to search for files in + roots: ['/src/plugins/demo'], + // The directory where Jest should output plugin coverage details, e.g. html report + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/demo', + // A list of reporter names that Jest uses when writing coverage reports, default: ["json"] + // "text" is available in console and is good for quick check + // "html" helps to dig into specific files and fix coverage + coverageReporters: ['text', 'html'], + // An array of regexp pattern strings that matched files to include/exclude for code coverage + collectCoverageFrom: ['/src/plugins/demo/{common,public,server}/**/*.{ts,tsx}'], +}; +``` + ## How plugin's interact with each other, and Core The lifecycle-specific contracts exposed by core services are always passed as the first argument to the equivalent lifecycle function in a plugin. diff --git a/dev_docs/tutorials/testing_plugins.mdx b/dev_docs/tutorials/testing_plugins.mdx index 55b662421cbd0..bc92af33d3493 100644 --- a/dev_docs/tutorials/testing_plugins.mdx +++ b/dev_docs/tutorials/testing_plugins.mdx @@ -928,6 +928,17 @@ describe('Case migrations v7.7.0 -> v7.8.0', () => { }); ``` +You can generate code coverage report for a single plugin. + +```bash +yarn jest --coverage --config src/plugins/console/jest.config.js +``` + +Html report should be available in `target/kibana-coverage/jest/src/plugins/console` path + +We run code coverage daily on CI and ["Kibana Stats cluster"](https://kibana-stats.elastic.dev/s/code-coverage/app/home) +can be used to view statistics. The report combines code coverage for all jest tests within Kibana repository. + #### Integration testing With more complicated migrations, the behavior of the migration may be dependent on values from other plugins which may be difficult or even impossible to test with unit tests. You need to actually bootstrap Kibana, load the plugins, and diff --git a/docs/developer/contributing/development-tests.asciidoc b/docs/developer/contributing/development-tests.asciidoc index e7a36d2866728..340e122b44c1b 100644 --- a/docs/developer/contributing/development-tests.asciidoc +++ b/docs/developer/contributing/development-tests.asciidoc @@ -56,6 +56,14 @@ kibana/src/plugins/dashboard/server$ yarn test:jest --coverage yarn jest --coverage --verbose --config /home/tyler/elastic/kibana/src/plugins/dashboard/jest.config.js server ---- +You can generate code coverage report for a single plugin. + +[source,bash] +---- +yarn jest --coverage --config src/plugins/console/jest.config.js +---- + +Html report is available in target/kibana-coverage/jest/path/to/plugin [discrete] === Running browser automation tests diff --git a/packages/kbn-ui-framework/README.md b/packages/kbn-ui-framework/README.md index 6ee1c407dad5d..b6a43d8e28353 100644 --- a/packages/kbn-ui-framework/README.md +++ b/packages/kbn-ui-framework/README.md @@ -19,6 +19,9 @@ You can run `node scripts/jest --watch` to watch for changes and run the tests a You can run `node scripts/jest --coverage` to generate a code coverage report to see how fully-tested the code is. +You can run `node scripts/jest --config path/to/plugin/jest.config.js --coverage` to generate +a code coverage report for a single plugin. + See the documentation in [`scripts/jest.js`](../scripts/jest.js) for more options. ## Creating components diff --git a/src/plugins/advanced_settings/jest.config.js b/src/plugins/advanced_settings/jest.config.js index 61909cd432df4..7900d7f39b6c6 100644 --- a/src/plugins/advanced_settings/jest.config.js +++ b/src/plugins/advanced_settings/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/advanced_settings'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/advanced_settings', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/advanced_settings/{public,server}/**/*.{ts,tsx}'], }; diff --git a/src/plugins/bfetch/jest.config.js b/src/plugins/bfetch/jest.config.js index 544328ed4baf3..d01c81c8f1d82 100644 --- a/src/plugins/bfetch/jest.config.js +++ b/src/plugins/bfetch/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/bfetch'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/bfetch', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/bfetch/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/src/plugins/chart_expressions/expression_tagcloud/jest.config.js b/src/plugins/chart_expressions/expression_tagcloud/jest.config.js index c88c150d6f649..412a133b0d292 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/jest.config.js +++ b/src/plugins/chart_expressions/expression_tagcloud/jest.config.js @@ -10,4 +10,10 @@ module.exports = { preset: '@kbn/test', rootDir: '../../../../', roots: ['/src/plugins/chart_expressions/expression_tagcloud'], + coverageDirectory: + '/target/kibana-coverage/jest/src/plugins/chart_expressions/expression_tagcloud', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/chart_expressions/expression_tagcloud/{common,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/src/plugins/charts/jest.config.js b/src/plugins/charts/jest.config.js index b6516c520ecf6..a4469b78b3306 100644 --- a/src/plugins/charts/jest.config.js +++ b/src/plugins/charts/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/charts'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/charts', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/charts/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/src/plugins/console/jest.config.js b/src/plugins/console/jest.config.js index 0fc1bcbcfb201..08dbc96e0136c 100644 --- a/src/plugins/console/jest.config.js +++ b/src/plugins/console/jest.config.js @@ -11,4 +11,7 @@ module.exports = { rootDir: '../../..', roots: ['/src/plugins/console'], testRunner: 'jasmine2', + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/console', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/console/{common,public,server}/**/*.{js,ts,tsx}'], }; diff --git a/src/plugins/dashboard/jest.config.js b/src/plugins/dashboard/jest.config.js index 0b9ac0428c3af..d99cfe57fcd37 100644 --- a/src/plugins/dashboard/jest.config.js +++ b/src/plugins/dashboard/jest.config.js @@ -11,4 +11,7 @@ module.exports = { rootDir: '../../..', roots: ['/src/plugins/dashboard'], testRunner: 'jasmine2', + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/dashboard', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/dashboard/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/src/plugins/data/jest.config.js b/src/plugins/data/jest.config.js index eba30c6cfd674..f8ab0e348376e 100644 --- a/src/plugins/data/jest.config.js +++ b/src/plugins/data/jest.config.js @@ -11,4 +11,7 @@ module.exports = { rootDir: '../../..', roots: ['/src/plugins/data'], testRunner: 'jasmine2', + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/data', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/data/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/src/plugins/discover/jest.config.js b/src/plugins/discover/jest.config.js index 4c0f09b2cc242..00f5d8016e3c4 100644 --- a/src/plugins/discover/jest.config.js +++ b/src/plugins/discover/jest.config.js @@ -11,4 +11,7 @@ module.exports = { rootDir: '../../..', roots: ['/src/plugins/discover'], testRunner: 'jasmine2', + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/discover', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/discover/{common,public,server}/**/*.{js,ts,tsx}'], }; diff --git a/src/plugins/embeddable/jest.config.js b/src/plugins/embeddable/jest.config.js index 6ac4e4486fcfd..37080a605e3d5 100644 --- a/src/plugins/embeddable/jest.config.js +++ b/src/plugins/embeddable/jest.config.js @@ -11,4 +11,7 @@ module.exports = { rootDir: '../../..', roots: ['/src/plugins/embeddable'], testRunner: 'jasmine2', + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/embeddable', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/embeddable/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/src/plugins/es_ui_shared/jest.config.js b/src/plugins/es_ui_shared/jest.config.js index cf525397bd75c..c311f5d9df2ed 100644 --- a/src/plugins/es_ui_shared/jest.config.js +++ b/src/plugins/es_ui_shared/jest.config.js @@ -10,4 +10,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/es_ui_shared'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/es_ui_shared', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/es_ui_shared/{__packages_do_not_import__,common,public,server,static}/**/*.{ts,tsx}', + ], }; diff --git a/src/plugins/expression_error/jest.config.js b/src/plugins/expression_error/jest.config.js index 64f3e9292ff07..27774f4003f9e 100644 --- a/src/plugins/expression_error/jest.config.js +++ b/src/plugins/expression_error/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/expression_error'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/expression_error', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/expression_error/{common,public}/**/*.{ts,tsx}'], }; diff --git a/src/plugins/expression_image/jest.config.js b/src/plugins/expression_image/jest.config.js index 3d5bc9f184c6a..ccefa3c20699e 100644 --- a/src/plugins/expression_image/jest.config.js +++ b/src/plugins/expression_image/jest.config.js @@ -10,4 +10,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/expression_image'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/expression_image', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/expression_image/{common,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/src/plugins/expression_metric/jest.config.js b/src/plugins/expression_metric/jest.config.js index 517409460895e..23546fc334816 100644 --- a/src/plugins/expression_metric/jest.config.js +++ b/src/plugins/expression_metric/jest.config.js @@ -10,4 +10,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/expression_metric'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/expression_metric', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/expression_metric/{common,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/src/plugins/expression_repeat_image/jest.config.js b/src/plugins/expression_repeat_image/jest.config.js index cf1039263840b..b30d782ef6e0e 100644 --- a/src/plugins/expression_repeat_image/jest.config.js +++ b/src/plugins/expression_repeat_image/jest.config.js @@ -10,4 +10,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/expression_repeat_image'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/expression_repeat_image', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/expression_repeat_image/{common,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/src/plugins/expression_reveal_image/jest.config.js b/src/plugins/expression_reveal_image/jest.config.js index aac5fad293846..c1d7fead721f5 100644 --- a/src/plugins/expression_reveal_image/jest.config.js +++ b/src/plugins/expression_reveal_image/jest.config.js @@ -10,4 +10,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/expression_reveal_image'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/expression_reveal_image', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/expression_reveal_image/{common,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/src/plugins/expression_shape/jest.config.js b/src/plugins/expression_shape/jest.config.js index a390c0154bbd0..bb2603cb012eb 100644 --- a/src/plugins/expression_shape/jest.config.js +++ b/src/plugins/expression_shape/jest.config.js @@ -10,4 +10,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/expression_shape'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/expression_shape', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/expression_shape/{common,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/src/plugins/expressions/jest.config.js b/src/plugins/expressions/jest.config.js index 721312f7d886c..24f27aadedd7b 100644 --- a/src/plugins/expressions/jest.config.js +++ b/src/plugins/expressions/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/expressions'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/expressions', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/expressions/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/src/plugins/field_formats/jest.config.js b/src/plugins/field_formats/jest.config.js index ea20fcfec6d09..6fc68ab97526e 100644 --- a/src/plugins/field_formats/jest.config.js +++ b/src/plugins/field_formats/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/field_formats'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/field_formats', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/field_formats/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/src/plugins/home/jest.config.js b/src/plugins/home/jest.config.js index 5107cc001d32f..c7450ebbf3104 100644 --- a/src/plugins/home/jest.config.js +++ b/src/plugins/home/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/home'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/home', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/home/{common,public,server}/**/*.{js,ts,tsx}'], }; diff --git a/src/plugins/index_pattern_editor/jest.config.js b/src/plugins/index_pattern_editor/jest.config.js index 0a018a42d06e6..bdf5fd8a0e9e2 100644 --- a/src/plugins/index_pattern_editor/jest.config.js +++ b/src/plugins/index_pattern_editor/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/index_pattern_editor'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/index_pattern_editor', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/index_pattern_editor/public/**/*.{ts,tsx}'], }; diff --git a/src/plugins/index_pattern_field_editor/jest.config.js b/src/plugins/index_pattern_field_editor/jest.config.js index fc358c37116c9..e1f8f57038e26 100644 --- a/src/plugins/index_pattern_field_editor/jest.config.js +++ b/src/plugins/index_pattern_field_editor/jest.config.js @@ -10,4 +10,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/index_pattern_field_editor'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/index_pattern_field_editor', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/index_pattern_field_editor/{common,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/src/plugins/index_pattern_management/jest.config.js b/src/plugins/index_pattern_management/jest.config.js index 8383d3bb6110d..6249d44e6b31f 100644 --- a/src/plugins/index_pattern_management/jest.config.js +++ b/src/plugins/index_pattern_management/jest.config.js @@ -11,4 +11,9 @@ module.exports = { rootDir: '../../..', roots: ['/src/plugins/index_pattern_management'], testRunner: 'jasmine2', + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/index_pattern_management', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/index_pattern_management/{public,server}/**/*.{ts,tsx}', + ], }; diff --git a/src/plugins/input_control_vis/jest.config.js b/src/plugins/input_control_vis/jest.config.js index 060ab9ff1a126..207a0b5265417 100644 --- a/src/plugins/input_control_vis/jest.config.js +++ b/src/plugins/input_control_vis/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/input_control_vis'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/input_control_vis', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/input_control_vis/public/**/*.{ts,tsx}'], }; diff --git a/src/plugins/inspector/jest.config.js b/src/plugins/inspector/jest.config.js index 67e90f449fa76..3583a69a94bd9 100644 --- a/src/plugins/inspector/jest.config.js +++ b/src/plugins/inspector/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/inspector'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/inspector', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/inspector/{common,public}/**/*.{ts,tsx}'], }; diff --git a/src/plugins/kibana_legacy/jest.config.js b/src/plugins/kibana_legacy/jest.config.js index c9b571b13f13f..a2bdf5649f900 100644 --- a/src/plugins/kibana_legacy/jest.config.js +++ b/src/plugins/kibana_legacy/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/kibana_legacy'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/kibana_legacy', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/kibana_legacy/public/**/*.{js,ts,tsx}'], }; diff --git a/src/plugins/kibana_overview/jest.config.js b/src/plugins/kibana_overview/jest.config.js index 4862a4967a3ca..00cf46dfe2f80 100644 --- a/src/plugins/kibana_overview/jest.config.js +++ b/src/plugins/kibana_overview/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/kibana_overview'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/kibana_overview', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/kibana_overview/{common,public}/**/*.{js,ts,tsx}'], }; diff --git a/src/plugins/kibana_react/jest.config.js b/src/plugins/kibana_react/jest.config.js index 159f7b01795dc..35bf9df51d615 100644 --- a/src/plugins/kibana_react/jest.config.js +++ b/src/plugins/kibana_react/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/kibana_react'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/kibana_react', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/kibana_react/{common,public}/**/*.{ts,tsx}'], }; diff --git a/src/plugins/kibana_usage_collection/jest.config.js b/src/plugins/kibana_usage_collection/jest.config.js index f199984a6ad6d..a52c3eb8cc06c 100644 --- a/src/plugins/kibana_usage_collection/jest.config.js +++ b/src/plugins/kibana_usage_collection/jest.config.js @@ -10,4 +10,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/kibana_usage_collection'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/kibana_usage_collection', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/kibana_usage_collection/{common,server}/**/*.{ts,tsx}', + ], }; diff --git a/src/plugins/kibana_utils/jest.config.js b/src/plugins/kibana_utils/jest.config.js index 48b7bb62a8e68..a01ededd1e7c0 100644 --- a/src/plugins/kibana_utils/jest.config.js +++ b/src/plugins/kibana_utils/jest.config.js @@ -10,4 +10,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/kibana_utils'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/kibana_utils', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/kibana_utils/{common,demos,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/src/plugins/legacy_export/jest.config.js b/src/plugins/legacy_export/jest.config.js index d5dd37c8249f9..3aa4ea98ff480 100644 --- a/src/plugins/legacy_export/jest.config.js +++ b/src/plugins/legacy_export/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/legacy_export'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/legacy_export', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/interactive_setup/public/**/*.{ts,tsx}'], }; diff --git a/src/plugins/management/jest.config.js b/src/plugins/management/jest.config.js index e821012e9dc2e..ee5e50a753693 100644 --- a/src/plugins/management/jest.config.js +++ b/src/plugins/management/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/management'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/management', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/management/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/src/plugins/maps_ems/jest.config.js b/src/plugins/maps_ems/jest.config.js index b7021c9119deb..4e2b09bb66305 100644 --- a/src/plugins/maps_ems/jest.config.js +++ b/src/plugins/maps_ems/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/maps_ems'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/maps_ems', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/maps_ems/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/src/plugins/navigation/jest.config.js b/src/plugins/navigation/jest.config.js index 98cd0e3760a17..e31374a542563 100644 --- a/src/plugins/navigation/jest.config.js +++ b/src/plugins/navigation/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/navigation'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/navigation', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/navigation/public/**/*.{ts,tsx}'], }; diff --git a/src/plugins/newsfeed/jest.config.js b/src/plugins/newsfeed/jest.config.js index 580185836c978..43caa9227b9f6 100644 --- a/src/plugins/newsfeed/jest.config.js +++ b/src/plugins/newsfeed/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/newsfeed'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/newsfeed', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/newsfeed/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/src/plugins/presentation_util/jest.config.js b/src/plugins/presentation_util/jest.config.js index 2250d70acb475..cb1e18fd6d736 100644 --- a/src/plugins/presentation_util/jest.config.js +++ b/src/plugins/presentation_util/jest.config.js @@ -10,4 +10,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/presentation_util'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/presentation_util', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/presentation_util/{common,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/src/plugins/saved_objects/jest.config.js b/src/plugins/saved_objects/jest.config.js index 77848e0dc6ed5..416385af2d214 100644 --- a/src/plugins/saved_objects/jest.config.js +++ b/src/plugins/saved_objects/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/saved_objects'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/saved_objects', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/saved_objects/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/src/plugins/saved_objects_management/jest.config.js b/src/plugins/saved_objects_management/jest.config.js index a8fd7db1b7b45..e986e5a310eae 100644 --- a/src/plugins/saved_objects_management/jest.config.js +++ b/src/plugins/saved_objects_management/jest.config.js @@ -10,4 +10,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/saved_objects_management'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/saved_objects_management', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/saved_objects_management/{common,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/src/plugins/saved_objects_tagging_oss/jest.config.js b/src/plugins/saved_objects_tagging_oss/jest.config.js index adb5b1e426180..e7b571e955ef1 100644 --- a/src/plugins/saved_objects_tagging_oss/jest.config.js +++ b/src/plugins/saved_objects_tagging_oss/jest.config.js @@ -10,4 +10,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/saved_objects_tagging_oss'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/saved_objects_tagging_oss', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/saved_objects_tagging_oss/{common,public}/**/*.{ts,tsx}', + ], }; diff --git a/src/plugins/screenshot_mode/jest.config.js b/src/plugins/screenshot_mode/jest.config.js index e84f3742f8c1d..e108ab786739f 100644 --- a/src/plugins/screenshot_mode/jest.config.js +++ b/src/plugins/screenshot_mode/jest.config.js @@ -10,4 +10,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/screenshot_mode'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/screenshot_mode', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/screenshot_mode/{common,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/src/plugins/security_oss/jest.config.js b/src/plugins/security_oss/jest.config.js index c62cad7e72cec..692d85f69a740 100644 --- a/src/plugins/security_oss/jest.config.js +++ b/src/plugins/security_oss/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/security_oss'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/security_oss', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/security_oss/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/src/plugins/share/jest.config.js b/src/plugins/share/jest.config.js index f347067849f71..aae58e3c57d1f 100644 --- a/src/plugins/share/jest.config.js +++ b/src/plugins/share/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/share'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/share', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/share/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/src/plugins/telemetry/jest.config.js b/src/plugins/telemetry/jest.config.js index 61e042b40b5d4..932c3c2851382 100644 --- a/src/plugins/telemetry/jest.config.js +++ b/src/plugins/telemetry/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/telemetry'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/telemetry', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/telemetry/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/src/plugins/telemetry_collection_manager/jest.config.js b/src/plugins/telemetry_collection_manager/jest.config.js index f9615de4ab60a..7f58aa46cabf2 100644 --- a/src/plugins/telemetry_collection_manager/jest.config.js +++ b/src/plugins/telemetry_collection_manager/jest.config.js @@ -10,4 +10,10 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/telemetry_collection_manager'], + coverageDirectory: + '/target/kibana-coverage/jest/src/plugins/telemetry_collection_manager', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/telemetry_collection_manager/{common,server}/**/*.{ts,tsx}', + ], }; diff --git a/src/plugins/telemetry_management_section/jest.config.js b/src/plugins/telemetry_management_section/jest.config.js index e1001bc787589..722496905de9e 100644 --- a/src/plugins/telemetry_management_section/jest.config.js +++ b/src/plugins/telemetry_management_section/jest.config.js @@ -10,4 +10,8 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/telemetry_management_section'], + coverageDirectory: + '/target/kibana-coverage/jest/src/plugins/telemetry_management_section', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/telemetry_management_section/public/**/*.{ts,tsx}'], }; diff --git a/src/plugins/ui_actions/jest.config.js b/src/plugins/ui_actions/jest.config.js index 8ee6a40e19541..f482574316632 100644 --- a/src/plugins/ui_actions/jest.config.js +++ b/src/plugins/ui_actions/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/ui_actions'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/ui_actions', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/ui_actions/public/**/*.{ts,tsx}'], }; diff --git a/src/plugins/url_forwarding/jest.config.js b/src/plugins/url_forwarding/jest.config.js index 5e452d70753d5..24a72465626f6 100644 --- a/src/plugins/url_forwarding/jest.config.js +++ b/src/plugins/url_forwarding/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/url_forwarding'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/url_forwarding', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/url_forwarding/public/**/*.{ts,tsx}'], }; diff --git a/src/plugins/usage_collection/jest.config.js b/src/plugins/usage_collection/jest.config.js index 25994eeec9820..f93d12327cc1b 100644 --- a/src/plugins/usage_collection/jest.config.js +++ b/src/plugins/usage_collection/jest.config.js @@ -10,4 +10,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/usage_collection'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/usage_collection', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/usage_collection/{common,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/src/plugins/vis_default_editor/jest.config.js b/src/plugins/vis_default_editor/jest.config.js index 52276e29c46c4..c921db167c1e9 100644 --- a/src/plugins/vis_default_editor/jest.config.js +++ b/src/plugins/vis_default_editor/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/vis_default_editor'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/vis_default_editor', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/vis_default_editor/public/**/*.{ts,tsx}'], }; diff --git a/src/plugins/vis_type_markdown/jest.config.js b/src/plugins/vis_type_markdown/jest.config.js index 8c73fde8d6618..835d37312eadd 100644 --- a/src/plugins/vis_type_markdown/jest.config.js +++ b/src/plugins/vis_type_markdown/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/vis_type_markdown'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/vis_type_markdown', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/vis_type_markdown/{public,server}/**/*.{ts,tsx}'], }; diff --git a/src/plugins/vis_type_table/jest.config.js b/src/plugins/vis_type_table/jest.config.js index 9c91b1b813e52..a5a925eada3f1 100644 --- a/src/plugins/vis_type_table/jest.config.js +++ b/src/plugins/vis_type_table/jest.config.js @@ -11,5 +11,9 @@ module.exports = { rootDir: '../../..', roots: ['/src/plugins/vis_type_table'], testRunner: 'jasmine2', - collectCoverageFrom: ['/src/plugins/vis_type_table/**/*.{js,ts,tsx}'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/vis_type_table', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/vis_type_table/{common,public,server}/**/*.{js,ts,tsx}', + ], }; diff --git a/src/plugins/vis_type_timelion/jest.config.js b/src/plugins/vis_type_timelion/jest.config.js index ed4eedaee2fbf..5da416935adb9 100644 --- a/src/plugins/vis_type_timelion/jest.config.js +++ b/src/plugins/vis_type_timelion/jest.config.js @@ -10,4 +10,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/vis_type_timelion'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/vis_type_timelion', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/vis_type_timelion/{common,public,server}/**/*.{js,ts,tsx}', + ], }; diff --git a/src/plugins/vis_type_timeseries/jest.config.js b/src/plugins/vis_type_timeseries/jest.config.js index 5007d995edc44..3d4333675f7d7 100644 --- a/src/plugins/vis_type_timeseries/jest.config.js +++ b/src/plugins/vis_type_timeseries/jest.config.js @@ -10,4 +10,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/vis_type_timeseries'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/vis_type_timeseries', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/vis_type_timeseries/{common,public,server}/**/*.{js,ts,tsx}', + ], }; diff --git a/src/plugins/vis_types/metric/jest.config.js b/src/plugins/vis_types/metric/jest.config.js index a84929a3805b8..e6de1dd63b34d 100644 --- a/src/plugins/vis_types/metric/jest.config.js +++ b/src/plugins/vis_types/metric/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../../..', roots: ['/src/plugins/vis_types/metric'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/vis_types/metric', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/vis_types/metric/{public,server}/**/*.{ts,tsx}'], }; diff --git a/src/plugins/vis_types/pie/jest.config.js b/src/plugins/vis_types/pie/jest.config.js index 505ea97f489f7..d9afd1d718c85 100644 --- a/src/plugins/vis_types/pie/jest.config.js +++ b/src/plugins/vis_types/pie/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../../..', roots: ['/src/plugins/vis_types/pie'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/vis_types/pie', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/vis_types/pie/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/src/plugins/vis_types/tagcloud/jest.config.js b/src/plugins/vis_types/tagcloud/jest.config.js index 20dfd8ad0d11c..9785690d5e8b4 100644 --- a/src/plugins/vis_types/tagcloud/jest.config.js +++ b/src/plugins/vis_types/tagcloud/jest.config.js @@ -11,4 +11,7 @@ module.exports = { rootDir: '../../../..', roots: ['/src/plugins/vis_types/tagcloud'], testRunner: 'jasmine2', + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/vis_types/tagcloud', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/vis_types/tagcloud/{public,server}/**/*.{ts,tsx}'], }; diff --git a/src/plugins/vis_types/vega/jest.config.js b/src/plugins/vis_types/vega/jest.config.js index d7e1653e891a5..33c2d8e7aa1ed 100644 --- a/src/plugins/vis_types/vega/jest.config.js +++ b/src/plugins/vis_types/vega/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../../..', roots: ['/src/plugins/vis_types/vega'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/vis_types/vega', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/vis_types/vega/{public,server}/**/*.{js,ts,tsx}'], }; diff --git a/src/plugins/vis_types/vislib/jest.config.js b/src/plugins/vis_types/vislib/jest.config.js index 6b6d7c3361ecf..cc80a320765d8 100644 --- a/src/plugins/vis_types/vislib/jest.config.js +++ b/src/plugins/vis_types/vislib/jest.config.js @@ -10,4 +10,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../../..', roots: ['/src/plugins/vis_types/vislib'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/vis_types/vislib', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/vis_types/vislib/{common,public,server}/**/*.{js,ts,tsx}', + ], }; diff --git a/src/plugins/vis_types/xy/jest.config.js b/src/plugins/vis_types/xy/jest.config.js index 57b041b575e3f..4cdb8f44012c7 100644 --- a/src/plugins/vis_types/xy/jest.config.js +++ b/src/plugins/vis_types/xy/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../../..', roots: ['/src/plugins/vis_types/xy'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/vis_types/xy', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/vis_types/xy/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/src/plugins/visualizations/jest.config.js b/src/plugins/visualizations/jest.config.js index 250bdc44e4de5..450e30a1de24d 100644 --- a/src/plugins/visualizations/jest.config.js +++ b/src/plugins/visualizations/jest.config.js @@ -10,4 +10,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/visualizations'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/visualizations', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/src/plugins/visualizations/{common,public,server}/**/*.{js,ts,tsx}', + ], }; diff --git a/src/plugins/visualize/jest.config.js b/src/plugins/visualize/jest.config.js index 22a9ffa161253..11ea368f57d25 100644 --- a/src/plugins/visualize/jest.config.js +++ b/src/plugins/visualize/jest.config.js @@ -10,4 +10,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/visualize'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/visualize', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/visualize/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/actions/jest.config.js b/x-pack/plugins/actions/jest.config.js index 3a9fb5019494a..2d3372a91890a 100644 --- a/x-pack/plugins/actions/jest.config.js +++ b/x-pack/plugins/actions/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/actions'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/actions', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/actions/{common,server}/**/*.{js,ts,tsx}'], }; diff --git a/x-pack/plugins/alerting/jest.config.js b/x-pack/plugins/alerting/jest.config.js index 1f34005415cca..05db974299b40 100644 --- a/x-pack/plugins/alerting/jest.config.js +++ b/x-pack/plugins/alerting/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/alerting'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/alerting', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/alerting/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/apm/jest.config.js b/x-pack/plugins/apm/jest.config.js index 5bce9bbfb5b1b..77639cb58d497 100644 --- a/x-pack/plugins/apm/jest.config.js +++ b/x-pack/plugins/apm/jest.config.js @@ -13,4 +13,9 @@ module.exports = { roots: ['/x-pack/plugins/apm'], setupFiles: ['/x-pack/plugins/apm/.storybook/jest_setup.js'], testPathIgnorePatterns: ['/x-pack/plugins/apm/e2e/'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/apm', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/apm/{common,public,server}/**/*.{js,ts,tsx}', + ], }; diff --git a/x-pack/plugins/banners/jest.config.js b/x-pack/plugins/banners/jest.config.js index e2d103c8e4a28..291bdb3436295 100644 --- a/x-pack/plugins/banners/jest.config.js +++ b/x-pack/plugins/banners/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/banners'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/banners', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/banners/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/canvas/jest.config.js b/x-pack/plugins/canvas/jest.config.js index 7524e06159a41..2bff284e94ad8 100644 --- a/x-pack/plugins/canvas/jest.config.js +++ b/x-pack/plugins/canvas/jest.config.js @@ -12,4 +12,9 @@ module.exports = { transform: { '^.+\\.stories\\.tsx?$': '@storybook/addon-storyshots/injectFileName', }, + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/canvas', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/canvas/{canvas_plugin_src,common,i18n,public,server,shareable_runtime}/**/*.{js,ts,tsx}', + ], }; diff --git a/x-pack/plugins/cases/jest.config.js b/x-pack/plugins/cases/jest.config.js index 6368eb8895ad1..3b1b0b1223191 100644 --- a/x-pack/plugins/cases/jest.config.js +++ b/x-pack/plugins/cases/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/cases'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/cases', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/cases/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/cloud/jest.config.js b/x-pack/plugins/cloud/jest.config.js index 68f63b4d8b5ac..4235e79e79268 100644 --- a/x-pack/plugins/cloud/jest.config.js +++ b/x-pack/plugins/cloud/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/cloud'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/cloud', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/cloud/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/cross_cluster_replication/jest.config.js b/x-pack/plugins/cross_cluster_replication/jest.config.js index 8d39781213dbd..87d557b57a6a7 100644 --- a/x-pack/plugins/cross_cluster_replication/jest.config.js +++ b/x-pack/plugins/cross_cluster_replication/jest.config.js @@ -9,4 +9,10 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/cross_cluster_replication'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/cross_cluster_replication', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/cross_cluster_replication/{common,public,server}/**/*.{js,ts,tsx}', + ], }; diff --git a/x-pack/plugins/dashboard_enhanced/jest.config.js b/x-pack/plugins/dashboard_enhanced/jest.config.js index 8ade9e0a09f5e..86ae949431e69 100644 --- a/x-pack/plugins/dashboard_enhanced/jest.config.js +++ b/x-pack/plugins/dashboard_enhanced/jest.config.js @@ -9,4 +9,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/dashboard_enhanced'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/dashboard_enhanced', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/dashboard_enhanced/{common,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/x-pack/plugins/data_enhanced/jest.config.js b/x-pack/plugins/data_enhanced/jest.config.js index 62b54b1ba36bc..e48de352c2075 100644 --- a/x-pack/plugins/data_enhanced/jest.config.js +++ b/x-pack/plugins/data_enhanced/jest.config.js @@ -9,4 +9,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/data_enhanced'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/data_enhanced', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/data_enhanced/{common,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/x-pack/plugins/data_visualizer/jest.config.js b/x-pack/plugins/data_visualizer/jest.config.js index 1c4974471bd79..46de590b709bb 100644 --- a/x-pack/plugins/data_visualizer/jest.config.js +++ b/x-pack/plugins/data_visualizer/jest.config.js @@ -9,4 +9,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/data_visualizer'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/data_visualizer', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/data_visualizer/{common,public,server}/**/*.{js,ts,tsx}', + ], }; diff --git a/x-pack/plugins/discover_enhanced/jest.config.js b/x-pack/plugins/discover_enhanced/jest.config.js index 7ef1b0e858ef1..603298832dd61 100644 --- a/x-pack/plugins/discover_enhanced/jest.config.js +++ b/x-pack/plugins/discover_enhanced/jest.config.js @@ -9,4 +9,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/discover_enhanced'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/discover_enhanced', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/discover_enhanced/{common,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/x-pack/plugins/drilldowns/jest.config.js b/x-pack/plugins/drilldowns/jest.config.js index 39259ca43b3b8..a5db93a916ebb 100644 --- a/x-pack/plugins/drilldowns/jest.config.js +++ b/x-pack/plugins/drilldowns/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/drilldowns'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/drilldowns', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/drilldowns/url_drilldown/public/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/embeddable_enhanced/jest.config.js b/x-pack/plugins/embeddable_enhanced/jest.config.js index 47b539ca226cd..fc9eaec5c972c 100644 --- a/x-pack/plugins/embeddable_enhanced/jest.config.js +++ b/x-pack/plugins/embeddable_enhanced/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/embeddable_enhanced'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/embeddable_enhanced', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/embeddable_enhanced/public/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/encrypted_saved_objects/jest.config.js b/x-pack/plugins/encrypted_saved_objects/jest.config.js index 213484c8e159d..78bfae78227db 100644 --- a/x-pack/plugins/encrypted_saved_objects/jest.config.js +++ b/x-pack/plugins/encrypted_saved_objects/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/encrypted_saved_objects'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/encrypted_saved_objects', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/encrypted_saved_objects/server/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/enterprise_search/jest.config.js b/x-pack/plugins/enterprise_search/jest.config.js index 7d10d7aa87bf2..263713697b7e0 100644 --- a/x-pack/plugins/enterprise_search/jest.config.js +++ b/x-pack/plugins/enterprise_search/jest.config.js @@ -10,11 +10,12 @@ module.exports = { rootDir: '../../..', roots: ['/x-pack/plugins/enterprise_search'], collectCoverage: true, - coverageReporters: ['text'], + coverageReporters: ['text', 'html'], collectCoverageFrom: [ '/x-pack/plugins/enterprise_search/**/*.{ts,tsx}', '!/x-pack/plugins/enterprise_search/public/*.ts', '!/x-pack/plugins/enterprise_search/server/*.ts', '!/x-pack/plugins/enterprise_search/public/applications/test_helpers/**/*.{ts,tsx}', ], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/enterprise_search', }; diff --git a/x-pack/plugins/event_log/jest.config.js b/x-pack/plugins/event_log/jest.config.js index de892b297deb9..c8bf86db09e0f 100644 --- a/x-pack/plugins/event_log/jest.config.js +++ b/x-pack/plugins/event_log/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/event_log'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/event_log', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/event_log/{common,server}/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/features/jest.config.js b/x-pack/plugins/features/jest.config.js index c39da2a15d2a2..7d333047c491f 100644 --- a/x-pack/plugins/features/jest.config.js +++ b/x-pack/plugins/features/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/features'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/features', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/features/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/file_upload/jest.config.js b/x-pack/plugins/file_upload/jest.config.js index b9a58c259b317..92b3c27c74f11 100644 --- a/x-pack/plugins/file_upload/jest.config.js +++ b/x-pack/plugins/file_upload/jest.config.js @@ -9,4 +9,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/file_upload'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/file_upload', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/file_upload/{common,public,server}/**/*.{js,ts,tsx}', + ], }; diff --git a/x-pack/plugins/fleet/jest.config.js b/x-pack/plugins/fleet/jest.config.js index f55b9b45140bf..5443318d52c8d 100644 --- a/x-pack/plugins/fleet/jest.config.js +++ b/x-pack/plugins/fleet/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/fleet'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/fleet', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/fleet/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/global_search/jest.config.js b/x-pack/plugins/global_search/jest.config.js index ed945fcb8e605..ba4c8130c375c 100644 --- a/x-pack/plugins/global_search/jest.config.js +++ b/x-pack/plugins/global_search/jest.config.js @@ -9,4 +9,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/global_search'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/global_search', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/global_search/{common,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/x-pack/plugins/global_search_bar/jest.config.js b/x-pack/plugins/global_search_bar/jest.config.js index 73cf5402a83a9..e00903df125c9 100644 --- a/x-pack/plugins/global_search_bar/jest.config.js +++ b/x-pack/plugins/global_search_bar/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/global_search_bar'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/global_search_bar', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/global_search_bar/public/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/global_search_providers/jest.config.js b/x-pack/plugins/global_search_providers/jest.config.js index b45fb5cdaa401..231b444585b03 100644 --- a/x-pack/plugins/global_search_providers/jest.config.js +++ b/x-pack/plugins/global_search_providers/jest.config.js @@ -9,4 +9,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/global_search_providers'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/global_search_providers', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/global_search_providers/{public,server}/**/*.{ts,tsx}', + ], }; diff --git a/x-pack/plugins/graph/jest.config.js b/x-pack/plugins/graph/jest.config.js index 8a4f3db30b04a..bd9c9d0938686 100644 --- a/x-pack/plugins/graph/jest.config.js +++ b/x-pack/plugins/graph/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/graph'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/graph', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/graph/{common,public,server}/**/*.{js,ts,tsx}'], }; diff --git a/x-pack/plugins/grokdebugger/jest.config.js b/x-pack/plugins/grokdebugger/jest.config.js index 3785cec03b410..56cd0339a7afd 100644 --- a/x-pack/plugins/grokdebugger/jest.config.js +++ b/x-pack/plugins/grokdebugger/jest.config.js @@ -9,4 +9,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/grokdebugger'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/grokdebugger', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/grokdebugger/{common,public,server}/**/*.{js,ts,tsx}', + ], }; diff --git a/x-pack/plugins/index_lifecycle_management/jest.config.js b/x-pack/plugins/index_lifecycle_management/jest.config.js index 94bbdbdc71e74..ec594e214106d 100644 --- a/x-pack/plugins/index_lifecycle_management/jest.config.js +++ b/x-pack/plugins/index_lifecycle_management/jest.config.js @@ -9,4 +9,10 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/index_lifecycle_management'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/index_lifecycle_management', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/index_lifecycle_management/{common,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/x-pack/plugins/index_management/jest.config.js b/x-pack/plugins/index_management/jest.config.js index 8c7eef134d9bf..8cd0af1f81147 100644 --- a/x-pack/plugins/index_management/jest.config.js +++ b/x-pack/plugins/index_management/jest.config.js @@ -9,4 +9,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/index_management'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/index_management', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/index_management/{common,public,server}/**/*.{js,ts,tsx}', + ], }; diff --git a/x-pack/plugins/infra/jest.config.js b/x-pack/plugins/infra/jest.config.js index ccc2f5b693a20..5631bc25a1452 100644 --- a/x-pack/plugins/infra/jest.config.js +++ b/x-pack/plugins/infra/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/infra'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/infra', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/infra/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/ingest_pipelines/jest.config.js b/x-pack/plugins/ingest_pipelines/jest.config.js index ba22288685801..e3e76e54a610d 100644 --- a/x-pack/plugins/ingest_pipelines/jest.config.js +++ b/x-pack/plugins/ingest_pipelines/jest.config.js @@ -9,4 +9,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/ingest_pipelines'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/ingest_pipelines', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/ingest_pipelines/{common,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/x-pack/plugins/lens/jest.config.js b/x-pack/plugins/lens/jest.config.js index 615e540eaedce..f1164df4eab86 100644 --- a/x-pack/plugins/lens/jest.config.js +++ b/x-pack/plugins/lens/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/lens'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/lens', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/lens/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/license_api_guard/jest.config.js b/x-pack/plugins/license_api_guard/jest.config.js index e0f348ceabd85..c6c1bc1bd501a 100644 --- a/x-pack/plugins/license_api_guard/jest.config.js +++ b/x-pack/plugins/license_api_guard/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/license_api_guard'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/license_api_guard', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/license_api_guard/server/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/license_management/jest.config.js b/x-pack/plugins/license_management/jest.config.js index b0ce5947f3cec..59634448ee26c 100644 --- a/x-pack/plugins/license_management/jest.config.js +++ b/x-pack/plugins/license_management/jest.config.js @@ -9,4 +9,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/license_management'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/license_management', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/license_management/{common,public,server}/**/*.{js,ts,tsx}', + ], }; diff --git a/x-pack/plugins/licensing/jest.config.js b/x-pack/plugins/licensing/jest.config.js index d497f6c7fb05b..5c5276534ebed 100644 --- a/x-pack/plugins/licensing/jest.config.js +++ b/x-pack/plugins/licensing/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/licensing'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/licensing', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/licensing/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/lists/jest.config.js b/x-pack/plugins/lists/jest.config.js index c05b17f57cf7e..cb9832920183f 100644 --- a/x-pack/plugins/lists/jest.config.js +++ b/x-pack/plugins/lists/jest.config.js @@ -6,6 +6,9 @@ */ module.exports = { + collectCoverageFrom: ['/x-pack/plugins/lists/{common,public,server}/**/*.{ts,tsx}'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/lists', + coverageReporters: ['text', 'html'], preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/lists'], diff --git a/x-pack/plugins/logstash/jest.config.js b/x-pack/plugins/logstash/jest.config.js index 98d7f8ccae9c7..7296aa46adbb0 100644 --- a/x-pack/plugins/logstash/jest.config.js +++ b/x-pack/plugins/logstash/jest.config.js @@ -9,4 +9,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/logstash'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/logstash', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/logstash/{common,public,server}/**/*.{js,ts,tsx}', + ], }; diff --git a/x-pack/plugins/maps/jest.config.js b/x-pack/plugins/maps/jest.config.js index 9e620095af880..c9bd7bf4cd0d4 100644 --- a/x-pack/plugins/maps/jest.config.js +++ b/x-pack/plugins/maps/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/maps'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/maps', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/maps/{common,public,server}/**/*.{js,ts,tsx}'], }; diff --git a/x-pack/plugins/metrics_entities/jest.config.js b/x-pack/plugins/metrics_entities/jest.config.js index 402532aa44c41..98a391223cc0f 100644 --- a/x-pack/plugins/metrics_entities/jest.config.js +++ b/x-pack/plugins/metrics_entities/jest.config.js @@ -6,6 +6,9 @@ */ module.exports = { + collectCoverageFrom: ['/x-pack/plugins/metrics_entities/{common,server}/**/*.{ts,tsx}'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/metrics_entities', + coverageReporters: ['text', 'html'], preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/metrics_entities'], diff --git a/x-pack/plugins/ml/jest.config.js b/x-pack/plugins/ml/jest.config.js index 2d162c1bdcb93..463fb9fb856cb 100644 --- a/x-pack/plugins/ml/jest.config.js +++ b/x-pack/plugins/ml/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/ml'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/ml', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/ml/{common,public,server}/**/*.{js,ts,tsx}'], }; diff --git a/x-pack/plugins/monitoring/jest.config.js b/x-pack/plugins/monitoring/jest.config.js index 76b32a2409d3a..772b1ff1e5810 100644 --- a/x-pack/plugins/monitoring/jest.config.js +++ b/x-pack/plugins/monitoring/jest.config.js @@ -9,4 +9,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/monitoring'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/monitoring', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/monitoring/{common,public,server}/**/*.{js,ts,tsx}', + ], }; diff --git a/x-pack/plugins/observability/jest.config.js b/x-pack/plugins/observability/jest.config.js index 6fdeab06df053..39d8067d0efe4 100644 --- a/x-pack/plugins/observability/jest.config.js +++ b/x-pack/plugins/observability/jest.config.js @@ -10,4 +10,9 @@ module.exports = { rootDir: '../../..', roots: ['/x-pack/plugins/observability'], setupFiles: ['/x-pack/plugins/observability/.storybook/jest_setup.js'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/observability', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/observability/{common,public,server}/**/*.{js,ts,tsx}', + ], }; diff --git a/x-pack/plugins/osquery/jest.config.js b/x-pack/plugins/osquery/jest.config.js index 7158ccc51aaf9..ea070e615b6bc 100644 --- a/x-pack/plugins/osquery/jest.config.js +++ b/x-pack/plugins/osquery/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/osquery'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/osquery', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/osquery/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/painless_lab/jest.config.js b/x-pack/plugins/painless_lab/jest.config.js index 2ca58ad18fc2d..364e9e5a08fa4 100644 --- a/x-pack/plugins/painless_lab/jest.config.js +++ b/x-pack/plugins/painless_lab/jest.config.js @@ -9,4 +9,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/painless_lab'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/painless_lab', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/painless_lab/{common,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/x-pack/plugins/remote_clusters/jest.config.js b/x-pack/plugins/remote_clusters/jest.config.js index 50c5195cdc16a..3467ea1d5b405 100644 --- a/x-pack/plugins/remote_clusters/jest.config.js +++ b/x-pack/plugins/remote_clusters/jest.config.js @@ -9,4 +9,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/remote_clusters'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/remote_clusters', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/remote_clusters/{common,public,server}/**/*.{js,ts,tsx}', + ], }; diff --git a/x-pack/plugins/reporting/jest.config.js b/x-pack/plugins/reporting/jest.config.js index 1968c565934a9..4354377fc26cd 100644 --- a/x-pack/plugins/reporting/jest.config.js +++ b/x-pack/plugins/reporting/jest.config.js @@ -9,4 +9,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/reporting'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/reporting', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/reporting/{common,public,server}/**/*.{js,ts,tsx}', + ], }; diff --git a/x-pack/plugins/rollup/jest.config.js b/x-pack/plugins/rollup/jest.config.js index 5566868f65952..937ff3524f9d6 100644 --- a/x-pack/plugins/rollup/jest.config.js +++ b/x-pack/plugins/rollup/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/rollup'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/rollup', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/rollup/{common,public,server}/**/*.{js,ts,tsx}'], }; diff --git a/x-pack/plugins/rule_registry/jest.config.js b/x-pack/plugins/rule_registry/jest.config.js index df8ac522e4b5d..463204b32e29f 100644 --- a/x-pack/plugins/rule_registry/jest.config.js +++ b/x-pack/plugins/rule_registry/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/rule_registry'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/rule_registry', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/rule_registry/{common,server}/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/runtime_fields/jest.config.js b/x-pack/plugins/runtime_fields/jest.config.js index 5c4b71d99224b..3af90818280cf 100644 --- a/x-pack/plugins/runtime_fields/jest.config.js +++ b/x-pack/plugins/runtime_fields/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/runtime_fields'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/runtime_fields', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/runtime_fields/public/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/saved_objects_tagging/jest.config.js b/x-pack/plugins/saved_objects_tagging/jest.config.js index a2bf58c4c3002..2a2d0371267e3 100644 --- a/x-pack/plugins/saved_objects_tagging/jest.config.js +++ b/x-pack/plugins/saved_objects_tagging/jest.config.js @@ -9,4 +9,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/saved_objects_tagging'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/saved_objects_tagging', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/saved_objects_tagging/{common,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/x-pack/plugins/searchprofiler/jest.config.js b/x-pack/plugins/searchprofiler/jest.config.js index e6f3cd3744f18..3071f75c18d96 100644 --- a/x-pack/plugins/searchprofiler/jest.config.js +++ b/x-pack/plugins/searchprofiler/jest.config.js @@ -9,4 +9,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/searchprofiler'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/searchprofiler', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/searchprofiler/{common,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/x-pack/plugins/security/jest.config.js b/x-pack/plugins/security/jest.config.js index e31b17583ffca..bc9a651eb78bf 100644 --- a/x-pack/plugins/security/jest.config.js +++ b/x-pack/plugins/security/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/security'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/security', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/security/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/security_solution/jest.config.js b/x-pack/plugins/security_solution/jest.config.js index 700eaebf6c202..6cfcb65bb5d68 100644 --- a/x-pack/plugins/security_solution/jest.config.js +++ b/x-pack/plugins/security_solution/jest.config.js @@ -9,4 +9,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/security_solution'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/security_solution', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/security_solution/{common,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/x-pack/plugins/snapshot_restore/jest.config.js b/x-pack/plugins/snapshot_restore/jest.config.js index f1c9213047ad5..b7dbd6f6c5786 100644 --- a/x-pack/plugins/snapshot_restore/jest.config.js +++ b/x-pack/plugins/snapshot_restore/jest.config.js @@ -9,4 +9,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/snapshot_restore'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/snapshot_restore', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/snapshot_restore/{common,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/x-pack/plugins/spaces/jest.config.js b/x-pack/plugins/spaces/jest.config.js index 4629d714d6d69..791f82af86ce0 100644 --- a/x-pack/plugins/spaces/jest.config.js +++ b/x-pack/plugins/spaces/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/spaces'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/spaces', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/spaces/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/stack_alerts/jest.config.js b/x-pack/plugins/stack_alerts/jest.config.js index 3d03438e02c09..f2f53fd9e0050 100644 --- a/x-pack/plugins/stack_alerts/jest.config.js +++ b/x-pack/plugins/stack_alerts/jest.config.js @@ -9,4 +9,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/stack_alerts'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/stack_alerts', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/stack_alerts/{common,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/x-pack/plugins/task_manager/jest.config.js b/x-pack/plugins/task_manager/jest.config.js index 49293eb21153a..416709552bd37 100644 --- a/x-pack/plugins/task_manager/jest.config.js +++ b/x-pack/plugins/task_manager/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/task_manager'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/task_manager', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/task_manager/server/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/telemetry_collection_xpack/jest.config.js b/x-pack/plugins/telemetry_collection_xpack/jest.config.js index 1f8e83ab2d86e..94f0906c2149e 100644 --- a/x-pack/plugins/telemetry_collection_xpack/jest.config.js +++ b/x-pack/plugins/telemetry_collection_xpack/jest.config.js @@ -9,4 +9,8 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/telemetry_collection_xpack'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/telemetry_collection_xpack', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/telemetry_collection_xpack/server/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/timelines/jest.config.js b/x-pack/plugins/timelines/jest.config.js index 12bc67dbb2f07..d26134d924e1d 100644 --- a/x-pack/plugins/timelines/jest.config.js +++ b/x-pack/plugins/timelines/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/timelines'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/timelines', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/timelines/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/transform/jest.config.js b/x-pack/plugins/transform/jest.config.js index 6b6a57d433392..2732cd66e2c94 100644 --- a/x-pack/plugins/transform/jest.config.js +++ b/x-pack/plugins/transform/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/transform'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/transform', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/transform/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/triggers_actions_ui/jest.config.js b/x-pack/plugins/triggers_actions_ui/jest.config.js index 38bced1dafb43..f6369b6b2b97e 100644 --- a/x-pack/plugins/triggers_actions_ui/jest.config.js +++ b/x-pack/plugins/triggers_actions_ui/jest.config.js @@ -9,4 +9,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/triggers_actions_ui'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/triggers_actions_ui', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/triggers_actions_ui/{common,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/x-pack/plugins/ui_actions_enhanced/jest.config.js b/x-pack/plugins/ui_actions_enhanced/jest.config.js index dd27310084e0d..8182b23b2ebe9 100644 --- a/x-pack/plugins/ui_actions_enhanced/jest.config.js +++ b/x-pack/plugins/ui_actions_enhanced/jest.config.js @@ -9,4 +9,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/ui_actions_enhanced'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/ui_actions_enhanced', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/ui_actions_enhanced/{common,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/x-pack/plugins/upgrade_assistant/jest.config.js b/x-pack/plugins/upgrade_assistant/jest.config.js index 65043acd878da..024cb4a49ab3d 100644 --- a/x-pack/plugins/upgrade_assistant/jest.config.js +++ b/x-pack/plugins/upgrade_assistant/jest.config.js @@ -9,4 +9,9 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/upgrade_assistant'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/upgrade_assistant', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/upgrade_assistant/{common,public,server}/**/*.{ts,tsx}', + ], }; diff --git a/x-pack/plugins/uptime/jest.config.js b/x-pack/plugins/uptime/jest.config.js index 9e97154843770..baafb5ce133e6 100644 --- a/x-pack/plugins/uptime/jest.config.js +++ b/x-pack/plugins/uptime/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/uptime'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/uptime', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/uptime/{common,public,server}/**/*.{ts,tsx}'], }; diff --git a/x-pack/plugins/watcher/jest.config.js b/x-pack/plugins/watcher/jest.config.js index d5189ae920637..b078f2ab665d7 100644 --- a/x-pack/plugins/watcher/jest.config.js +++ b/x-pack/plugins/watcher/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/watcher'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/watcher', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/watcher/{common,public,server}/**/*.{js,ts,tsx}'], }; diff --git a/x-pack/plugins/xpack_legacy/jest.config.js b/x-pack/plugins/xpack_legacy/jest.config.js index 98d2dd0aa824c..5ad0fa36264d1 100644 --- a/x-pack/plugins/xpack_legacy/jest.config.js +++ b/x-pack/plugins/xpack_legacy/jest.config.js @@ -9,4 +9,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/x-pack/plugins/xpack_legacy'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/xpack_legacy', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/xpack_legacy/server/**/*.{ts,tsx}'], }; From 275d8c38b223dcbda0d19eba1ed70ed4daac0a73 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 9 Sep 2021 05:00:53 -0400 Subject: [PATCH 035/139] [Integrations] Fixed wording (#111519) (#111678) for integration upgrade Co-authored-by: juliaElastic <90178898+juliaElastic@users.noreply.github.com> --- .../fleet/public/components/package_policy_actions_menu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx index a87cd7e39bfb2..2030f57764756 100644 --- a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx +++ b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx @@ -87,7 +87,7 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{ > , // FIXME: implement Copy package policy action From 5c9ad9a81b3a7f9dba700c5358cf9eb93d18d94f Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 9 Sep 2021 11:33:42 +0100 Subject: [PATCH 036/139] [7.x] [ML] Datafeed preview based job validation check (#111684) --- .../ml/common/constants/messages.test.mock.ts | 3 + .../ml/common/constants/messages.test.ts | 6 + .../plugins/ml/common/constants/messages.ts | 24 +++ .../components/validation_step/validation.tsx | 6 +- .../job_validation/job_validation.test.ts | 150 ++++++++++++++---- .../models/job_validation/job_validation.ts | 9 +- .../validate_datafeed_preview.ts | 38 +++++ .../ml/server/routes/job_validation.ts | 6 +- .../server/routes/schemas/datafeeds_schema.ts | 2 +- .../apis/ml/job_validation/validate.ts | 2 +- .../ml/anomaly_detection/date_nanos_job.ts | 3 +- 11 files changed, 209 insertions(+), 40 deletions(-) create mode 100644 x-pack/plugins/ml/server/models/job_validation/validate_datafeed_preview.ts diff --git a/x-pack/plugins/ml/common/constants/messages.test.mock.ts b/x-pack/plugins/ml/common/constants/messages.test.mock.ts index 6e539617604c1..fbfff20adc5c2 100644 --- a/x-pack/plugins/ml/common/constants/messages.test.mock.ts +++ b/x-pack/plugins/ml/common/constants/messages.test.mock.ts @@ -78,4 +78,7 @@ export const nonBasicIssuesMessages = [ { id: 'missing_summary_count_field_name', }, + { + id: 'datafeed_preview_failed', + }, ]; diff --git a/x-pack/plugins/ml/common/constants/messages.test.ts b/x-pack/plugins/ml/common/constants/messages.test.ts index 1141eea2c176d..6ac6d65f6f5c6 100644 --- a/x-pack/plugins/ml/common/constants/messages.test.ts +++ b/x-pack/plugins/ml/common/constants/messages.test.ts @@ -173,6 +173,12 @@ describe('Constants: Messages parseMessages()', () => { text: 'A job configured with a datafeed with aggregations must set summary_count_field_name; use doc_count or suitable alternative.', }, + { + id: 'datafeed_preview_failed', + status: 'error', + text: + 'The datafeed preview failed. This may be due to an error in the job or datafeed configurations.', + }, ]); }); }); diff --git a/x-pack/plugins/ml/common/constants/messages.ts b/x-pack/plugins/ml/common/constants/messages.ts index 0327e8746c7d8..fd3b9aa9d19b9 100644 --- a/x-pack/plugins/ml/common/constants/messages.ts +++ b/x-pack/plugins/ml/common/constants/messages.ts @@ -626,6 +626,30 @@ export const getMessages = once((docLinks?: DocLinksStart) => { 'the UNIX epoch beginning. Timestamps before 01/01/1970 00:00:00 (UTC) are not supported for machine learning jobs.', }), }, + datafeed_preview_no_documents: { + status: VALIDATION_STATUS.WARNING, + heading: i18n.translate( + 'xpack.ml.models.jobValidation.messages.datafeedPreviewNoDocumentsHeading', + { + defaultMessage: 'Datafeed preview', + } + ), + text: i18n.translate( + 'xpack.ml.models.jobValidation.messages.datafeedPreviewNoDocumentsMessage', + { + defaultMessage: + 'Running the datafeed preview over the current job configuration produces no results. ' + + 'If the index contains no documents this warning can be ignored, otherwise the job may be misconfigured.', + } + ), + }, + datafeed_preview_failed: { + status: VALIDATION_STATUS.ERROR, + text: i18n.translate('xpack.ml.models.jobValidation.messages.datafeedPreviewFailedMessage', { + defaultMessage: + 'The datafeed preview failed. This may be due to an error in the job or datafeed configurations.', + }), + }, }; }); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx index a1c7eab6b746f..d2a0e83200d92 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, FC, useContext, useEffect } from 'react'; +import React, { Fragment, FC, useContext, useEffect, useState } from 'react'; import { WizardNav } from '../wizard_nav'; import { WIZARD_STEPS, StepProps } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; @@ -22,6 +22,7 @@ const idFilterList = [ export const ValidationStep: FC = ({ setCurrentStep, isCurrentStep }) => { const { jobCreator, jobCreatorUpdate, jobValidator } = useContext(JobCreatorContext); + const [nextActive, setNextActive] = useState(false); if (jobCreator.type === JOB_TYPE.ADVANCED) { // for advanced jobs, ignore time range warning as the @@ -52,6 +53,7 @@ export const ValidationStep: FC = ({ setCurrentStep, isCurrentStep }) // keep a record of the advanced validation in the jobValidator function setIsValid(valid: boolean) { jobValidator.advancedValid = valid; + setNextActive(valid); } return ( @@ -69,7 +71,7 @@ export const ValidationStep: FC = ({ setCurrentStep, isCurrentStep }) setCurrentStep(WIZARD_STEPS.JOB_DETAILS)} next={() => setCurrentStep(WIZARD_STEPS.SUMMARY)} - nextActive={true} + nextActive={nextActive} /> )} diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts index a5483491f1357..e890020eb726d 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts @@ -10,6 +10,7 @@ import { IScopedClusterClient } from 'kibana/server'; import { validateJob, ValidateJobPayload } from './job_validation'; import { ES_CLIENT_TOTAL_HITS_RELATION } from '../../../common/types/es_client'; import type { MlClient } from '../../lib/ml_client'; +import type { AuthorizationHeader } from '../../lib/request_authorization'; const callAs = { fieldCaps: () => Promise.resolve({ body: { fields: [] } }), @@ -19,6 +20,8 @@ const callAs = { }), }; +const authHeader: AuthorizationHeader = {}; + const mlClusterClient = ({ asCurrentUser: callAs, asInternalUser: callAs, @@ -34,18 +37,19 @@ const mlClient = ({ }, }, }), + previewDatafeed: () => Promise.resolve({ body: [{}] }), } as unknown) as MlClient; // Note: The tests cast `payload` as any // so we can simulate possible runtime payloads // that don't satisfy the TypeScript specs. describe('ML - validateJob', () => { - it('basic validation messages', () => { + it('basic validation messages', async () => { const payload = ({ job: { analysis_config: { detectors: [] } }, } as unknown) as ValidateJobPayload; - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids).toStrictEqual([ @@ -58,14 +62,14 @@ describe('ML - validateJob', () => { }); const jobIdTests = (testIds: string[], messageId: string) => { - const promises = testIds.map((id) => { + const promises = testIds.map(async (id) => { const payload = ({ job: { analysis_config: { detectors: [] }, job_id: id, }, } as unknown) as ValidateJobPayload; - return validateJob(mlClusterClient, mlClient, payload).catch(() => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).catch(() => { new Error('Promise should not fail for jobIdTests.'); }); }); @@ -86,7 +90,7 @@ describe('ML - validateJob', () => { job: { analysis_config: { detectors: [] }, groups: testIds }, } as unknown) as ValidateJobPayload; - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids.includes(messageId)).toBe(true); }); @@ -126,7 +130,7 @@ describe('ML - validateJob', () => { const payload = ({ job: { analysis_config: { bucket_span: format, detectors: [] } }, } as unknown) as ValidateJobPayload; - return validateJob(mlClusterClient, mlClient, payload).catch(() => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).catch(() => { new Error('Promise should not fail for bucketSpanFormatTests.'); }); }); @@ -150,7 +154,7 @@ describe('ML - validateJob', () => { return bucketSpanFormatTests(validBucketSpanFormats, 'bucket_span_valid'); }); - it('at least one detector function is empty', () => { + it('at least one detector function is empty', async () => { const payload = ({ job: { analysis_config: { detectors: [] as Array<{ function?: string }> } }, } as unknown) as ValidateJobPayload; @@ -165,13 +169,13 @@ describe('ML - validateJob', () => { function: undefined, }); - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids.includes('detectors_function_empty')).toBe(true); }); }); - it('detector function is not empty', () => { + it('detector function is not empty', async () => { const payload = ({ job: { analysis_config: { detectors: [] as Array<{ function?: string }> } }, } as unknown) as ValidateJobPayload; @@ -179,37 +183,37 @@ describe('ML - validateJob', () => { function: 'count', }); - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids.includes('detectors_function_not_empty')).toBe(true); }); }); - it('invalid index fields', () => { + it('invalid index fields', async () => { const payload = ({ job: { analysis_config: { detectors: [] } }, fields: {}, } as unknown) as ValidateJobPayload; - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids.includes('index_fields_invalid')).toBe(true); }); }); - it('valid index fields', () => { + it('valid index fields', async () => { const payload = ({ job: { analysis_config: { detectors: [] } }, fields: { testField: {} }, } as unknown) as ValidateJobPayload; - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids.includes('index_fields_valid')).toBe(true); }); }); - const getBasicPayload = (): any => ({ + const getBasicPayload = (): ValidateJobPayload => ({ job: { job_id: 'test', analysis_config: { @@ -231,7 +235,7 @@ describe('ML - validateJob', () => { const payload = getBasicPayload() as any; delete payload.job.analysis_config.influencers; - validateJob(mlClusterClient, mlClient, payload).then( + validateJob(mlClusterClient, mlClient, payload, authHeader).then( () => done( new Error('Promise should not resolve for this test when influencers is not an Array.') @@ -240,10 +244,10 @@ describe('ML - validateJob', () => { ); }); - it('detect duplicate detectors', () => { + it('detect duplicate detectors', async () => { const payload = getBasicPayload() as any; payload.job.analysis_config.detectors.push({ function: 'count' }); - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids).toStrictEqual([ 'job_id_valid', @@ -256,7 +260,7 @@ describe('ML - validateJob', () => { }); }); - it('dedupe duplicate messages', () => { + it('dedupe duplicate messages', async () => { const payload = getBasicPayload() as any; // in this test setup, the following configuration passes // the duplicate detectors check, but would return the same @@ -266,7 +270,7 @@ describe('ML - validateJob', () => { { function: 'count', by_field_name: 'airline' }, { function: 'count', partition_field_name: 'airline' }, ]; - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids).toStrictEqual([ 'job_id_valid', @@ -278,9 +282,9 @@ describe('ML - validateJob', () => { }); }); - it('basic validation passes, extended checks return some messages', () => { + it('basic validation passes, extended checks return some messages', async () => { const payload = getBasicPayload(); - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids).toStrictEqual([ 'job_id_valid', @@ -291,8 +295,8 @@ describe('ML - validateJob', () => { }); }); - it('categorization job using mlcategory passes aggregatable field check', () => { - const payload: any = { + it('categorization job using mlcategory passes aggregatable field check', async () => { + const payload: ValidateJobPayload = { job: { job_id: 'categorization_test', analysis_config: { @@ -312,7 +316,7 @@ describe('ML - validateJob', () => { fields: { testField: {} }, }; - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids).toStrictEqual([ 'job_id_valid', @@ -325,8 +329,8 @@ describe('ML - validateJob', () => { }); }); - it('non-existent field reported as non aggregatable', () => { - const payload: any = { + it('non-existent field reported as non aggregatable', async () => { + const payload: ValidateJobPayload = { job: { job_id: 'categorization_test', analysis_config: { @@ -345,7 +349,7 @@ describe('ML - validateJob', () => { fields: { testField: {} }, }; - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids).toStrictEqual([ 'job_id_valid', @@ -357,8 +361,8 @@ describe('ML - validateJob', () => { }); }); - it('script field not reported as non aggregatable', () => { - const payload: any = { + it('script field not reported as non aggregatable', async () => { + const payload: ValidateJobPayload = { job: { job_id: 'categorization_test', analysis_config: { @@ -387,7 +391,7 @@ describe('ML - validateJob', () => { fields: { testField: {} }, }; - return validateJob(mlClusterClient, mlClient, payload).then((messages) => { + return validateJob(mlClusterClient, mlClient, payload, authHeader).then((messages) => { const ids = messages.map((m) => m.id); expect(ids).toStrictEqual([ 'job_id_valid', @@ -399,4 +403,88 @@ describe('ML - validateJob', () => { ]); }); }); + + it('datafeed preview contains no docs', async () => { + const payload: ValidateJobPayload = { + job: { + job_id: 'categorization_test', + analysis_config: { + bucket_span: '15m', + detectors: [ + { + function: 'count', + partition_field_name: 'custom_script_field', + }, + ], + influencers: [''], + }, + data_description: { time_field: '@timestamp' }, + datafeed_config: { + indices: [], + }, + }, + fields: { testField: {} }, + }; + + const mlClientEmptyDatafeedPreview = ({ + ...mlClient, + previewDatafeed: () => Promise.resolve({ body: [] }), + } as unknown) as MlClient; + + return validateJob(mlClusterClient, mlClientEmptyDatafeedPreview, payload, authHeader).then( + (messages) => { + const ids = messages.map((m) => m.id); + expect(ids).toStrictEqual([ + 'job_id_valid', + 'detectors_function_not_empty', + 'index_fields_valid', + 'field_not_aggregatable', + 'time_field_invalid', + 'datafeed_preview_no_documents', + ]); + } + ); + }); + + it('datafeed preview failed', async () => { + const payload: ValidateJobPayload = { + job: { + job_id: 'categorization_test', + analysis_config: { + bucket_span: '15m', + detectors: [ + { + function: 'count', + partition_field_name: 'custom_script_field', + }, + ], + influencers: [''], + }, + data_description: { time_field: '@timestamp' }, + datafeed_config: { + indices: [], + }, + }, + fields: { testField: {} }, + }; + + const mlClientEmptyDatafeedPreview = ({ + ...mlClient, + previewDatafeed: () => Promise.reject({}), + } as unknown) as MlClient; + + return validateJob(mlClusterClient, mlClientEmptyDatafeedPreview, payload, authHeader).then( + (messages) => { + const ids = messages.map((m) => m.id); + expect(ids).toStrictEqual([ + 'job_id_valid', + 'detectors_function_not_empty', + 'index_fields_valid', + 'field_not_aggregatable', + 'time_field_invalid', + 'datafeed_preview_failed', + ]); + } + ); + }); }); diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts index 80eba7b864051..838f188455d44 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts @@ -6,7 +6,7 @@ */ import Boom from '@hapi/boom'; -import { IScopedClusterClient } from 'kibana/server'; +import type { IScopedClusterClient } from 'kibana/server'; import { TypeOf } from '@kbn/config-schema'; import { fieldsServiceProvider } from '../fields_service'; import { getMessages, MessageId, JobValidationMessage } from '../../../common/constants/messages'; @@ -17,12 +17,14 @@ import { basicJobValidation, uniqWithIsEqual } from '../../../common/util/job_ut import { validateBucketSpan } from './validate_bucket_span'; import { validateCardinality } from './validate_cardinality'; import { validateInfluencers } from './validate_influencers'; +import { validateDatafeedPreview } from './validate_datafeed_preview'; import { validateModelMemoryLimit } from './validate_model_memory_limit'; import { validateTimeRange, isValidTimeField } from './validate_time_range'; import { validateJobSchema } from '../../routes/schemas/job_validation_schema'; -import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; +import type { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; import type { MlClient } from '../../lib/ml_client'; import { getDatafeedAggregations } from '../../../common/util/datafeed_utils'; +import type { AuthorizationHeader } from '../../lib/request_authorization'; export type ValidateJobPayload = TypeOf; @@ -34,6 +36,7 @@ export async function validateJob( client: IScopedClusterClient, mlClient: MlClient, payload: ValidateJobPayload, + authHeader: AuthorizationHeader, isSecurityDisabled?: boolean ) { const messages = getMessages(); @@ -107,6 +110,8 @@ export async function validateJob( if (datafeedAggregations !== undefined && !job.analysis_config?.summary_count_field_name) { validationMessages.push({ id: 'missing_summary_count_field_name' }); } + + validationMessages.push(...(await validateDatafeedPreview(mlClient, authHeader, job))); } else { validationMessages = basicValidation.messages; validationMessages.push({ id: 'skipped_extended_tests' }); diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_datafeed_preview.ts b/x-pack/plugins/ml/server/models/job_validation/validate_datafeed_preview.ts new file mode 100644 index 0000000000000..e009dcf49fdab --- /dev/null +++ b/x-pack/plugins/ml/server/models/job_validation/validate_datafeed_preview.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { MlClient } from '../../lib/ml_client'; +import type { AuthorizationHeader } from '../../lib/request_authorization'; +import type { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; +import type { JobValidationMessage } from '../../../common/constants/messages'; + +export async function validateDatafeedPreview( + mlClient: MlClient, + authHeader: AuthorizationHeader, + job: CombinedJob +): Promise { + const { datafeed_config: datafeed, ...tempJob } = job; + try { + const { body } = ((await mlClient.previewDatafeed( + { + body: { + job_config: tempJob, + datafeed_config: datafeed, + }, + }, + authHeader + // previewDatafeed response type is incorrect + )) as unknown) as { body: unknown[] }; + + if (Array.isArray(body) === false || body.length === 0) { + return [{ id: 'datafeed_preview_no_documents' }]; + } + return []; + } catch (error) { + return [{ id: 'datafeed_preview_failed' }]; + } +} diff --git a/x-pack/plugins/ml/server/routes/job_validation.ts b/x-pack/plugins/ml/server/routes/job_validation.ts index 9309592dfc474..b75eab20e7bc0 100644 --- a/x-pack/plugins/ml/server/routes/job_validation.ts +++ b/x-pack/plugins/ml/server/routes/job_validation.ts @@ -8,9 +8,9 @@ import Boom from '@hapi/boom'; import { IScopedClusterClient } from 'kibana/server'; import { TypeOf } from '@kbn/config-schema'; -import { AnalysisConfig, Datafeed } from '../../common/types/anomaly_detection_jobs'; +import type { AnalysisConfig, Datafeed } from '../../common/types/anomaly_detection_jobs'; import { wrapError } from '../client/error_wrapper'; -import { RouteInitialization } from '../types'; +import type { RouteInitialization } from '../types'; import { estimateBucketSpanSchema, modelMemoryLimitSchema, @@ -20,6 +20,7 @@ import { import { estimateBucketSpanFactory } from '../models/bucket_span_estimator'; import { calculateModelMemoryLimitProvider } from '../models/calculate_model_memory_limit'; import { validateJob, validateCardinality } from '../models/job_validation'; +import { getAuthorizationHeader } from '../lib/request_authorization'; import type { MlClient } from '../lib/ml_client'; type CalculateModelMemoryLimitPayload = TypeOf; @@ -192,6 +193,7 @@ export function jobValidationRoutes({ router, mlLicense, routeGuard }: RouteInit client, mlClient, request.body, + getAuthorizationHeader(request), mlLicense.isSecurityEnabled() === false ); diff --git a/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts b/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts index 118d2e4140ced..27e1b6afe3364 100644 --- a/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts @@ -52,7 +52,7 @@ export const datafeedConfigSchema = schema.object({ runtime_mappings: schema.maybe(schema.any()), scroll_size: schema.maybe(schema.number()), delayed_data_check_config: schema.maybe(schema.any()), - indices_options: indicesOptionsSchema, + indices_options: schema.maybe(indicesOptionsSchema), }); export const datafeedIdSchema = schema.object({ datafeedId: schema.string() }); diff --git a/x-pack/test/api_integration/apis/ml/job_validation/validate.ts b/x-pack/test/api_integration/apis/ml/job_validation/validate.ts index 06d966851abfd..293b0e94351d0 100644 --- a/x-pack/test/api_integration/apis/ml/job_validation/validate.ts +++ b/x-pack/test/api_integration/apis/ml/job_validation/validate.ts @@ -184,7 +184,7 @@ export default ({ getService }: FtrProviderContext) => { expect(body.length).to.eql( expectedResponse.length, - `Response body should have ${expectedResponse.length} entries (got ${body})` + `Response body should have ${expectedResponse.length} entries (got ${JSON.stringify(body)})` ); for (const entry of expectedResponse) { const responseEntry = body.find((obj: any) => obj.id === entry.id); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts index d351e8f7057e4..5f8d346ee4473 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts @@ -114,7 +114,8 @@ export default function ({ getService }: FtrProviderContext) { }, ]; - describe('job on data set with date_nanos time field', function () { + // test skipped until https://github.com/elastic/elasticsearch/pull/77109 is fixed + describe.skip('job on data set with date_nanos time field', function () { this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/event_rate_nanos'); From e7309999874f3b7270fd72fd9a7d54b526ccc042 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 9 Sep 2021 06:46:22 -0400 Subject: [PATCH 037/139] Fix focus jumps from Case description box back to Alerts table with first keystroke (#111273) (#111692) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Pablo Machado --- .../public/components/create/form_context.tsx | 15 ++++++++++++++- .../cases/public/components/create/title.tsx | 1 + 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/public/components/create/form_context.tsx b/x-pack/plugins/cases/public/components/create/form_context.tsx index 65c102583455a..f59e1822c70be 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.tsx @@ -113,7 +113,20 @@ export const FormContext: React.FC = ({ : null, [children, connectors, isLoadingConnectors] ); - return
{childrenWithExtraProp}
; + return ( +
{ + // It avoids the focus scaping from the flyout when enter is pressed. + // https://github.com/elastic/kibana/issues/111120 + if (e.key === 'Enter') { + e.stopPropagation(); + } + }} + form={form} + > + {childrenWithExtraProp} +
+ ); }; FormContext.displayName = 'FormContext'; diff --git a/x-pack/plugins/cases/public/components/create/title.tsx b/x-pack/plugins/cases/public/components/create/title.tsx index cc51a805b5c38..ae8f517173132 100644 --- a/x-pack/plugins/cases/public/components/create/title.tsx +++ b/x-pack/plugins/cases/public/components/create/title.tsx @@ -21,6 +21,7 @@ const TitleComponent: React.FC = ({ isLoading }) => ( idAria: 'caseTitle', 'data-test-subj': 'caseTitle', euiFieldProps: { + autoFocus: true, fullWidth: true, disabled: isLoading, }, From 77419a8de056c2f3d624ac5d99ad64fa850d098b Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 9 Sep 2021 06:51:03 -0400 Subject: [PATCH 038/139] [Canvas] `Expression types` and `Function form` refactor. (#107516) (#111693) * Refactored `FormFunction` to ts. * Converted components to ts and added types for expression_types. * Fixed types and refactored `function_component`. * Added types to base_form, function_form, transform, etc. * Arg types added. * Moved model to ts and fixed all types. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Yaroslav Kuznietsov --- .../arg_add_popover/arg_add_popover.tsx | 7 +- .../datasource_component.stories.tsx | 1 - .../components/function_form/function_form.js | 38 ----- .../function_form/function_form.tsx | 60 +++++++ ...mponent.js => function_form_component.tsx} | 30 +--- .../function_form_context_error.tsx | 18 +-- .../function_form_context_pending.js | 46 ------ .../function_form_context_pending.tsx | 47 ++++++ .../public/components/function_form/index.js | 115 -------------- .../public/components/function_form/index.tsx | 150 ++++++++++++++++++ .../canvas/public/expression_types/arg.js | 75 --------- .../canvas/public/expression_types/arg.ts | 145 +++++++++++++++++ .../public/expression_types/arg_type.ts | 33 ++++ .../{arg_type.js => arg_type_registry.ts} | 18 +-- .../{base_form.js => base_form.ts} | 12 +- .../public/expression_types/datasource.js | 76 --------- .../public/expression_types/datasource.tsx | 99 ++++++++++++ .../expression_types/datasource_registry.ts | 18 +++ .../{function_form.js => function_form.tsx} | 106 ++++++++++--- .../canvas/public/expression_types/index.js | 13 -- .../canvas/public/expression_types/index.ts | 23 +++ .../canvas/public/expression_types/model.js | 72 --------- .../canvas/public/expression_types/model.ts | 80 ++++++++++ .../public/expression_types/model_registry.ts | 18 +++ .../public/expression_types/transform.js | 31 ---- .../public/expression_types/transform.ts | 24 +++ .../expression_types/transform_registry.ts | 18 +++ .../canvas/public/expression_types/types.ts | 22 +++ .../canvas/public/expression_types/view.js | 38 ----- .../canvas/public/expression_types/view.ts | 39 +++++ .../public/expression_types/view_registry.ts | 18 +++ ...ession_type.js => find_expression_type.ts} | 14 +- x-pack/plugins/canvas/public/registries.ts | 1 - x-pack/plugins/canvas/types/state.ts | 11 +- 34 files changed, 923 insertions(+), 593 deletions(-) delete mode 100644 x-pack/plugins/canvas/public/components/function_form/function_form.js create mode 100644 x-pack/plugins/canvas/public/components/function_form/function_form.tsx rename x-pack/plugins/canvas/public/components/function_form/{function_form_component.js => function_form_component.tsx} (50%) delete mode 100644 x-pack/plugins/canvas/public/components/function_form/function_form_context_pending.js create mode 100644 x-pack/plugins/canvas/public/components/function_form/function_form_context_pending.tsx delete mode 100644 x-pack/plugins/canvas/public/components/function_form/index.js create mode 100644 x-pack/plugins/canvas/public/components/function_form/index.tsx delete mode 100644 x-pack/plugins/canvas/public/expression_types/arg.js create mode 100644 x-pack/plugins/canvas/public/expression_types/arg.ts create mode 100644 x-pack/plugins/canvas/public/expression_types/arg_type.ts rename x-pack/plugins/canvas/public/expression_types/{arg_type.js => arg_type_registry.ts} (52%) rename x-pack/plugins/canvas/public/expression_types/{base_form.js => base_form.ts} (72%) delete mode 100644 x-pack/plugins/canvas/public/expression_types/datasource.js create mode 100644 x-pack/plugins/canvas/public/expression_types/datasource.tsx create mode 100644 x-pack/plugins/canvas/public/expression_types/datasource_registry.ts rename x-pack/plugins/canvas/public/expression_types/{function_form.js => function_form.tsx} (57%) delete mode 100644 x-pack/plugins/canvas/public/expression_types/index.js create mode 100644 x-pack/plugins/canvas/public/expression_types/index.ts delete mode 100644 x-pack/plugins/canvas/public/expression_types/model.js create mode 100644 x-pack/plugins/canvas/public/expression_types/model.ts create mode 100644 x-pack/plugins/canvas/public/expression_types/model_registry.ts delete mode 100644 x-pack/plugins/canvas/public/expression_types/transform.js create mode 100644 x-pack/plugins/canvas/public/expression_types/transform.ts create mode 100644 x-pack/plugins/canvas/public/expression_types/transform_registry.ts create mode 100644 x-pack/plugins/canvas/public/expression_types/types.ts delete mode 100644 x-pack/plugins/canvas/public/expression_types/view.js create mode 100644 x-pack/plugins/canvas/public/expression_types/view.ts create mode 100644 x-pack/plugins/canvas/public/expression_types/view_registry.ts rename x-pack/plugins/canvas/public/lib/{find_expression_type.js => find_expression_type.ts} (70%) diff --git a/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx b/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx index e1cd5c55393fb..0368cd3d9facf 100644 --- a/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx +++ b/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx @@ -11,8 +11,7 @@ import { EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Popover } from '../popover'; import { ArgAdd } from '../arg_add'; -// @ts-expect-error untyped local -import { Arg } from '../../expression_types/arg'; +import type { Arg } from '../../expression_types/arg'; const strings = { getAddAriaLabel: () => @@ -51,8 +50,8 @@ export const ArgAddPopover: FC = ({ options }) => { options.map((opt) => ( { opt.onValueAdd(); closePopover(); diff --git a/x-pack/plugins/canvas/public/components/datasource/__stories__/datasource_component.stories.tsx b/x-pack/plugins/canvas/public/components/datasource/__stories__/datasource_component.stories.tsx index 27fc9e8871c1f..157b612afbb23 100644 --- a/x-pack/plugins/canvas/public/components/datasource/__stories__/datasource_component.stories.tsx +++ b/x-pack/plugins/canvas/public/components/datasource/__stories__/datasource_component.stories.tsx @@ -12,7 +12,6 @@ import React from 'react'; // @ts-expect-error untyped local import { DatasourceComponent } from '../datasource_component'; import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component'; -// @ts-expect-error untyped local import { Datasource } from '../../../../public/expression_types/datasource'; const TestDatasource = ({ args }: any) => ( diff --git a/x-pack/plugins/canvas/public/components/function_form/function_form.js b/x-pack/plugins/canvas/public/components/function_form/function_form.js deleted file mode 100644 index 3f1bd094286b4..0000000000000 --- a/x-pack/plugins/canvas/public/components/function_form/function_form.js +++ /dev/null @@ -1,38 +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 PropTypes from 'prop-types'; -import { compose, branch, renderComponent } from 'recompose'; -import { FunctionFormComponent } from './function_form_component'; -import { FunctionUnknown } from './function_unknown'; -import { FunctionFormContextPending } from './function_form_context_pending'; -import { FunctionFormContextError } from './function_form_context_error'; - -// helper to check the state of the passed in expression type -function checkState(state) { - return ({ context, expressionType }) => { - const matchState = !context || context.state === state; - return expressionType && expressionType.requiresContext && matchState; - }; -} - -// alternate render paths based on expression state -const branches = [ - // if no expressionType was provided, render the ArgTypeUnknown component - branch((props) => !props.expressionType, renderComponent(FunctionUnknown)), - // if the expressionType is in a pending state, render ArgTypeContextPending - branch(checkState('pending'), renderComponent(FunctionFormContextPending)), - // if the expressionType is in an error state, render ArgTypeContextError - branch(checkState('error'), renderComponent(FunctionFormContextError)), -]; - -export const FunctionForm = compose(...branches)(FunctionFormComponent); - -FunctionForm.propTypes = { - context: PropTypes.object, - expressionType: PropTypes.object, -}; diff --git a/x-pack/plugins/canvas/public/components/function_form/function_form.tsx b/x-pack/plugins/canvas/public/components/function_form/function_form.tsx new file mode 100644 index 0000000000000..abe31f0105108 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/function_form/function_form.tsx @@ -0,0 +1,60 @@ +/* + * 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 { FunctionFormComponent } from './function_form_component'; +import { FunctionUnknown } from './function_unknown'; +import { FunctionFormContextPending } from './function_form_context_pending'; +import { FunctionFormContextError } from './function_form_context_error'; +import { ExpressionContext } from '../../../types'; +import { RenderArgData, ExpressionType } from '../../expression_types/types'; + +type FunctionFormProps = RenderArgData; + +// helper to check the state of the passed in expression type +function is( + state: ExpressionContext['state'], + expressionType: ExpressionType, + context?: ExpressionContext +) { + const matchState = !context || context.state === state; + return expressionType && expressionType.requiresContext && matchState; +} + +export const FunctionForm: React.FunctionComponent = (props) => { + const { expressionType, context } = props; + + if (!expressionType) { + return ; + } + + if (is('pending', expressionType, context)) { + return ( + + ); + } + + if (is('error', expressionType, context)) { + return ( + + ); + } + + return ; +}; diff --git a/x-pack/plugins/canvas/public/components/function_form/function_form_component.js b/x-pack/plugins/canvas/public/components/function_form/function_form_component.tsx similarity index 50% rename from x-pack/plugins/canvas/public/components/function_form/function_form_component.js rename to x-pack/plugins/canvas/public/components/function_form/function_form_component.tsx index fc953c8dde352..8e625db94b498 100644 --- a/x-pack/plugins/canvas/public/components/function_form/function_form_component.js +++ b/x-pack/plugins/canvas/public/components/function_form/function_form_component.tsx @@ -5,11 +5,14 @@ * 2.0. */ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { FunctionComponent } from 'react'; +import { RenderArgData } from '../../expression_types/types'; -export const FunctionFormComponent = (props) => { +type FunctionFormComponentProps = RenderArgData; + +export const FunctionFormComponent: FunctionComponent = (props) => { const passedProps = { + name: props.name, argResolver: props.argResolver, args: props.args, argType: props.argType, @@ -24,27 +27,8 @@ export const FunctionFormComponent = (props) => { onValueAdd: props.onValueAdd, onValueChange: props.onValueChange, onValueRemove: props.onValueRemove, + updateContext: props.updateContext, }; return
{props.expressionType.render(passedProps)}
; }; - -FunctionFormComponent.propTypes = { - // props passed into expression type render functions - argResolver: PropTypes.func.isRequired, - args: PropTypes.object.isRequired, - argType: PropTypes.string.isRequired, - argTypeDef: PropTypes.object.isRequired, - filterGroups: PropTypes.array.isRequired, - context: PropTypes.object, - expressionIndex: PropTypes.number.isRequired, - expressionType: PropTypes.object.isRequired, - nextArgType: PropTypes.string, - nextExpressionType: PropTypes.object, - onAssetAdd: PropTypes.func.isRequired, - onValueAdd: PropTypes.func.isRequired, - onValueChange: PropTypes.func.isRequired, - onValueChange: PropTypes.func.isRequired, - onValueRemove: PropTypes.func.isRequired, - onValueRemove: PropTypes.func.isRequired, -}; diff --git a/x-pack/plugins/canvas/public/components/function_form/function_form_context_error.tsx b/x-pack/plugins/canvas/public/components/function_form/function_form_context_error.tsx index 2ee709edbf91c..88ad97c52a68f 100644 --- a/x-pack/plugins/canvas/public/components/function_form/function_form_context_error.tsx +++ b/x-pack/plugins/canvas/public/components/function_form/function_form_context_error.tsx @@ -6,11 +6,11 @@ */ import React, { FunctionComponent } from 'react'; -import PropTypes from 'prop-types'; import { i18n } from '@kbn/i18n'; +import { ExpressionContext } from '../../../types'; const strings = { - getContextErrorMessage: (errorMessage: string) => + getContextErrorMessage: (errorMessage: string | null = '') => i18n.translate('xpack.canvas.functionForm.contextError', { defaultMessage: 'ERROR: {errorMessage}', values: { @@ -18,18 +18,14 @@ const strings = { }, }), }; -interface Props { - context: { - error: string; - }; +interface FunctionFormContextErrorProps { + context: ExpressionContext; } -export const FunctionFormContextError: FunctionComponent = ({ context }) => ( +export const FunctionFormContextError: FunctionComponent = ({ + context, +}) => (
{strings.getContextErrorMessage(context.error)}
); - -FunctionFormContextError.propTypes = { - context: PropTypes.shape({ error: PropTypes.string }).isRequired, -}; diff --git a/x-pack/plugins/canvas/public/components/function_form/function_form_context_pending.js b/x-pack/plugins/canvas/public/components/function_form/function_form_context_pending.js deleted file mode 100644 index f4a2a71bb03e8..0000000000000 --- a/x-pack/plugins/canvas/public/components/function_form/function_form_context_pending.js +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { Loading } from '../loading'; - -export class FunctionFormContextPending extends React.PureComponent { - static propTypes = { - context: PropTypes.object, - contextExpression: PropTypes.string, - expressionType: PropTypes.object.isRequired, - updateContext: PropTypes.func.isRequired, - }; - - componentDidMount() { - this.fetchContext(this.props); - } - - UNSAFE_componentWillReceiveProps(newProps) { - const oldContext = this.props.contextExpression; - const newContext = newProps.contextExpression; - const forceUpdate = newProps.expressionType.requiresContext && oldContext !== newContext; - this.fetchContext(newProps, forceUpdate); - } - - fetchContext = (props, force = false) => { - // dispatch context update if none is provided - const { expressionType, context, updateContext } = props; - if (force || (context == null && expressionType.requiresContext)) { - updateContext(); - } - }; - - render() { - return ( -
- -
- ); - } -} diff --git a/x-pack/plugins/canvas/public/components/function_form/function_form_context_pending.tsx b/x-pack/plugins/canvas/public/components/function_form/function_form_context_pending.tsx new file mode 100644 index 0000000000000..6cd7b59a2d214 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/function_form/function_form_context_pending.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useEffect } from 'react'; +import usePrevious from 'react-use/lib/usePrevious'; +import { Loading } from '../loading'; +import { CanvasElement, ExpressionContext } from '../../../types'; +import { ExpressionType } from '../../expression_types/types'; + +interface FunctionFormContextPendingProps { + context?: ExpressionContext; + contextExpression?: string; + expressionType: ExpressionType; + updateContext: (element?: CanvasElement) => void; +} + +export const FunctionFormContextPending: React.FunctionComponent = ( + props +) => { + const { contextExpression, expressionType, context, updateContext } = props; + const prevContextExpression = usePrevious(contextExpression); + const fetchContext = useCallback( + (force = false) => { + // dispatch context update if none is provided + if (force || (context == null && expressionType.requiresContext)) { + updateContext(); + } + }, + [context, expressionType.requiresContext, updateContext] + ); + + useEffect(() => { + const forceUpdate = + expressionType.requiresContext && prevContextExpression !== contextExpression; + fetchContext(forceUpdate); + }, [contextExpression, expressionType, fetchContext, prevContextExpression]); + + return ( +
+ +
+ ); +}; diff --git a/x-pack/plugins/canvas/public/components/function_form/index.js b/x-pack/plugins/canvas/public/components/function_form/index.js deleted file mode 100644 index 76eb23d800b00..0000000000000 --- a/x-pack/plugins/canvas/public/components/function_form/index.js +++ /dev/null @@ -1,115 +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 PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { findExpressionType } from '../../lib/find_expression_type'; -import { getId } from '../../lib/get_id'; -import { createAsset } from '../../state/actions/assets'; -import { - fetchContext, - setArgumentAtIndex, - addArgumentValueAtIndex, - deleteArgumentAtIndex, -} from '../../state/actions/elements'; -import { - getSelectedElement, - getSelectedPage, - getContextForIndex, - getGlobalFilterGroups, -} from '../../state/selectors/workpad'; -import { getAssets } from '../../state/selectors/assets'; -import { findExistingAsset } from '../../lib/find_existing_asset'; -import { FunctionForm as Component } from './function_form'; - -const mapStateToProps = (state, { expressionIndex }) => ({ - context: getContextForIndex(state, expressionIndex), - element: getSelectedElement(state), - pageId: getSelectedPage(state), - assets: getAssets(state), - filterGroups: getGlobalFilterGroups(state), -}); - -const mapDispatchToProps = (dispatch, { expressionIndex }) => ({ - addArgument: (element, pageId) => (argName, argValue) => () => { - dispatch( - addArgumentValueAtIndex({ index: expressionIndex, element, pageId, argName, value: argValue }) - ); - }, - updateContext: (element) => () => dispatch(fetchContext(expressionIndex, element)), - setArgument: (element, pageId) => (argName, valueIndex) => (value) => { - dispatch( - setArgumentAtIndex({ - index: expressionIndex, - element, - pageId, - argName, - value, - valueIndex, - }) - ); - }, - deleteArgument: (element, pageId) => (argName, argIndex) => () => { - dispatch( - deleteArgumentAtIndex({ - index: expressionIndex, - element, - pageId, - argName, - argIndex, - }) - ); - }, - onAssetAdd: (type, content) => { - // make the ID here and pass it into the action - const assetId = getId('asset'); - dispatch(createAsset(type, content, assetId)); - - // then return the id, so the caller knows the id that will be created - return assetId; - }, -}); - -const mergeProps = (stateProps, dispatchProps, ownProps) => { - const { element, pageId, assets } = stateProps; - const { argType, nextArgType } = ownProps; - const { - updateContext, - setArgument, - addArgument, - deleteArgument, - onAssetAdd, - ...dispatchers - } = dispatchProps; - - return { - ...stateProps, - ...dispatchers, - ...ownProps, - updateContext: updateContext(element), - expressionType: findExpressionType(argType), - nextExpressionType: nextArgType ? findExpressionType(nextArgType) : nextArgType, - onValueChange: setArgument(element, pageId), - onValueAdd: addArgument(element, pageId), - onValueRemove: deleteArgument(element, pageId), - onAssetAdd: (type, content) => { - const existingId = findExistingAsset(type, content, assets); - if (existingId) { - return existingId; - } - return onAssetAdd(type, content); - }, - }; -}; - -export const FunctionForm = connect(mapStateToProps, mapDispatchToProps, mergeProps)(Component); - -FunctionForm.propTypes = { - expressionIndex: PropTypes.number, - argType: PropTypes.string, - nextArgType: PropTypes.string, -}; diff --git a/x-pack/plugins/canvas/public/components/function_form/index.tsx b/x-pack/plugins/canvas/public/components/function_form/index.tsx new file mode 100644 index 0000000000000..9caf18e9e8b4e --- /dev/null +++ b/x-pack/plugins/canvas/public/components/function_form/index.tsx @@ -0,0 +1,150 @@ +/* + * 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 } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { Ast } from '@kbn/interpreter/common'; +import { + ExpressionAstExpression, + ExpressionValue, +} from '../../../../../../src/plugins/expressions'; +import { findExpressionType } from '../../lib/find_expression_type'; +import { getId } from '../../lib/get_id'; +// @ts-expect-error unconverted action function +import { createAsset } from '../../state/actions/assets'; +import { + fetchContext, + setArgumentAtIndex, + addArgumentValueAtIndex, + deleteArgumentAtIndex, + // @ts-expect-error untyped local +} from '../../state/actions/elements'; +import { + getSelectedElement, + getSelectedPage, + getContextForIndex, + getGlobalFilterGroups, +} from '../../state/selectors/workpad'; +import { getAssets } from '../../state/selectors/assets'; +// @ts-expect-error unconverted lib +import { findExistingAsset } from '../../lib/find_existing_asset'; +import { FunctionForm as Component } from './function_form'; +import { ArgType, ArgTypeDef } from '../../expression_types/types'; +import { State, ExpressionContext, CanvasElement, AssetType } from '../../../types'; + +interface FunctionFormProps { + name: string; + argResolver: (ast: ExpressionAstExpression) => Promise; + args: Record> | null; + argType: ArgType; + argTypeDef: ArgTypeDef; + expressionIndex: number; + nextArgType?: ArgType; +} + +export const FunctionForm: React.FunctionComponent = (props) => { + const { expressionIndex, argType, nextArgType } = props; + const dispatch = useDispatch(); + const context = useSelector((state) => + getContextForIndex(state, expressionIndex) + ); + const element = useSelector((state) => + getSelectedElement(state) + ); + const pageId = useSelector((state) => getSelectedPage(state)); + const assets = useSelector((state) => getAssets(state)); + const filterGroups = useSelector((state) => getGlobalFilterGroups(state)); + const addArgument = useCallback( + (argName: string, argValue: string | Ast | null) => () => { + dispatch( + addArgumentValueAtIndex({ + index: expressionIndex, + element, + pageId, + argName, + value: argValue, + }) + ); + }, + [dispatch, element, expressionIndex, pageId] + ); + + const updateContext = useCallback(() => dispatch(fetchContext(expressionIndex, element)), [ + dispatch, + element, + expressionIndex, + ]); + + const setArgument = useCallback( + (argName: string, valueIndex: number) => (value: string | Ast | null) => { + dispatch( + setArgumentAtIndex({ + index: expressionIndex, + element, + pageId, + argName, + value, + valueIndex, + }) + ); + }, + [dispatch, element, expressionIndex, pageId] + ); + + const deleteArgument = useCallback( + (argName: string, argIndex: number) => () => { + dispatch( + deleteArgumentAtIndex({ + index: expressionIndex, + element, + pageId, + argName, + argIndex, + }) + ); + }, + [dispatch, element, expressionIndex, pageId] + ); + + const onAssetAddDispatch = useCallback( + (type: AssetType['type'], content: AssetType['value']) => { + // make the ID here and pass it into the action + const assetId = getId('asset'); + dispatch(createAsset(type, content, assetId)); + + // then return the id, so the caller knows the id that will be created + return assetId; + }, + [dispatch] + ); + + const onAssetAdd = useCallback( + (type: AssetType['type'], content: AssetType['value']) => { + const existingId = findExistingAsset(type, content, assets); + if (existingId) { + return existingId; + } + return onAssetAddDispatch(type, content); + }, + [assets, onAssetAddDispatch] + ); + + return ( + + ); +}; diff --git a/x-pack/plugins/canvas/public/expression_types/arg.js b/x-pack/plugins/canvas/public/expression_types/arg.js deleted file mode 100644 index c3b351e5634ec..0000000000000 --- a/x-pack/plugins/canvas/public/expression_types/arg.js +++ /dev/null @@ -1,75 +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 { createElement } from 'react'; -import { pick } from 'lodash'; -import { ArgForm } from '../components/arg_form'; -import { argTypeRegistry } from './arg_type'; - -export class Arg { - constructor(props) { - const argType = argTypeRegistry.get(props.argType); - if (!argType) { - throw new Error(`Invalid arg type: ${props.argType}`); - } - if (!props.name) { - throw new Error('Args must have a name property'); - } - - // properties that can be overridden - const defaultProps = { - multi: false, - required: false, - types: [], - default: argType.default != null ? argType.default : null, - options: {}, - resolve: () => ({}), - }; - - const viewOverrides = { - argType, - ...pick(props, [ - 'name', - 'displayName', - 'help', - 'multi', - 'required', - 'types', - 'default', - 'resolve', - 'options', - ]), - }; - - Object.assign(this, defaultProps, argType, viewOverrides); - } - - // TODO: Document what these otherProps are. Maybe make them named arguments? - render({ onValueChange, onValueRemove, argValue, key, label, ...otherProps }) { - // This is everything the arg_type template needs to render - const templateProps = { - ...otherProps, - ...this.resolve(otherProps), - onValueChange, - argValue, - typeInstance: this, - }; - - const formProps = { - key, - argTypeInstance: this, - valueMissing: this.required && argValue == null, - label, - onValueChange, - onValueRemove, - templateProps, - argId: key, - }; - - return createElement(ArgForm, formProps); - } -} diff --git a/x-pack/plugins/canvas/public/expression_types/arg.ts b/x-pack/plugins/canvas/public/expression_types/arg.ts new file mode 100644 index 0000000000000..0fc1c996f327c --- /dev/null +++ b/x-pack/plugins/canvas/public/expression_types/arg.ts @@ -0,0 +1,145 @@ +/* + * 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 { merge } from 'lodash'; +import { createElement } from 'react'; +import { Ast } from '@kbn/interpreter/common'; +// @ts-expect-error unconverted components +import { ArgForm } from '../components/arg_form'; +import { argTypeRegistry } from './arg_type_registry'; +import type { ArgType, ArgTypeDef, ExpressionType } from './types'; +import { + AssetType, + CanvasElement, + ExpressionAstExpression, + ExpressionValue, + ExpressionContext, +} from '../../types'; +import { BaseFormProps } from './base_form'; + +interface ArtOwnProps { + argType: ArgType; + multi?: boolean; + required?: boolean; + types?: string[]; + default?: string | null; + resolve?: (...args: any[]) => any; + options?: { + include?: string[]; + confirm?: string; + labelValue?: string; + choices?: Array<{ name: string; value: string }>; + min?: number; + max?: number; + shapes?: string[]; + }; +} +export type ArgProps = ArtOwnProps & BaseFormProps; + +export interface DataArg { + argValue?: string | Ast | null; + skipRender?: boolean; + label?: string; + valueIndex: number; + key?: string; + labels?: string[]; + contextExpression?: string; + name: string; + argResolver: (ast: ExpressionAstExpression) => Promise; + args: Record> | null; + argType: ArgType; + argTypeDef?: ArgTypeDef; + filterGroups: string[]; + context?: ExpressionContext; + expressionIndex: number; + expressionType: ExpressionType; + nextArgType?: ArgType; + nextExpressionType?: ExpressionType; + onValueAdd: (argName: string, argValue: string | Ast | null) => () => void; + onAssetAdd: (type: AssetType['type'], content: AssetType['value']) => string; + onValueChange: (value: Ast | string) => void; + onValueRemove: () => void; + updateContext: (element?: CanvasElement) => void; + typeInstance?: ExpressionType; +} + +export class Arg { + argType?: ArgType; + multi?: boolean; + required?: boolean; + types?: string[]; + default?: string | null; + resolve?: (...args: any[]) => any; + options?: { + include: string[]; + }; + name: string = ''; + displayName?: string; + help?: string; + + constructor(props: ArgProps) { + const argType = argTypeRegistry.get(props.argType); + if (!argType) { + throw new Error(`Invalid arg type: ${props.argType}`); + } + if (!props.name) { + throw new Error('Args must have a name property'); + } + + // properties that can be overridden + const defaultProps = { + multi: false, + required: false, + types: [], + default: argType.default != null ? argType.default : null, + options: {}, + resolve: () => ({}), + }; + + const { name, displayName, help, multi, types, options } = props; + + merge(this, defaultProps, argType, { + argType, + name, + displayName, + help, + multi, + types, + default: props.default, + resolve: props.resolve, + required: props.required, + options, + }); + } + + // TODO: Document what these otherProps are. Maybe make them named arguments? + render(data: DataArg) { + const { onValueChange, onValueRemove, argValue, key, label, ...otherProps } = data; + // This is everything the arg_type template needs to render + const templateProps = { + ...otherProps, + ...this.resolve?.(otherProps), + onValueChange, + argValue, + typeInstance: this, + }; + + const formProps = { + key, + argTypeInstance: this, + valueMissing: this.required && argValue == null, + label, + onValueChange, + onValueRemove, + templateProps, + argId: key, + options: this.options, + }; + + return createElement(ArgForm, formProps); + } +} diff --git a/x-pack/plugins/canvas/public/expression_types/arg_type.ts b/x-pack/plugins/canvas/public/expression_types/arg_type.ts new file mode 100644 index 0000000000000..2345b07d79807 --- /dev/null +++ b/x-pack/plugins/canvas/public/expression_types/arg_type.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { templateFromReactComponent } from '../lib/template_from_react_component'; +import { BaseForm, BaseFormProps } from './base_form'; + +interface ArgTypeOwnProps { + simpleTemplate: ReturnType; + template?: ReturnType; + default?: string; + resolveArgValue?: boolean; +} + +export type ArgTypeProps = ArgTypeOwnProps & BaseFormProps; + +export class ArgType extends BaseForm { + simpleTemplate: ReturnType; + template?: ReturnType; + default?: string; + resolveArgValue: boolean; + + constructor(props: ArgTypeProps) { + super(props); + this.simpleTemplate = props.simpleTemplate; + this.template = props.template; + this.default = props.default; + this.resolveArgValue = Boolean(props.resolveArgValue); + } +} diff --git a/x-pack/plugins/canvas/public/expression_types/arg_type.js b/x-pack/plugins/canvas/public/expression_types/arg_type_registry.ts similarity index 52% rename from x-pack/plugins/canvas/public/expression_types/arg_type.js rename to x-pack/plugins/canvas/public/expression_types/arg_type_registry.ts index 30273cedd818c..579245d0d312b 100644 --- a/x-pack/plugins/canvas/public/expression_types/arg_type.js +++ b/x-pack/plugins/canvas/public/expression_types/arg_type_registry.ts @@ -4,23 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { Registry } from '@kbn/interpreter/common'; -import { BaseForm } from './base_form'; - -export class ArgType extends BaseForm { - constructor(props) { - super(props); - - this.simpleTemplate = props.simpleTemplate; - this.template = props.template; - this.default = props.default; - this.resolveArgValue = Boolean(props.resolveArgValue); - } -} +import { ArgType, ArgTypeProps } from './arg_type'; -class ArgTypeRegistry extends Registry { - wrapper(obj) { +class ArgTypeRegistry extends Registry { + wrapper(obj: ArgTypeProps) { return new ArgType(obj); } } diff --git a/x-pack/plugins/canvas/public/expression_types/base_form.js b/x-pack/plugins/canvas/public/expression_types/base_form.ts similarity index 72% rename from x-pack/plugins/canvas/public/expression_types/base_form.js rename to x-pack/plugins/canvas/public/expression_types/base_form.ts index 7ca2d6a78b656..11d6b24ee3e86 100644 --- a/x-pack/plugins/canvas/public/expression_types/base_form.js +++ b/x-pack/plugins/canvas/public/expression_types/base_form.ts @@ -5,8 +5,18 @@ * 2.0. */ +export interface BaseFormProps { + name: string; + displayName?: string; + help?: string; +} + export class BaseForm { - constructor(props) { + name: string; + displayName: string; + help: string; + + constructor(props: BaseFormProps) { if (!props.name) { throw new Error('Expression specs require a name property'); } diff --git a/x-pack/plugins/canvas/public/expression_types/datasource.js b/x-pack/plugins/canvas/public/expression_types/datasource.js deleted file mode 100644 index f8bbff702a4e9..0000000000000 --- a/x-pack/plugins/canvas/public/expression_types/datasource.js +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { Registry } from '@kbn/interpreter/common'; -import { RenderToDom } from '../components/render_to_dom'; -import { ExpressionFormHandlers } from '../../common/lib/expression_form_handlers'; -import { BaseForm } from './base_form'; - -const defaultTemplate = () => ( -
-

This datasource has no interface. Use the expression editor to make changes.

-
-); - -class DatasourceWrapper extends React.PureComponent { - static propTypes = { - spec: PropTypes.object.isRequired, - datasourceProps: PropTypes.object.isRequired, - handlers: PropTypes.object.isRequired, - }; - - componentDidUpdate() { - this.callRenderFn(); - } - - componentWillUnmount() { - this.props.handlers.destroy(); - } - - callRenderFn = () => { - const { spec, datasourceProps, handlers } = this.props; - const { template } = spec; - template(this.domNode, datasourceProps, handlers); - }; - - render() { - return ( - { - this.domNode = domNode; - this.callRenderFn(); - }} - /> - ); - } -} - -export class Datasource extends BaseForm { - constructor(props) { - super(props); - - this.template = props.template || defaultTemplate; - this.image = props.image; - } - - render(props = {}) { - const expressionFormHandlers = new ExpressionFormHandlers(); - return ( - - ); - } -} - -class DatasourceRegistry extends Registry { - wrapper(obj) { - return new Datasource(obj); - } -} - -export const datasourceRegistry = new DatasourceRegistry(); diff --git a/x-pack/plugins/canvas/public/expression_types/datasource.tsx b/x-pack/plugins/canvas/public/expression_types/datasource.tsx new file mode 100644 index 0000000000000..0afb9bdd2f96a --- /dev/null +++ b/x-pack/plugins/canvas/public/expression_types/datasource.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useRef, useCallback } from 'react'; +import { Ast } from '@kbn/interpreter/common'; +import { RenderToDom } from '../components/render_to_dom'; +import { BaseForm, BaseFormProps } from './base_form'; +import { ExpressionFormHandlers } from '../../common/lib'; +import { ExpressionFunction } from '../../types'; + +const defaultTemplate = () => ( +
+

This datasource has no interface. Use the expression editor to make changes.

+
+); + +type TemplateFn = ( + domNode: HTMLElement, + config: DatasourceRenderProps, + handlers: ExpressionFormHandlers +) => void; + +export type DatasourceProps = { + template?: TemplateFn; + image?: string; + requiresContext?: boolean; +} & BaseFormProps; + +export interface DatasourceRenderProps { + args: Record> | null; + updateArgs: (...args: any[]) => void; + datasourceDef: ExpressionFunction; + isInvalid: boolean; + setInvalid: (invalid: boolean) => void; + defaultIndex: string; + renderError: (...args: any[]) => void; +} + +interface DatasourceWrapperProps { + handlers: ExpressionFormHandlers; + spec: Datasource; + datasourceProps: DatasourceRenderProps; +} + +const DatasourceWrapper: React.FunctionComponent = (props) => { + const domNodeRef = useRef(); + const { spec, datasourceProps, handlers } = props; + + const callRenderFn = useCallback(() => { + const { template } = spec; + + if (!domNodeRef.current) { + return; + } + + template(domNodeRef.current, datasourceProps, handlers); + }, [datasourceProps, handlers, spec]); + + useEffect(() => { + callRenderFn(); + return () => { + handlers.destroy(); + }; + }, [callRenderFn, handlers, props]); + + return ( + { + domNodeRef.current = domNode; + callRenderFn(); + }} + /> + ); +}; + +export class Datasource extends BaseForm { + template: TemplateFn | React.FC; + image?: string; + requiresContext?: boolean; + + constructor(props: DatasourceProps) { + super(props); + + this.template = props.template ?? defaultTemplate; + this.image = props.image; + this.requiresContext = props.requiresContext; + } + + render(props: DatasourceRenderProps) { + const expressionFormHandlers = new ExpressionFormHandlers(); + return ( + + ); + } +} diff --git a/x-pack/plugins/canvas/public/expression_types/datasource_registry.ts b/x-pack/plugins/canvas/public/expression_types/datasource_registry.ts new file mode 100644 index 0000000000000..b6427fac9d4a0 --- /dev/null +++ b/x-pack/plugins/canvas/public/expression_types/datasource_registry.ts @@ -0,0 +1,18 @@ +/* + * 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 { Registry } from '@kbn/interpreter/common'; +import { Datasource } from './datasource'; +import type { Datasource as DatasourceType, DatasourceProps } from './datasource'; + +class DatasourceRegistry extends Registry { + wrapper(obj: DatasourceProps): DatasourceType { + return new Datasource(obj); + } +} + +export const datasourceRegistry = new DatasourceRegistry(); diff --git a/x-pack/plugins/canvas/public/expression_types/function_form.js b/x-pack/plugins/canvas/public/expression_types/function_form.tsx similarity index 57% rename from x-pack/plugins/canvas/public/expression_types/function_form.js rename to x-pack/plugins/canvas/public/expression_types/function_form.tsx index 2f4e983e9fd4e..70279453ac658 100644 --- a/x-pack/plugins/canvas/public/expression_types/function_form.js +++ b/x-pack/plugins/canvas/public/expression_types/function_form.tsx @@ -5,25 +5,76 @@ * 2.0. */ +import React, { ReactElement } from 'react'; import { EuiCallOut } from '@elastic/eui'; -import React from 'react'; import { isPlainObject, uniq, last, compact } from 'lodash'; -import { fromExpression } from '@kbn/interpreter/common'; +import { Ast, fromExpression } from '@kbn/interpreter/common'; import { ArgAddPopover } from '../components/arg_add_popover'; +// @ts-expect-error unconverted components import { SidebarSection } from '../components/sidebar/sidebar_section'; +// @ts-expect-error unconverted components import { SidebarSectionTitle } from '../components/sidebar/sidebar_section_title'; -import { BaseForm } from './base_form'; -import { Arg } from './arg'; +import { BaseForm, BaseFormProps } from './base_form'; +import { Arg, ArgProps } from './arg'; +import { ArgType, ArgTypeDef, ExpressionType } from './types'; +import { + AssetType, + CanvasElement, + DatatableColumn, + ExpressionAstExpression, + ExpressionContext, + ExpressionValue, +} from '../../types'; + +export interface DataArg { + arg: Arg | undefined; + argValues?: Array; + skipRender?: boolean; + label?: 'string'; +} + +export type RenderArgData = BaseFormProps & { + argType: ArgType; + argTypeDef?: ArgTypeDef; + args: Record> | null; + argResolver: (ast: ExpressionAstExpression) => Promise; + context?: ExpressionContext; + contextExpression?: string; + expressionIndex: number; + expressionType: ExpressionType; + filterGroups: string[]; + nextArgType?: ArgType; + nextExpressionType?: ExpressionType; + onValueAdd: (argName: string, argValue: string | Ast | null) => () => void; + onValueChange: (argName: string, argIndex: number) => (value: string | Ast) => void; + onValueRemove: (argName: string, argIndex: number) => () => void; + onAssetAdd: (type: AssetType['type'], content: AssetType['value']) => string; + updateContext: (element?: CanvasElement) => void; + typeInstance?: ExpressionType; + columns?: DatatableColumn[]; +}; + +export type RenderArgProps = { + typeInstance: FunctionForm; +} & RenderArgData; + +export type FunctionFormProps = { + args?: ArgProps[]; + resolve?: (...args: any[]) => any; +} & BaseFormProps; export class FunctionForm extends BaseForm { - constructor(props) { + args: ArgProps[]; + resolve: (...args: any[]) => any; + + constructor(props: FunctionFormProps) { super({ ...props }); this.args = props.args || []; this.resolve = props.resolve || (() => ({})); } - renderArg(props, dataArg) { + renderArg(props: RenderArgProps, dataArg: DataArg) { const { onValueRemove, onValueChange, ...passedProps } = props; const { arg, argValues, skipRender, label } = dataArg; const { argType, expressionIndex } = passedProps; @@ -32,22 +83,24 @@ export class FunctionForm extends BaseForm { if (!arg || skipRender) { return null; } - - const renderArgWithProps = (argValue, valueIndex) => + const renderArgWithProps = ( + argValue: string | Ast | null, + valueIndex: number + ): ReactElement | null => arg.render({ key: `${argType}-${expressionIndex}-${arg.name}-${valueIndex}`, ...passedProps, label, valueIndex, - argValue, onValueChange: onValueChange(arg.name, valueIndex), onValueRemove: onValueRemove(arg.name, valueIndex), + argValue: argValue ?? null, }); // render the argument's template, wrapped in a remove control // if the argument is required but not included, render the control anyway if (!argValues && arg.required) { - return renderArgWithProps({ type: undefined, value: '' }, 0); + return renderArgWithProps(null, 0); } // render all included argument controls @@ -55,7 +108,7 @@ export class FunctionForm extends BaseForm { } // TODO: Argument adding isn't very good, we should improve this UI - getAddableArg(props, dataArg) { + getAddableArg(props: RenderArgProps, dataArg: DataArg) { const { onValueAdd } = props; const { arg, argValues, skipRender } = dataArg; @@ -68,47 +121,54 @@ export class FunctionForm extends BaseForm { } const value = arg.default == null ? null : fromExpression(arg.default, 'argument'); - return { arg, onValueAdd: onValueAdd(arg.name, value) }; } - resolveArg() { + resolveArg(...args: unknown[]) { // basically a no-op placeholder return {}; } - render(data = {}) { + render(data: RenderArgData) { + if (!data) { + data = { + args: null, + argTypeDef: undefined, + } as RenderArgData; + } const { args, argTypeDef } = data; // Don't instaniate these until render time, to give the registries a chance to populate. const argInstances = this.args.map((argSpec) => new Arg(argSpec)); - if (!isPlainObject(args)) { + if (args === null || !isPlainObject(args)) { throw new Error(`Form "${this.name}" expects "args" object`); } // get a mapping of arg values from the expression and from the renderable's schema const argNames = uniq(argInstances.map((arg) => arg.name).concat(Object.keys(args))); const dataArgs = argNames.map((argName) => { - const arg = argInstances.find((arg) => arg.name === argName); - + const arg = argInstances.find((argument) => argument.name === argName); // if arg is not multi, only preserve the last value found // otherwise, leave the value alone (including if the arg is not defined) const isMulti = arg && arg.multi; - const argValues = args[argName] && !isMulti ? [last(args[argName])] : args[argName]; + const argValues = args[argName] && !isMulti ? [last(args[argName]) ?? null] : args[argName]; return { arg, argValues }; }); // props are passed to resolve and the returned object is mixed into the template props const props = { ...data, ...this.resolve(data), typeInstance: this }; - try { // allow a hook to override the data args const resolvedDataArgs = dataArgs.map((d) => ({ ...d, ...this.resolveArg(d, props) })); - const argumentForms = compact(resolvedDataArgs.map((d) => this.renderArg(props, d))); - const addableArgs = compact(resolvedDataArgs.map((d) => this.getAddableArg(props, d))); + const argumentForms = compact( + resolvedDataArgs.map((dataArg) => this.renderArg(props, dataArg)) + ); + const addableArgs = compact( + resolvedDataArgs.map((dataArg) => this.getAddableArg(props, dataArg)) + ); if (!addableArgs.length && !argumentForms.length) { return null; @@ -116,13 +176,13 @@ export class FunctionForm extends BaseForm { return ( - + {addableArgs.length === 0 ? null : } {argumentForms} ); - } catch (e) { + } catch (e: any) { return (

{e.message}

diff --git a/x-pack/plugins/canvas/public/expression_types/index.js b/x-pack/plugins/canvas/public/expression_types/index.js deleted file mode 100644 index ce1af41669086..0000000000000 --- a/x-pack/plugins/canvas/public/expression_types/index.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { Datasource, datasourceRegistry } from './datasource'; -export { Transform, transformRegistry } from './transform'; -export { Model, modelRegistry } from './model'; -export { View, viewRegistry } from './view'; -export { ArgType, argTypeRegistry } from './arg_type'; -export { Arg } from './arg'; diff --git a/x-pack/plugins/canvas/public/expression_types/index.ts b/x-pack/plugins/canvas/public/expression_types/index.ts new file mode 100644 index 0000000000000..88cb9aa1548a2 --- /dev/null +++ b/x-pack/plugins/canvas/public/expression_types/index.ts @@ -0,0 +1,23 @@ +/* + * 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 { Datasource } from './datasource'; +export { datasourceRegistry } from './datasource_registry'; + +export { Transform } from './transform'; +export { transformRegistry } from './transform_registry'; + +export { Model } from './model'; +export { modelRegistry } from './model_registry'; + +export { View } from './view'; +export { viewRegistry } from './view_registry'; + +export { ArgType } from './arg_type'; +export { argTypeRegistry } from './arg_type_registry'; + +export { Arg } from './arg'; diff --git a/x-pack/plugins/canvas/public/expression_types/model.js b/x-pack/plugins/canvas/public/expression_types/model.js deleted file mode 100644 index 1ba4f74a0ba5a..0000000000000 --- a/x-pack/plugins/canvas/public/expression_types/model.js +++ /dev/null @@ -1,72 +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 { get, pick } from 'lodash'; -import { Registry } from '@kbn/interpreter/common'; -import { FunctionForm } from './function_form'; - -const NO_NEXT_EXP = 'no next expression'; -const MISSING_MODEL_ARGS = 'missing model args'; - -function getModelArgs(expressionType) { - if (!expressionType) { - return NO_NEXT_EXP; - } - - if (!expressionType.modelArgs) { - return MISSING_MODEL_ARGS; - } - - return expressionType.modelArgs.length > 0 ? expressionType.modelArgs : MISSING_MODEL_ARGS; -} - -export class Model extends FunctionForm { - constructor(props) { - super(props); - - const propNames = ['requiresContext']; - const defaultProps = { - requiresContext: true, - }; - - Object.assign(this, defaultProps, pick(props, propNames)); - } - - resolveArg(dataArg, props) { - // custom argument resolver - // uses `modelArgs` from following expression to control which arguments get rendered - const { nextExpressionType } = props; - const modelArgs = getModelArgs(nextExpressionType); - - // if there is no following expression, or no modelArgs, argument is shown by default - if (modelArgs === NO_NEXT_EXP || modelArgs === MISSING_MODEL_ARGS) { - return { skipRender: false }; - } - - // if argument is missing from modelArgs, mark it as skipped - const argName = get(dataArg, 'arg.name'); - const modelArg = modelArgs.find((modelArg) => { - if (Array.isArray(modelArg)) { - return modelArg[0] === argName; - } - return modelArg === argName; - }); - - return { - label: Array.isArray(modelArg) ? get(modelArg[1], 'label') : null, - skipRender: !modelArg, - }; - } -} - -class ModelRegistry extends Registry { - wrapper(obj) { - return new Model(obj); - } -} - -export const modelRegistry = new ModelRegistry(); diff --git a/x-pack/plugins/canvas/public/expression_types/model.ts b/x-pack/plugins/canvas/public/expression_types/model.ts new file mode 100644 index 0000000000000..49f8346212f96 --- /dev/null +++ b/x-pack/plugins/canvas/public/expression_types/model.ts @@ -0,0 +1,80 @@ +/* + * 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 { merge } from 'lodash'; +import { FunctionForm, FunctionFormProps } from './function_form'; +import { Arg, View } from './types'; + +const NO_NEXT_EXP = 'no next expression'; +const MISSING_MODEL_ARGS = 'missing model args'; + +interface ModelOwnProps { + nextExpressionType?: View; + requiresContext?: boolean; + default?: string; + resolveArgValue?: boolean; + modelArgs: string[] | Arg[]; +} + +interface DataArg { + arg: Arg; +} + +export type ModelProps = ModelOwnProps & FunctionFormProps; + +function getModelArgs(expressionType?: View) { + if (!expressionType) { + return NO_NEXT_EXP; + } + + if (!expressionType?.modelArgs) { + return MISSING_MODEL_ARGS; + } + + return expressionType?.modelArgs.length > 0 ? expressionType?.modelArgs : MISSING_MODEL_ARGS; +} + +export class Model extends FunctionForm { + requiresContext?: boolean; + + constructor(props: ModelProps) { + super(props); + + const defaultProps = { requiresContext: true }; + const { requiresContext } = props; + + merge(this, defaultProps, { requiresContext }); + } + + resolveArg(dataArg: DataArg, props: ModelProps) { + // custom argument resolver + // uses `modelArgs` from following expression to control which arguments get rendered + const { nextExpressionType } = props; + const modelArgs: Array | string = getModelArgs(nextExpressionType); + + // if there is no following expression, or no modelArgs, argument is shown by default + if (modelArgs === NO_NEXT_EXP || modelArgs === MISSING_MODEL_ARGS) { + return { skipRender: false }; + } + + // if argument is missing from modelArgs, mark it as skipped + const argName = dataArg?.arg?.name; + const modelArg = + typeof modelArgs !== 'string' && + modelArgs.find((arg) => { + if (Array.isArray(arg)) { + return arg[0] === argName; + } + return arg === argName; + }); + + return { + label: Array.isArray(modelArg) ? modelArg[1]?.label : null, + skipRender: !modelArg, + }; + } +} diff --git a/x-pack/plugins/canvas/public/expression_types/model_registry.ts b/x-pack/plugins/canvas/public/expression_types/model_registry.ts new file mode 100644 index 0000000000000..f5c290f2fe2e9 --- /dev/null +++ b/x-pack/plugins/canvas/public/expression_types/model_registry.ts @@ -0,0 +1,18 @@ +/* + * 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 { Registry } from '@kbn/interpreter/common'; +import { Model } from './model'; +import type { ModelProps, Model as ModelType } from './model'; + +class ModelRegistry extends Registry { + wrapper(obj: ModelProps): ModelType { + return new Model(obj); + } +} + +export const modelRegistry = new ModelRegistry(); diff --git a/x-pack/plugins/canvas/public/expression_types/transform.js b/x-pack/plugins/canvas/public/expression_types/transform.js deleted file mode 100644 index 6a190e72aded7..0000000000000 --- a/x-pack/plugins/canvas/public/expression_types/transform.js +++ /dev/null @@ -1,31 +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 { pick } from 'lodash'; -import { Registry } from '@kbn/interpreter/common'; -import { FunctionForm } from './function_form'; - -export class Transform extends FunctionForm { - constructor(props) { - super(props); - - const propNames = ['requiresContext']; - const defaultProps = { - requiresContext: true, - }; - - Object.assign(this, defaultProps, pick(props, propNames)); - } -} - -class TransformRegistry extends Registry { - wrapper(obj) { - return new Transform(obj); - } -} - -export const transformRegistry = new TransformRegistry(); diff --git a/x-pack/plugins/canvas/public/expression_types/transform.ts b/x-pack/plugins/canvas/public/expression_types/transform.ts new file mode 100644 index 0000000000000..6b901b5ae7126 --- /dev/null +++ b/x-pack/plugins/canvas/public/expression_types/transform.ts @@ -0,0 +1,24 @@ +/* + * 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 { merge } from 'lodash'; +import { FunctionForm, FunctionFormProps } from './function_form'; + +export type TransformProps = { requiresContext: boolean } & FunctionFormProps; + +export class Transform extends FunctionForm { + requiresContext?: boolean; + + constructor(props: TransformProps) { + super(props); + const { requiresContext } = props; + const defaultProps = { + requiresContext: true, + }; + + merge(this, defaultProps, { requiresContext }); + } +} diff --git a/x-pack/plugins/canvas/public/expression_types/transform_registry.ts b/x-pack/plugins/canvas/public/expression_types/transform_registry.ts new file mode 100644 index 0000000000000..a69f5fcf554fe --- /dev/null +++ b/x-pack/plugins/canvas/public/expression_types/transform_registry.ts @@ -0,0 +1,18 @@ +/* + * 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 { Registry } from '@kbn/interpreter/common'; +import { Transform } from './transform'; +import type { Transform as TransformType, TransformProps } from './transform'; + +class TransformRegistry extends Registry { + wrapper(obj: TransformProps): TransformType { + return new Transform(obj); + } +} + +export const transformRegistry = new TransformRegistry(); diff --git a/x-pack/plugins/canvas/public/expression_types/types.ts b/x-pack/plugins/canvas/public/expression_types/types.ts new file mode 100644 index 0000000000000..704dae83c8a55 --- /dev/null +++ b/x-pack/plugins/canvas/public/expression_types/types.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Transform } from './transform'; +import type { View } from './view'; +import type { Datasource } from './datasource'; +import type { Model } from './model'; + +export type ArgType = string; + +export type ArgTypeDef = View | Model | Transform | Datasource; + +export { Transform, View, Datasource, Model }; +export type { Arg } from './arg'; + +export type ExpressionType = View | Model | Transform; + +export type { RenderArgData } from './function_form'; diff --git a/x-pack/plugins/canvas/public/expression_types/view.js b/x-pack/plugins/canvas/public/expression_types/view.js deleted file mode 100644 index a4613e51ecbee..0000000000000 --- a/x-pack/plugins/canvas/public/expression_types/view.js +++ /dev/null @@ -1,38 +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 { pick } from 'lodash'; -import { Registry } from '@kbn/interpreter/common'; -import { FunctionForm } from './function_form'; - -export class View extends FunctionForm { - constructor(props) { - super(props); - - const propNames = ['help', 'modelArgs', 'requiresContext']; - const defaultProps = { - help: `Element: ${props.name}`, - requiresContext: true, - }; - - Object.assign(this, defaultProps, pick(props, propNames)); - - this.modelArgs = this.modelArgs || []; - - if (!Array.isArray(this.modelArgs)) { - throw new Error(`${this.name} element is invalid, modelArgs must be an array`); - } - } -} - -class ViewRegistry extends Registry { - wrapper(obj) { - return new View(obj); - } -} - -export const viewRegistry = new ViewRegistry(); diff --git a/x-pack/plugins/canvas/public/expression_types/view.ts b/x-pack/plugins/canvas/public/expression_types/view.ts new file mode 100644 index 0000000000000..ae9c37678c396 --- /dev/null +++ b/x-pack/plugins/canvas/public/expression_types/view.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { merge } from 'lodash'; +import { FunctionForm, FunctionFormProps } from './function_form'; +import { Arg } from './types'; + +interface ViewOwnProps { + modelArgs: string[] | Arg[]; + requiresContext?: boolean; + default?: string; + resolveArgValue?: boolean; +} + +export type ViewProps = ViewOwnProps & FunctionFormProps; + +export class View extends FunctionForm { + modelArgs: string[] | Arg[] = []; + requiresContext?: boolean; + + constructor(props: ViewProps) { + super(props); + const { help, modelArgs, requiresContext } = props; + const defaultProps = { + help: `Element: ${props.name}`, + requiresContext: true, + }; + + merge(this, defaultProps, { help, modelArgs: modelArgs || [], requiresContext }); + + if (!Array.isArray(this.modelArgs)) { + throw new Error(`${this.name} element is invalid, modelArgs must be an array`); + } + } +} diff --git a/x-pack/plugins/canvas/public/expression_types/view_registry.ts b/x-pack/plugins/canvas/public/expression_types/view_registry.ts new file mode 100644 index 0000000000000..108a65c73ec60 --- /dev/null +++ b/x-pack/plugins/canvas/public/expression_types/view_registry.ts @@ -0,0 +1,18 @@ +/* + * 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 { Registry } from '@kbn/interpreter/common'; +import { View } from './view'; +import type { View as ViewType, ViewProps } from './view'; + +class ViewRegistry extends Registry { + wrapper(obj: ViewProps): ViewType { + return new View(obj); + } +} + +export const viewRegistry = new ViewRegistry(); diff --git a/x-pack/plugins/canvas/public/lib/find_expression_type.js b/x-pack/plugins/canvas/public/lib/find_expression_type.ts similarity index 70% rename from x-pack/plugins/canvas/public/lib/find_expression_type.js rename to x-pack/plugins/canvas/public/lib/find_expression_type.ts index 037b64b334fd6..cb054414b8725 100644 --- a/x-pack/plugins/canvas/public/lib/find_expression_type.js +++ b/x-pack/plugins/canvas/public/lib/find_expression_type.ts @@ -5,19 +5,19 @@ * 2.0. */ -//import { datasourceRegistry } from '../expression_types/datasource'; -import { transformRegistry } from '../expression_types/transform'; -import { modelRegistry } from '../expression_types/model'; -import { viewRegistry } from '../expression_types/view'; +import { transformRegistry } from '../expression_types/transform_registry'; +import { modelRegistry } from '../expression_types/model_registry'; +import { viewRegistry } from '../expression_types/view_registry'; +import { ArgType, ExpressionType } from '../expression_types/types'; -const expressionTypes = ['view', 'model', 'transform', 'datasource']; +const expressionTypes: ArgType[] = ['view', 'model', 'transform', 'datasource']; -export function findExpressionType(name, type) { +export function findExpressionType(name: string, type?: ArgType | null) { const checkTypes = expressionTypes.filter( (expressionType) => type == null || expressionType === type ); - const matches = checkTypes.reduce((acc, checkType) => { + const matches = checkTypes.reduce((acc: ExpressionType[], checkType) => { let expression; switch (checkType) { case 'view': diff --git a/x-pack/plugins/canvas/public/registries.ts b/x-pack/plugins/canvas/public/registries.ts index 1ad7fa6905c22..56d89affce5ea 100644 --- a/x-pack/plugins/canvas/public/registries.ts +++ b/x-pack/plugins/canvas/public/registries.ts @@ -20,7 +20,6 @@ import { modelRegistry, transformRegistry, viewRegistry, - // @ts-expect-error untyped local } from './expression_types'; import { SetupRegistries } from './plugin_api'; diff --git a/x-pack/plugins/canvas/types/state.ts b/x-pack/plugins/canvas/types/state.ts index cc42839ddfac7..30ded5f2a9c7e 100644 --- a/x-pack/plugins/canvas/types/state.ts +++ b/x-pack/plugins/canvas/types/state.ts @@ -16,6 +16,7 @@ import { Style, Range, } from 'src/plugins/expressions'; +import { Datasource, Model, Transform, View } from '../public/expression_types'; import { AssetType } from './assets'; import { CanvasWorkpad } from './canvas'; @@ -51,7 +52,11 @@ type ExpressionType = | KibanaContext | PointSeries | Style - | Range; + | Range + | View + | Model + | Datasource + | Transform; export interface ExpressionRenderable { state: 'ready' | 'pending'; @@ -60,9 +65,9 @@ export interface ExpressionRenderable { } export interface ExpressionContext { - state: 'ready' | 'pending'; + state: 'ready' | 'pending' | 'error'; value: ExpressionType; - error: null; + error: null | string; } export interface ResolvedArgType { From 67871d5719b96d1f5cb7242e700b226e3dd4ed3b Mon Sep 17 00:00:00 2001 From: Ignacio Rivas Date: Thu, 9 Sep 2021 13:38:13 +0200 Subject: [PATCH 039/139] [Upgrade Assistant] External links with checkpoint time-range applied (#111252) * Bound query around last checkpoint date * Fix tests * Also test discover url contains search params * Small refactor * Keep state about lastCheckpoint in parent component * Remove space * Address CR changes Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../helpers/app_context.mock.ts | 19 +++++++--- .../fix_logs_step/fix_logs_step.test.tsx | 23 ++++++++++-- .../deprecations_count_checkpoint.tsx | 32 +++++------------ .../overview/fix_logs_step/external_links.tsx | 36 ++++++++++++------- .../overview/fix_logs_step/fix_logs_step.tsx | 16 +++++++-- .../public/application/lib/logs_checkpoint.ts | 30 ++++++++++++++++ .../upgrade_assistant/public/plugin.ts | 3 +- .../plugins/upgrade_assistant/public/types.ts | 3 -- .../plugins/upgrade_assistant/tsconfig.json | 1 + 9 files changed, 113 insertions(+), 50 deletions(-) create mode 100644 x-pack/plugins/upgrade_assistant/public/application/lib/logs_checkpoint.ts diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/app_context.mock.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/app_context.mock.ts index 7550055d8242d..42c05c2d80d37 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/app_context.mock.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/app_context.mock.ts @@ -33,13 +33,24 @@ const idToUrlMap = { SNAPSHOT_RESTORE_LOCATOR: 'snapshotAndRestoreUrl', DISCOVER_APP_LOCATOR: 'discoverUrl', }; +type IdKey = keyof typeof idToUrlMap; + +const stringifySearchParams = (params: Record) => { + const stringifiedParams = Object.keys(params).reduce((list, key) => { + const value = typeof params[key] === 'object' ? JSON.stringify(params[key]) : params[key]; + + return { ...list, [key]: value }; + }, {}); + + return new URLSearchParams(stringifiedParams).toString(); +}; const shareMock = sharePluginMock.createSetupContract(); -shareMock.url.locators.get = (id) => ({ - // @ts-expect-error This object is missing some properties that we're not using in the UI +// @ts-expect-error This object is missing some properties that we're not using in the UI +shareMock.url.locators.get = (id: IdKey) => ({ useUrl: (): string | undefined => idToUrlMap[id], - // @ts-expect-error This object is missing some properties that we're not using in the UI - getUrl: (): string | undefined => idToUrlMap[id], + getUrl: (params: Record): string | undefined => + `${idToUrlMap[id]}?${stringifySearchParams(params)}`, }); export const getAppContextMock = () => ({ diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_logs_step/fix_logs_step.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_logs_step/fix_logs_step.test.tsx index acc64e2872642..e19ea5b1dfd99 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_logs_step/fix_logs_step.test.tsx +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/fix_logs_step/fix_logs_step.test.tsx @@ -7,6 +7,20 @@ import { act } from 'react-dom/test-utils'; +// Once the logs team register the kibana locators in their app, we should be able +// to remove this mock and follow a similar approach to how discover link is tested. +// See: https://github.com/elastic/kibana/issues/104855 +const MOCKED_TIME = '2021-09-05T10:49:01.805Z'; +jest.mock('../../../../public/application/lib/logs_checkpoint', () => { + const originalModule = jest.requireActual('../../../../public/application/lib/logs_checkpoint'); + + return { + __esModule: true, + ...originalModule, + loadLogsCheckpoint: jest.fn().mockReturnValue('2021-09-05T10:49:01.805Z'), + }; +}); + import { DeprecationLoggingStatus } from '../../../../common/types'; import { DEPRECATION_LOGS_SOURCE_ID } from '../../../../common/constants'; import { setupEnvironment } from '../../helpers'; @@ -180,7 +194,7 @@ describe('Overview - Fix deprecation logs step', () => { expect(exists('viewObserveLogs')).toBe(true); expect(find('viewObserveLogs').props().href).toBe( - `/app/logs/stream?sourceId=${DEPRECATION_LOGS_SOURCE_ID}` + `/app/logs/stream?sourceId=${DEPRECATION_LOGS_SOURCE_ID}&logPosition=(end:now,start:'${MOCKED_TIME}')` ); }); @@ -194,7 +208,12 @@ describe('Overview - Fix deprecation logs step', () => { component.update(); expect(exists('viewDiscoverLogs')).toBe(true); - expect(find('viewDiscoverLogs').props().href).toBe('discoverUrl'); + + const decodedUrl = decodeURIComponent(find('viewDiscoverLogs').props().href); + expect(decodedUrl).toContain('discoverUrl'); + ['"language":"kuery"', '"query":"@timestamp+>'].forEach((param) => { + expect(decodedUrl).toContain(param); + }); }); }); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecations_count_checkpoint/deprecations_count_checkpoint.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecations_count_checkpoint/deprecations_count_checkpoint.tsx index f0a4096687f6c..244583e9154c1 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecations_count_checkpoint/deprecations_count_checkpoint.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/deprecations_count_checkpoint/deprecations_count_checkpoint.tsx @@ -5,17 +5,13 @@ * 2.0. */ -import React, { FunctionComponent, useState, useEffect } from 'react'; +import React, { FunctionComponent, useEffect } from 'react'; import moment from 'moment-timezone'; import { FormattedDate, FormattedTime, FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { EuiCallOut, EuiButton, EuiLoadingContent } from '@elastic/eui'; import { useAppContext } from '../../../../app_context'; -import { Storage } from '../../../../../shared_imports'; - -const LS_SETTING_ID = 'kibana.upgradeAssistant.lastCheckpoint'; -const localStorage = new Storage(window.localStorage); const i18nTexts = { calloutTitle: (warningsCount: number, previousCheck: string) => ( @@ -51,32 +47,22 @@ const i18nTexts = { ), }; -const getPreviousCheckpointDate = () => { - const storedValue = moment(localStorage.get(LS_SETTING_ID)); - - if (storedValue.isValid()) { - return storedValue.toISOString(); - } - - const now = moment().toISOString(); - localStorage.set(LS_SETTING_ID, now); - - return now; -}; - interface Props { + checkpoint: string; + setCheckpoint: (value: string) => void; setHasNoDeprecationLogs: (hasNoLogs: boolean) => void; } export const DeprecationsCountCheckpoint: FunctionComponent = ({ + checkpoint, + setCheckpoint, setHasNoDeprecationLogs, }) => { const { services: { api }, } = useAppContext(); - const [previousCheck, setPreviousCheck] = useState(getPreviousCheckpointDate()); const { data, error, isLoading, resendRequest, isInitialRequest } = api.getDeprecationLogsCount( - previousCheck + checkpoint ); const logsCount = data?.count || 0; @@ -87,9 +73,7 @@ export const DeprecationsCountCheckpoint: FunctionComponent = ({ const onResetClick = () => { const now = moment().toISOString(); - - setPreviousCheck(now); - localStorage.set(LS_SETTING_ID, now); + setCheckpoint(now); }; useEffect(() => { @@ -126,7 +110,7 @@ export const DeprecationsCountCheckpoint: FunctionComponent = ({ return ( { - const { indexPatterns: indexPatternService } = dataService; +interface Props { + checkpoint: string; +} - const results = await indexPatternService.find(DEPRECATION_LOGS_INDEX_PATTERN); +const getDeprecationIndexPatternId = async (dataService: DataPublicPluginStart) => { + const results = await dataService.dataViews.find(DEPRECATION_LOGS_INDEX_PATTERN); // Since the find might return also results with wildcard matchers we need to find the // index pattern that has an exact match with our title. const deprecationIndexPattern = results.find( @@ -30,7 +33,7 @@ const getDeprecationIndexPatternId = async (dataService: DataPublicPluginStart) if (deprecationIndexPattern) { return deprecationIndexPattern.id; } else { - const newIndexPattern = await indexPatternService.createAndSave({ + const newIndexPattern = await dataService.dataViews.createAndSave({ title: DEPRECATION_LOGS_INDEX_PATTERN, allowNoIndex: true, }); @@ -38,7 +41,7 @@ const getDeprecationIndexPatternId = async (dataService: DataPublicPluginStart) } }; -const DiscoverAppLink: FunctionComponent = () => { +const DiscoverAppLink: FunctionComponent = ({ checkpoint }) => { const { services: { data: dataService }, plugins: { share }, @@ -55,12 +58,19 @@ const DiscoverAppLink: FunctionComponent = () => { return; } - const url = await locator.getUrl({ indexPatternId }); + const url = await locator.getUrl({ + indexPatternId, + query: { + language: 'kuery', + query: `@timestamp > "${checkpoint}"`, + }, + }); + setDiscoveryUrl(url); }; getDiscoveryUrl(); - }, [dataService, share.url.locators]); + }, [dataService, checkpoint, share.url.locators]); return ( @@ -72,14 +82,16 @@ const DiscoverAppLink: FunctionComponent = () => { ); }; -const ObservabilityAppLink: FunctionComponent = () => { +const ObservabilityAppLink: FunctionComponent = ({ checkpoint }) => { const { services: { core: { http }, }, } = useAppContext(); const logStreamUrl = http?.basePath?.prepend( - `/app/logs/stream?sourceId=${DEPRECATION_LOGS_SOURCE_ID}` + `/app/logs/stream?sourceId=${DEPRECATION_LOGS_SOURCE_ID}&logPosition=(end:now,start:${encode( + checkpoint + )})` ); return ( @@ -92,7 +104,7 @@ const ObservabilityAppLink: FunctionComponent = () => { ); }; -export const ExternalLinks: FunctionComponent = () => { +export const ExternalLinks: FunctionComponent = ({ checkpoint }) => { return ( @@ -106,7 +118,7 @@ export const ExternalLinks: FunctionComponent = () => {

- +
@@ -120,7 +132,7 @@ export const ExternalLinks: FunctionComponent = () => {

- +
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/fix_logs_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/fix_logs_step.tsx index 189dc4e39c0b2..c0977847d121c 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/fix_logs_step.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_logs_step/fix_logs_step.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FunctionComponent } from 'react'; +import React, { FunctionComponent, useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiText, EuiSpacer, EuiPanel, EuiCallOut } from '@elastic/eui'; @@ -15,6 +15,7 @@ import { ExternalLinks } from './external_links'; import { DeprecationsCountCheckpoint } from './deprecations_count_checkpoint'; import { useDeprecationLogging } from './use_deprecation_logging'; import { DeprecationLoggingToggle } from './deprecation_logging_toggle'; +import { loadLogsCheckpoint, saveLogsCheckpoint } from '../../../lib/logs_checkpoint'; import type { OverviewStepProps } from '../../types'; const i18nTexts = { @@ -54,6 +55,11 @@ interface Props { const FixLogsStep: FunctionComponent = ({ setIsComplete }) => { const state = useDeprecationLogging(); + const [checkpoint, setCheckpoint] = useState(loadLogsCheckpoint()); + + useEffect(() => { + saveLogsCheckpoint(checkpoint); + }, [checkpoint]); return ( <> @@ -86,14 +92,18 @@ const FixLogsStep: FunctionComponent = ({ setIsComplete }) => {

{i18nTexts.analyzeTitle}

- +

{i18nTexts.deprecationsCountCheckpointTitle}

- + )} diff --git a/x-pack/plugins/upgrade_assistant/public/application/lib/logs_checkpoint.ts b/x-pack/plugins/upgrade_assistant/public/application/lib/logs_checkpoint.ts new file mode 100644 index 0000000000000..59c3adaed95df --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/lib/logs_checkpoint.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment-timezone'; + +import { Storage } from '../../shared_imports'; + +const SETTING_ID = 'kibana.upgradeAssistant.lastCheckpoint'; +const localStorage = new Storage(window.localStorage); + +export const loadLogsCheckpoint = () => { + const storedValue = moment(localStorage.get(SETTING_ID)); + + if (storedValue.isValid()) { + return storedValue.toISOString(); + } + + const now = moment().toISOString(); + localStorage.set(SETTING_ID, now); + + return now; +}; + +export const saveLogsCheckpoint = (value: string) => { + localStorage.set(SETTING_ID, value); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/plugin.ts b/x-pack/plugins/upgrade_assistant/public/plugin.ts index 75069725c3f59..1b33ec676e1c0 100644 --- a/x-pack/plugins/upgrade_assistant/public/plugin.ts +++ b/x-pack/plugins/upgrade_assistant/public/plugin.ts @@ -42,7 +42,7 @@ export class UpgradeAssistantUIPlugin title: pluginName, order: 1, async mount(params) { - const [coreStart, { discover, data }] = await coreSetup.getStartServices(); + const [coreStart, { data }] = await coreSetup.getStartServices(); const { chrome: { docTitle }, @@ -61,7 +61,6 @@ export class UpgradeAssistantUIPlugin core: coreStart, data, history: params.history, - discover, api: apiService, breadcrumbs: breadcrumbService, }, diff --git a/x-pack/plugins/upgrade_assistant/public/types.ts b/x-pack/plugins/upgrade_assistant/public/types.ts index db04e59ccd362..d59e9b158b74e 100644 --- a/x-pack/plugins/upgrade_assistant/public/types.ts +++ b/x-pack/plugins/upgrade_assistant/public/types.ts @@ -6,7 +6,6 @@ */ import { ScopedHistory } from 'kibana/public'; -import { DiscoverStart } from 'src/plugins/discover/public'; import { ManagementSetup } from 'src/plugins/management/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { SharePluginSetup } from 'src/plugins/share/public'; @@ -30,7 +29,6 @@ export interface SetupDependencies { export interface StartDependencies { licensing: LicensingPluginStart; - discover: DiscoverStart; data: DataPublicPluginStart; } @@ -43,7 +41,6 @@ export interface AppDependencies { }; services: { core: CoreStart; - discover: DiscoverStart; data: DataPublicPluginStart; breadcrumbs: BreadcrumbService; history: ScopedHistory; diff --git a/x-pack/plugins/upgrade_assistant/tsconfig.json b/x-pack/plugins/upgrade_assistant/tsconfig.json index 39d7404ebea9d..4336acb77c2eb 100644 --- a/x-pack/plugins/upgrade_assistant/tsconfig.json +++ b/x-pack/plugins/upgrade_assistant/tsconfig.json @@ -7,6 +7,7 @@ "declarationMap": true }, "include": [ + "../../../typings/**/*", "__jest__/**/*", "common/**/*", "public/**/*", From ddfbe87593e3bc7b4cd60f3c97aab04c45231f89 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 9 Sep 2021 09:07:46 -0400 Subject: [PATCH 040/139] [7.x] [APM] Use full path in route definition (#111501) (#111675) * [APM] Use full path in route definition (#111501) * [APM] Use new path syntax in waterfall (#111689) Co-authored-by: Dario Gieselaar --- .../src/create_router.test.tsx | 30 +- .../src/create_router.ts | 23 +- .../src/types/index.ts | 259 +++++++++++------- .../apm/dev_docs/routing_and_linking.md | 4 +- .../public/components/app/TraceLink/index.tsx | 2 +- .../backend_detail_dependencies_table.tsx | 2 +- .../backend_error_rate_chart.tsx | 2 +- .../backend_latency_chart.tsx | 2 +- .../backend_throughput_chart.tsx | 2 +- .../app/backend_detail_overview/index.tsx | 4 +- .../app/error_group_details/index.tsx | 4 +- .../app/error_group_overview/index.tsx | 2 +- .../service_dependencies_breakdown_chart.tsx | 2 +- .../app/service_inventory/index.tsx | 2 +- .../components/app/service_logs/index.tsx | 2 +- .../components/app/service_map/Controls.tsx | 2 +- .../service_map/Popover/backend_contents.tsx | 6 +- .../service_map/Popover/service_contents.tsx | 4 +- .../components/app/service_map/index.tsx | 2 +- .../components/app/service_metrics/index.tsx | 2 +- .../app/service_node_metrics/index.tsx | 4 +- .../app/service_node_overview/index.tsx | 2 +- .../components/app/service_overview/index.tsx | 4 +- .../index.tsx | 2 +- .../service_overview_errors_table/index.tsx | 2 +- ...ice_overview_instances_chart_and_table.tsx | 2 +- .../index.tsx | 2 +- .../use_instance_details_fetcher.tsx | 2 +- .../service_overview_throughput_chart.tsx | 2 +- .../app/service_profiling/index.tsx | 2 +- .../app/transaction_details/index.tsx | 4 +- .../transaction_details_tabs.tsx | 2 +- .../use_waterfall_fetcher.ts | 2 +- .../waterfall_with_summary/index.tsx | 2 +- .../Waterfall/FlyoutTopLevelProperties.tsx | 2 +- .../span_flyout/sticky_span_properties.tsx | 2 +- .../Waterfall/waterfall_item.tsx | 4 +- .../components/app/transaction_link/index.tsx | 2 +- .../app/transaction_overview/index.tsx | 2 +- .../components/routing/apm_route_config.tsx | 4 +- .../public/components/routing/home/index.tsx | 4 +- .../service_detail/apm_service_wrapper.tsx | 4 +- .../routing/service_detail/index.tsx | 34 +-- ...redirect_to_default_service_route_view.tsx | 2 +- .../components/routing/settings/index.tsx | 16 +- .../analyze_data_button.tsx | 2 +- .../templates/apm_service_template/index.tsx | 24 +- .../public/components/shared/backend_link.tsx | 4 +- .../shared/charts/breakdown_chart/index.tsx | 2 +- .../latency_chart/latency_chart.stories.tsx | 2 +- .../use_transaction_breakdown.ts | 2 +- .../charts/transaction_charts/ml_header.tsx | 2 +- .../transaction_error_rate_chart/index.tsx | 2 +- .../public/components/shared/service_link.tsx | 4 +- .../shared/time_comparison/index.tsx | 2 +- .../apm_backend/apm_backend_context.tsx | 2 +- .../apm_service/apm_service_context.tsx | 2 +- .../use_service_transaction_types_fetcher.tsx | 2 +- .../use_error_group_distribution_fetcher.tsx | 2 +- .../apm/public/hooks/use_search_strategy.ts | 2 +- .../use_service_metric_charts_fetcher.ts | 2 +- .../use_transaction_latency_chart_fetcher.ts | 2 +- .../use_transaction_trace_samples_fetcher.ts | 2 +- 63 files changed, 306 insertions(+), 224 deletions(-) diff --git a/packages/kbn-typed-react-router-config/src/create_router.test.tsx b/packages/kbn-typed-react-router-config/src/create_router.test.tsx index 3fb37f813e2e1..61ba8eb157ee3 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.test.tsx +++ b/packages/kbn-typed-react-router-config/src/create_router.test.tsx @@ -43,17 +43,23 @@ describe('createRouter', () => { }), }, { - path: '/services/:serviceName', + path: '/services', element: <>, - params: t.type({ - path: t.type({ - serviceName: t.string, - }), - query: t.type({ - transactionType: t.string, - environment: t.string, - }), - }), + children: [ + { + element: <>, + path: '/services/{serviceName}', + params: t.type({ + path: t.type({ + serviceName: t.string, + }), + query: t.type({ + transactionType: t.string, + environment: t.string, + }), + }), + }, + ], }, { path: '/traces', @@ -131,7 +137,7 @@ describe('createRouter', () => { '/services/opbeans-java?rangeFrom=now-15m&rangeTo=now&environment=production&transactionType=request' ); - const serviceOverviewParams = router.getParams('/services/:serviceName', history.location); + const serviceOverviewParams = router.getParams('/services/{serviceName}', history.location); expect(serviceOverviewParams).toEqual({ path: { @@ -250,7 +256,7 @@ describe('createRouter', () => { describe('link', () => { it('returns a link for the given route', () => { - const serviceOverviewLink = router.link('/services/:serviceName', { + const serviceOverviewLink = router.link('/services/{serviceName}', { path: { serviceName: 'opbeans-java' }, query: { rangeFrom: 'now-15m', diff --git a/packages/kbn-typed-react-router-config/src/create_router.ts b/packages/kbn-typed-react-router-config/src/create_router.ts index 370d8b48e53b4..7f2ac818fc9b9 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.ts +++ b/packages/kbn-typed-react-router-config/src/create_router.ts @@ -25,22 +25,24 @@ import { Route, Router } from './types'; const deepExactRt: typeof deepExactRtTyped = deepExactRtNonTyped; const mergeRt: typeof mergeRtTyped = mergeRtNonTyped; +function toReactRouterPath(path: string) { + return path.replace(/(?:{([^\/]+)})/, ':$1'); +} + export function createRouter(routes: TRoutes): Router { const routesByReactRouterConfig = new Map(); const reactRouterConfigsByRoute = new Map(); const reactRouterConfigs = routes.map((route) => toReactRouterConfigRoute(route)); - function toReactRouterConfigRoute(route: Route, prefix: string = ''): ReactRouterConfig { - const path = `${prefix}${route.path}`.replace(/\/{2,}/g, '/').replace(/\/$/, '') || '/'; + function toReactRouterConfigRoute(route: Route): ReactRouterConfig { const reactRouterConfig: ReactRouterConfig = { component: () => route.element, routes: - (route.children as Route[] | undefined)?.map((child) => - toReactRouterConfigRoute(child, path) - ) ?? [], + (route.children as Route[] | undefined)?.map((child) => toReactRouterConfigRoute(child)) ?? + [], exact: !route.children?.length, - path, + path: toReactRouterPath(route.path), }; routesByReactRouterConfig.set(reactRouterConfig, route); @@ -71,11 +73,11 @@ export function createRouter(routes: TRoutes): Router match.route.path === path); + : findLastIndex(matches, (match) => match.route.path === toReactRouterPath(path)); if (matchIndex !== -1) { break; @@ -135,11 +137,12 @@ export function createRouter(routes: TRoutes): Router { - return part.startsWith(':') ? paramsWithBuiltInDefaults.path[part.split(':')[1]] : part; + const match = part.match(/(?:{([a-zA-Z]+)})/); + return match ? paramsWithBuiltInDefaults.path[match[1]] : part; }) .join('/'); - const matches = matchRoutesConfig(reactRouterConfigs, path); + const matches = matchRoutesConfig(reactRouterConfigs, toReactRouterPath(path)); if (!matches.length) { throw new Error(`No matching route found for ${path}`); diff --git a/packages/kbn-typed-react-router-config/src/types/index.ts b/packages/kbn-typed-react-router-config/src/types/index.ts index 4d26d2879d5e7..e6c70001ef4b6 100644 --- a/packages/kbn-typed-react-router-config/src/types/index.ts +++ b/packages/kbn-typed-react-router-config/src/types/index.ts @@ -13,7 +13,97 @@ import { RequiredKeys, ValuesType } from 'utility-types'; // import { unconst } from '../unconst'; import { NormalizePath } from './utils'; -export type PathsOf = keyof MapRoutes & string; +type PathsOfRoute = + | TRoute['path'] + | (TRoute extends { children: Route[] } + ? AppendPath | PathsOf + : never); + +export type PathsOf = TRoutes extends [] + ? never + : TRoutes extends [Route] + ? PathsOfRoute + : TRoutes extends [Route, Route] + ? PathsOfRoute | PathsOfRoute + : TRoutes extends [Route, Route, Route] + ? PathsOfRoute | PathsOfRoute | PathsOfRoute + : TRoutes extends [Route, Route, Route, Route] + ? + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + : TRoutes extends [Route, Route, Route, Route, Route] + ? + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + : TRoutes extends [Route, Route, Route, Route, Route, Route] + ? + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + : TRoutes extends [Route, Route, Route, Route, Route, Route, Route] + ? + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + : TRoutes extends [Route, Route, Route, Route, Route, Route, Route, Route] + ? + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + : TRoutes extends [Route, Route, Route, Route, Route, Route, Route, Route, Route] + ? + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + : TRoutes extends [Route, Route, Route, Route, Route, Route, Route, Route, Route, Route] + ? + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + : TRoutes extends [Route, Route, Route, Route, Route, Route, Route, Route, Route, Route, Route] + ? + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + | PathsOfRoute + : string; export interface RouteMatch { route: TRoute; @@ -167,29 +257,17 @@ type MaybeUnion, U extends Record> = [key in keyof U]: key extends keyof T ? T[key] | U[key] : U[key]; }; -type MapRoute< - TRoute extends Route, - TPrefix extends string, - TParents extends Route[] = [] -> = TRoute extends Route +type MapRoute = TRoute extends Route ? MaybeUnion< { - [key in AppendPath]: TRoute & { parents: TParents }; + [key in TRoute['path']]: TRoute & { parents: TParents }; }, TRoute extends { children: Route[] } ? MaybeUnion< - MapRoutes< - TRoute['children'], - AppendPath, - [...TParents, TRoute] - >, + MapRoutes, { - [key in AppendPath>]: ValuesType< - MapRoutes< - TRoute['children'], - AppendPath, - [...TParents, TRoute] - > + [key in AppendPath]: ValuesType< + MapRoutes >; } > @@ -197,74 +275,68 @@ type MapRoute< > : {}; -type MapRoutes< - TRoutes, - TPrefix extends string = '', - TParents extends Route[] = [] -> = TRoutes extends [Route] - ? MapRoute +type MapRoutes = TRoutes extends [Route] + ? MapRoute : TRoutes extends [Route, Route] - ? MapRoute & MapRoute + ? MapRoute & MapRoute : TRoutes extends [Route, Route, Route] - ? MapRoute & - MapRoute & - MapRoute + ? MapRoute & MapRoute & MapRoute : TRoutes extends [Route, Route, Route, Route] - ? MapRoute & - MapRoute & - MapRoute & - MapRoute + ? MapRoute & + MapRoute & + MapRoute & + MapRoute : TRoutes extends [Route, Route, Route, Route, Route] - ? MapRoute & - MapRoute & - MapRoute & - MapRoute & - MapRoute + ? MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute : TRoutes extends [Route, Route, Route, Route, Route, Route] - ? MapRoute & - MapRoute & - MapRoute & - MapRoute & - MapRoute & - MapRoute + ? MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute : TRoutes extends [Route, Route, Route, Route, Route, Route, Route] - ? MapRoute & - MapRoute & - MapRoute & - MapRoute & - MapRoute & - MapRoute & - MapRoute + ? MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute : TRoutes extends [Route, Route, Route, Route, Route, Route, Route, Route] - ? MapRoute & - MapRoute & - MapRoute & - MapRoute & - MapRoute & - MapRoute & - MapRoute & - MapRoute + ? MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute : TRoutes extends [Route, Route, Route, Route, Route, Route, Route, Route, Route] - ? MapRoute & - MapRoute & - MapRoute & - MapRoute & - MapRoute & - MapRoute & - MapRoute & - MapRoute & - MapRoute + ? MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute : TRoutes extends [Route, Route, Route, Route, Route, Route, Route, Route, Route, Route] - ? MapRoute & - MapRoute & - MapRoute & - MapRoute & - MapRoute & - MapRoute & - MapRoute & - MapRoute & - MapRoute & - MapRoute + ? MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute & + MapRoute : {}; // const element = null as any; @@ -279,11 +351,11 @@ type MapRoutes< // element, // children: [ // { -// path: '/agent-configuration', +// path: '/settings/agent-configuration', // element, // }, // { -// path: '/agent-configuration/create', +// path: '/settings/agent-configuration/create', // element, // params: t.partial({ // query: t.partial({ @@ -292,7 +364,7 @@ type MapRoutes< // }), // }, // { -// path: '/agent-configuration/edit', +// path: '/settings/agent-configuration/edit', // element, // params: t.partial({ // query: t.partial({ @@ -301,23 +373,23 @@ type MapRoutes< // }), // }, // { -// path: '/apm-indices', +// path: '/settings/apm-indices', // element, // }, // { -// path: '/customize-ui', +// path: '/settings/customize-ui', // element, // }, // { -// path: '/schema', +// path: '/settings/schema', // element, // }, // { -// path: '/anomaly-detection', +// path: '/settings/anomaly-detection', // element, // }, // { -// path: '/', +// path: '/settings', // element, // }, // ], @@ -346,15 +418,15 @@ type MapRoutes< // ]), // children: [ // { -// path: '/overview', +// path: '/services/:serviceName/overview', // element, // }, // { -// path: '/transactions', +// path: '/services/:serviceName/transactions', // element, // }, // { -// path: '/errors', +// path: '/services/:serviceName/errors', // element, // children: [ // { @@ -367,7 +439,7 @@ type MapRoutes< // }), // }, // { -// path: '/', +// path: '/services/:serviceName', // element, // params: t.partial({ // query: t.partial({ @@ -381,19 +453,19 @@ type MapRoutes< // ], // }, // { -// path: '/foo', +// path: '/services/:serviceName/foo', // element, // }, // { -// path: '/bar', +// path: '/services/:serviceName/bar', // element, // }, // { -// path: '/baz', +// path: '/services/:serviceName/baz', // element, // }, // { -// path: '/', +// path: '/services/:serviceName', // element, // }, // ], @@ -436,6 +508,7 @@ type MapRoutes< // type Bar = ValuesType>['route']['path']; // type Foo = OutputOf; +// type Baz = OutputOf; // const { path }: Foo = {} as any; diff --git a/x-pack/plugins/apm/dev_docs/routing_and_linking.md b/x-pack/plugins/apm/dev_docs/routing_and_linking.md index 7c5a00f43fe4b..478de0081fca4 100644 --- a/x-pack/plugins/apm/dev_docs/routing_and_linking.md +++ b/x-pack/plugins/apm/dev_docs/routing_and_linking.md @@ -18,13 +18,13 @@ Routes (and their parameters) are defined in [public/components/routing/apm_conf #### Parameter handling -Path (like `serviceName` in '/services/:serviceName/transactions') and query parameters are defined in the route definitions. +Path (like `serviceName` in '/services/{serviceName}/transactions') and query parameters are defined in the route definitions. For each parameter, an io-ts runtime type needs to be present: ```tsx { - route: '/services/:serviceName', + route: '/services/{serviceName}', element: , params: t.intersection([ t.type({ diff --git a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx index 36ebb239fd7dd..2733ee0ddbdba 100644 --- a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx @@ -23,7 +23,7 @@ export function TraceLink() { const { path: { traceId }, query: { rangeFrom, rangeTo }, - } = useApmParams('/link-to/trace/:traceId'); + } = useApmParams('/link-to/trace/{traceId}'); const { data = { transaction: null }, status } = useFetcher( (callApmApi) => { diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_detail_dependencies_table.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_detail_dependencies_table.tsx index 4812d17183c5f..f98358e3a9c27 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_detail_dependencies_table.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_detail_dependencies_table.tsx @@ -24,7 +24,7 @@ export function BackendDetailDependenciesTable() { const { query: { rangeFrom, rangeTo, kuery, environment }, - } = useApmParams('/backends/:backendName/overview'); + } = useApmParams('/backends/{backendName}/overview'); const { start, end } = useTimeRange({ rangeFrom, rangeTo }); diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_error_rate_chart.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_error_rate_chart.tsx index 16ab5cefdc658..d48178a8522be 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_error_rate_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_error_rate_chart.tsx @@ -31,7 +31,7 @@ export function BackendFailedTransactionRateChart({ const { query: { kuery, environment, rangeFrom, rangeTo }, - } = useApmParams('/backends/:backendName/overview'); + } = useApmParams('/backends/{backendName}/overview'); const { start, end } = useTimeRange({ rangeFrom, rangeTo }); diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_latency_chart.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_latency_chart.tsx index 99f46e77b60f1..759d153988875 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_latency_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_latency_chart.tsx @@ -27,7 +27,7 @@ export function BackendLatencyChart({ height }: { height: number }) { const { query: { rangeFrom, rangeTo, kuery, environment }, - } = useApmParams('/backends/:backendName/overview'); + } = useApmParams('/backends/{backendName}/overview'); const { start, end } = useTimeRange({ rangeFrom, rangeTo }); diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_throughput_chart.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_throughput_chart.tsx index ba4bdafe94bdf..2cfc7ea317628 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_throughput_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_throughput_chart.tsx @@ -23,7 +23,7 @@ export function BackendThroughputChart({ height }: { height: number }) { const { query: { rangeFrom, rangeTo, kuery, environment }, - } = useApmParams('/backends/:backendName/overview'); + } = useApmParams('/backends/{backendName}/overview'); const { start, end } = useTimeRange({ rangeFrom, rangeTo }); diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx index 1060e20e9c595..16120a6f5b429 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx @@ -33,7 +33,7 @@ export function BackendDetailOverview() { const { path: { backendName }, query: { rangeFrom, rangeTo, environment, kuery }, - } = useApmParams('/backends/:backendName/overview'); + } = useApmParams('/backends/{backendName}/overview'); const apmRouter = useApmRouter(); @@ -46,7 +46,7 @@ export function BackendDetailOverview() { }, { title: backendName, - href: apmRouter.link('/backends/:backendName/overview', { + href: apmRouter.link('/backends/{backendName}/overview', { path: { backendName }, query: { rangeFrom, diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx index 3929a055bd77b..9145e019c37ea 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx @@ -103,13 +103,13 @@ export function ErrorGroupDetails() { const { path: { groupId }, query: { rangeFrom, rangeTo, environment, kuery }, - } = useApmParams('/services/:serviceName/errors/:groupId'); + } = useApmParams('/services/{serviceName}/errors/{groupId}'); const { start, end } = useTimeRange({ rangeFrom, rangeTo }); useBreadcrumb({ title: groupId, - href: apmRouter.link('/services/:serviceName/errors/:groupId', { + href: apmRouter.link('/services/{serviceName}/errors/{groupId}', { path: { serviceName, groupId, diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx index 7fdedb8f7e7b9..97a3c38b65986 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx @@ -27,7 +27,7 @@ export function ErrorGroupOverview() { const { query: { environment, kuery, sortField, sortDirection, rangeFrom, rangeTo }, - } = useApmParams('/services/:serviceName/errors'); + } = useApmParams('/services/{serviceName}/errors'); const { start, end } = useTimeRange({ rangeFrom, rangeTo }); diff --git a/x-pack/plugins/apm/public/components/app/service_dependencies/service_dependencies_breakdown_chart.tsx b/x-pack/plugins/apm/public/components/app/service_dependencies/service_dependencies_breakdown_chart.tsx index 1ce6d54754719..426328a8ce9f0 100644 --- a/x-pack/plugins/apm/public/components/app/service_dependencies/service_dependencies_breakdown_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/service_dependencies/service_dependencies_breakdown_chart.tsx @@ -22,7 +22,7 @@ export function ServiceDependenciesBreakdownChart({ const { query: { kuery, environment, rangeFrom, rangeTo }, - } = useApmParams('/services/:serviceName/dependencies'); + } = useApmParams('/services/{serviceName}/dependencies'); const { start, end } = useTimeRange({ rangeFrom, rangeTo }); diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx index c822e32ea1fc6..c72c0cfbbceed 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx @@ -42,7 +42,7 @@ function useServicesFetcher() { const { query: { rangeFrom, rangeTo, environment, kuery }, - } = useApmParams('/services/:serviceName', '/services'); + } = useApmParams('/services/{serviceName}', '/services'); const { start, end } = useTimeRange({ rangeFrom, rangeTo }); diff --git a/x-pack/plugins/apm/public/components/app/service_logs/index.tsx b/x-pack/plugins/apm/public/components/app/service_logs/index.tsx index ac4a4fb51ce8a..79818473d26b1 100644 --- a/x-pack/plugins/apm/public/components/app/service_logs/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_logs/index.tsx @@ -27,7 +27,7 @@ export function ServiceLogs() { const { query: { environment, kuery, rangeFrom, rangeTo }, - } = useApmParams('/services/:serviceName/logs'); + } = useApmParams('/services/{serviceName}/logs'); const { start, end } = useTimeRange({ rangeFrom, rangeTo }); diff --git a/x-pack/plugins/apm/public/components/app/service_map/Controls.tsx b/x-pack/plugins/apm/public/components/app/service_map/Controls.tsx index f46b1232b00fd..dd34110a8ffc6 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Controls.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Controls.tsx @@ -107,7 +107,7 @@ export function Controls() { const { query: { kuery }, - } = useApmParams('/service-map', '/services/:serviceName/service-map'); + } = useApmParams('/service-map', '/services/{serviceName}/service-map'); const [zoom, setZoom] = useState((cy && cy.zoom()) || 1); const duration = parseInt(theme.eui.euiAnimSpeedFast, 10); diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx index 9bc30ee67d2c7..c01cf4579fdbd 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx @@ -27,7 +27,7 @@ export function BackendContents({ }: ContentsProps) { const { query } = useApmParams( '/service-map', - '/services/:serviceName/service-map' + '/services/{serviceName}/service-map' ); const apmRouter = useApmRouter(); @@ -57,11 +57,11 @@ export function BackendContents({ ); const isLoading = status === FETCH_STATUS.LOADING; - const detailsUrl = apmRouter.link('/backends/:backendName/overview', { + const detailsUrl = apmRouter.link('/backends/{backendName}/overview', { path: { backendName }, query: query as TypeOf< ApmRoutes, - '/backends/:backendName/overview' + '/backends/{backendName}/overview' >['query'], }); diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/service_contents.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/service_contents.tsx index eb13a854925c4..5eef580793d10 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/service_contents.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/service_contents.tsx @@ -63,12 +63,12 @@ export function ServiceContents({ const isLoading = status === FETCH_STATUS.LOADING; - const detailsUrl = apmRouter.link('/services/:serviceName', { + const detailsUrl = apmRouter.link('/services/{serviceName}', { path: { serviceName }, query: { rangeFrom, rangeTo, environment, kuery }, }); - const focusUrl = apmRouter.link('/services/:serviceName/service-map', { + const focusUrl = apmRouter.link('/services/{serviceName}/service-map', { path: { serviceName }, query: { rangeFrom, rangeTo, environment, kuery }, }); diff --git a/x-pack/plugins/apm/public/components/app/service_map/index.tsx b/x-pack/plugins/apm/public/components/app/service_map/index.tsx index c3a6dca165131..97b4f548f4bf9 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/index.tsx @@ -83,7 +83,7 @@ export function ServiceMapHome() { export function ServiceMapServiceDetail() { const { query: { environment, kuery, rangeFrom, rangeTo }, - } = useApmParams('/services/:serviceName/service-map'); + } = useApmParams('/services/{serviceName}/service-map'); const { start, end } = useTimeRange({ rangeFrom, rangeTo }); return ( { setSampleActivePage(0); diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/FlyoutTopLevelProperties.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/FlyoutTopLevelProperties.tsx index 6bbcfcf545ee1..8954081f9ab47 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/FlyoutTopLevelProperties.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/FlyoutTopLevelProperties.tsx @@ -27,7 +27,7 @@ export function FlyoutTopLevelProperties({ transaction }: Props) { const { urlParams: { latencyAggregationType }, } = useUrlParams(); - const { query } = useApmParams('/services/:serviceName/transactions/view'); + const { query } = useApmParams('/services/{serviceName}/transactions/view'); if (!transaction) { return null; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/sticky_span_properties.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/sticky_span_properties.tsx index 97e353d22ccf6..2e02dcee95371 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/sticky_span_properties.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/span_flyout/sticky_span_properties.tsx @@ -33,7 +33,7 @@ interface Props { } export function StickySpanProperties({ span, transaction }: Props) { - const { query } = useApmParams('/services/:serviceName/transactions/view'); + const { query } = useApmParams('/services/{serviceName}/transactions/view'); const { environment, latencyAggregationType } = query; const trackEvent = useUiTracker(); diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_item.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_item.tsx index 1a4fa4f5fe836..4001a0624a809 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_item.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/Waterfall/waterfall_item.tsx @@ -230,9 +230,9 @@ function RelatedErrors({ }) { const apmRouter = useApmRouter(); const theme = useTheme(); - const { query } = useApmParams('/services/:serviceName/transactions/view'); + const { query } = useApmParams('/services/{serviceName}/transactions/view'); - const href = apmRouter.link(`/services/:serviceName/errors`, { + const href = apmRouter.link(`/services/{serviceName}/errors`, { path: { serviceName: item.doc.service.name }, query: { ...query, diff --git a/x-pack/plugins/apm/public/components/app/transaction_link/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_link/index.tsx index 25cbf2d319587..468a90f6b17de 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_link/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_link/index.tsx @@ -22,7 +22,7 @@ export function TransactionLink() { const { path: { transactionId }, query: { rangeFrom, rangeTo }, - } = useApmParams('/link-to/transaction/:transactionId'); + } = useApmParams('/link-to/transaction/{transactionId}'); const { data = { transaction: null }, status } = useFetcher( (callApmApi) => { diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx index 571ba99d9bf08..a1362f7373e2a 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx @@ -26,7 +26,7 @@ export function TransactionOverview() { rangeTo, transactionType: transactionTypeFromUrl, }, - } = useApmParams('/services/:serviceName/transactions'); + } = useApmParams('/services/{serviceName}/transactions'); const { start, end } = useTimeRange({ rangeFrom, rangeTo }); diff --git a/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx b/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx index b751ef3f71190..5377cb81b372e 100644 --- a/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx +++ b/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx @@ -21,7 +21,7 @@ import { settings } from './settings'; */ const apmRoutes = route([ { - path: '/link-to/transaction/:transactionId', + path: '/link-to/transaction/{transactionId}', element: , params: t.intersection([ t.type({ @@ -38,7 +38,7 @@ const apmRoutes = route([ ]), }, { - path: '/link-to/trace/:traceId', + path: '/link-to/trace/{traceId}', element: , params: t.intersection([ t.type({ diff --git a/x-pack/plugins/apm/public/components/routing/home/index.tsx b/x-pack/plugins/apm/public/components/routing/home/index.tsx index 1430f5d8e4756..1736a22e9b540 100644 --- a/x-pack/plugins/apm/public/components/routing/home/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/home/index.tsx @@ -104,7 +104,7 @@ export const home = { }), children: [ { - path: '/:backendName/overview', + path: '/backends/{backendName}/overview', element: , params: t.type({ path: t.type({ @@ -113,7 +113,7 @@ export const home = { }), }, page({ - path: '/', + path: '/backends', title: DependenciesInventoryTitle, element: , }), diff --git a/x-pack/plugins/apm/public/components/routing/service_detail/apm_service_wrapper.tsx b/x-pack/plugins/apm/public/components/routing/service_detail/apm_service_wrapper.tsx index aa69aa4fa7965..ef929331f3c1c 100644 --- a/x-pack/plugins/apm/public/components/routing/service_detail/apm_service_wrapper.tsx +++ b/x-pack/plugins/apm/public/components/routing/service_detail/apm_service_wrapper.tsx @@ -15,7 +15,7 @@ export function ApmServiceWrapper() { const { path: { serviceName }, query, - } = useApmParams('/services/:serviceName'); + } = useApmParams('/services/{serviceName}'); const router = useApmRouter(); @@ -26,7 +26,7 @@ export function ApmServiceWrapper() { }, { title: serviceName, - href: router.link('/services/:serviceName', { + href: router.link('/services/{serviceName}', { query, path: { serviceName }, }), diff --git a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx index 5124087369ee4..9b87cc338bb9b 100644 --- a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx @@ -62,7 +62,7 @@ function page({ } export const serviceDetail = { - path: '/services/:serviceName', + path: '/services/{serviceName}', element: , params: t.intersection([ t.type({ @@ -97,7 +97,7 @@ export const serviceDetail = { }, children: [ page({ - path: '/overview', + path: '/services/{serviceName}/overview', element: , tab: 'overview', title: i18n.translate('xpack.apm.views.overview.title', { @@ -110,7 +110,7 @@ export const serviceDetail = { }), { ...page({ - path: '/transactions', + path: '/services/{serviceName}/transactions', tab: 'transactions', title: i18n.translate('xpack.apm.views.transactions.title', { defaultMessage: 'Transactions', @@ -123,7 +123,7 @@ export const serviceDetail = { }), children: [ { - path: '/view', + path: '/services/{serviceName}/transactions/view', element: , params: t.type({ query: t.intersection([ @@ -138,13 +138,13 @@ export const serviceDetail = { }), }, { - path: '/', + path: '/services/{serviceName}/transactions', element: , }, ], }, page({ - path: '/dependencies', + path: '/services/{serviceName}/dependencies', element: , tab: 'dependencies', title: i18n.translate('xpack.apm.views.dependencies.title', { @@ -156,7 +156,7 @@ export const serviceDetail = { }), { ...page({ - path: '/errors', + path: '/services/{serviceName}/errors', tab: 'errors', title: i18n.translate('xpack.apm.views.errors.title', { defaultMessage: 'Errors', @@ -173,7 +173,7 @@ export const serviceDetail = { }), children: [ { - path: '/:groupId', + path: '/services/{serviceName}/errors/{groupId}', element: , params: t.type({ path: t.type({ @@ -182,13 +182,13 @@ export const serviceDetail = { }), }, { - path: '/', + path: '/services/{serviceName}/errors', element: , }, ], }, page({ - path: '/metrics', + path: '/services/{serviceName}/metrics', tab: 'metrics', title: i18n.translate('xpack.apm.views.metrics.title', { defaultMessage: 'Metrics', @@ -197,7 +197,7 @@ export const serviceDetail = { }), { ...page({ - path: '/nodes', + path: '/services/{serviceName}/nodes', tab: 'nodes', title: i18n.translate('xpack.apm.views.nodes.title', { defaultMessage: 'JVMs', @@ -206,7 +206,7 @@ export const serviceDetail = { }), children: [ { - path: '/:serviceNodeName/metrics', + path: '/services/{serviceName}/nodes/{serviceNodeName}/metrics', element: , params: t.type({ path: t.type({ @@ -215,7 +215,7 @@ export const serviceDetail = { }), }, { - path: '/', + path: '/services/{serviceName}/nodes', element: , params: t.partial({ query: t.partial({ @@ -229,7 +229,7 @@ export const serviceDetail = { ], }, page({ - path: '/service-map', + path: '/services/{serviceName}/service-map', tab: 'service-map', title: i18n.translate('xpack.apm.views.serviceMap.title', { defaultMessage: 'Service Map', @@ -240,7 +240,7 @@ export const serviceDetail = { }, }), page({ - path: '/logs', + path: '/services/{serviceName}/logs', tab: 'logs', title: i18n.translate('xpack.apm.views.logs.title', { defaultMessage: 'Logs', @@ -251,7 +251,7 @@ export const serviceDetail = { }, }), page({ - path: '/profiling', + path: '/services/{serviceName}/profiling', tab: 'profiling', title: i18n.translate('xpack.apm.views.serviceProfiling.title', { defaultMessage: 'Profiling', @@ -259,7 +259,7 @@ export const serviceDetail = { element: , }), { - path: '/', + path: '/services/{serviceName}/', element: , }, ], diff --git a/x-pack/plugins/apm/public/components/routing/service_detail/redirect_to_default_service_route_view.tsx b/x-pack/plugins/apm/public/components/routing/service_detail/redirect_to_default_service_route_view.tsx index 66595430f618d..1cd61ef6e9243 100644 --- a/x-pack/plugins/apm/public/components/routing/service_detail/redirect_to_default_service_route_view.tsx +++ b/x-pack/plugins/apm/public/components/routing/service_detail/redirect_to_default_service_route_view.tsx @@ -13,7 +13,7 @@ export function RedirectToDefaultServiceRouteView() { const { path: { serviceName }, query, - } = useApmParams('/services/:serviceName/*'); + } = useApmParams('/services/{serviceName}/*'); const search = qs.stringify(query); diff --git a/x-pack/plugins/apm/public/components/routing/settings/index.tsx b/x-pack/plugins/apm/public/components/routing/settings/index.tsx index e844f05050d17..e33f60e5593b0 100644 --- a/x-pack/plugins/apm/public/components/routing/settings/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/settings/index.tsx @@ -58,7 +58,7 @@ export const settings = { ), children: [ page({ - path: '/agent-configuration', + path: '/settings/agent-configuration', tab: 'agent-configurations', title: i18n.translate( 'xpack.apm.views.settings.agentConfiguration.title', @@ -68,7 +68,7 @@ export const settings = { }), { ...page({ - path: '/agent-configuration/create', + path: '/settings/agent-configuration/create', title: i18n.translate( 'xpack.apm.views.settings.createAgentConfiguration.title', { defaultMessage: 'Create Agent Configuration' } @@ -84,7 +84,7 @@ export const settings = { }, { ...page({ - path: '/agent-configuration/edit', + path: '/settings/agent-configuration/edit', title: i18n.translate( 'xpack.apm.views.settings.editAgentConfiguration.title', { defaultMessage: 'Edit Agent Configuration' } @@ -101,7 +101,7 @@ export const settings = { }), }, page({ - path: '/apm-indices', + path: '/settings/apm-indices', title: i18n.translate('xpack.apm.views.settings.indices.title', { defaultMessage: 'Indices', }), @@ -109,7 +109,7 @@ export const settings = { element: , }), page({ - path: '/customize-ui', + path: '/settings/customize-ui', title: i18n.translate('xpack.apm.views.settings.customizeUI.title', { defaultMessage: 'Customize app', }), @@ -117,7 +117,7 @@ export const settings = { element: , }), page({ - path: '/schema', + path: '/settings/schema', title: i18n.translate('xpack.apm.views.settings.schema.title', { defaultMessage: 'Schema', }), @@ -125,7 +125,7 @@ export const settings = { tab: 'schema', }), page({ - path: '/anomaly-detection', + path: '/settings/anomaly-detection', title: i18n.translate('xpack.apm.views.settings.anomalyDetection.title', { defaultMessage: 'Anomaly detection', }), @@ -133,7 +133,7 @@ export const settings = { tab: 'anomaly-detection', }), { - path: '/', + path: '/settings', element: , }, ], diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx index 03fe39e818eaa..068d7bb1c242f 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx @@ -47,7 +47,7 @@ export function AnalyzeDataButton() { const { query: { rangeFrom, rangeTo, environment }, - } = useApmParams('/services/:serviceName'); + } = useApmParams('/services/{serviceName}'); const basepath = services.http?.basePath.get(); const canShowDashboard = services.application?.capabilities.dashboard.show; diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx index bb00c631fe171..0ae718c79cf39 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx @@ -72,7 +72,7 @@ function TemplateWithContext({ path: { serviceName }, query, query: { rangeFrom, rangeTo }, - } = useApmParams('/services/:serviceName/*'); + } = useApmParams('/services/{serviceName}/*'); const { start, end } = useTimeRange({ rangeFrom, rangeTo }); @@ -82,7 +82,7 @@ function TemplateWithContext({ useBreadcrumb({ title, - href: router.link(`/services/:serviceName/${selectedTab}` as const, { + href: router.link(`/services/{serviceName}/${selectedTab}` as const, { path: { serviceName }, query, }), @@ -162,7 +162,7 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { const { path: { serviceName }, query: queryFromUrl, - } = useApmParams(`/services/:serviceName/${selectedTab}` as const); + } = useApmParams(`/services/{serviceName}/${selectedTab}` as const); const query = omit( queryFromUrl, @@ -175,7 +175,7 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { const tabs: Tab[] = [ { key: 'overview', - href: router.link('/services/:serviceName/overview', { + href: router.link('/services/{serviceName}/overview', { path: { serviceName }, query, }), @@ -185,7 +185,7 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { }, { key: 'transactions', - href: router.link('/services/:serviceName/transactions', { + href: router.link('/services/{serviceName}/transactions', { path: { serviceName }, query, }), @@ -195,7 +195,7 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { }, { key: 'dependencies', - href: router.link('/services/:serviceName/dependencies', { + href: router.link('/services/{serviceName}/dependencies', { path: { serviceName }, query, }), @@ -207,7 +207,7 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { }, { key: 'errors', - href: router.link('/services/:serviceName/errors', { + href: router.link('/services/{serviceName}/errors', { path: { serviceName }, query, }), @@ -217,7 +217,7 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { }, { key: 'metrics', - href: router.link('/services/:serviceName/metrics', { + href: router.link('/services/{serviceName}/metrics', { path: { serviceName }, query, }), @@ -228,7 +228,7 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { }, { key: 'nodes', - href: router.link('/services/:serviceName/nodes', { + href: router.link('/services/{serviceName}/nodes', { path: { serviceName }, query, }), @@ -239,7 +239,7 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { }, { key: 'service-map', - href: router.link('/services/:serviceName/service-map', { + href: router.link('/services/{serviceName}/service-map', { path: { serviceName }, query, }), @@ -249,7 +249,7 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { }, { key: 'logs', - href: router.link('/services/:serviceName/logs', { + href: router.link('/services/{serviceName}/logs', { path: { serviceName }, query, }), @@ -261,7 +261,7 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { }, { key: 'profiling', - href: router.link('/services/:serviceName/profiling', { + href: router.link('/services/{serviceName}/profiling', { path: { serviceName, }, diff --git a/x-pack/plugins/apm/public/components/shared/backend_link.tsx b/x-pack/plugins/apm/public/components/shared/backend_link.tsx index caae47184510a..342c668d2efdb 100644 --- a/x-pack/plugins/apm/public/components/shared/backend_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/backend_link.tsx @@ -18,7 +18,7 @@ const StyledLink = euiStyled(EuiLink)`${truncate('100%')};`; interface BackendLinkProps { backendName: string; - query: TypeOf['query']; + query: TypeOf['query']; subtype?: string; type?: string; onClick?: React.ComponentProps['onClick']; @@ -35,7 +35,7 @@ export function BackendLink({ return ( - + diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/use_transaction_breakdown.ts b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/use_transaction_breakdown.ts index bb56338531df3..789461379d044 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/use_transaction_breakdown.ts +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/use_transaction_breakdown.ts @@ -24,7 +24,7 @@ export function useTransactionBreakdown({ const { query: { rangeFrom, rangeTo }, - } = useApmParams('/services/:serviceName'); + } = useApmParams('/services/{serviceName}'); const { start, end } = useTimeRange({ rangeFrom, rangeTo }); diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx index f69b7e7004510..76e85b1d9998d 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx @@ -36,7 +36,7 @@ export function MLHeader({ hasValidMlLicense, mlJobId }: Props) { const { query: { kuery }, - } = useApmParams('/services/:serviceName'); + } = useApmParams('/services/{serviceName}'); if (!hasValidMlLicense || !mlJobId) { return null; diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx index 2e8578e29297c..ae24a5e53444e 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx @@ -63,7 +63,7 @@ export function TransactionErrorRateChart({ const { query: { rangeFrom, rangeTo }, - } = useApmParams('/services/:serviceName'); + } = useApmParams('/services/{serviceName}'); const { start, end } = useTimeRange({ rangeFrom, rangeTo }); diff --git a/x-pack/plugins/apm/public/components/shared/service_link.tsx b/x-pack/plugins/apm/public/components/shared/service_link.tsx index a09ce958fdcab..d8f346f63b2ae 100644 --- a/x-pack/plugins/apm/public/components/shared/service_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/service_link.tsx @@ -19,7 +19,7 @@ const StyledLink = euiStyled(EuiLink)`${truncate('100%')};`; interface ServiceLinkProps { agentName?: AgentName; - query: TypeOf['query']; + query: TypeOf['query']; serviceName: string; } @@ -33,7 +33,7 @@ export function ServiceLink({ return ( Date: Thu, 9 Sep 2021 07:20:58 -0700 Subject: [PATCH 041/139] Use correct SnapshotSnapshotInfo type in UA API integration test. (#111627) --- .../apis/upgrade_assistant/cloud_backup_status.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/test/api_integration/apis/upgrade_assistant/cloud_backup_status.ts b/x-pack/test/api_integration/apis/upgrade_assistant/cloud_backup_status.ts index 7095c115ae7e1..3a62897c83e16 100644 --- a/x-pack/test/api_integration/apis/upgrade_assistant/cloud_backup_status.ts +++ b/x-pack/test/api_integration/apis/upgrade_assistant/cloud_backup_status.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -54,13 +55,12 @@ export default function ({ getService }: FtrProviderContext) { describe('Cloud backup status', () => { describe('get', () => { describe('with backups present', () => { - // Needs SnapshotInfo type https://github.com/elastic/elasticsearch-specification/issues/685 - let mostRecentSnapshot: any; + let mostRecentSnapshot: estypes.SnapshotSnapshotInfo; before(async () => { await createCloudRepository(); await createCloudSnapshot('test_snapshot_1'); - mostRecentSnapshot = (await createCloudSnapshot('test_snapshot_2')).body.snapshot; + mostRecentSnapshot = (await createCloudSnapshot('test_snapshot_2')).body.snapshot!; }); after(async () => { From 99b1d13dfdf4333475a52c257acb93fddef4b052 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 9 Sep 2021 09:29:27 -0500 Subject: [PATCH 042/139] skip flaky suite, #109564 --- .../functional/apps/graph/feature_controls/graph_security.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts b/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts index cc121dccfb13e..913a5034bacc5 100644 --- a/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts +++ b/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts @@ -16,7 +16,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const appsMenu = getService('appsMenu'); const globalNav = getService('globalNav'); - describe('security', () => { + // FLAKY https://github.com/elastic/kibana/issues/109564 + describe.skip('security', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); // ensure we're logged out so we can login as the appropriate users From d91e8656f48da070062bbcfc1269c08c989e9f9e Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 9 Sep 2021 10:32:03 -0400 Subject: [PATCH 043/139] [Security Solution] add timeline id to context (#111435) (#111712) * add timeline context * remove an unused file Co-authored-by: Angela Chuang <6295984+angorayc@users.noreply.github.com> --- .../use_get_timeline_id_from_dom.tsx | 37 ------ .../event_details/table/action_cell.tsx | 10 +- .../events_viewer/events_viewer.tsx | 105 +++++++-------- .../hover_actions/use_hover_actions.tsx | 8 +- .../components/fields_browser/field_name.tsx | 8 +- .../timelines/components/timeline/index.tsx | 63 ++++----- .../components/t_grid/integrated/index.tsx | 122 +++++++++--------- .../public/components/t_grid/shared/index.tsx | 4 +- .../components/t_grid/standalone/index.tsx | 7 +- x-pack/plugins/timelines/public/index.ts | 1 + 10 files changed, 166 insertions(+), 199 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/common/components/drag_and_drop/use_get_timeline_id_from_dom.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/use_get_timeline_id_from_dom.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/use_get_timeline_id_from_dom.tsx deleted file mode 100644 index fcb547842aec4..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/use_get_timeline_id_from_dom.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect, useState } from 'react'; - -import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles'; - -export const useGetTimelineId = function ( - elem: React.MutableRefObject, - getTimelineId: boolean = false -) { - const [timelineId, setTimelineId] = useState(null); - - useEffect(() => { - let startElem: Element | (Node & ParentNode) | null = elem.current; - if (startElem != null && getTimelineId) { - for (; startElem && startElem !== document; startElem = startElem.parentNode) { - const myElem: Element = startElem as Element; - if ( - myElem != null && - myElem.classList != null && - myElem.classList.contains(SELECTOR_TIMELINE_GLOBAL_CONTAINER) && - myElem.hasAttribute('data-timeline-id') - ) { - setTimelineId(myElem.getAttribute('data-timeline-id')); - break; - } - } - } - }, [elem, getTimelineId]); - - return timelineId; -}; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx index 454c047c9facf..74d46cf3431dc 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/action_cell.tsx @@ -5,13 +5,13 @@ * 2.0. */ -import React, { useCallback, useState, useRef } from 'react'; +import React, { useCallback, useState, useContext } from 'react'; import { HoverActions } from '../../hover_actions'; import { useActionCellDataProvider } from './use_action_cell_data_provider'; import { EventFieldsData, FieldsData } from '../types'; -import { useGetTimelineId } from '../../drag_and_drop/use_get_timeline_id_from_dom'; import { ColumnHeaderOptions } from '../../../../../common/types/timeline'; import { BrowserField } from '../../../containers/source'; +import { TimelineContext } from '../../../../../../timelines/public'; interface Props { contextId: string; @@ -52,12 +52,9 @@ export const ActionCell: React.FC = React.memo( values, }); - const draggableRef = useRef(null); const [showTopN, setShowTopN] = useState(false); - const [goGetTimelineId, setGoGetTimelineId] = useState(false); - const timelineIdFind = useGetTimelineId(draggableRef, goGetTimelineId); + const { timelineId: timelineIdFind } = useContext(TimelineContext); const [hoverActionsOwnFocus] = useState(false); - const toggleTopN = useCallback(() => { setShowTopN((prevShowTopN) => { const newShowTopN = !prevShowTopN; @@ -76,7 +73,6 @@ export const ActionCell: React.FC = React.memo( dataProvider={actionCellConfig?.dataProvider} enableOverflowButton={true} field={data.field} - goGetTimelineId={setGoGetTimelineId} isObjectArray={data.isObjectArray} onFilterAdded={onFilterAdded} ownFocus={hoverActionsOwnFocus} diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index 057d28b0112ad..c8b8cf57c698d 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -52,6 +52,7 @@ import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/component import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline'; import { useDeepEqualSelector } from '../../hooks/use_selector'; import { defaultControlColumn } from '../../../timelines/components/timeline/body/control_columns'; +import { TimelineContext } from '../../../../../timelines/public'; export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px const UTILITY_BAR_HEIGHT = 19; // px @@ -287,7 +288,7 @@ const EventsViewerComponent: React.FC = ({ const leadingControlColumns: ControlColumnProps[] = [defaultControlColumn]; const trailingControlColumns: ControlColumnProps[] = []; - + const timelineContext = useMemo(() => ({ timelineId: id }), [id]); return ( = ({ {utilityBar && !resolverIsShowing(graphEventId) && ( {utilityBar?.(refetch, totalCountMinusDeleted)} )} - - + + + - {graphEventId && } - - - -