From ce1846511f105f06ea4900057df53129aa33272a Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 8 Jul 2020 08:33:26 +0100 Subject: [PATCH 01/13] fixed api url in example plugin (#70934) This PR fixes a API URL used in the Alerting Example plugin which was broken when the Api Endpoint was migrated to it's new location --- examples/alerting_example/public/components/view_alert.tsx | 4 ++-- .../alerting_example/public/components/view_astros_alert.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/alerting_example/public/components/view_alert.tsx b/examples/alerting_example/public/components/view_alert.tsx index 75a515bfa1b25..0f7fc70648a9e 100644 --- a/examples/alerting_example/public/components/view_alert.tsx +++ b/examples/alerting_example/public/components/view_alert.tsx @@ -49,10 +49,10 @@ export const ViewAlertPage = withRouter(({ http, id }: Props) => { useEffect(() => { if (!alert) { - http.get(`${BASE_ALERT_API_PATH}/${id}`).then(setAlert); + http.get(`${BASE_ALERT_API_PATH}/alert/${id}`).then(setAlert); } if (!alertState) { - http.get(`${BASE_ALERT_API_PATH}/${id}/state`).then(setAlertState); + http.get(`${BASE_ALERT_API_PATH}/alert/${id}/state`).then(setAlertState); } }, [alert, alertState, http, id]); diff --git a/examples/alerting_example/public/components/view_astros_alert.tsx b/examples/alerting_example/public/components/view_astros_alert.tsx index 19f235a3f3e4e..b2d3cec269b72 100644 --- a/examples/alerting_example/public/components/view_astros_alert.tsx +++ b/examples/alerting_example/public/components/view_astros_alert.tsx @@ -55,10 +55,10 @@ export const ViewPeopleInSpaceAlertPage = withRouter(({ http, id }: Props) => { useEffect(() => { if (!alert) { - http.get(`${BASE_ALERT_API_PATH}/${id}`).then(setAlert); + http.get(`${BASE_ALERT_API_PATH}/alert/${id}`).then(setAlert); } if (!alertState) { - http.get(`${BASE_ALERT_API_PATH}/${id}/state`).then(setAlertState); + http.get(`${BASE_ALERT_API_PATH}/alert/${id}/state`).then(setAlertState); } }, [alert, alertState, http, id]); From acd5da8b9d3c5ae767256387b0b36f716a55ca27 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Wed, 8 Jul 2020 08:45:20 +0100 Subject: [PATCH 02/13] [Functional test] Add retry for dashboard save (#70950) --- test/functional/page_objects/dashboard_page.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 92482a3779771..7c325ba6d4aec 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -290,14 +290,16 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide dashboardName: string, saveOptions: SaveDashboardOptions = { waitDialogIsClosed: true } ) { - await this.enterDashboardTitleAndClickSave(dashboardName, saveOptions); + await retry.try(async () => { + await this.enterDashboardTitleAndClickSave(dashboardName, saveOptions); - if (saveOptions.needsConfirm) { - await this.clickSave(); - } + if (saveOptions.needsConfirm) { + await this.clickSave(); + } - // Confirm that the Dashboard has actually been saved - await testSubjects.existOrFail('saveDashboardSuccess'); + // Confirm that the Dashboard has actually been saved + await testSubjects.existOrFail('saveDashboardSuccess'); + }); const message = await PageObjects.common.closeToast(); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.common.waitForSaveModalToClose(); From bb96f5dd948ed5ad25924bf0140709eb6dbfae50 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 8 Jul 2020 10:10:32 +0200 Subject: [PATCH 03/13] [ML] Transforms/DFA: Refactor list action buttons so modals won't unmount after button click. (#70555) Related to #70383 and #63455. Refactors the action buttons of the transform and data frame analytics jobs list: Previously custom actions included state and JSX for e.g. confirmation modals. Problem with that: If the actions list popover hides, the modal would unmount too. Since EUI's behaviour will change with the release/merge of #70383, we needed a refactor that solves that issue right now. With this PR, state management for UI behaviour that follows after a button click like the confirmation modals was moved to a custom hook which is part of the outer level of the buttons itself. The modal now also gets mounted on the outer level. This way we won't loose the modals state and DOM rendering when the action button hides. Note that this PR doesn't fix the nested buttons issue (#63455) yet. For that we need EUI issue #70383 to be in Kibana which will arrive with EUI v26.3.0 via #70243. So there will be one follow up to that which will focus on getting rid of the nested button structure. --- .../evaluate_panel.tsx | 2 +- .../exploration_results_table.tsx | 2 +- .../outlier_exploration.tsx | 2 +- .../regression_exploration/evaluate_panel.tsx | 2 +- .../clone_button.test.ts} | 2 +- .../clone_button.tsx} | 6 +- .../components/action_clone/index.ts | 12 + .../action_delete.test.tsx | 32 ++- .../action_delete/delete_button.tsx | 64 +++++ .../action_delete/delete_button_modal.tsx | 108 ++++++++ .../components/action_delete/index.ts | 9 + .../action_delete/use_delete_action.ts | 140 ++++++++++ .../edit_button.tsx} | 40 +-- .../edit_button_flyout.tsx} | 9 +- .../components/action_edit/index.ts | 9 + .../components/action_edit/use_edit_action.ts | 37 +++ .../components/action_start/index.ts | 9 + .../components/action_start/start_button.tsx | 66 +++++ .../action_start/start_button_modal.tsx | 51 ++++ .../action_start/use_start_action.ts | 38 +++ .../components/action_stop/index.ts | 7 + .../components/action_stop/stop_button.tsx | 57 ++++ .../action_view/get_view_action.tsx | 22 ++ .../components/action_view/index.ts | 8 + .../components/action_view/view_button.tsx | 61 +++++ .../analytics_list/action_delete.tsx | 248 ------------------ .../analytics_list/action_start.tsx | 120 --------- .../components/analytics_list/actions.tsx | 157 ----------- .../analytics_list/analytics_list.tsx | 54 ++-- .../analytics_list/expanded_row.tsx | 2 +- .../components/analytics_list/use_actions.tsx | 81 ++++++ .../{columns.tsx => use_columns.tsx} | 22 +- .../hooks/use_create_analytics_form/state.ts | 2 +- .../use_create_analytics_form.ts | 5 +- .../components/analytics_panel/table.tsx | 6 +- .../clone_button.tsx} | 16 +- .../components/action_clone/index.ts | 7 + .../__snapshots__/delete_button.test.tsx.snap | 22 ++ .../delete_button.test.tsx} | 12 +- .../action_delete/delete_button.tsx | 77 ++++++ .../delete_button_modal.tsx} | 154 +++-------- .../components/action_delete/index.ts | 9 + .../action_delete/use_delete_action.ts | 75 ++++++ .../edit_button.tsx} | 37 +-- .../components/action_edit/index.ts | 8 + .../components/action_edit/use_edit_action.ts | 26 ++ .../__snapshots__/start_button.test.tsx.snap | 20 ++ .../components/action_start/index.ts | 9 + .../start_button.test.tsx} | 12 +- .../components/action_start/start_button.tsx | 106 ++++++++ .../action_start/start_button_modal.tsx | 55 ++++ .../action_start/use_start_action.ts | 42 +++ .../__snapshots__/stop_button.test.tsx.snap} | 13 +- .../components/action_stop/index.ts | 7 + .../stop_button.test.tsx} | 11 +- .../stop_button.tsx} | 20 +- .../__snapshots__/action_delete.test.tsx.snap | 23 -- .../__snapshots__/action_start.test.tsx.snap | 23 -- .../transform_list/action_start.tsx | 168 ------------ .../components/transform_list/actions.tsx | 45 ---- .../transform_list/transform_list.test.tsx | 1 + .../transform_list/transform_list.tsx | 32 ++- ...{actions.test.tsx => use_actions.test.tsx} | 10 +- .../components/transform_list/use_actions.tsx | 79 ++++++ ...{columns.test.tsx => use_columns.test.tsx} | 10 +- .../{columns.tsx => use_columns.tsx} | 10 +- 66 files changed, 1523 insertions(+), 1108 deletions(-) rename x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/{analytics_list/action_clone.test.ts => action_clone/clone_button.test.ts} (99%) rename x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/{analytics_list/action_clone.tsx => action_clone/clone_button.tsx} (98%) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/index.ts rename x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/{analytics_list => action_delete}/action_delete.test.tsx (78%) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_button.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_button_modal.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/index.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.ts rename x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/{analytics_list/action_edit.tsx => action_edit/edit_button.tsx} (55%) rename x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/{analytics_list/edit_analytics_flyout.tsx => action_edit/edit_button_flyout.tsx} (97%) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/index.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/use_edit_action.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/index.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_button.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_button_modal.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/use_start_action.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/index.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_button.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/get_view_action.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/index.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/view_button.tsx delete mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx delete mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx delete mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx rename x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/{columns.tsx => use_columns.tsx} (93%) rename x-pack/plugins/transform/public/app/sections/transform_management/components/{transform_list/action_clone.tsx => action_clone/clone_button.tsx} (80%) create mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/index.ts create mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/__snapshots__/delete_button.test.tsx.snap rename x-pack/plugins/transform/public/app/sections/transform_management/components/{transform_list/action_delete.test.tsx => action_delete/delete_button.test.tsx} (74%) create mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_button.tsx rename x-pack/plugins/transform/public/app/sections/transform_management/components/{transform_list/action_delete.tsx => action_delete/delete_button_modal.tsx} (54%) create mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/index.ts create mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/use_delete_action.ts rename x-pack/plugins/transform/public/app/sections/transform_management/components/{transform_list/action_edit.tsx => action_edit/edit_button.tsx} (53%) create mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/index.ts create mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.ts create mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/__snapshots__/start_button.test.tsx.snap create mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/index.ts rename x-pack/plugins/transform/public/app/sections/transform_management/components/{transform_list/action_start.test.tsx => action_start/start_button.test.tsx} (74%) create mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_button.tsx create mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_button_modal.tsx create mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.ts rename x-pack/plugins/transform/public/app/sections/transform_management/components/{transform_list/__snapshots__/action_stop.test.tsx.snap => action_stop/__snapshots__/stop_button.test.tsx.snap} (78%) create mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/index.ts rename x-pack/plugins/transform/public/app/sections/transform_management/components/{transform_list/action_stop.test.tsx => action_stop/stop_button.test.tsx} (76%) rename x-pack/plugins/transform/public/app/sections/transform_management/components/{transform_list/action_stop.tsx => action_stop/stop_button.tsx} (85%) delete mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_delete.test.tsx.snap delete mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_start.test.tsx.snap delete mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.tsx delete mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.tsx rename x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/{actions.test.tsx => use_actions.test.tsx} (63%) create mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx rename x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/{columns.test.tsx => use_columns.test.tsx} (67%) rename x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/{columns.tsx => use_columns.tsx} (96%) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx index 45f883c4ccd94..86e2c5fd2fb94 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx @@ -30,7 +30,7 @@ import { DataFrameAnalyticsConfig, } from '../../../../common'; import { isKeywordAndTextType } from '../../../../common/fields'; -import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; +import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/use_columns'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; import { isResultsSearchBoolQuery, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx index 755bac699ce40..8395a11bd6fda 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx @@ -29,7 +29,7 @@ import { SEARCH_SIZE, defaultSearchQuery, } from '../../../../common'; -import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; +import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/use_columns'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; import { ExplorationTitle } from '../exploration_title'; import { ExplorationQueryBar } from '../exploration_query_bar'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx index 9afb50c11fad7..9341c0aa1a338 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx @@ -29,7 +29,7 @@ import { getToastNotifications } from '../../../../../util/dependency_cache'; import { defaultSearchQuery, useResultsViewConfig, INDEX_STATUS } from '../../../../common'; -import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; +import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/use_columns'; import { ExplorationQueryBar } from '../exploration_query_bar'; import { ExplorationTitle } from '../exploration_title'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx index f6e8e0047671f..d31b7734f9969 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx @@ -27,7 +27,7 @@ import { Eval, DataFrameAnalyticsConfig, } from '../../../../common'; -import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; +import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/use_columns'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; import { EvaluateStat } from './evaluate_stat'; import { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.test.ts similarity index 99% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.test.ts index 4227c19fec5af..006cccf3b4610 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isAdvancedConfig } from './action_clone'; +import { isAdvancedConfig } from './clone_button'; describe('Analytics job clone action', () => { describe('isAdvancedConfig', () => { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx similarity index 98% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx index bff54bc283296..f8b6fdfbe2119 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx @@ -19,7 +19,7 @@ import { DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES, } from '../../hooks/use_create_analytics_form'; import { State } from '../../hooks/use_create_analytics_form/state'; -import { DataFrameAnalyticsListRow } from './common'; +import { DataFrameAnalyticsListRow } from '../analytics_list/common'; import { checkPermission } from '../../../../../capabilities/check_capabilities'; import { extractErrorMessage } from '../../../../../../../common/util/errors'; @@ -343,7 +343,7 @@ export function getCloneAction(createAnalyticsForm: CreateAnalyticsFormProps) { }; } -interface CloneActionProps { +interface CloneButtonProps { item: DataFrameAnalyticsListRow; createAnalyticsForm: CreateAnalyticsFormProps; } @@ -353,7 +353,7 @@ interface CloneActionProps { * Replace with {@link getCloneAction} as soon as all the actions are refactored * to support EuiContext with a valid DOM structure without nested buttons. */ -export const CloneAction: FC = ({ createAnalyticsForm, item }) => { +export const CloneButton: FC = ({ createAnalyticsForm, item }) => { const canCreateDataFrameAnalytics: boolean = checkPermission('canCreateDataFrameAnalytics'); const buttonText = i18n.translate('xpack.ml.dataframe.analyticsList.cloneJobButtonLabel', { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/index.ts new file mode 100644 index 0000000000000..b3d7189ff8cda --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + extractCloningConfig, + isAdvancedConfig, + CloneButton, + CloneDataFrameAnalyticsConfig, +} from './clone_button'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/action_delete.test.tsx similarity index 78% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/action_delete.test.tsx index 33217f127f998..8d6272c5df860 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/action_delete.test.tsx @@ -7,14 +7,17 @@ import React from 'react'; import { fireEvent, render } from '@testing-library/react'; import * as CheckPrivilige from '../../../../../capabilities/check_capabilities'; -import mockAnalyticsListItem from './__mocks__/analytics_list_item.json'; -import { DeleteAction } from './action_delete'; +import mockAnalyticsListItem from '../analytics_list/__mocks__/analytics_list_item.json'; import { I18nProvider } from '@kbn/i18n/react'; import { coreMock as mockCoreServices, i18nServiceMock, } from '../../../../../../../../../../src/core/public/mocks'; +import { DeleteButton } from './delete_button'; +import { DeleteButtonModal } from './delete_button_modal'; +import { useDeleteAction } from './use_delete_action'; + jest.mock('../../../../../capabilities/check_capabilities', () => ({ checkPermission: jest.fn(() => false), createPermissionFailureMessage: jest.fn(), @@ -41,14 +44,18 @@ describe('DeleteAction', () => { }); test('When canDeleteDataFrameAnalytics permission is false, button should be disabled.', () => { - const { getByTestId } = render(); + const { getByTestId } = render( + {}} /> + ); expect(getByTestId('mlAnalyticsJobDeleteButton')).toHaveAttribute('disabled'); }); test('When canDeleteDataFrameAnalytics permission is true, button should not be disabled.', () => { const mock = jest.spyOn(CheckPrivilige, 'checkPermission'); mock.mockImplementation((p) => p === 'canDeleteDataFrameAnalytics'); - const { getByTestId } = render(); + const { getByTestId } = render( + {}} /> + ); expect(getByTestId('mlAnalyticsJobDeleteButton')).not.toHaveAttribute('disabled'); @@ -57,11 +64,12 @@ describe('DeleteAction', () => { test('When job is running, delete button should be disabled.', () => { const { getByTestId } = render( - {}} /> ); @@ -72,9 +80,21 @@ describe('DeleteAction', () => { test('should allow to delete target index by default.', () => { const mock = jest.spyOn(CheckPrivilige, 'checkPermission'); mock.mockImplementation((p) => p === 'canDeleteDataFrameAnalytics'); + + const TestComponent = () => { + const deleteAction = useDeleteAction(); + + return ( + <> + {deleteAction.isModalVisible && } + + + ); + }; + const { getByTestId, queryByTestId } = render( - + ); const deleteButton = getByTestId('mlAnalyticsJobDeleteButton'); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_button.tsx new file mode 100644 index 0000000000000..7da3bced48576 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_button.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; +import { + checkPermission, + createPermissionFailureMessage, +} from '../../../../../capabilities/check_capabilities'; +import { isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from '../analytics_list/common'; + +interface DeleteButtonProps { + item: DataFrameAnalyticsListRow; + onClick: (item: DataFrameAnalyticsListRow) => void; +} + +export const DeleteButton: FC = ({ item, onClick }) => { + const disabled = isDataFrameAnalyticsRunning(item.stats.state); + const canDeleteDataFrameAnalytics: boolean = checkPermission('canDeleteDataFrameAnalytics'); + + const buttonDeleteText = i18n.translate('xpack.ml.dataframe.analyticsList.deleteActionName', { + defaultMessage: 'Delete', + }); + + const buttonDisabled = disabled || !canDeleteDataFrameAnalytics; + let deleteButton = ( + onClick(item)} + aria-label={buttonDeleteText} + style={{ padding: 0 }} + > + {buttonDeleteText} + + ); + + if (disabled || !canDeleteDataFrameAnalytics) { + deleteButton = ( + + {deleteButton} + + ); + } + + return deleteButton; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_button_modal.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_button_modal.tsx new file mode 100644 index 0000000000000..f94dccee479bd --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_button_modal.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiConfirmModal, + EuiOverlayMask, + EuiSwitch, + EuiFlexGroup, + EuiFlexItem, + EUI_MODAL_CONFIRM_BUTTON, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { DeleteAction } from './use_delete_action'; + +export const DeleteButtonModal: FC = ({ + closeModal, + deleteAndCloseModal, + deleteTargetIndex, + deleteIndexPattern, + indexPatternExists, + item, + toggleDeleteIndex, + toggleDeleteIndexPattern, + userCanDeleteIndex, +}) => { + if (item === undefined) { + return null; + } + + const indexName = item.config.dest.index; + + return ( + + +

+ +

+ + + + {userCanDeleteIndex && ( + + )} + + + {userCanDeleteIndex && indexPatternExists && ( + + )} + + +
+
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/index.ts new file mode 100644 index 0000000000000..ef891d7c4a128 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { DeleteButton } from './delete_button'; +export { DeleteButtonModal } from './delete_button_modal'; +export { useDeleteAction } from './use_delete_action'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.ts new file mode 100644 index 0000000000000..f924cf3afcba5 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { IIndexPattern } from 'src/plugins/data/common'; + +import { extractErrorMessage } from '../../../../../../../common/util/errors'; + +import { useMlKibana } from '../../../../../contexts/kibana'; + +import { + deleteAnalytics, + deleteAnalyticsAndDestIndex, + canDeleteIndex, +} from '../../services/analytics_service'; + +import { DataFrameAnalyticsListRow } from '../analytics_list/common'; + +export type DeleteAction = ReturnType; +export const useDeleteAction = () => { + const [item, setItem] = useState(); + + const [isModalVisible, setModalVisible] = useState(false); + const [deleteTargetIndex, setDeleteTargetIndex] = useState(true); + const [deleteIndexPattern, setDeleteIndexPattern] = useState(true); + const [userCanDeleteIndex, setUserCanDeleteIndex] = useState(false); + const [indexPatternExists, setIndexPatternExists] = useState(false); + + const { savedObjects, notifications } = useMlKibana().services; + const savedObjectsClient = savedObjects.client; + + const indexName = item?.config.dest.index ?? ''; + + const checkIndexPatternExists = async () => { + try { + const response = await savedObjectsClient.find({ + type: 'index-pattern', + perPage: 10, + search: `"${indexName}"`, + searchFields: ['title'], + fields: ['title'], + }); + const ip = response.savedObjects.find( + (obj) => obj.attributes.title.toLowerCase() === indexName.toLowerCase() + ); + if (ip !== undefined) { + setIndexPatternExists(true); + } + } catch (e) { + const { toasts } = notifications; + const error = extractErrorMessage(e); + + toasts.addDanger( + i18n.translate( + 'xpack.ml.dataframe.analyticsList.errorWithCheckingIfIndexPatternExistsNotificationErrorMessage', + { + defaultMessage: + 'An error occurred checking if index pattern {indexPattern} exists: {error}', + values: { indexPattern: indexName, error }, + } + ) + ); + } + }; + const checkUserIndexPermission = () => { + try { + const userCanDelete = canDeleteIndex(indexName); + if (userCanDelete) { + setUserCanDeleteIndex(true); + } + } catch (e) { + const { toasts } = notifications; + const error = extractErrorMessage(e); + + toasts.addDanger( + i18n.translate( + 'xpack.ml.dataframe.analyticsList.errorWithCheckingIfUserCanDeleteIndexNotificationErrorMessage', + { + defaultMessage: + 'An error occurred checking if user can delete {destinationIndex}: {error}', + values: { destinationIndex: indexName, error }, + } + ) + ); + } + }; + + useEffect(() => { + // Check if an index pattern exists corresponding to current DFA job + // if pattern does exist, show it to user + checkIndexPatternExists(); + + // Check if an user has permission to delete the index & index pattern + checkUserIndexPermission(); + }, []); + + const closeModal = () => setModalVisible(false); + const deleteAndCloseModal = () => { + setModalVisible(false); + + if (item !== undefined) { + if ((userCanDeleteIndex && deleteTargetIndex) || (userCanDeleteIndex && deleteIndexPattern)) { + deleteAnalyticsAndDestIndex( + item, + deleteTargetIndex, + indexPatternExists && deleteIndexPattern + ); + } else { + deleteAnalytics(item); + } + } + }; + const toggleDeleteIndex = () => setDeleteTargetIndex(!deleteTargetIndex); + const toggleDeleteIndexPattern = () => setDeleteIndexPattern(!deleteIndexPattern); + + const openModal = (newItem: DataFrameAnalyticsListRow) => { + setItem(newItem); + setModalVisible(true); + }; + + return { + closeModal, + deleteAndCloseModal, + deleteTargetIndex, + deleteIndexPattern, + indexPatternExists, + isModalVisible, + item, + openModal, + toggleDeleteIndex, + toggleDeleteIndexPattern, + userCanDeleteIndex, + }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_edit.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button.tsx similarity index 55% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_edit.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button.tsx index 041b52d0322c4..0acb215336faf 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_edit.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button.tsx @@ -4,44 +4,37 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, FC } from 'react'; +import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; +import { EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; import { checkPermission } from '../../../../../capabilities/check_capabilities'; -import { DataFrameAnalyticsListRow } from './common'; -import { EditAnalyticsFlyout } from './edit_analytics_flyout'; - -interface EditActionProps { - item: DataFrameAnalyticsListRow; +interface EditButtonProps { + onClick: () => void; } -export const EditAction: FC = ({ item }) => { +export const EditButton: FC = ({ onClick }) => { const canCreateDataFrameAnalytics: boolean = checkPermission('canCreateDataFrameAnalytics'); - const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); - const closeFlyout = () => setIsFlyoutVisible(false); - const showFlyout = () => setIsFlyoutVisible(true); - const buttonEditText = i18n.translate('xpack.ml.dataframe.analyticsList.editActionName', { defaultMessage: 'Edit', }); + const buttonDisabled = !canCreateDataFrameAnalytics; const editButton = ( - - {buttonEditText} - + {buttonEditText} + ); if (!canCreateDataFrameAnalytics) { @@ -57,10 +50,5 @@ export const EditAction: FC = ({ item }) => { ); } - return ( - <> - {editButton} - {isFlyoutVisible && } - - ); + return editButton; }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/edit_analytics_flyout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button_flyout.tsx similarity index 97% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/edit_analytics_flyout.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button_flyout.tsx index b6aed9321e4e3..728f53bf69ee2 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/edit_analytics_flyout.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button_flyout.tsx @@ -32,20 +32,17 @@ import { MemoryInputValidatorResult, } from '../../../../../../../common/util/validators'; import { extractErrorMessage } from '../../../../../../../common/util/errors'; -import { DataFrameAnalyticsListRow, DATA_FRAME_TASK_STATE } from './common'; +import { DATA_FRAME_TASK_STATE } from '../analytics_list/common'; import { useRefreshAnalyticsList, UpdateDataFrameAnalyticsConfig, } from '../../../../common/analytics'; -interface EditAnalyticsJobFlyoutProps { - closeFlyout: () => void; - item: DataFrameAnalyticsListRow; -} +import { EditAction } from './use_edit_action'; let mmLValidator: (value: any) => MemoryInputValidatorResult; -export const EditAnalyticsFlyout: FC = ({ closeFlyout, item }) => { +export const EditButtonFlyout: FC> = ({ closeFlyout, item }) => { const { id: jobId, config } = item; const { state } = item.stats; const initialAllowLazyStart = diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/index.ts new file mode 100644 index 0000000000000..cfb0bba16ca18 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { EditButton } from './edit_button'; +export { EditButtonFlyout } from './edit_button_flyout'; +export { isEditActionFlyoutVisible, useEditAction } from './use_edit_action'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/use_edit_action.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/use_edit_action.ts new file mode 100644 index 0000000000000..82a7bcc91997a --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/use_edit_action.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState } from 'react'; + +import { DataFrameAnalyticsListRow } from '../analytics_list/common'; + +export const isEditActionFlyoutVisible = (editAction: any): editAction is Required => { + return editAction.isFlyoutVisible === true && editAction.item !== undefined; +}; + +export interface EditAction { + isFlyoutVisible: boolean; + item?: DataFrameAnalyticsListRow; + closeFlyout: () => void; + openFlyout: (newItem: DataFrameAnalyticsListRow) => void; +} +export const useEditAction = () => { + const [item, setItem] = useState(); + + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + const closeFlyout = () => setIsFlyoutVisible(false); + const openFlyout = (newItem: DataFrameAnalyticsListRow) => { + setItem(newItem); + setIsFlyoutVisible(true); + }; + + return { + isFlyoutVisible, + item, + closeFlyout, + openFlyout, + }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/index.ts new file mode 100644 index 0000000000000..df6bbb7c61908 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { StartButton } from './start_button'; +export { StartButtonModal } from './start_button_modal'; +export { useStartAction } from './use_start_action'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_button.tsx new file mode 100644 index 0000000000000..279a335de8f42 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_button.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; + +import { + checkPermission, + createPermissionFailureMessage, +} from '../../../../../capabilities/check_capabilities'; + +import { DataFrameAnalyticsListRow, isCompletedAnalyticsJob } from '../analytics_list/common'; + +interface StartButtonProps { + item: DataFrameAnalyticsListRow; + onClick: (item: DataFrameAnalyticsListRow) => void; +} + +export const StartButton: FC = ({ item, onClick }) => { + const canStartStopDataFrameAnalytics: boolean = checkPermission('canStartStopDataFrameAnalytics'); + + const buttonStartText = i18n.translate('xpack.ml.dataframe.analyticsList.startActionName', { + defaultMessage: 'Start', + }); + + // Disable start for analytics jobs which have completed. + const completeAnalytics = isCompletedAnalyticsJob(item.stats); + + const disabled = !canStartStopDataFrameAnalytics || completeAnalytics; + + let startButton = ( + onClick(item)} + aria-label={buttonStartText} + data-test-subj="mlAnalyticsJobStartButton" + > + {buttonStartText} + + ); + + if (!canStartStopDataFrameAnalytics || completeAnalytics) { + startButton = ( + + {startButton} + + ); + } + + return startButton; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_button_modal.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_button_modal.tsx new file mode 100644 index 0000000000000..664dbe5c62b2f --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_button_modal.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiConfirmModal, EuiOverlayMask, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; + +import { StartAction } from './use_start_action'; + +export const StartButtonModal: FC = ({ closeModal, item, startAndCloseModal }) => { + return ( + <> + {item !== undefined && ( + + +

+ {i18n.translate('xpack.ml.dataframe.analyticsList.startModalBody', { + defaultMessage: + 'A data frame analytics job will increase search and indexing load in your cluster. Please stop the analytics job if excessive load is experienced. Are you sure you want to start this analytics job?', + })} +

+
+
+ )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/use_start_action.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/use_start_action.ts new file mode 100644 index 0000000000000..8eb6b990827ac --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/use_start_action.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState } from 'react'; + +import { DataFrameAnalyticsListRow } from '../analytics_list/common'; +import { startAnalytics } from '../../services/analytics_service'; + +export type StartAction = ReturnType; +export const useStartAction = () => { + const [isModalVisible, setModalVisible] = useState(false); + + const [item, setItem] = useState(); + + const closeModal = () => setModalVisible(false); + const startAndCloseModal = () => { + if (item !== undefined) { + setModalVisible(false); + startAnalytics(item); + } + }; + + const openModal = (newItem: DataFrameAnalyticsListRow) => { + setItem(newItem); + setModalVisible(true); + }; + + return { + closeModal, + isModalVisible, + item, + openModal, + startAndCloseModal, + }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/index.ts new file mode 100644 index 0000000000000..858b6c70501b3 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { StopButton } from './stop_button'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_button.tsx new file mode 100644 index 0000000000000..b8395f2f7c2a0 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_button.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; + +import { + checkPermission, + createPermissionFailureMessage, +} from '../../../../../capabilities/check_capabilities'; + +import { stopAnalytics } from '../../services/analytics_service'; + +import { DataFrameAnalyticsListRow } from '../analytics_list/common'; + +const buttonStopText = i18n.translate('xpack.ml.dataframe.analyticsList.stopActionName', { + defaultMessage: 'Stop', +}); + +interface StopButtonProps { + item: DataFrameAnalyticsListRow; +} + +export const StopButton: FC = ({ item }) => { + const canStartStopDataFrameAnalytics: boolean = checkPermission('canStartStopDataFrameAnalytics'); + + const stopButton = ( + stopAnalytics(item)} + aria-label={buttonStopText} + data-test-subj="mlAnalyticsJobStopButton" + > + {buttonStopText} + + ); + if (!canStartStopDataFrameAnalytics) { + return ( + + {stopButton} + + ); + } + + return stopButton; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/get_view_action.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/get_view_action.tsx new file mode 100644 index 0000000000000..e31670ea42ceb --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/get_view_action.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EuiTableActionsColumnType } from '@elastic/eui'; + +import { DataFrameAnalyticsListRow } from '../analytics_list/common'; + +import { ViewButton } from './view_button'; + +export const getViewAction = ( + isManagementTable: boolean = false +): EuiTableActionsColumnType['actions'][number] => ({ + isPrimary: true, + render: (item: DataFrameAnalyticsListRow) => ( + + ), +}); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/index.ts new file mode 100644 index 0000000000000..5ac12c12071fd --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/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; + * you may not use this file except in compliance with the Elastic License. + */ + +export { getViewAction } from './get_view_action'; +export { ViewButton } from './view_button'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/view_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/view_button.tsx new file mode 100644 index 0000000000000..17a18c374dfa6 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/view_button.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonEmpty } from '@elastic/eui'; + +import { + getAnalysisType, + isRegressionAnalysis, + isOutlierAnalysis, + isClassificationAnalysis, +} from '../../../../common/analytics'; +import { useMlKibana } from '../../../../../contexts/kibana'; + +import { getResultsUrl, DataFrameAnalyticsListRow } from '../analytics_list/common'; + +interface ViewButtonProps { + item: DataFrameAnalyticsListRow; + isManagementTable: boolean; +} + +export const ViewButton: FC = ({ item, isManagementTable }) => { + const { + services: { + application: { navigateToUrl, navigateToApp }, + }, + } = useMlKibana(); + + const analysisType = getAnalysisType(item.config.analysis); + const isDisabled = + !isRegressionAnalysis(item.config.analysis) && + !isOutlierAnalysis(item.config.analysis) && + !isClassificationAnalysis(item.config.analysis); + + const url = getResultsUrl(item.id, analysisType); + const navigator = isManagementTable + ? () => navigateToApp('ml', { path: url }) + : () => navigateToUrl(url); + + return ( + + {i18n.translate('xpack.ml.dataframe.analyticsList.viewActionName', { + defaultMessage: 'View', + })} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx deleted file mode 100644 index 38ef00914e8fb..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx +++ /dev/null @@ -1,248 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment, FC, useState, useEffect } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiButtonEmpty, - EuiConfirmModal, - EuiOverlayMask, - EuiToolTip, - EuiSwitch, - EuiFlexGroup, - EuiFlexItem, - EUI_MODAL_CONFIRM_BUTTON, -} from '@elastic/eui'; -import { IIndexPattern } from 'src/plugins/data/common'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { extractErrorMessage } from '../../../../../../../common/util/errors'; -import { - deleteAnalytics, - deleteAnalyticsAndDestIndex, - canDeleteIndex, -} from '../../services/analytics_service'; -import { - checkPermission, - createPermissionFailureMessage, -} from '../../../../../capabilities/check_capabilities'; -import { useMlKibana } from '../../../../../contexts/kibana'; -import { isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common'; - -interface DeleteActionProps { - item: DataFrameAnalyticsListRow; -} - -export const DeleteAction: FC = ({ item }) => { - const disabled = isDataFrameAnalyticsRunning(item.stats.state); - const canDeleteDataFrameAnalytics: boolean = checkPermission('canDeleteDataFrameAnalytics'); - - const [isModalVisible, setModalVisible] = useState(false); - const [deleteTargetIndex, setDeleteTargetIndex] = useState(true); - const [deleteIndexPattern, setDeleteIndexPattern] = useState(true); - const [userCanDeleteIndex, setUserCanDeleteIndex] = useState(false); - const [indexPatternExists, setIndexPatternExists] = useState(false); - - const { savedObjects, notifications } = useMlKibana().services; - const savedObjectsClient = savedObjects.client; - - const indexName = item.config.dest.index; - - const checkIndexPatternExists = async () => { - try { - const response = await savedObjectsClient.find({ - type: 'index-pattern', - perPage: 10, - search: `"${indexName}"`, - searchFields: ['title'], - fields: ['title'], - }); - const ip = response.savedObjects.find( - (obj) => obj.attributes.title.toLowerCase() === indexName.toLowerCase() - ); - if (ip !== undefined) { - setIndexPatternExists(true); - } - } catch (e) { - const { toasts } = notifications; - const error = extractErrorMessage(e); - - toasts.addDanger( - i18n.translate( - 'xpack.ml.dataframe.analyticsList.errorWithCheckingIfIndexPatternExistsNotificationErrorMessage', - { - defaultMessage: - 'An error occurred checking if index pattern {indexPattern} exists: {error}', - values: { indexPattern: indexName, error }, - } - ) - ); - } - }; - const checkUserIndexPermission = () => { - try { - const userCanDelete = canDeleteIndex(indexName); - if (userCanDelete) { - setUserCanDeleteIndex(true); - } - } catch (e) { - const { toasts } = notifications; - const error = extractErrorMessage(e); - - toasts.addDanger( - i18n.translate( - 'xpack.ml.dataframe.analyticsList.errorWithCheckingIfUserCanDeleteIndexNotificationErrorMessage', - { - defaultMessage: - 'An error occurred checking if user can delete {destinationIndex}: {error}', - values: { destinationIndex: indexName, error }, - } - ) - ); - } - }; - - useEffect(() => { - // Check if an index pattern exists corresponding to current DFA job - // if pattern does exist, show it to user - checkIndexPatternExists(); - - // Check if an user has permission to delete the index & index pattern - checkUserIndexPermission(); - }, []); - - const closeModal = () => setModalVisible(false); - const deleteAndCloseModal = () => { - setModalVisible(false); - - if ((userCanDeleteIndex && deleteTargetIndex) || (userCanDeleteIndex && deleteIndexPattern)) { - deleteAnalyticsAndDestIndex( - item, - deleteTargetIndex, - indexPatternExists && deleteIndexPattern - ); - } else { - deleteAnalytics(item); - } - }; - const openModal = () => setModalVisible(true); - const toggleDeleteIndex = () => setDeleteTargetIndex(!deleteTargetIndex); - const toggleDeleteIndexPattern = () => setDeleteIndexPattern(!deleteIndexPattern); - - const buttonDeleteText = i18n.translate('xpack.ml.dataframe.analyticsList.deleteActionName', { - defaultMessage: 'Delete', - }); - - let deleteButton = ( - - {buttonDeleteText} - - ); - - if (disabled || !canDeleteDataFrameAnalytics) { - deleteButton = ( - - {deleteButton} - - ); - } - - return ( - - {deleteButton} - {isModalVisible && ( - - -

- -

- - - - {userCanDeleteIndex && ( - - )} - - - {userCanDeleteIndex && indexPatternExists && ( - - )} - - -
-
- )} -
- ); -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx deleted file mode 100644 index 74eb1d0b02782..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment, FC, useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiButtonEmpty, - EuiConfirmModal, - EuiOverlayMask, - EuiToolTip, - EUI_MODAL_CONFIRM_BUTTON, -} from '@elastic/eui'; - -import { startAnalytics } from '../../services/analytics_service'; - -import { - checkPermission, - createPermissionFailureMessage, -} from '../../../../../capabilities/check_capabilities'; - -import { DataFrameAnalyticsListRow, isCompletedAnalyticsJob } from './common'; - -interface StartActionProps { - item: DataFrameAnalyticsListRow; -} - -export const StartAction: FC = ({ item }) => { - const canStartStopDataFrameAnalytics: boolean = checkPermission('canStartStopDataFrameAnalytics'); - - const [isModalVisible, setModalVisible] = useState(false); - - const closeModal = () => setModalVisible(false); - const startAndCloseModal = () => { - setModalVisible(false); - startAnalytics(item); - }; - const openModal = () => setModalVisible(true); - - const buttonStartText = i18n.translate('xpack.ml.dataframe.analyticsList.startActionName', { - defaultMessage: 'Start', - }); - - // Disable start for analytics jobs which have completed. - const completeAnalytics = isCompletedAnalyticsJob(item.stats); - - let startButton = ( - - {buttonStartText} - - ); - - if (!canStartStopDataFrameAnalytics || completeAnalytics) { - startButton = ( - - {startButton} - - ); - } - - return ( - - {startButton} - {isModalVisible && ( - - -

- {i18n.translate('xpack.ml.dataframe.analyticsList.startModalBody', { - defaultMessage: - 'A data frame analytics job will increase search and indexing load in your cluster. Please stop the analytics job if excessive load is experienced. Are you sure you want to start this analytics job?', - })} -

-
-
- )} -
- ); -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx deleted file mode 100644 index b03a3a4c4edb2..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx +++ /dev/null @@ -1,157 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; - -import { - checkPermission, - createPermissionFailureMessage, -} from '../../../../../capabilities/check_capabilities'; - -import { - getAnalysisType, - isRegressionAnalysis, - isOutlierAnalysis, - isClassificationAnalysis, -} from '../../../../common/analytics'; -import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; -import { useMlKibana } from '../../../../../contexts/kibana'; -import { CloneAction } from './action_clone'; - -import { getResultsUrl, isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common'; -import { stopAnalytics } from '../../services/analytics_service'; - -import { StartAction } from './action_start'; -import { EditAction } from './action_edit'; -import { DeleteAction } from './action_delete'; - -interface Props { - item: DataFrameAnalyticsListRow; - isManagementTable: boolean; -} - -const AnalyticsViewButton: FC = ({ item, isManagementTable }) => { - const { - services: { - application: { navigateToUrl, navigateToApp }, - }, - } = useMlKibana(); - - const analysisType = getAnalysisType(item.config.analysis); - const isDisabled = - !isRegressionAnalysis(item.config.analysis) && - !isOutlierAnalysis(item.config.analysis) && - !isClassificationAnalysis(item.config.analysis); - - const url = getResultsUrl(item.id, analysisType); - const navigator = isManagementTable - ? () => navigateToApp('ml', { path: url }) - : () => navigateToUrl(url); - - return ( - - {i18n.translate('xpack.ml.dataframe.analyticsList.viewActionName', { - defaultMessage: 'View', - })} - - ); -}; - -interface Action { - isPrimary?: boolean; - render: (item: DataFrameAnalyticsListRow) => any; -} - -export const getAnalyticsViewAction = (isManagementTable: boolean = false): Action => ({ - isPrimary: true, - render: (item: DataFrameAnalyticsListRow) => ( - - ), -}); - -export const getActions = ( - createAnalyticsForm: CreateAnalyticsFormProps, - isManagementTable: boolean -) => { - const canStartStopDataFrameAnalytics: boolean = checkPermission('canStartStopDataFrameAnalytics'); - const actions: Action[] = [getAnalyticsViewAction(isManagementTable)]; - - if (isManagementTable === false) { - actions.push( - ...[ - { - render: (item: DataFrameAnalyticsListRow) => { - if (!isDataFrameAnalyticsRunning(item.stats.state)) { - return ; - } - - const buttonStopText = i18n.translate( - 'xpack.ml.dataframe.analyticsList.stopActionName', - { - defaultMessage: 'Stop', - } - ); - - const stopButton = ( - stopAnalytics(item)} - aria-label={buttonStopText} - data-test-subj="mlAnalyticsJobStopButton" - > - {buttonStopText} - - ); - if (!canStartStopDataFrameAnalytics) { - return ( - - {stopButton} - - ); - } - - return stopButton; - }, - }, - { - render: (item: DataFrameAnalyticsListRow) => { - return ; - }, - }, - { - render: (item: DataFrameAnalyticsListRow) => { - return ; - }, - }, - { - render: (item: DataFrameAnalyticsListRow) => { - return ; - }, - }, - ] - ); - } - - return actions; -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index dac0de4c7a533..405231aef5774 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useState, useEffect } from 'react'; +import React, { FC, useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; @@ -25,7 +25,6 @@ import { ANALYSIS_CONFIG_TYPE, } from '../../../../common'; import { checkPermission } from '../../../../../capabilities/check_capabilities'; -import { getTaskStateBadge, getJobTypeBadge } from './columns'; import { DataFrameAnalyticsListColumn, @@ -38,7 +37,7 @@ import { FieldClause, } from './common'; import { getAnalyticsFactory } from '../../services/analytics_service'; -import { getColumns } from './columns'; +import { getTaskStateBadge, getJobTypeBadge, useColumns } from './use_columns'; import { ExpandedRow } from './expanded_row'; import { ProgressBar, @@ -232,6 +231,14 @@ export const DataFrameAnalyticsList: FC = ({ setIsLoading(false); }; + const { columns, modals } = useColumns( + expandedRowItemIds, + setExpandedRowItemIds, + isManagementTable, + isMlEnabledInSpace, + createAnalyticsForm + ); + // Before the analytics have been loaded for the first time, display the loading indicator only. // Otherwise a user would see 'No data frame analytics found' during the initial loading. if (!isInitialized) { @@ -240,7 +247,7 @@ export const DataFrameAnalyticsList: FC = ({ if (typeof errorMessage !== 'undefined') { return ( - + <> = ({ >
{JSON.stringify(errorMessage)}
-
+ ); } if (analytics.length === 0) { return ( - + <> = ({ {isSourceIndexModalVisible === true && ( setIsSourceIndexModalVisible(false)} /> )} - + ); } - const columns = getColumns( - expandedRowItemIds, - setExpandedRowItemIds, - isManagementTable, - isMlEnabledInSpace, - createAnalyticsForm - ); - const sorting = { sort: { field: sortField, @@ -349,26 +348,6 @@ export const DataFrameAnalyticsList: FC = ({ view: getTaskStateBadge(val), })), }, - // For now analytics jobs are batch only - /* - { - type: 'field_value_selection', - field: 'mode', - name: i18n.translate('xpack.ml.dataframe.analyticsList.modeFilter', { - defaultMessage: 'Mode', - }), - multiSelect: false, - options: Object.values(DATA_FRAME_MODE).map(val => ({ - value: val, - name: val, - view: ( - - {val} - - ), - })), - }, - */ ], }; @@ -386,7 +365,8 @@ export const DataFrameAnalyticsList: FC = ({ }; return ( - + <> + {modals} {analyticsStats && ( @@ -435,6 +415,6 @@ export const DataFrameAnalyticsList: FC = ({ {isSourceIndexModalVisible === true && ( setIsSourceIndexModalVisible(false)} /> )} - + ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx index 0ee57fe5be141..4d029ff1d9546 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx @@ -24,7 +24,7 @@ import { loadEvalData, Eval, } from '../../../../common'; -import { getTaskStateBadge } from './columns'; +import { getTaskStateBadge } from './use_columns'; import { getDataFrameAnalyticsProgressPhase, isCompletedAnalyticsJob } from './common'; import { isRegressionAnalysis, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx new file mode 100644 index 0000000000000..e75d938116991 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EuiTableActionsColumnType } from '@elastic/eui'; + +import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; +import { CloneButton } from '../action_clone'; +import { useDeleteAction, DeleteButton, DeleteButtonModal } from '../action_delete'; +import { + isEditActionFlyoutVisible, + useEditAction, + EditButton, + EditButtonFlyout, +} from '../action_edit'; +import { useStartAction, StartButton, StartButtonModal } from '../action_start'; +import { StopButton } from '../action_stop'; +import { getViewAction } from '../action_view'; + +import { isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common'; + +export const useActions = ( + createAnalyticsForm: CreateAnalyticsFormProps, + isManagementTable: boolean +): { + actions: EuiTableActionsColumnType['actions']; + modals: JSX.Element | null; +} => { + const deleteAction = useDeleteAction(); + const editAction = useEditAction(); + const startAction = useStartAction(); + + let modals: JSX.Element | null = null; + + const actions: EuiTableActionsColumnType['actions'] = [ + getViewAction(isManagementTable), + ]; + + if (isManagementTable === false) { + modals = ( + <> + {startAction.isModalVisible && } + {deleteAction.isModalVisible && } + {isEditActionFlyoutVisible(editAction) && } + + ); + actions.push( + ...[ + { + render: (item: DataFrameAnalyticsListRow) => { + if (!isDataFrameAnalyticsRunning(item.stats.state)) { + return ; + } + return ; + }, + }, + { + render: (item: DataFrameAnalyticsListRow) => { + return editAction.openFlyout(item)} />; + }, + }, + { + render: (item: DataFrameAnalyticsListRow) => { + return ; + }, + }, + { + render: (item: DataFrameAnalyticsListRow) => { + return ; + }, + }, + ] + ); + } + + return { actions, modals }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx similarity index 93% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx index a3d2e65386c19..fa88396461cd7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx @@ -33,7 +33,7 @@ import { DataFrameAnalyticsListRow, DataFrameAnalyticsStats, } from './common'; -import { getActions } from './actions'; +import { useActions } from './use_actions'; enum TASK_STATE_COLOR { analyzing = 'primary', @@ -141,14 +141,14 @@ export const getDFAnalyticsJobIdLink = (item: DataFrameAnalyticsListRow) => ( {item.id} ); -export const getColumns = ( +export const useColumns = ( expandedRowItemIds: DataFrameAnalyticsId[], setExpandedRowItemIds: React.Dispatch>, isManagementTable: boolean = false, isMlEnabledInSpace: boolean = true, createAnalyticsForm?: CreateAnalyticsFormProps ) => { - const actions = getActions(createAnalyticsForm!, isManagementTable); + const { actions, modals } = useActions(createAnalyticsForm!, isManagementTable); function toggleDetails(item: DataFrameAnalyticsListRow) { const index = expandedRowItemIds.indexOf(item.config.id); @@ -253,20 +253,6 @@ export const getColumns = ( width: '100px', 'data-test-subj': 'mlAnalyticsTableColumnStatus', }, - // For now there is batch mode only so we hide this column for now. - /* - { - name: i18n.translate('xpack.ml.dataframe.analyticsList.mode', { defaultMessage: 'Mode' }), - sortable: (item: DataFrameAnalyticsListRow) => item.mode, - truncateText: true, - render(item: DataFrameAnalyticsListRow) { - const mode = item.mode; - const color = 'hollow'; - return {mode}; - }, - width: '100px', - }, - */ progressColumn, { name: i18n.translate('xpack.ml.dataframe.analyticsList.tableActionLabel', { @@ -293,5 +279,5 @@ export const getColumns = ( } } - return columns; + return { columns, modals }; }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index da6e2e440a26e..cedbe9094cb20 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -13,7 +13,7 @@ import { DataFrameAnalyticsConfig, ANALYSIS_CONFIG_TYPE, } from '../../../../common/analytics'; -import { CloneDataFrameAnalyticsConfig } from '../../components/analytics_list/action_clone'; +import { CloneDataFrameAnalyticsConfig } from '../../components/action_clone'; export enum DEFAULT_MODEL_MEMORY_LIMIT { regression = '100mb', diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index f95d2f572a406..4c312be26613c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -18,10 +18,7 @@ import { DataFrameAnalyticsId, DataFrameAnalyticsConfig, } from '../../../../common'; -import { - extractCloningConfig, - isAdvancedConfig, -} from '../../components/analytics_list/action_clone'; +import { extractCloningConfig, isAdvancedConfig } from '../../components/action_clone'; import { ActionDispatchers, ACTION } from './actions'; import { reducer } from './reducer'; diff --git a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/table.tsx b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/table.tsx index f2e6ff7885b16..1eeff6287867d 100644 --- a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/table.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/table.tsx @@ -22,8 +22,8 @@ import { import { getTaskStateBadge, progressColumn, -} from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/columns'; -import { getAnalyticsViewAction } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/actions'; +} from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns'; +import { getViewAction } from '../../../data_frame_analytics/pages/analytics_management/components/action_view'; import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils'; const MlInMemoryTable = mlInMemoryTableFactory(); @@ -82,7 +82,7 @@ export const AnalyticsTable: FC = ({ items }) => { name: i18n.translate('xpack.ml.overview.analyticsList.tableActionLabel', { defaultMessage: 'Actions', }), - actions: [getAnalyticsViewAction()], + actions: [getViewAction()], width: '100px', }, ]; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/clone_button.tsx similarity index 80% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/clone_button.tsx index aa78dfb4315f9..4686ede7bc2c2 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/clone_button.tsx @@ -7,7 +7,7 @@ import React, { FC, useContext } from 'react'; import { useHistory } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; -import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; +import { EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; import { createCapabilityFailureMessage, @@ -20,7 +20,7 @@ interface CloneActionProps { itemId: string; } -export const CloneAction: FC = ({ itemId }) => { +export const CloneButton: FC = ({ itemId }) => { const history = useHistory(); const { canCreateTransform } = useContext(AuthorizationContext).capabilities; @@ -34,17 +34,15 @@ export const CloneAction: FC = ({ itemId }) => { } const cloneButton = ( - - {buttonCloneText} - + {buttonCloneText} + ); if (!canCreateTransform) { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/index.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/index.ts new file mode 100644 index 0000000000000..727cc87c70f2c --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { CloneButton } from './clone_button'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/__snapshots__/delete_button.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/__snapshots__/delete_button.test.tsx.snap new file mode 100644 index 0000000000000..3980cc5d5a1ae --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/__snapshots__/delete_button.test.tsx.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Transform: Transform List Actions Minimal initialization 1`] = ` + + + + + Delete + + +`; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_button.test.tsx similarity index 74% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_button.test.tsx index fdd0b821f54fd..63f8243b403d3 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_button.test.tsx @@ -5,10 +5,10 @@ */ import { shallow } from 'enzyme'; -import React from 'react'; +import React, { ComponentProps } from 'react'; import { TransformListRow } from '../../../../common'; -import { DeleteAction } from './action_delete'; +import { DeleteButton } from './delete_button'; import transformListRow from '../../../../common/__mocks__/transform_list_row.json'; @@ -18,13 +18,13 @@ jest.mock('../../../../../app/app_dependencies'); describe('Transform: Transform List Actions ', () => { test('Minimal initialization', () => { const item: TransformListRow = transformListRow; - const props = { - disabled: false, + const props: ComponentProps = { + forceDisable: false, items: [item], - deleteTransform(d: TransformListRow) {}, + onClick: () => {}, }; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_button.tsx new file mode 100644 index 0000000000000..b81c3ebc34ca0 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_button.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useContext } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; +import { TRANSFORM_STATE } from '../../../../../../common'; +import { + AuthorizationContext, + createCapabilityFailureMessage, +} from '../../../../lib/authorization'; +import { TransformListRow } from '../../../../common'; + +interface DeleteButtonProps { + items: TransformListRow[]; + forceDisable?: boolean; + onClick: (items: TransformListRow[]) => void; +} + +const transformCanNotBeDeleted = (i: TransformListRow) => + ![TRANSFORM_STATE.STOPPED, TRANSFORM_STATE.FAILED].includes(i.stats.state); + +export const DeleteButton: FC = ({ items, forceDisable, onClick }) => { + const isBulkAction = items.length > 1; + + const disabled = items.some(transformCanNotBeDeleted); + const { canDeleteTransform } = useContext(AuthorizationContext).capabilities; + + const buttonDeleteText = i18n.translate('xpack.transform.transformList.deleteActionName', { + defaultMessage: 'Delete', + }); + const bulkDeleteButtonDisabledText = i18n.translate( + 'xpack.transform.transformList.deleteBulkActionDisabledToolTipContent', + { + defaultMessage: 'One or more selected transforms must be stopped in order to be deleted.', + } + ); + const deleteButtonDisabledText = i18n.translate( + 'xpack.transform.transformList.deleteActionDisabledToolTipContent', + { + defaultMessage: 'Stop the transform in order to delete it.', + } + ); + + const buttonDisabled = forceDisable === true || disabled || !canDeleteTransform; + let deleteButton = ( + onClick(items)} + aria-label={buttonDeleteText} + > + {buttonDeleteText} + + ); + + if (disabled || !canDeleteTransform) { + let content; + if (disabled) { + content = isBulkAction ? bulkDeleteButtonDisabledText : deleteButtonDisabledText; + } else { + content = createCapabilityFailureMessage('canDeleteTransform'); + } + + deleteButton = ( + + {deleteButton} + + ); + } + + return deleteButton; +}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_button_modal.tsx similarity index 54% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_button_modal.tsx index 79a9e45e317e5..668e535198649 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_button_modal.tsx @@ -4,88 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useContext, useMemo, useState } from 'react'; +import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; import { EUI_MODAL_CONFIRM_BUTTON, - EuiButtonEmpty, EuiConfirmModal, EuiFlexGroup, EuiFlexItem, EuiOverlayMask, EuiSpacer, EuiSwitch, - EuiToolTip, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { TRANSFORM_STATE } from '../../../../../../common'; -import { useDeleteIndexAndTargetIndex, useDeleteTransforms } from '../../../../hooks'; -import { - AuthorizationContext, - createCapabilityFailureMessage, -} from '../../../../lib/authorization'; -import { TransformListRow } from '../../../../common'; - -interface DeleteActionProps { - items: TransformListRow[]; - forceDisable?: boolean; -} -const transformCanNotBeDeleted = (i: TransformListRow) => - ![TRANSFORM_STATE.STOPPED, TRANSFORM_STATE.FAILED].includes(i.stats.state); +import { DeleteAction } from './use_delete_action'; -export const DeleteAction: FC = ({ items, forceDisable }) => { +export const DeleteButtonModal: FC = ({ + closeModal, + deleteAndCloseModal, + deleteDestIndex, + deleteIndexPattern, + indexPatternExists, + items, + shouldForceDelete, + toggleDeleteIndex, + toggleDeleteIndexPattern, + userCanDeleteIndex, +}) => { const isBulkAction = items.length > 1; - const disabled = items.some(transformCanNotBeDeleted); - const shouldForceDelete = useMemo( - () => items.some((i: TransformListRow) => i.stats.state === TRANSFORM_STATE.FAILED), - [items] - ); - const { canDeleteTransform } = useContext(AuthorizationContext).capabilities; - const deleteTransforms = useDeleteTransforms(); - const { - userCanDeleteIndex, - deleteDestIndex, - indexPatternExists, - deleteIndexPattern, - toggleDeleteIndex, - toggleDeleteIndexPattern, - } = useDeleteIndexAndTargetIndex(items); - - const [isModalVisible, setModalVisible] = useState(false); - - const closeModal = () => setModalVisible(false); - const deleteAndCloseModal = () => { - setModalVisible(false); - - const shouldDeleteDestIndex = userCanDeleteIndex && deleteDestIndex; - const shouldDeleteDestIndexPattern = - userCanDeleteIndex && indexPatternExists && deleteIndexPattern; - // if we are deleting multiple transforms, then force delete all if at least one item has failed - // else, force delete only when the item user picks has failed - const forceDelete = isBulkAction - ? shouldForceDelete - : items[0] && items[0] && items[0].stats.state === TRANSFORM_STATE.FAILED; - deleteTransforms(items, shouldDeleteDestIndex, shouldDeleteDestIndexPattern, forceDelete); - }; - const openModal = () => setModalVisible(true); - - const buttonDeleteText = i18n.translate('xpack.transform.transformList.deleteActionName', { - defaultMessage: 'Delete', - }); - const bulkDeleteButtonDisabledText = i18n.translate( - 'xpack.transform.transformList.deleteBulkActionDisabledToolTipContent', - { - defaultMessage: 'One or more selected transforms must be stopped in order to be deleted.', - } - ); - const deleteButtonDisabledText = i18n.translate( - 'xpack.transform.transformList.deleteActionDisabledToolTipContent', - { - defaultMessage: 'Stop the transform in order to delete it.', - } - ); const bulkDeleteModalTitle = i18n.translate( 'xpack.transform.transformList.bulkDeleteModalTitle', { @@ -203,63 +151,23 @@ export const DeleteAction: FC = ({ items, forceDisable }) => ); - let deleteButton = ( - - {buttonDeleteText} - - ); - - if (disabled || !canDeleteTransform) { - let content; - if (disabled) { - content = isBulkAction ? bulkDeleteButtonDisabledText : deleteButtonDisabledText; - } else { - content = createCapabilityFailureMessage('canDeleteTransform'); - } - - deleteButton = ( - - {deleteButton} - - ); - } - return ( - - {deleteButton} - {isModalVisible && ( - - - {isBulkAction ? bulkDeleteModalContent : deleteModalContent} - - - )} - + + + {isBulkAction ? bulkDeleteModalContent : deleteModalContent} + + ); }; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/index.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/index.ts new file mode 100644 index 0000000000000..ef891d7c4a128 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { DeleteButton } from './delete_button'; +export { DeleteButtonModal } from './delete_button_modal'; +export { useDeleteAction } from './use_delete_action'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/use_delete_action.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/use_delete_action.ts new file mode 100644 index 0000000000000..d76eebe954d7b --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/use_delete_action.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useMemo, useState } from 'react'; + +import { TRANSFORM_STATE } from '../../../../../../common'; + +import { TransformListRow } from '../../../../common'; +import { useDeleteIndexAndTargetIndex, useDeleteTransforms } from '../../../../hooks'; + +export type DeleteAction = ReturnType; +export const useDeleteAction = () => { + const deleteTransforms = useDeleteTransforms(); + + const [isModalVisible, setModalVisible] = useState(false); + const [items, setItems] = useState([]); + + const isBulkAction = items.length > 1; + const shouldForceDelete = useMemo( + () => items.some((i: TransformListRow) => i.stats.state === TRANSFORM_STATE.FAILED), + [items] + ); + + const closeModal = () => setModalVisible(false); + + const { + userCanDeleteIndex, + deleteDestIndex, + indexPatternExists, + deleteIndexPattern, + toggleDeleteIndex, + toggleDeleteIndexPattern, + } = useDeleteIndexAndTargetIndex(items); + + const deleteAndCloseModal = () => { + setModalVisible(false); + + const shouldDeleteDestIndex = userCanDeleteIndex && deleteDestIndex; + const shouldDeleteDestIndexPattern = + userCanDeleteIndex && indexPatternExists && deleteIndexPattern; + // if we are deleting multiple transforms, then force delete all if at least one item has failed + // else, force delete only when the item user picks has failed + const forceDelete = isBulkAction + ? shouldForceDelete + : items[0] && items[0] && items[0].stats.state === TRANSFORM_STATE.FAILED; + deleteTransforms(items, shouldDeleteDestIndex, shouldDeleteDestIndexPattern, forceDelete); + }; + + const openModal = (newItems: TransformListRow[]) => { + // EUI issue: Might trigger twice, one time as an array, + // one time as a single object. See https://github.com/elastic/eui/issues/3679 + if (Array.isArray(newItems)) { + setItems(newItems); + setModalVisible(true); + } + }; + + return { + closeModal, + deleteAndCloseModal, + deleteDestIndex, + deleteIndexPattern, + indexPatternExists, + isModalVisible, + items, + openModal, + shouldForceDelete, + toggleDeleteIndex, + toggleDeleteIndexPattern, + userCanDeleteIndex, + }; +}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_edit.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/edit_button.tsx similarity index 53% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_edit.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/edit_button.tsx index dfb4cd443e904..6ba8e7aeba20f 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_edit.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/edit_button.tsx @@ -4,47 +4,37 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext, useState, FC } from 'react'; +import React, { useContext, FC } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; +import { EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; -import { TransformPivotConfig } from '../../../../common'; import { createCapabilityFailureMessage, AuthorizationContext, } from '../../../../lib/authorization'; -import { EditTransformFlyout } from '../edit_transform_flyout'; - -interface EditActionProps { - config: TransformPivotConfig; +interface EditButtonProps { + onClick: () => void; } - -export const EditAction: FC = ({ config }) => { +export const EditButton: FC = ({ onClick }) => { const { canCreateTransform } = useContext(AuthorizationContext).capabilities; - const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); - const closeFlyout = () => setIsFlyoutVisible(false); - const showFlyout = () => setIsFlyoutVisible(true); - const buttonEditText = i18n.translate('xpack.transform.transformList.editActionName', { defaultMessage: 'Edit', }); const editButton = ( - - {buttonEditText} - + {buttonEditText} + ); if (!canCreateTransform) { @@ -57,10 +47,5 @@ export const EditAction: FC = ({ config }) => { ); } - return ( - <> - {editButton} - {isFlyoutVisible && } - - ); + return editButton; }; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/index.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/index.ts new file mode 100644 index 0000000000000..17a2ad9444f8d --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/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; + * you may not use this file except in compliance with the Elastic License. + */ + +export { EditButton } from './edit_button'; +export { useEditAction } from './use_edit_action'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.ts new file mode 100644 index 0000000000000..ace3ec8f636e6 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState } from 'react'; + +import { TransformPivotConfig } from '../../../../common'; + +export const useEditAction = () => { + const [config, setConfig] = useState(); + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + const closeFlyout = () => setIsFlyoutVisible(false); + const showFlyout = (newConfig: TransformPivotConfig) => { + setConfig(newConfig); + setIsFlyoutVisible(true); + }; + + return { + config, + closeFlyout, + isFlyoutVisible, + showFlyout, + }; +}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/__snapshots__/start_button.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/__snapshots__/start_button.test.tsx.snap new file mode 100644 index 0000000000000..231a1f30f2c31 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/__snapshots__/start_button.test.tsx.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Transform: Transform List Actions Minimal initialization 1`] = ` + + + + + Start + + +`; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/index.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/index.ts new file mode 100644 index 0000000000000..df6bbb7c61908 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { StartButton } from './start_button'; +export { StartButtonModal } from './start_button_modal'; +export { useStartAction } from './use_start_action'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_button.test.tsx similarity index 74% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_button.test.tsx index 2de115236c4dc..b88e1257f56ad 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_button.test.tsx @@ -5,10 +5,10 @@ */ import { shallow } from 'enzyme'; -import React from 'react'; +import React, { ComponentProps } from 'react'; import { TransformListRow } from '../../../../common'; -import { StartAction } from './action_start'; +import { StartButton } from './start_button'; import transformListRow from '../../../../common/__mocks__/transform_list_row.json'; @@ -18,13 +18,13 @@ jest.mock('../../../../../app/app_dependencies'); describe('Transform: Transform List Actions ', () => { test('Minimal initialization', () => { const item: TransformListRow = transformListRow; - const props = { - disabled: false, + const props: ComponentProps = { + forceDisable: false, items: [item], - startTransform(d: TransformListRow) {}, + onClick: () => {}, }; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_button.tsx new file mode 100644 index 0000000000000..a0fe1bfbb9544 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_button.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useContext } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; + +import { TRANSFORM_STATE } from '../../../../../../common'; + +import { + createCapabilityFailureMessage, + AuthorizationContext, +} from '../../../../lib/authorization'; +import { TransformListRow, isCompletedBatchTransform } from '../../../../common'; + +interface StartButtonProps { + items: TransformListRow[]; + forceDisable?: boolean; + onClick: (items: TransformListRow[]) => void; +} +export const StartButton: FC = ({ items, forceDisable, onClick }) => { + const { canStartStopTransform } = useContext(AuthorizationContext).capabilities; + const isBulkAction = items.length > 1; + + const buttonStartText = i18n.translate('xpack.transform.transformList.startActionName', { + defaultMessage: 'Start', + }); + + // Disable start for batch transforms which have completed. + const completedBatchTransform = items.some((i: TransformListRow) => isCompletedBatchTransform(i)); + // Disable start action if one of the transforms is already started or trying to restart will throw error + const startedTransform = items.some( + (i: TransformListRow) => i.stats.state === TRANSFORM_STATE.STARTED + ); + + let startedTransformMessage; + let completedBatchTransformMessage; + + if (isBulkAction === true) { + startedTransformMessage = i18n.translate( + 'xpack.transform.transformList.startedTransformBulkToolTip', + { + defaultMessage: 'One or more transforms are already started.', + } + ); + completedBatchTransformMessage = i18n.translate( + 'xpack.transform.transformList.completeBatchTransformBulkActionToolTip', + { + defaultMessage: + 'One or more transforms are completed batch transforms and cannot be restarted.', + } + ); + } else { + startedTransformMessage = i18n.translate( + 'xpack.transform.transformList.startedTransformToolTip', + { + defaultMessage: '{transformId} is already started.', + values: { transformId: items[0] && items[0].config.id }, + } + ); + completedBatchTransformMessage = i18n.translate( + 'xpack.transform.transformList.completeBatchTransformToolTip', + { + defaultMessage: '{transformId} is a completed batch transform and cannot be restarted.', + values: { transformId: items[0] && items[0].config.id }, + } + ); + } + + const actionIsDisabled = + !canStartStopTransform || completedBatchTransform || startedTransform || items.length === 0; + + let content: string | undefined; + if (actionIsDisabled && items.length > 0) { + if (!canStartStopTransform) { + content = createCapabilityFailureMessage('canStartStopTransform'); + } else if (completedBatchTransform) { + content = completedBatchTransformMessage; + } else if (startedTransform) { + content = startedTransformMessage; + } + } + + const disabled = forceDisable === true || actionIsDisabled; + + const startButton = ( + onClick(items)} + > + {buttonStartText} + + ); + if (disabled && content !== undefined) { + return ( + + {startButton} + + ); + } + return startButton; +}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_button_modal.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_button_modal.tsx new file mode 100644 index 0000000000000..2ef0d20c45116 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_button_modal.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiConfirmModal, EuiOverlayMask, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; + +import { StartAction } from './use_start_action'; + +export const StartButtonModal: FC = ({ + closeModal, + isModalVisible, + items, + startAndCloseModal, +}) => { + const isBulkAction = items.length > 1; + + const bulkStartModalTitle = i18n.translate('xpack.transform.transformList.bulkStartModalTitle', { + defaultMessage: 'Start {count} {count, plural, one {transform} other {transforms}}?', + values: { count: items && items.length }, + }); + const startModalTitle = i18n.translate('xpack.transform.transformList.startModalTitle', { + defaultMessage: 'Start {transformId}', + values: { transformId: items[0] && items[0].config.id }, + }); + + return ( + + +

+ {i18n.translate('xpack.transform.transformList.startModalBody', { + defaultMessage: + 'A transform will increase search and indexing load in your cluster. Please stop the transform if excessive load is experienced. Are you sure you want to start {count, plural, one {this} other {these}} {count} {count, plural, one {transform} other {transforms}}?', + values: { count: items.length }, + })} +

+
+
+ ); +}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.ts new file mode 100644 index 0000000000000..32d2dc6dabf86 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState } from 'react'; + +import { TransformListRow } from '../../../../common'; +import { useStartTransforms } from '../../../../hooks'; + +export type StartAction = ReturnType; +export const useStartAction = () => { + const startTransforms = useStartTransforms(); + + const [isModalVisible, setModalVisible] = useState(false); + const [items, setItems] = useState([]); + + const closeModal = () => setModalVisible(false); + + const startAndCloseModal = () => { + setModalVisible(false); + startTransforms(items); + }; + + const openModal = (newItems: TransformListRow[]) => { + // EUI issue: Might trigger twice, one time as an array, + // one time as a single object. See https://github.com/elastic/eui/issues/3679 + if (Array.isArray(newItems)) { + setItems(newItems); + setModalVisible(true); + } + }; + + return { + closeModal, + isModalVisible, + items, + openModal, + startAndCloseModal, + }; +}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_stop.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/__snapshots__/stop_button.test.tsx.snap similarity index 78% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_stop.test.tsx.snap rename to x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/__snapshots__/stop_button.test.tsx.snap index 97d393bc8128b..dd81bf34bf582 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_stop.test.tsx.snap +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/__snapshots__/stop_button.test.tsx.snap @@ -6,16 +6,17 @@ exports[`Transform: Transform List Actions Minimal initialization delay="regular" position="top" > - + + Stop - + `; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/index.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/index.ts new file mode 100644 index 0000000000000..858b6c70501b3 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { StopButton } from './stop_button'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_button.test.tsx similarity index 76% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_button.test.tsx index a97097d909848..d9c07a9dccc8f 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_button.test.tsx @@ -5,10 +5,10 @@ */ import { shallow } from 'enzyme'; -import React from 'react'; +import React, { ComponentProps } from 'react'; import { TransformListRow } from '../../../../common'; -import { StopAction } from './action_stop'; +import { StopButton } from './stop_button'; import transformListRow from '../../../../common/__mocks__/transform_list_row.json'; @@ -18,13 +18,12 @@ jest.mock('../../../../../app/app_dependencies'); describe('Transform: Transform List Actions ', () => { test('Minimal initialization', () => { const item: TransformListRow = transformListRow; - const props = { - disabled: false, + const props: ComponentProps = { + forceDisable: false, items: [item], - stopTransform(d: TransformListRow) {}, }; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_button.tsx similarity index 85% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_button.tsx index 3f35bef458951..2c67ea3e83ecc 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_button.tsx @@ -6,7 +6,7 @@ import React, { FC, useContext } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; +import { EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; import { TRANSFORM_STATE } from '../../../../../../common'; @@ -17,12 +17,11 @@ import { } from '../../../../lib/authorization'; import { useStopTransforms } from '../../../../hooks'; -interface StopActionProps { +interface StopButtonProps { items: TransformListRow[]; forceDisable?: boolean; } - -export const StopAction: FC = ({ items, forceDisable }) => { +export const StopButton: FC = ({ items, forceDisable }) => { const isBulkAction = items.length > 1; const { canStartStopTransform } = useContext(AuthorizationContext).capabilities; const stopTransforms = useStopTransforms(); @@ -57,18 +56,17 @@ export const StopAction: FC = ({ items, forceDisable }) => { stopTransforms(items); }; + const disabled = forceDisable === true || !canStartStopTransform || stoppedTransform === true; + const stopButton = ( - - {buttonStopText} - + {buttonStopText} + ); if (!canStartStopTransform || stoppedTransform) { return ( diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_delete.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_delete.test.tsx.snap deleted file mode 100644 index da5ad27c9d6b1..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_delete.test.tsx.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transform: Transform List Actions Minimal initialization 1`] = ` - - - - Delete - - - -`; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_start.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_start.test.tsx.snap deleted file mode 100644 index d534f05d3be96..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_start.test.tsx.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transform: Transform List Actions Minimal initialization 1`] = ` - - - - Start - - - -`; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.tsx deleted file mode 100644 index 9edfe7fab70a0..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.tsx +++ /dev/null @@ -1,168 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment, FC, useContext, useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiButtonEmpty, - EuiConfirmModal, - EuiOverlayMask, - EuiToolTip, - EUI_MODAL_CONFIRM_BUTTON, -} from '@elastic/eui'; - -import { TRANSFORM_STATE } from '../../../../../../common'; - -import { useStartTransforms } from '../../../../hooks'; -import { - createCapabilityFailureMessage, - AuthorizationContext, -} from '../../../../lib/authorization'; -import { TransformListRow, isCompletedBatchTransform } from '../../../../common'; - -interface StartActionProps { - items: TransformListRow[]; - forceDisable?: boolean; -} - -export const StartAction: FC = ({ items, forceDisable }) => { - const isBulkAction = items.length > 1; - const { canStartStopTransform } = useContext(AuthorizationContext).capabilities; - const startTransforms = useStartTransforms(); - - const [isModalVisible, setModalVisible] = useState(false); - - const closeModal = () => setModalVisible(false); - const startAndCloseModal = () => { - setModalVisible(false); - startTransforms(items); - }; - const openModal = () => setModalVisible(true); - - const buttonStartText = i18n.translate('xpack.transform.transformList.startActionName', { - defaultMessage: 'Start', - }); - - // Disable start for batch transforms which have completed. - const completedBatchTransform = items.some((i: TransformListRow) => isCompletedBatchTransform(i)); - // Disable start action if one of the transforms is already started or trying to restart will throw error - const startedTransform = items.some( - (i: TransformListRow) => i.stats.state === TRANSFORM_STATE.STARTED - ); - - let startedTransformMessage; - let completedBatchTransformMessage; - - if (isBulkAction === true) { - startedTransformMessage = i18n.translate( - 'xpack.transform.transformList.startedTransformBulkToolTip', - { - defaultMessage: 'One or more transforms are already started.', - } - ); - completedBatchTransformMessage = i18n.translate( - 'xpack.transform.transformList.completeBatchTransformBulkActionToolTip', - { - defaultMessage: - 'One or more transforms are completed batch transforms and cannot be restarted.', - } - ); - } else { - startedTransformMessage = i18n.translate( - 'xpack.transform.transformList.startedTransformToolTip', - { - defaultMessage: '{transformId} is already started.', - values: { transformId: items[0] && items[0].config.id }, - } - ); - completedBatchTransformMessage = i18n.translate( - 'xpack.transform.transformList.completeBatchTransformToolTip', - { - defaultMessage: '{transformId} is a completed batch transform and cannot be restarted.', - values: { transformId: items[0] && items[0].config.id }, - } - ); - } - - const actionIsDisabled = !canStartStopTransform || completedBatchTransform || startedTransform; - - let startButton = ( - - {buttonStartText} - - ); - - if (actionIsDisabled) { - let content; - if (!canStartStopTransform) { - content = createCapabilityFailureMessage('canStartStopTransform'); - } else if (completedBatchTransform) { - content = completedBatchTransformMessage; - } else if (startedTransform) { - content = startedTransformMessage; - } - - startButton = ( - - {startButton} - - ); - } - - const bulkStartModalTitle = i18n.translate('xpack.transform.transformList.bulkStartModalTitle', { - defaultMessage: 'Start {count} {count, plural, one {transform} other {transforms}}?', - values: { count: items && items.length }, - }); - const startModalTitle = i18n.translate('xpack.transform.transformList.startModalTitle', { - defaultMessage: 'Start {transformId}', - values: { transformId: items[0] && items[0].config.id }, - }); - - return ( - - {startButton} - {isModalVisible && ( - - -

- {i18n.translate('xpack.transform.transformList.startModalBody', { - defaultMessage: - 'A transform will increase search and indexing load in your cluster. Please stop the transform if excessive load is experienced. Are you sure you want to start {count, plural, one {this} other {these}} {count} {count, plural, one {transform} other {transforms}}?', - values: { count: items.length }, - })} -

-
-
- )} -
- ); -}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.tsx deleted file mode 100644 index 343b5e4db67e3..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.tsx +++ /dev/null @@ -1,45 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; - -import { TRANSFORM_STATE } from '../../../../../../common'; - -import { TransformListRow } from '../../../../common'; - -import { CloneAction } from './action_clone'; -import { DeleteAction } from './action_delete'; -import { EditAction } from './action_edit'; -import { StartAction } from './action_start'; -import { StopAction } from './action_stop'; - -export const getActions = ({ forceDisable }: { forceDisable: boolean }) => { - return [ - { - render: (item: TransformListRow) => { - if (item.stats.state === TRANSFORM_STATE.STOPPED) { - return ; - } - return ; - }, - }, - { - render: (item: TransformListRow) => { - return ; - }, - }, - { - render: (item: TransformListRow) => { - return ; - }, - }, - { - render: (item: TransformListRow) => { - return ; - }, - }, - ]; -}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx index 5e0363d0a7a15..70b3dc7c2bffe 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { TransformList } from './transform_list'; jest.mock('../../../../../shared_imports'); +jest.mock('../../../../../app/app_dependencies'); describe('Transform: Transform List ', () => { test('Minimal initialization', () => { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx index b1eea4a09fca3..9df4113fa9a8b 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx @@ -35,13 +35,12 @@ import { AuthorizationContext } from '../../../../lib/authorization'; import { CreateTransformButton } from '../create_transform_button'; import { RefreshTransformListButton } from '../refresh_transform_list_button'; -import { getTaskStateBadge } from './columns'; -import { DeleteAction } from './action_delete'; -import { StartAction } from './action_start'; -import { StopAction } from './action_stop'; +import { useDeleteAction, DeleteButton, DeleteButtonModal } from '../action_delete'; +import { useStartAction, StartButton, StartButtonModal } from '../action_start'; +import { StopButton } from '../action_stop'; import { ItemIdToExpandedRowMap, Clause, TermClause, FieldClause, Value } from './common'; -import { getColumns } from './columns'; +import { getTaskStateBadge, useColumns } from './use_columns'; import { ExpandedRow } from './expanded_row'; function getItemIdToExpandedRowMap( @@ -90,6 +89,8 @@ export const TransformList: FC = ({ const [transformSelection, setTransformSelection] = useState([]); const [isActionsMenuOpen, setIsActionsMenuOpen] = useState(false); + const bulkStartAction = useStartAction(); + const bulkDeleteAction = useDeleteAction(); const [searchError, setSearchError] = useState(undefined); @@ -185,6 +186,12 @@ export const TransformList: FC = ({ setIsLoading(false); }; + const { columns, modals: singleActionModals } = useColumns( + expandedRowItemIds, + setExpandedRowItemIds, + transformSelection + ); + // Before the transforms have been loaded for the first time, display the loading indicator only. // Otherwise a user would see 'No transforms found' during the initial loading. if (!isInitialized) { @@ -231,8 +238,6 @@ export const TransformList: FC = ({ ); } - const columns = getColumns(expandedRowItemIds, setExpandedRowItemIds, transformSelection); - const sorting = { sort: { field: sortField, @@ -252,13 +257,13 @@ export const TransformList: FC = ({ const bulkActionMenuItems = [
- +
,
- +
,
- +
, ]; @@ -375,6 +380,13 @@ export const TransformList: FC = ({ return (
+ {/* Bulk Action Modals */} + {bulkStartAction.isModalVisible && } + {bulkDeleteAction.isModalVisible && } + + {/* Single Action Modals */} + {singleActionModals} + { - test('getActions()', () => { - const actions = getActions({ forceDisable: false }); + test('useActions()', () => { + const { result } = renderHook(() => useActions({ forceDisable: false })); + const actions: ReturnType['actions'] = result.current.actions; expect(actions).toHaveLength(4); expect(typeof actions[0].render).toBe('function'); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx new file mode 100644 index 0000000000000..a6b1aa1a1b5c5 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EuiTableComputedColumnType } from '@elastic/eui'; + +import { TRANSFORM_STATE } from '../../../../../../common'; + +import { TransformListRow } from '../../../../common'; + +import { CloneButton } from '../action_clone'; +import { useDeleteAction, DeleteButton, DeleteButtonModal } from '../action_delete'; +import { EditTransformFlyout } from '../edit_transform_flyout'; +import { useEditAction, EditButton } from '../action_edit'; +import { useStartAction, StartButton, StartButtonModal } from '../action_start'; +import { StopButton } from '../action_stop'; + +export const useActions = ({ + forceDisable, +}: { + forceDisable: boolean; +}): { actions: Array>; modals: JSX.Element } => { + const deleteAction = useDeleteAction(); + const editAction = useEditAction(); + const startAction = useStartAction(); + + return { + modals: ( + <> + {startAction.isModalVisible && } + {editAction.config && editAction.isFlyoutVisible && ( + + )} + {deleteAction.isModalVisible && } + + ), + actions: [ + { + render: (item: TransformListRow) => { + if (item.stats.state === TRANSFORM_STATE.STOPPED) { + return ( + + ); + } + return ; + }, + }, + { + render: (item: TransformListRow) => { + return editAction.showFlyout(item.config)} />; + }, + }, + { + render: (item: TransformListRow) => { + return ; + }, + }, + { + render: (item: TransformListRow) => { + return ( + + ); + }, + }, + ], + }; +}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx similarity index 67% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.test.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx index 3c75c33caf840..94d3e5322a2e8 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx @@ -4,13 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getColumns } from './columns'; +import { renderHook } from '@testing-library/react-hooks'; + +import { useColumns } from './use_columns'; jest.mock('../../../../../shared_imports'); +jest.mock('../../../../../app/app_dependencies'); describe('Transform: Job List Columns', () => { - test('getColumns()', () => { - const columns = getColumns([], () => {}, []); + test('useColumns()', () => { + const { result } = renderHook(() => useColumns([], () => {}, [])); + const columns: ReturnType['columns'] = result.current.columns; expect(columns).toHaveLength(7); expect(columns[0].isExpander).toBeTruthy(); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx similarity index 96% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx index 5ed2566e8a194..d2d8c7084941d 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx @@ -30,7 +30,7 @@ import { TransformStats, TRANSFORM_LIST_COLUMN, } from '../../../../common'; -import { getActions } from './actions'; +import { useActions } from './use_actions'; enum STATE_COLOR { aborting = 'warning', @@ -64,12 +64,12 @@ export const getTaskStateBadge = ( ); }; -export const getColumns = ( +export const useColumns = ( expandedRowItemIds: TransformId[], setExpandedRowItemIds: React.Dispatch>, transformSelection: TransformListRow[] ) => { - const actions = getActions({ forceDisable: transformSelection.length > 0 }); + const { actions, modals } = useActions({ forceDisable: transformSelection.length > 0 }); function toggleDetails(item: TransformListRow) { const index = expandedRowItemIds.indexOf(item.config.id); @@ -223,10 +223,10 @@ export const getColumns = ( }, { name: i18n.translate('xpack.transform.tableActionLabel', { defaultMessage: 'Actions' }), - actions, + actions: actions as EuiTableActionsColumnType['actions'], width: '80px', }, ]; - return columns; + return { columns, modals }; }; From 0ea7f9ff6e32a1d4b3bd9a008ceb46fa00935060 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Wed, 8 Jul 2020 11:48:06 +0300 Subject: [PATCH 04/13] [Functional test] Increase the timeout on opening a saved visualization (#70952) * fixes the flakiness on hybrid visualization test * increase timeout to 20 sec to find and click the hybrid visualization --- test/functional/page_objects/visualize_page.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index 49133d8b13836..a08598fc42d68 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -257,7 +257,7 @@ export function VisualizePageProvider({ getService, getPageObjects }: FtrProvide public async openSavedVisualization(vizName: string) { const dataTestSubj = `visListingTitleLink-${vizName.split(' ').join('-')}`; - await testSubjects.click(dataTestSubj); + await testSubjects.click(dataTestSubj, 20000); await header.waitUntilLoadingHasFinished(); } From a0a3e2f9ab2de8c37b9a014ff9c47aa31e87c6cb Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Wed, 8 Jul 2020 11:10:03 +0200 Subject: [PATCH 05/13] fix: remove only consecutive ticks in TSVB (#70981) Co-authored-by: Elastic Machine --- package.json | 2 +- packages/kbn-ui-shared-deps/package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index bb28c9e27e9f7..6178bb07067d7 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "@babel/plugin-transform-modules-commonjs": "^7.10.1", "@babel/register": "^7.10.1", "@elastic/apm-rum": "^5.2.0", - "@elastic/charts": "19.8.0", + "@elastic/charts": "19.8.1", "@elastic/datemath": "5.0.3", "@elastic/ems-client": "7.9.3", "@elastic/eui": "24.1.0", diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index f4d9beb038966..6ea4a621f92f6 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,7 +9,7 @@ "kbn:watch": "node scripts/build --dev --watch" }, "dependencies": { - "@elastic/charts": "19.8.0", + "@elastic/charts": "19.8.1", "@elastic/eui": "24.1.0", "@elastic/numeral": "^2.5.0", "@kbn/i18n": "1.0.0", diff --git a/yarn.lock b/yarn.lock index ac5f653fdf3d5..acf7c3a1e8754 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2144,10 +2144,10 @@ dependencies: "@elastic/apm-rum-core" "^5.3.0" -"@elastic/charts@19.8.0": - version "19.8.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-19.8.0.tgz#d8439288e2574053ca9e6eee6f3b00bf04917803" - integrity sha512-px0mX0UBtFhbt5O4JAqOZPYC+K9avVmjgKPoIqQBMnnwkKtuKGH1mQ7XZro3E7COJ4WQ5nGxWtC+ewlFQP3zww== +"@elastic/charts@19.8.1": + version "19.8.1" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-19.8.1.tgz#27653823911c26e4703c73588367473215beaf0f" + integrity sha512-vONCrcZ8bZ+C16+bKLoLyNrMC/b2UvYNoPbYcnB5XYAg5a68finvXEcWD6Y+qa7GLaO2CMe5J9eSjLWXHHDmLg== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" From 949941758f1f2633f36210f8c681c320740c22ef Mon Sep 17 00:00:00 2001 From: Shahzad Date: Wed, 8 Jul 2020 11:26:34 +0200 Subject: [PATCH 06/13] [RUM Dashboard] New rum services api to replace usage of get services API (#70746) --- .../RumDashboard/Charts/PageLoadDistChart.tsx | 2 +- .../PercentileAnnotations.tsx | 2 +- .../PageLoadDistribution/index.tsx | 2 +- .../components/app/RumDashboard/index.tsx | 8 +- .../__snapshots__/queries.test.ts.snap | 55 ++ .../rum_client/get_page_load_distribution.ts | 41 +- .../lib/rum_client/get_pl_dist_breakdown.ts | 10 +- .../server/lib/rum_client/get_rum_services.ts | 48 ++ .../apm/server/lib/rum_client/queries.test.ts | 10 + .../apm/server/routes/create_apm_api.ts | 2 + .../plugins/apm/server/routes/rum_client.ts | 13 + .../apm/typings/elasticsearch/aggregations.ts | 1 + .../es_archiver/rum_8.0.0/data.json.gz | Bin 0 -> 11144 bytes .../es_archiver/rum_8.0.0/mappings.json | 600 ++++++++++++++++++ .../apm_api_integration/trial/tests/index.ts | 1 + .../trial/tests/rum_services.ts | 47 ++ 16 files changed, 821 insertions(+), 21 deletions(-) create mode 100644 x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts create mode 100644 x-pack/test/apm_api_integration/trial/fixtures/es_archiver/rum_8.0.0/data.json.gz create mode 100644 x-pack/test/apm_api_integration/trial/fixtures/es_archiver/rum_8.0.0/mappings.json create mode 100644 x-pack/test/apm_api_integration/trial/tests/rum_services.ts diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx index e17a8046b5c6a..6c5b539fcecfa 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx @@ -119,7 +119,7 @@ export function PageLoadDistChart({ xScaleType={ScaleType.Linear} yScaleType={ScaleType.Linear} data={data?.pageLoadDistribution ?? []} - curve={CurveType.CURVE_NATURAL} + curve={CurveType.CURVE_CATMULL_ROM} /> {breakdowns.map(({ name, type }) => ( ): LineAnnotationDatum[] { return Object.entries(values ?? {}).map((value) => ({ - dataValue: Math.round(value[1] / 1000), + dataValue: value[1], details: `${(+value[0]).toFixed(0)}`, })); } diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx index 7d48cee49b104..81503e16f7bcf 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx @@ -68,7 +68,7 @@ export const PageLoadDistribution = () => { ); const onPercentileChange = (min: number, max: number) => { - setPercentileRange({ min: min * 1000, max: max * 1000 }); + setPercentileRange({ min, max }); }; return ( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx index 3ddaa66b8de5e..3380a81c7bfab 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx @@ -46,7 +46,7 @@ export function RumOverview() { (callApmApi) => { if (start && end) { return callApmApi({ - pathname: '/api/apm/services', + pathname: '/api/apm/rum-client/services', params: { query: { start, @@ -68,11 +68,7 @@ export function RumOverview() { {!isRumServiceRoute && ( <> - service.serviceName) ?? [] - } - /> + {' '} diff --git a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap index c006d01637483..602eb88ba8940 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap @@ -70,6 +70,9 @@ Object { "durPercentiles": Object { "percentiles": Object { "field": "transaction.duration.us", + "hdr": Object { + "number_of_significant_value_digits": 3, + }, "percents": Array [ 50, 75, @@ -179,3 +182,55 @@ Object { "index": "myIndex", } `; + +exports[`rum client dashboard queries fetches rum services 1`] = ` +Object { + "body": Object { + "aggs": Object { + "services": Object { + "terms": Object { + "field": "service.name", + "size": 1000, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + Object { + "term": Object { + "transaction.type": "page-load", + }, + }, + Object { + "exists": Object { + "field": "transaction.marks.navigationTiming.fetchStart", + }, + }, + Object { + "term": Object { + "my.custom.ui.filter": "foo-bar", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts index 43af18999547d..e847a87264759 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts @@ -12,6 +12,12 @@ import { SetupUIFilters, } from '../helpers/setup_request'; +export const MICRO_TO_SEC = 1000000; + +export function microToSec(val: number) { + return Math.round((val / MICRO_TO_SEC + Number.EPSILON) * 100) / 100; +} + export async function getPageLoadDistribution({ setup, minPercentile, @@ -42,6 +48,9 @@ export async function getPageLoadDistribution({ percentiles: { field: 'transaction.duration.us', percents: [50, 75, 90, 95, 99], + hdr: { + number_of_significant_value_digits: 3, + }, }, }, }, @@ -59,20 +68,29 @@ export async function getPageLoadDistribution({ return null; } - const minDuration = aggregations?.minDuration.value ?? 0; + const { durPercentiles, minDuration } = aggregations ?? {}; - const minPerc = minPercentile ? +minPercentile : minDuration; + const minPerc = minPercentile + ? +minPercentile * MICRO_TO_SEC + : minDuration?.value ?? 0; - const maxPercQuery = aggregations?.durPercentiles.values['99.0'] ?? 10000; + const maxPercQuery = durPercentiles?.values['99.0'] ?? 10000; - const maxPerc = maxPercentile ? +maxPercentile : maxPercQuery; + const maxPerc = maxPercentile ? +maxPercentile * MICRO_TO_SEC : maxPercQuery; const pageDist = await getPercentilesDistribution(setup, minPerc, maxPerc); + + Object.entries(durPercentiles?.values ?? {}).forEach(([key, val]) => { + if (durPercentiles?.values?.[key]) { + durPercentiles.values[key] = microToSec(val as number); + } + }); + return { pageLoadDistribution: pageDist, - percentiles: aggregations?.durPercentiles.values, - minDuration: minPerc, - maxDuration: maxPerc, + percentiles: durPercentiles?.values, + minDuration: microToSec(minPerc), + maxDuration: microToSec(maxPerc), }; } @@ -81,9 +99,9 @@ const getPercentilesDistribution = async ( minDuration: number, maxDuration: number ) => { - const stepValue = (maxDuration - minDuration) / 50; + const stepValue = (maxDuration - minDuration) / 100; const stepValues = []; - for (let i = 1; i < 51; i++) { + for (let i = 1; i < 101; i++) { stepValues.push((stepValue * i + minDuration).toFixed(2)); } @@ -103,6 +121,9 @@ const getPercentilesDistribution = async ( field: 'transaction.duration.us', values: stepValues, keyed: false, + hdr: { + number_of_significant_value_digits: 3, + }, }, }, }, @@ -117,7 +138,7 @@ const getPercentilesDistribution = async ( return pageDist.map(({ key, value }, index: number, arr) => { return { - x: Math.round(key / 1000), + x: microToSec(key), y: index === 0 ? value : value - arr[index - 1].value, }; }); diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts index 5ae6bd1540f7c..ea9d701e64c3d 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts @@ -17,6 +17,7 @@ import { USER_AGENT_NAME, USER_AGENT_OS, } from '../../../common/elasticsearch_fieldnames'; +import { MICRO_TO_SEC, microToSec } from './get_page_load_distribution'; export const getBreakdownField = (breakdown: string) => { switch (breakdown) { @@ -38,7 +39,9 @@ export const getPageLoadDistBreakdown = async ( maxDuration: number, breakdown: string ) => { - const stepValue = (maxDuration - minDuration) / 50; + // convert secs to micros + const stepValue = + (maxDuration * MICRO_TO_SEC - minDuration * MICRO_TO_SEC) / 50; const stepValues = []; for (let i = 1; i < 51; i++) { @@ -67,6 +70,9 @@ export const getPageLoadDistBreakdown = async ( field: 'transaction.duration.us', values: stepValues, keyed: false, + hdr: { + number_of_significant_value_digits: 3, + }, }, }, }, @@ -86,7 +92,7 @@ export const getPageLoadDistBreakdown = async ( name: String(key), data: pageDist.values?.map(({ key: pKey, value }, index: number, arr) => { return { - x: Math.round(pKey / 1000), + x: microToSec(pKey), y: index === 0 ? value : value - arr[index - 1].value, }; }), diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts b/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts new file mode 100644 index 0000000000000..5957a25239307 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; +import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { + Setup, + SetupTimeRange, + SetupUIFilters, +} from '../helpers/setup_request'; + +export async function getRumServices({ + setup, +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; +}) { + const projection = getRumOverviewProjection({ + setup, + }); + + const params = mergeProjection(projection, { + body: { + size: 0, + query: { + bool: projection.body.query.bool, + }, + aggs: { + services: { + terms: { + field: 'service.name', + size: 1000, + }, + }, + }, + }, + }); + + const { client } = setup; + + const response = await client.search(params); + + const result = response.aggregations?.services.buckets ?? []; + + return result.map(({ key }) => key as string); +} diff --git a/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts b/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts index 5f5a48eced746..37432672c5d89 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts @@ -11,6 +11,7 @@ import { import { getClientMetrics } from './get_client_metrics'; import { getPageViewTrends } from './get_page_view_trends'; import { getPageLoadDistribution } from './get_page_load_distribution'; +import { getRumServices } from './get_rum_services'; describe('rum client dashboard queries', () => { let mock: SearchParamsMock; @@ -49,4 +50,13 @@ describe('rum client dashboard queries', () => { ); expect(mock.params).toMatchSnapshot(); }); + + it('fetches rum services', async () => { + mock = await inspectSearchParams((setup) => + getRumServices({ + setup, + }) + ); + expect(mock.params).toMatchSnapshot(); + }); }); diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index c314debcd8049..513c44904683e 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -76,6 +76,7 @@ import { rumPageViewsTrendRoute, rumPageLoadDistributionRoute, rumPageLoadDistBreakdownRoute, + rumServicesRoute, } from './rum_client'; import { observabilityDashboardHasDataRoute, @@ -172,6 +173,7 @@ const createApmApi = () => { .add(rumPageLoadDistributionRoute) .add(rumPageLoadDistBreakdownRoute) .add(rumClientMetricsRoute) + .add(rumServicesRoute) // Observability dashboard .add(observabilityDashboardHasDataRoute) diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts index 75651f646a50d..01e549632a0bc 100644 --- a/x-pack/plugins/apm/server/routes/rum_client.ts +++ b/x-pack/plugins/apm/server/routes/rum_client.ts @@ -12,6 +12,7 @@ import { rangeRt, uiFiltersRt } from './default_api_types'; import { getPageViewTrends } from '../lib/rum_client/get_page_view_trends'; import { getPageLoadDistribution } from '../lib/rum_client/get_page_load_distribution'; import { getPageLoadDistBreakdown } from '../lib/rum_client/get_pl_dist_breakdown'; +import { getRumServices } from '../lib/rum_client/get_rum_services'; export const percentileRangeRt = t.partial({ minPercentile: t.string, @@ -91,3 +92,15 @@ export const rumPageViewsTrendRoute = createRoute(() => ({ return getPageViewTrends({ setup, breakdowns }); }, })); + +export const rumServicesRoute = createRoute(() => ({ + path: '/api/apm/rum-client/services', + params: { + query: t.intersection([uiFiltersRt, rangeRt]), + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + return getRumServices({ setup }); + }, +})); diff --git a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts index a340aa24aebfb..ac7499c23e926 100644 --- a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts +++ b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts @@ -150,6 +150,7 @@ export interface AggregationOptionsByType { field: string; values: string[]; keyed?: boolean; + hdr?: { number_of_significant_value_digits: number }; }; } diff --git a/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/rum_8.0.0/data.json.gz b/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/rum_8.0.0/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..ad3f2351ed30aff77f40ff7098e37ebd0915a0ef GIT binary patch literal 11144 zcmZ9yRa9JE6Sa%GOK|8$8<#YJ;L9+!KO(aCdhNgy8N0e%|l= z7w4QkFZa#bW7Mwo)SN{ZgMskhgG79sWA8>akovswsG0lEPLoN>|6zST%VTHcWVwcP zL`7q1+5FmpQIMod96Fq&d4KXki%^I-jypzyM1CQt@rlsQVf1QLb>jWRWz__8@L(nV2lylI;$ssJTjM zJzT#%;}*P!ov{Kh=nk)EE^Zt|I+hnD9;TZ8J4EjKt{;;>a66H4PSA64jkK5G_mLEz z-9HorZa9vA8)IAXrP?zT2a#oc`!F^>lT;!g{Al*8$w%P+!ry4LZEbPSb?fl5qOW9T zY4_jkM33uv+6PB&w|BQUb{;la!ffKzyIaohQgKFWg}-s?3aWn`CK-&Ic=mtXcv%;G zT5?sAEN5kzsUzB*LyUZOeLNEA$0;^fu+zC$qo`TBU%6uHpS+?`D7L)k*J-#uIXE!) z)r`4&`7zUQ5`Y||*V{+WH7x39;@c4O*_&6qBi3sHr}`gJcUe}1YDrq{#t16-=l3dx zuU_1>ZP_!DtA37*Pe&(rFD~Y8UKC_>Uj|xTT+g?SmC|BGg^kzL6?*^jr?M0R&O;5c zSPT@t^s8J9Hb<#<|3Q6n4Dj}FLzUIlV<=Fo!N?k^Y^aPQfEl3$y90yQ-Fy+uxGJbf5EbSm6!H@;lj_kBEU6EQZXh>7W29T*w8U+dEQAoop|CHniL z&*ksoh7pUL`(tj=HR(O4su?AhTaBir?W&uQ_N$nsKJBL>_u5q-&(HJDUhtOI0=~ZE z&kT;^AygG6X^kuxJ;CbApEGe%x93vH8fqqXtxWU5Y(%?CgdrTCHVc1-xvC(YPnAy` zzVi@XHl(^*NYlN%^@_L>@<-&}+v_`W@L*%lZ*MW_bs9ffx!`#AI-I!}&;HO_oBow} zBrx##=wvTj)APbHKzmdy;Meui%UT8*_r_S@Z6?tjj;8Ab9-qPY=J?A ziN7#F$9Sz7xM+i2CW4DJJ5O`B&h_l=?NvL6zTkti-jUtWAsC~(dy5Vct?+x^*weKg zhHeZ-?AG<1aHyLIicfT8aB@{W|Fdl-ZSX#sXmAR`uWcNuefGW)Q(m6?Ew9>g`M2tq z823byNX66HSgNtnoL^f{bD`ocTU^hrTUPN()d;3(eaW!?9G7qH@0KEASyoPUM;njv z;)lGPoIvq!FP9Jb&!YlqHbPJICih;-bYF{doU)xfoE^!UmZUrquM5sSgNFwy&^PVy z+qP!1>h!~F!8u9)f%tka<{Wwi6 z7{m4NCQRMbq7Q40RoyMV-<#ifdODs~{|RgNS-2n_)7@}4@OK;ayT@~_t;(GFRoPZw zIAcO2wSM`eqevX2)EV2%-cLZVW~AYzk1JR*C!QK`}Je#N|7~zhP(b>9c^NP z!rqq$JBAEH0>`SCX{T>4Z(T0zPCgo{W_>>oMzA9!m^i5Y>XkA_tDz7hgbA3EaZH%wF;Aw6iZ-_s-wDz#5GqHT^ z8GbZ6?sVX&_ik(4Shp5UvO{|wv+yXXdiJI78C_JKSlSWKvM z^lwt`Vd&kqEkaUxVUVCobSRe82F!;>Lctjb87j>c5n@3@g5nz6pwwgmg7L%Pbu_>& z{hK8)(DJ&?fTG6p{72$)KJ_^lwOFDq^{LVYp{(;dgS({1Apl?o>~Bc1hz) z-)@Ndj?ZNRHLxP2iCBC-?unAtUW_u6I?!oP#^iSOdCjCPD|F1#S1ZmJQ zB)qEfTh50u?v3{J6u15*#wXK@-&sQ&ED|b+D*NvU6p%FE@z+D{7UtNy+>+PP|Kg(j z&|Qo}61Wi!yITH$4--)2Pud(?Hs)v&nS3U=6R*NU4#ojGR}$EeqlZeos`D4YU?E|v zv}p>PF-CM`FAO4VR>_)z@zub#i#LK`F_&HeQL3&i@|~c@GujU<<~a!hkR{8q^Au)q z2ruBuyt5_Q)>4A2mK?rek=sBHkM4PEY86zDqo>cPsBk1D5wAh75S1aJ$M`qfBxR%K zbdtz+?RT4ARg0XE5v!N*)YeF!*ZE8@*Lt9ub@Bx&IpY?*g0SXd7a%G%O(hlouF1b` z-NeL$Xs!S2gWKAN6W6t)Lx1NS@b4<3r(I>*^`Z(%%u;S5e!FtkwbWB05R!a{zSLt6 z;nySylrK^ATcWnzhb?4YmLj&`5z{saYzr$%tvVw%ZjTmo^PO%A(%3j!imI>YB&03M zdoxme*2u*9AP_5>l*kh6rS?~Bd_9(B>@&;oeii`wDdZVo^(9ISyfUQXt=gSP5>>n; zZ1h+Wp?T}*#Urv1WwG7T61<9ec+e5dKQ|X}w|FS+mD>WQ>oSD&xe9o;S$b)rFvMim_1>~hR@P#+j)#)l zy$~~n4~WA>x77=} z*TUvWP;$y@4-gPfIL1Hv-q4hT;7~tDmYh}07c1uncsCnSF-k=@zQl-prz^Zgsawh3&7?(` z*gC{II75Pi3JpV><#4do!$&3xX78imL|8S`YTr!7SputkMr13uinaDsA78p>*Fn^x zmSM>yYDF#`Ek{h{dsJLPo9w9wP3=N}Vc40*1O*y7cqB@Um* z+f`v)K4Ypht9#b0d>Ji$F~SPtCX2e!c`)@^({dk9LozeLEd_mmlp1)7uMGe`9(yOp z@7xd^Y+ZVGeJn^{!F6}}`$zJ2Z>ljZVoN<*L`a2JI}{ZCz<2?=uyLX-QtCVrQP9sz z-<1HiyWS0EUlK8TMHQBE=St9i-&0riw#Q}t3jneDx$}~bp-;1Dmx!^BO@T2R4wKoW zzojr^<160)90V|?Lk^Q)KV=)}KS;0f4335}y-5^P=Ywpd`#<#iJ=(pnlGsA)JDnyZy)iv>ctoLw;HSW*GG=UN%Q3%g zqjDTYG|*dEy2g5mWo8f#9w{w;ZQ;So=Fs^yv>{JZ=yC*5zY(AH@C>+ntWQ#iOh2Z{ z3YpW3jQ+~6x>)JOTiHCL3*TQ+CJ8g~F;?U9o7yNTiaiW=weag-&Z2lD0r_15tR2`q)A9DcZ* z-2V1AHlM6K;Yz^NuUD@~Ja~G;@(2-DdK0I8%+7_M1kL*y;#^4+s^JzE8yOo(6Y4LF zK|1#yuOwST=KYM3oMUPO8!R^w%gKC8KbR&IH`FHGkqL6re5N2+)Ks&Kr^Q(ALH5t4 zMm!|LBvj9fO*yo*77M~ZL|f<}#fZ+*JG`ldQC$PkLEj2OdrQtENX`;3Z0bxBb(zlE zMAeniV~J(Q7tnsXo8}GB{dHa)UAAO!ruiSm8|$n5Uvx*8Ztj&KQO8q1kuNYhjB8>T zD%yCzUP$k=Z>wfdV_GM`;i?TKrq&LN`wAT5x6}>-elN;Qn#91Ym<)en0f+eI0obkUpZ zi??YDdrmA)COLlgjQ7makF5!R8vVNdgO&QDTjD{w=Px7PH6)XIKESVbVoO-lO=*i> zw{z^$Co@^x>~VEX-fd4V=Y>RO=PM>0=Hyrbze)au0%7K7A{z8q0Z%C6UzjDw`A|Bb zMZpqSrromkp;}=;Qykn)T(!C|+fIlqpxvc@xm!>!ETM}uEGi)Pi&(jk@EnLnQQPKt z2%KkrU1y+Q{l9W^+)qXy=sQ5r{#@NMSb)~QC&jvX2BybzQn?AZ)2qFqiLNw6T6yHL z*_Mzb=Ek?=7*3=dP>3T5!B3=2q(sk^b#(2ODaQpJGx9R}pDUq#jEhU$-?R_{a1c9y zVP^ZZpYr-^45V1oWck+$`6ly5A=-@{n5TuZf_R@QMYBG#kbm7n>A93USo}%YbmB>@ z?f;XorHSw>^lxbYWRzS~RK?Z9&EDOY%0^O~~ro{O=12v@T_GG6Io(%-4US8mae&%&NDLUxx8r;Bm=f^3o zeUa9@Oj)dWYQL=M;?f;dljkSYGh3$SQdp)&e4Oj5=T=MOhx!S1!76LX?gW_m1K?Nw z>e37;i!yBc#_5OWrmhr8Xm2GwpXsvPnH1TkEhvPiX?C#c`X5{2JBRiZ4J6KK+z}7J zGFhL*-Ln3@zP$Zs30>-GyrdodlQOV*;dnkYM--aTo??j(WAsOucE2d_9-P)Y2a~7- zW9=~H@v9zc|1}~WT2&2 z0{mWv^(-nY`&UhXX8?bP->tcZ3axb`X_@VbIBEc!TeC{-m29V(zJxcOgOkOJ=BB1T z_vsqPoYB`7lU8gz0sKaBn*N^2yzcP+YBs+ld96D*p8D3|_cPXUM|G<-OUS7Q!mcrR zI0XI%>m(FE-dAU^lMs_?Rbz%@UN%%n7S8v1IHYL4zrDHW;)&3VJB)jTlfNQWeFtB| z@uW{$(GTAtZ8$<&D&^9j&2bWLuVosuKg-&fiJKu!Gfik8QD)N5qA2F+y>} z3Su$mKl+w~$iDENGy}uf1|=Fw>S|p{a$DWqL#*iDV6CKfFW1`EiH21kOfC1d%G#~m zq|h00R^qA6-K5;G0a5?fh%%`bMQT|P?lW8`FCmh?1^3)UYX}y3^+~1Il-bT=+wjoES##=a`#i#IvtTMo)e751rU&v}Cq7>vGHU*$4*Qbn4q9yA~2 zc>tPJ={d0}sS!dUop`V*Af&8(c+Y)bO*m&@?#G#7!9G%$-Jf>(z6eI3{Q*@KK= zlh2eypG410eIqZpW6PHU2mb4mLY2w@F6%q@g8+QJB1yLrR$EaRb<;9299P zv9Yer-JjzCMT|sBsH$_I_kEU(C8K6DIcr3gETk5PJYO*5)6eu3qRhT6RM%AA|AE0e z?VWs;l?YCXu;*dyoD3&ny|C4^Txf4%hbqu3$<6|9Q?LspW!WpQbw)s##3BSNNC_B8 z*8o=jf_zT9Nz{*!lq5c)BZn^K4J?gied;C7w->)l0Ywy~h5EtQFf07&H*yC(Wbo;6 zB#j5SdI$yYH6|@>jgNc#z1$BT9ds8KpGvAnexZ54tGYG0MhTai~asLYa@jc{$UlltS&JrDXKr+RfOk^v5GcI?fy2 zFol(KxY&qjs%(b3o-BUk0-f*PU-fH$J4X6s*p;4QYs&EZs%}(+pWomBHsY>!XYkqX6 z7I;*@C*?v?c!Hb(3@nn({FE=xte3TDgW#7(S0kZG z1wDN4z;6FUH(9|db^13?m$_I2lt4dCKYb%>kn^0DI4K!JUWZhLwTI1)K`6^e#11tR z1%*LZg6hJtr|dd2gXSp@VQfzPnm9YtGxBu`alJv>)(FKK`dp&-A$6$f2WQNpIYDK< zh|E;ufuyZGgmej&nK#!*79v?~qridC&w)988z(b?Wy2>U*IWx@Y2yE`Qh`+_N&$~Q z-O&HUL5F8(9g}GH`M`;_WmEpkJahh8Jab$_@&DuXjvFs*`doy! zf!Tof^zY5h940pLevpnhduj6-f8ZiQ+t*Oo@L}W)iqxVkaNpu^wGb6xe)CCN${1l2 zxYiCJL8S=al3mY`?;3(Gb2TsJP`%%AW{5bF6sl=ZbZlv=uIbHfrdjlia!AvYi}9#K zo|}5!GoTr5ah5=zfg=l}KzB-EJ-9hO%9T;U*9C5f0R$>$1(v&dP zx;LaJTuH7+i;5CS7a<(t^c`z_(QH!9LnDEap-f+!qKhtRC)aFknkFP=#r1yjQ>jph zI0n*wLIb2B@~j>-&kgLR{I!y z-_dLRyddFF0;B##OZu5bL}$QR%_=d^3}2_vT!u}j@T8gkx^=`M^Pkzx!8hMr`TRuY z0p(=Ba>R59CXp?XZH>4x(RWwIK~oSerbPXpUevC#v!c@qtMLp|`e{pIWh8;Zv70&B zA6O*}pz^Svet=BIoG--^EpC#3)xh%0wn-u*Vi~fTjQT@>`WP?V*#;ua9I!m?qF9NL zgmxxleh#yI)w68$Qyeszp>g2;{J>afem+KEEUr2|a5W0}f_G+q5u>O(a~(581Y)79 z7fJosXL-Jb4ITMsD-%A!ikKA8W!Zkew;N$>ussI+eluw--m+}z~Kb zoPneQ*k-$4@e?%^ILdRyY-zo*dJYOcKGN}GiyI5m7XmE2j$IWL%{ES52~W~< zOUTk7vCQ+?M_=Lm>=j0h4oV3`2exC|K`9Dtc)HlJ@ zGFzpSt-tdJFY*cDq6#ZbgZL(3HlZ<_(36lD1e&%c|3m-}Jx>Gj@6+Ry!9&{Bj+Gd% z%b#+}PGOO)iay+An5{YrCO&JLuU>@4db~`oK-jmRm1Rm!&9>~fpnpbg4nj#Q(xoMx zE2vcZX9>9OZ(xqp?t!#^#-JiX>>M8SsZ$5lyW$LH~~n!)>p@3L4s!+|Pce z#Z&Mem?#Ke#TcsOc*JT)-|6btk?Qs~RA4AEP%oKL9+bEY=Hm=10~%m%35wX*P5iO( z!DM~!#|=EhX)NVRLJv2-j{^LBCO#A&;AA-oil*=^`hR(umqSb6KMEqcVmT|E+C}hK zY0fMQ=ncDE;b%BEcRSWKRiCmlGM38qWff&=_gpUl4+Fa%sprG|NYt&L3fiy#i`Y0JmyxIp9mlly=amgf;9GB z`~M_V-lC!Qe+pLrkGAbH*0t`tN)r{Zf@ULU1=;M3MB7w#+^cAbfNb4CMvlh)4SL4hMcP|}N-8^4`g!k3Y zp85bc`EnVO3G^|vsIPd!=>tYDkz0~aNTQX;p_er_UE9aVvEnONTPc!xiR(3?a?fxQkP(u7s~KGMVK|A5%!V>~$w7D$Kij>(oTb>y z-Ydr#CDCG2E#!@i#66D6JkWVdby`R~tuRtXIXWuqnYo0~-XYi<5SecFk)Z}l-Rtkx zCxP0$2zwaz~3aksrIe5zNHUwcQ}fV33Hv2m$d|# zq7+QA=ZY9`1)(esf~Abw4B>1B-syKfC{-4!x24eIQqe;saYa%&gaW{wX%wY;EgJ}iQScp)or=el@KaupN%us>id zl%Gyi4AF%~@!L#lI>Jd+?cIw1m9%zw#G&zzz8<=OhhexbH*<&oqP}EXm-N>gXN$-d zLcR?-S*=@A!Z|Lq($|}t?#;`^0ng9Krzi&8-bo`^H%t=(Ng;Sth@5pU@Ir^kNykE# zQ9?%Tb4Q6-Frt=yos@fvdy9)9bF|+;)2l}oUHs@Hq+1Eqtkr3-&uz+4rg%R9@ulo8 ztE44J>?+@Wz#{L-BZdaj_d_;;$&nEZeLbOUqX1g^SKWN)NzENP`ApvQHX40oL@}C& zh@2_Wz=~X#gWwbNQ@`|jd_=z58?35G&(QPsU8bZ&!LJ|tn36a|7+;%sOawkB;3h$F zHEWkOoJoa;<2*5p>6eionYi-N_7l3tJJt1+HjmMt#9!5n5KW!F(*F7*>`%jK(74 zZ0uP106qkZN#D9^O*kgAA+}`dxlj`*!vK%yJe%@vP*Z|I0glPg{x`ySkM2JjA7%;l%_%M|cQuA>aT9e5l@!rFin-)g0% z!Veqb#~HG5@LEOg;H(_s@HHv9E-b zCA~3UwzF9XVbU;w#I2P5oe=y2yv$km7i+)S^=t4hLIA)3oD)U59NTN!Z^@`*le{Zo z_nJN!s)@AqC|Nm|;}u>2u^|-EQ6T1ck!6Bo>wY`&OB1?qruSb;oZUxa8%ddSF2NuH16u`)U-?|;43ugT|G3aK*= z#frfHF`3g)NEyP;)3hChu6q;|V!IvF_wV^DT|eb1r6`dfE;@=X1llr4!HNUwry$ir5Ug3aH!+&@_W(n>^axUq&eT zne#0~Xm`fa81g1+?yKEz48ne9zs-5ppBNUn%(sNSHO7wvgs&A1CJA)mWLr zks#i$s1~c1uk&B*eou^hOBK3JMmpM6O#{}R$P%HXv#|HAP3oI_+x6_q{ ztYR2SBuXUA=z}Dea-_4pZjB<`57ucKbM-Qw2=z}%3b_iKd`C&82Ddkt=z?ScN~Xtb z%kaLq6s)O9UI^PLyK~^!@S!ww=#op;qO&$m;N#Jun8icjR9-5Z>i6BT!YpTt5C7D@ zuLLg#1bPtIAuIWlR~WE>_7*iFD#PuJRqMHwVcC`tQJX{kmi#skz<2CQo@l!={}!Q2 z56UOuECrU}$#*X*W1mpir8c`#U&Qhuw+yT~BI^(@|GViSWR{i!qX<`HflO523|h2f zz5au5i%~%zZ~VTU(X>Pad= zdlcfFd-1L6YC=$%Vj&$f6c0J!9N$z1X$vi8PTflaO2!h^wwnBf_#fQyP@`m!ruJp- zIUn&56Xr!HW3J275$^7Y5jv`7eSWPgelL;LqAs%ol_7?dMwUjFo2kyhZGhJ&ejx0< z9E>D|UGm>nMs0OrL_ao@uqYGpsSbp3>ng(8uq+hcs>tSS{fX3)jxINAwX>mZw_DWS zkXPr2%$=!&@2z+?|36{v+MCy~7CWHQ?<9m|f9qS1|H{DO@vb&Yt;127cuhv@;El#{ z&zG>g4}boyT$1n`m=CdZQ#I^e-;P)s{EeOTMC-(v$?fts){`l)i2sa-Sowu;<&x6v zF4ofFUvZ(iiHo)bn8WN^;=#lk4r`$rG)~!R2+z5qy%%b?<8oV_mS!~Dy`U=tYkg(; z&-8uG`n(ATT`;04hHz8WKcV)&?t1)64g>Do==4!6zTFv-X&Vd)k$)%BpqmXWnH^}A^$N`;&X=(0UGoxI*lrp;bdE^Hn|)UIm@gZyZdxgg71GYo zE@AV&0nixclpj=TYSVa>qxeJR=gt>pzhBKJ;@{$!}aVW`%Kq9h4 z->#ZW}iZacO0eTM$e8WKxm76Wrc;443a9AYBZ+GDPQV^k^%G=aSb^j-vs z4fOecB_Q#hr8W^l1}gu55{;FbeI&`{+VC-1J#jD&dH7$H6a%lR{A0P#>0N$r$v*$v zB(=nAV}Q?2_4*H$5`(lOty5t5ziS>Gd0s=8!nqQxQk})B$iIw>aah7v89$Xj70EdW5>>@rc^lC8oNL1<%PgL7P3a$E7<@E0b!Lln{>)&$rtw6UAF)j&)7~aoRBk?kHJAj+x@!)yy;x zE5?YxC8X3KpF)ZVixykU4JV|7#qq#Xm04~g9re|z!zw|*o?&K1Z4!=Gj98q+p=X;G zQ+YSJZ;9;BD6=FPj?_iab;g_a^&^6r?>=kxbSopq>$=K!fX(p74)H~&=5Enp6=H+-@bSUFPTV=k~TCQ8A& zjxh{Xz8z^RY=j#ctENp=z*n2|I1n$o^C@`*cr)6}6Bt zp)gT311~am0tcG~`a7jdGmbxWUX9=H`-<52ANnly?OJZCy#Ke5@#ThJ@y=HT6Mluw zd^je~Eq+AwXA5J!V@*sDE9Ca$HvZ8dmG z3aT`K<>U>D+$xOKN`as|;ONHkSk|FgtLNDo8s_3nHJfV9PBEhVNrz0aL`hlkW*($L zTySDHH6x`u`&f;5bX+VBs=|sIM*Ut z?y={UqZjbBZ;t=7zk10r#TMUAAu0uJP6z^4@+Idw298DWu@Ft#M}}>~^w>QlErFGF zTm(){#j(Ve_2JtJV+M~Wpg8a{Lb!$l4G_6V)O{ZJ!f{K52DK~-NA=BDrd5{=QRqr8 u8amktq=Y4A)1OD#!!*<9Uv~~<>trZZ%Y3@WSswp|ZiLP8hSm-l;r{^9yF5?; literal 0 HcmV?d00001 diff --git a/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/rum_8.0.0/mappings.json b/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/rum_8.0.0/mappings.json new file mode 100644 index 0000000000000..48ac74d97dfa7 --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/rum_8.0.0/mappings.json @@ -0,0 +1,600 @@ +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "apm-8.0.0-transaction-005", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "version": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "client": { + "properties": { + "geo": { + "properties": { + "continent_name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "country_iso_code": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "location": { + "properties": { + "lat": { + "type": "float" + }, + "lon": { + "type": "float" + } + } + } + } + }, + "ip": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ecs": { + "properties": { + "version": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "event": { + "properties": { + "ingested": { + "type": "date" + } + } + }, + "http": { + "properties": { + "request": { + "properties": { + "referrer": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "response": { + "properties": { + "decoded_body_size": { + "type": "long" + }, + "encoded_body_size": { + "type": "long" + }, + "transfer_size": { + "type": "long" + } + } + } + } + }, + "observer": { + "properties": { + "ephemeral_id": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "hostname": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "id": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "type": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "version": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "version_major": { + "type": "long" + } + } + }, + "processor": { + "properties": { + "event": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "service": { + "properties": { + "language": { + "properties": { + "name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "version": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "source": { + "properties": { + "ip": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "timestamp": { + "properties": { + "us": { + "type": "long" + } + } + }, + "trace": { + "properties": { + "id": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "transaction": { + "properties": { + "custom": { + "properties": { + "userConfig": { + "properties": { + "featureFlags": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "showDashboard": { + "type": "boolean" + } + } + } + } + }, + "duration": { + "properties": { + "us": { + "type": "long" + } + } + }, + "id": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "marks": { + "properties": { + "agent": { + "properties": { + "domComplete": { + "type": "long" + }, + "domInteractive": { + "type": "long" + }, + "firstContentfulPaint": { + "type": "float" + }, + "largestContentfulPaint": { + "type": "float" + }, + "timeToFirstByte": { + "type": "long" + } + } + }, + "navigationTiming": { + "properties": { + "connectEnd": { + "type": "long" + }, + "connectStart": { + "type": "long" + }, + "domComplete": { + "type": "long" + }, + "domContentLoadedEventEnd": { + "type": "long" + }, + "domContentLoadedEventStart": { + "type": "long" + }, + "domInteractive": { + "type": "long" + }, + "domLoading": { + "type": "long" + }, + "domainLookupEnd": { + "type": "long" + }, + "domainLookupStart": { + "type": "long" + }, + "fetchStart": { + "type": "long" + }, + "loadEventEnd": { + "type": "long" + }, + "loadEventStart": { + "type": "long" + }, + "requestStart": { + "type": "long" + }, + "responseEnd": { + "type": "long" + }, + "responseStart": { + "type": "long" + } + } + } + } + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "page": { + "properties": { + "referer": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "sampled": { + "type": "boolean" + }, + "span_count": { + "properties": { + "started": { + "type": "long" + } + } + }, + "type": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "url": { + "properties": { + "domain": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "full": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "original": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "path": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "port": { + "type": "long" + }, + "scheme": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "user": { + "properties": { + "email": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "id": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "user_agent": { + "properties": { + "device": { + "properties": { + "name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "original": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "os": { + "properties": { + "full": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "version": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "version": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/apm_api_integration/trial/tests/index.ts b/x-pack/test/apm_api_integration/trial/tests/index.ts index 1a00f7e2df9e8..37328badcb794 100644 --- a/x-pack/test/apm_api_integration/trial/tests/index.ts +++ b/x-pack/test/apm_api_integration/trial/tests/index.ts @@ -11,5 +11,6 @@ export default function observabilityApiIntegrationTests({ loadTestFile }: FtrPr this.tags('ciGroup1'); loadTestFile(require.resolve('./annotations')); loadTestFile(require.resolve('./service_maps')); + loadTestFile(require.resolve('./rum_services')); }); } diff --git a/x-pack/test/apm_api_integration/trial/tests/rum_services.ts b/x-pack/test/apm_api_integration/trial/tests/rum_services.ts new file mode 100644 index 0000000000000..5505387de54a7 --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/tests/rum_services.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +export default function rumServicesApiTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('RUM Services', () => { + describe('when there is no data', () => { + it('returns empty list', async () => { + const response = await supertest.get( + '/api/apm/rum-client/services?start=2020-06-28T10%3A24%3A46.055Z&end=2020-07-29T10%3A24%3A46.055Z&uiFilters=%7B%22agentName%22%3A%5B%22js-base%22%2C%22rum-js%22%5D%7D' + ); + + expect(response.status).to.be(200); + expect(response.body).to.eql([]); + }); + }); + + describe('when there is data', () => { + before(async () => { + await esArchiver.load('8.0.0'); + await esArchiver.load('rum_8.0.0'); + }); + after(async () => { + await esArchiver.unload('8.0.0'); + await esArchiver.unload('rum_8.0.0'); + }); + + it('returns rum services list', async () => { + const response = await supertest.get( + '/api/apm/rum-client/services?start=2020-06-28T10%3A24%3A46.055Z&end=2020-07-29T10%3A24%3A46.055Z&uiFilters=%7B%22agentName%22%3A%5B%22js-base%22%2C%22rum-js%22%5D%7D' + ); + + expect(response.status).to.be(200); + + expect(response.body).to.eql(['client', 'opbean-client-rum']); + }); + }); + }); +} From 0ebddcf788de0f20379e4faec20467ce8ee55695 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Wed, 8 Jul 2020 15:01:07 +0300 Subject: [PATCH 07/13] add a test interceptors do not have access to request body (#70929) --- .../http/integration_tests/lifecycle.test.ts | 137 +++++++++++++++++- 1 file changed, 136 insertions(+), 1 deletion(-) diff --git a/src/core/server/http/integration_tests/lifecycle.test.ts b/src/core/server/http/integration_tests/lifecycle.test.ts index 879cbc689f8e7..cbab14115ba6b 100644 --- a/src/core/server/http/integration_tests/lifecycle.test.ts +++ b/src/core/server/http/integration_tests/lifecycle.test.ts @@ -19,6 +19,7 @@ import supertest from 'supertest'; import request from 'request'; +import { schema } from '@kbn/config-schema'; import { ensureRawRequest } from '../router'; import { HttpService } from '../http_service'; @@ -222,6 +223,39 @@ describe('OnPreAuth', () => { await supertest(innerServer.listener).get('/').expect(200, { customField: 'undefined' }); }); + + it('has no access to request body', async () => { + const { registerOnPreAuth, server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + let requestBody = null; + registerOnPreAuth((req, res, t) => { + requestBody = req.body; + return t.next(); + }); + + router.post( + { + path: '/', + validate: { + body: schema.object({ + term: schema.string(), + }), + }, + }, + (context, req, res) => res.ok({ body: req.body.term }) + ); + + await server.start(); + + await supertest(innerServer.listener) + .post('/') + .send({ + term: 'foo', + }) + .expect(200, 'foo'); + + expect(requestBody).toStrictEqual({}); + }); }); describe('OnPostAuth', () => { @@ -356,6 +390,39 @@ describe('OnPostAuth', () => { await supertest(innerServer.listener).get('/').expect(200, { customField: 'undefined' }); }); + + it('has no access to request body', async () => { + const { registerOnPostAuth, server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + let requestBody = null; + registerOnPostAuth((req, res, t) => { + requestBody = req.body; + return t.next(); + }); + + router.post( + { + path: '/', + validate: { + body: schema.object({ + term: schema.string(), + }), + }, + }, + (context, req, res) => res.ok({ body: req.body.term }) + ); + + await server.start(); + + await supertest(innerServer.listener) + .post('/') + .send({ + term: 'foo', + }) + .expect(200, 'foo'); + + expect(requestBody).toStrictEqual({}); + }); }); describe('Auth', () => { @@ -852,10 +919,43 @@ describe('Auth', () => { await supertest(innerServer.listener).get('/').expect(200, { customField: 'undefined' }); }); + + it('has no access to request body', async () => { + const { registerAuth, server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + let requestBody = null; + registerAuth((req, res, t) => { + requestBody = req.body; + return t.authenticated({}); + }); + + router.post( + { + path: '/', + validate: { + body: schema.object({ + term: schema.string(), + }), + }, + }, + (context, req, res) => res.ok({ body: req.body.term }) + ); + + await server.start(); + + await supertest(innerServer.listener) + .post('/') + .send({ + term: 'foo', + }) + .expect(200, 'foo'); + + expect(requestBody).toStrictEqual({}); + }); }); describe('OnPreResponse', () => { - it('supports registering response inceptors', async () => { + it('supports registering response interceptors', async () => { const { registerOnPreResponse, server: innerServer, createRouter } = await server.setup( setupDeps ); @@ -1001,4 +1101,39 @@ describe('OnPreResponse', () => { await supertest(innerServer.listener).get('/').expect(200); }); + + it('has no access to request body', async () => { + const { registerOnPreResponse, server: innerServer, createRouter } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + let requestBody = null; + registerOnPreResponse((req, res, t) => { + requestBody = req.body; + return t.next(); + }); + + router.post( + { + path: '/', + validate: { + body: schema.object({ + term: schema.string(), + }), + }, + }, + (context, req, res) => res.ok({ body: req.body.term }) + ); + + await server.start(); + + await supertest(innerServer.listener) + .post('/') + .send({ + term: 'foo', + }) + .expect(200, 'foo'); + + expect(requestBody).toStrictEqual({}); + }); }); From 1c91b1c9c23275d2bcc74f93359d72af38065d16 Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Wed, 8 Jul 2020 15:06:53 +0300 Subject: [PATCH 08/13] [Visualize] Add unit tests (#70410) * Reactify visualize app * Fix typescript failures after merging master * Make sure refresh button works * Subscribe filter manager fetches * Use redirect to landing page * Update savedSearch type * Add check for TSVB is loaded * Add unit tests for useSavedVisInstance effect * Fix comments * Fix uiState persistence on vis load * Remove extra div around TableListView * Update DTS selectors * Add error handling for embeddable * Add unit tests for createVisualizeAppState * Add unit tests for useChromeVisibility * Add filter_manager.mock * Add unit tests for useVisualizeAppState * Use app state stub * Add unit tests for useLinkedSearchUpdates * Add unit tests for useEditorUpdates * Remove extra argument from useEditorUpdates effect * Update comments, fix typos * Remove extra div wrapper * Apply design suggestions * Revert accidental config changes * Add unit tests for useEditorUpdates * Use visualize services mock * Add unit tests for getVisualizationInstance * Fix eslint warnings Co-authored-by: Elastic Machine --- .../filter_manager/filter_manager.mock.ts | 42 +++ src/plugins/data/public/query/mocks.ts | 5 +- src/plugins/visualizations/public/mocks.ts | 4 +- .../visualize/public/application/types.ts | 2 +- .../utils/create_visualize_app_state.test.ts | 134 +++++++ .../utils/get_visualization_instance.test.ts | 124 +++++++ .../public/application/utils/mocks.ts | 43 +++ .../public/application/utils/stubs.ts | 89 +++++ .../utils/use/use_chrome_visibility.test.ts | 55 +++ .../utils/use/use_editor_updates.test.ts | 327 ++++++++++++++++++ .../use/use_linked_search_updates.test.ts | 76 ++++ .../utils/use/use_saved_vis_instance.test.ts | 224 ++++++++++++ .../utils/use/use_visualize_app_state.test.ts | 210 +++++++++++ 13 files changed, 1331 insertions(+), 4 deletions(-) create mode 100644 src/plugins/data/public/query/filter_manager/filter_manager.mock.ts create mode 100644 src/plugins/visualize/public/application/utils/create_visualize_app_state.test.ts create mode 100644 src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts create mode 100644 src/plugins/visualize/public/application/utils/mocks.ts create mode 100644 src/plugins/visualize/public/application/utils/stubs.ts create mode 100644 src/plugins/visualize/public/application/utils/use/use_chrome_visibility.test.ts create mode 100644 src/plugins/visualize/public/application/utils/use/use_editor_updates.test.ts create mode 100644 src/plugins/visualize/public/application/utils/use/use_linked_search_updates.test.ts create mode 100644 src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts create mode 100644 src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts diff --git a/src/plugins/data/public/query/filter_manager/filter_manager.mock.ts b/src/plugins/data/public/query/filter_manager/filter_manager.mock.ts new file mode 100644 index 0000000000000..c95a943be7713 --- /dev/null +++ b/src/plugins/data/public/query/filter_manager/filter_manager.mock.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable } from 'rxjs'; +import { FilterManager } from './filter_manager'; + +export const createFilterManagerMock = () => { + const filterManager = ({ + mergeIncomingFilters: jest.fn(), + handleStateUpdate: jest.fn(), + getFilters: jest.fn(), + getAppFilters: jest.fn(), + getGlobalFilters: jest.fn(), + getPartitionedFilters: jest.fn(), + getUpdates$: jest.fn(() => new Observable()), + getFetches$: jest.fn(() => new Observable()), + addFilters: jest.fn(), + setFilters: jest.fn(), + setGlobalFilters: jest.fn(), + setAppFilters: jest.fn(), + removeFilter: jest.fn(), + removeAll: jest.fn(), + } as unknown) as jest.Mocked; + + return filterManager; +}; diff --git a/src/plugins/data/public/query/mocks.ts b/src/plugins/data/public/query/mocks.ts index 0c19f71277bc5..41896107bb868 100644 --- a/src/plugins/data/public/query/mocks.ts +++ b/src/plugins/data/public/query/mocks.ts @@ -20,12 +20,13 @@ import { Observable } from 'rxjs'; import { QueryService, QuerySetup, QueryStart } from '.'; import { timefilterServiceMock } from './timefilter/timefilter_service.mock'; +import { createFilterManagerMock } from './filter_manager/filter_manager.mock'; type QueryServiceClientContract = PublicMethodsOf; const createSetupContractMock = () => { const setupContract: jest.Mocked = { - filterManager: jest.fn() as any, + filterManager: createFilterManagerMock(), timefilter: timefilterServiceMock.createSetupContract(), state$: new Observable(), }; @@ -36,7 +37,7 @@ const createSetupContractMock = () => { const createStartContractMock = () => { const startContract: jest.Mocked = { addToQueryLog: jest.fn(), - filterManager: jest.fn() as any, + filterManager: createFilterManagerMock(), savedQueries: jest.fn() as any, state$: new Observable(), timefilter: timefilterServiceMock.createStartContract(), diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts index 05644eddc5fca..e0ec4801b3caf 100644 --- a/src/plugins/visualizations/public/mocks.ts +++ b/src/plugins/visualizations/public/mocks.ts @@ -39,7 +39,9 @@ const createStartContract = (): VisualizationsStart => ({ get: jest.fn(), all: jest.fn(), getAliases: jest.fn(), - savedVisualizationsLoader: {} as any, + savedVisualizationsLoader: { + get: jest.fn(), + } as any, showNewVisModal: jest.fn(), createVis: jest.fn(), convertFromSerializedVis: jest.fn(), diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts index a6adaf1f3c62b..02ae1cc155dd2 100644 --- a/src/plugins/visualize/public/application/types.ts +++ b/src/plugins/visualize/public/application/types.ts @@ -50,7 +50,7 @@ export type PureVisState = SavedVisState; export interface VisualizeAppState { filters: Filter[]; - uiState: PersistedState; + uiState: Record; vis: PureVisState; query: Query; savedQuery?: string; diff --git a/src/plugins/visualize/public/application/utils/create_visualize_app_state.test.ts b/src/plugins/visualize/public/application/utils/create_visualize_app_state.test.ts new file mode 100644 index 0000000000000..885eec8a68d2d --- /dev/null +++ b/src/plugins/visualize/public/application/utils/create_visualize_app_state.test.ts @@ -0,0 +1,134 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IKbnUrlStateStorage } from 'src/plugins/kibana_utils/public'; +import { createVisualizeAppState } from './create_visualize_app_state'; +import { migrateAppState } from './migrate_app_state'; +import { visualizeAppStateStub } from './stubs'; + +const mockStartStateSync = jest.fn(); +const mockStopStateSync = jest.fn(); + +jest.mock('../../../../kibana_utils/public', () => ({ + createStateContainer: jest.fn(() => 'stateContainer'), + syncState: jest.fn(() => ({ + start: mockStartStateSync, + stop: mockStopStateSync, + })), +})); +jest.mock('./migrate_app_state', () => ({ + migrateAppState: jest.fn(() => 'migratedAppState'), +})); + +const { createStateContainer, syncState } = jest.requireMock('../../../../kibana_utils/public'); + +describe('createVisualizeAppState', () => { + const kbnUrlStateStorage = ({ + set: jest.fn(), + get: jest.fn(() => ({ linked: false })), + } as unknown) as IKbnUrlStateStorage; + + const { stateContainer, stopStateSync } = createVisualizeAppState({ + stateDefaults: visualizeAppStateStub, + kbnUrlStateStorage, + }); + const transitions = createStateContainer.mock.calls[0][1]; + + test('should initialize visualize app state', () => { + expect(kbnUrlStateStorage.get).toHaveBeenCalledWith('_a'); + expect(migrateAppState).toHaveBeenCalledWith({ + ...visualizeAppStateStub, + linked: false, + }); + expect(kbnUrlStateStorage.set).toHaveBeenCalledWith('_a', 'migratedAppState', { + replace: true, + }); + expect(createStateContainer).toHaveBeenCalled(); + expect(syncState).toHaveBeenCalled(); + expect(mockStartStateSync).toHaveBeenCalled(); + }); + + test('should return the stateContainer and stopStateSync', () => { + expect(stateContainer).toBe('stateContainer'); + stopStateSync(); + expect(stopStateSync).toHaveBeenCalledTimes(1); + }); + + describe('stateContainer transitions', () => { + test('set', () => { + const newQuery = { query: '', language: '' }; + expect(transitions.set(visualizeAppStateStub)('query', newQuery)).toEqual({ + ...visualizeAppStateStub, + query: newQuery, + }); + }); + + test('setVis', () => { + const newVis = { data: 'data' }; + expect(transitions.setVis(visualizeAppStateStub)(newVis)).toEqual({ + ...visualizeAppStateStub, + vis: { + ...visualizeAppStateStub.vis, + ...newVis, + }, + }); + }); + + test('unlinkSavedSearch', () => { + const params = { + query: { query: '', language: '' }, + parentFilters: [{ test: 'filter2' }], + }; + expect(transitions.unlinkSavedSearch(visualizeAppStateStub)(params)).toEqual({ + ...visualizeAppStateStub, + query: params.query, + filters: [...visualizeAppStateStub.filters, { test: 'filter2' }], + linked: false, + }); + }); + + test('updateVisState: should not include resctricted param types', () => { + const newVisState = { + a: 1, + _b: 2, + $c: 3, + d: () => {}, + }; + expect(transitions.updateVisState(visualizeAppStateStub)(newVisState)).toEqual({ + ...visualizeAppStateStub, + vis: { a: 1 }, + }); + }); + + test('updateSavedQuery: add savedQuery', () => { + const savedQueryId = '123test'; + expect(transitions.updateSavedQuery(visualizeAppStateStub)(savedQueryId)).toEqual({ + ...visualizeAppStateStub, + savedQuery: savedQueryId, + }); + }); + + test('updateSavedQuery: remove savedQuery from state', () => { + const savedQueryId = '123test'; + expect( + transitions.updateSavedQuery({ ...visualizeAppStateStub, savedQuery: savedQueryId })() + ).toEqual(visualizeAppStateStub); + }); + }); +}); diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts new file mode 100644 index 0000000000000..31f0fc5f94479 --- /dev/null +++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts @@ -0,0 +1,124 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createSavedSearchesLoader } from '../../../../discover/public'; +import { getVisualizationInstance } from './get_visualization_instance'; +import { createVisualizeServicesMock } from './mocks'; +import { VisualizeServices } from '../types'; +import { BehaviorSubject } from 'rxjs'; + +const mockSavedSearchObj = {}; +const mockGetSavedSearch = jest.fn(() => mockSavedSearchObj); + +jest.mock('../../../../discover/public', () => ({ + createSavedSearchesLoader: jest.fn(() => ({ + get: mockGetSavedSearch, + })), +})); + +describe('getVisualizationInstance', () => { + const serializedVisMock = { + type: 'area', + }; + let savedVisMock: any; + let visMock: any; + let mockServices: jest.Mocked; + let subj: BehaviorSubject; + + beforeEach(() => { + mockServices = createVisualizeServicesMock(); + subj = new BehaviorSubject({}); + visMock = { + type: {}, + data: {}, + }; + savedVisMock = {}; + // @ts-expect-error + mockServices.savedVisualizations.get.mockImplementation(() => savedVisMock); + // @ts-expect-error + mockServices.visualizations.convertToSerializedVis.mockImplementation(() => serializedVisMock); + // @ts-expect-error + mockServices.visualizations.createVis.mockImplementation(() => visMock); + // @ts-expect-error + mockServices.createVisEmbeddableFromObject.mockImplementation(() => ({ + getOutput$: jest.fn(() => subj.asObservable()), + })); + }); + + test('should create new instances of savedVis, vis and embeddableHandler', async () => { + const opts = { + type: 'area', + indexPattern: 'my_index_pattern', + }; + const { savedVis, savedSearch, vis, embeddableHandler } = await getVisualizationInstance( + mockServices, + opts + ); + + expect(mockServices.savedVisualizations.get).toHaveBeenCalledWith(opts); + expect(savedVisMock.searchSourceFields).toEqual({ + index: opts.indexPattern, + }); + expect(mockServices.visualizations.convertToSerializedVis).toHaveBeenCalledWith(savedVisMock); + expect(mockServices.visualizations.createVis).toHaveBeenCalledWith( + serializedVisMock.type, + serializedVisMock + ); + expect(mockServices.createVisEmbeddableFromObject).toHaveBeenCalledWith(visMock, { + timeRange: undefined, + filters: undefined, + id: '', + }); + + expect(vis).toBe(visMock); + expect(savedVis).toBe(savedVisMock); + expect(embeddableHandler).toBeDefined(); + expect(savedSearch).toBeUndefined(); + }); + + test('should load existing vis by id and call vis type setup if exists', async () => { + const newVisObj = { data: {} }; + visMock.type.setup = jest.fn(() => newVisObj); + const { vis } = await getVisualizationInstance(mockServices, 'saved_vis_id'); + + expect(mockServices.savedVisualizations.get).toHaveBeenCalledWith('saved_vis_id'); + expect(savedVisMock.searchSourceFields).toBeUndefined(); + expect(visMock.type.setup).toHaveBeenCalledWith(visMock); + expect(vis).toBe(newVisObj); + }); + + test('should create saved search instance if vis based on saved search id', async () => { + visMock.data.savedSearchId = 'saved_search_id'; + const { savedSearch } = await getVisualizationInstance(mockServices, 'saved_vis_id'); + + expect(createSavedSearchesLoader).toHaveBeenCalled(); + expect(mockGetSavedSearch).toHaveBeenCalledWith(visMock.data.savedSearchId); + expect(savedSearch).toBe(mockSavedSearchObj); + }); + + test('should subscribe on embeddable handler updates and send toasts on errors', async () => { + await getVisualizationInstance(mockServices, 'saved_vis_id'); + + subj.next({ + error: 'error', + }); + + expect(mockServices.toastNotifications.addError).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/visualize/public/application/utils/mocks.ts b/src/plugins/visualize/public/application/utils/mocks.ts new file mode 100644 index 0000000000000..09e7ba23875ca --- /dev/null +++ b/src/plugins/visualize/public/application/utils/mocks.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { coreMock } from '../../../../../core/public/mocks'; +import { dataPluginMock } from '../../../../data/public/mocks'; +import { visualizationsPluginMock } from '../../../../visualizations/public/mocks'; +import { VisualizeServices } from '../types'; + +export const createVisualizeServicesMock = () => { + const coreStartMock = coreMock.createStart(); + const dataStartMock = dataPluginMock.createStartContract(); + const toastNotifications = coreStartMock.notifications.toasts; + const visualizations = visualizationsPluginMock.createStartContract(); + + return ({ + ...coreStartMock, + data: dataStartMock, + toastNotifications, + history: { + replace: jest.fn(), + location: { pathname: '' }, + }, + visualizations, + savedVisualizations: visualizations.savedVisualizationsLoader, + createVisEmbeddableFromObject: visualizations.__LEGACY.createVisEmbeddableFromObject, + } as unknown) as jest.Mocked; +}; diff --git a/src/plugins/visualize/public/application/utils/stubs.ts b/src/plugins/visualize/public/application/utils/stubs.ts new file mode 100644 index 0000000000000..1bbd738a739cf --- /dev/null +++ b/src/plugins/visualize/public/application/utils/stubs.ts @@ -0,0 +1,89 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { VisualizeAppState } from '../types'; + +export const visualizeAppStateStub: VisualizeAppState = { + uiState: { + vis: { + defaultColors: { + '0 - 2': 'rgb(165,0,38)', + '2 - 3': 'rgb(255,255,190)', + '3 - 4': 'rgb(0,104,55)', + }, + }, + }, + query: { query: '', language: 'kuery' }, + filters: [], + vis: { + title: '[eCommerce] Average Sold Quantity', + type: 'gauge', + aggs: [ + { + id: '1', + enabled: true, + // @ts-expect-error + type: 'avg', + schema: 'metric', + params: { field: 'total_quantity', customLabel: 'average items' }, + }, + ], + params: { + type: 'gauge', + addTooltip: true, + addLegend: true, + isDisplayWarning: false, + gauge: { + extendRange: true, + percentageMode: false, + gaugeType: 'Circle', + gaugeStyle: 'Full', + backStyle: 'Full', + orientation: 'vertical', + colorSchema: 'Green to Red', + gaugeColorMode: 'Labels', + colorsRange: [ + { from: 0, to: 2 }, + { from: 2, to: 3 }, + { from: 3, to: 4 }, + ], + invertColors: true, + labels: { show: true, color: 'black' }, + scale: { show: false, labels: false, color: '#333' }, + type: 'meter', + style: { + bgWidth: 0.9, + width: 0.9, + mask: false, + bgMask: false, + maskBars: 50, + bgFill: '#eee', + bgColor: false, + subText: 'per order', + fontSize: 60, + labelColor: true, + }, + minAngle: 0, + maxAngle: 6.283185307179586, + alignment: 'horizontal', + }, + }, + }, + linked: false, +}; diff --git a/src/plugins/visualize/public/application/utils/use/use_chrome_visibility.test.ts b/src/plugins/visualize/public/application/utils/use/use_chrome_visibility.test.ts new file mode 100644 index 0000000000000..904816db22278 --- /dev/null +++ b/src/plugins/visualize/public/application/utils/use/use_chrome_visibility.test.ts @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; + +import { chromeServiceMock } from '../../../../../../core/public/mocks'; +import { useChromeVisibility } from './use_chrome_visibility'; + +describe('useChromeVisibility', () => { + const chromeMock = chromeServiceMock.createStartContract(); + + test('should set up a subscription for chrome visibility', () => { + const { result } = renderHook(() => useChromeVisibility(chromeMock)); + + expect(chromeMock.getIsVisible$).toHaveBeenCalled(); + expect(result.current).toEqual(false); + }); + + test('should change chrome visibility to true if change was emitted', () => { + const { result } = renderHook(() => useChromeVisibility(chromeMock)); + const behaviorSubj = chromeMock.getIsVisible$.mock.results[0].value; + act(() => { + behaviorSubj.next(true); + }); + + expect(result.current).toEqual(true); + }); + + test('should destroy a subscription', () => { + const { unmount } = renderHook(() => useChromeVisibility(chromeMock)); + const behaviorSubj = chromeMock.getIsVisible$.mock.results[0].value; + const subscription = behaviorSubj.observers[0]; + subscription.unsubscribe = jest.fn(); + + unmount(); + + expect(subscription.unsubscribe).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/visualize/public/application/utils/use/use_editor_updates.test.ts b/src/plugins/visualize/public/application/utils/use/use_editor_updates.test.ts new file mode 100644 index 0000000000000..3546ee7b321bb --- /dev/null +++ b/src/plugins/visualize/public/application/utils/use/use_editor_updates.test.ts @@ -0,0 +1,327 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { EventEmitter } from 'events'; + +import { useEditorUpdates } from './use_editor_updates'; +import { + VisualizeServices, + VisualizeAppStateContainer, + SavedVisInstance, + IEditorController, +} from '../../types'; +import { visualizeAppStateStub } from '../stubs'; +import { createVisualizeServicesMock } from '../mocks'; + +describe('useEditorUpdates', () => { + const eventEmitter = new EventEmitter(); + const setHasUnsavedChangesMock = jest.fn(); + let mockServices: jest.Mocked; + + beforeEach(() => { + mockServices = createVisualizeServicesMock(); + // @ts-expect-error + mockServices.visualizations.convertFromSerializedVis.mockImplementation(() => ({ + visState: visualizeAppStateStub.vis, + })); + }); + + test('should not create any subscriptions if app state container is not ready', () => { + const { result } = renderHook(() => + useEditorUpdates( + mockServices, + eventEmitter, + setHasUnsavedChangesMock, + null, + undefined, + undefined + ) + ); + + expect(result.current).toEqual({ + isEmbeddableRendered: false, + currentAppState: undefined, + }); + }); + + let unsubscribeStateUpdatesMock: jest.Mock; + let appState: VisualizeAppStateContainer; + let savedVisInstance: SavedVisInstance; + let visEditorController: IEditorController; + let timeRange: any; + let mockFilters: any; + + beforeEach(() => { + unsubscribeStateUpdatesMock = jest.fn(); + appState = ({ + getState: jest.fn(() => visualizeAppStateStub), + subscribe: jest.fn(() => unsubscribeStateUpdatesMock), + transitions: { + set: jest.fn(), + }, + } as unknown) as VisualizeAppStateContainer; + savedVisInstance = ({ + vis: { + uiState: { + on: jest.fn(), + off: jest.fn(), + setSilent: jest.fn(), + getChanges: jest.fn(() => visualizeAppStateStub.uiState), + }, + data: {}, + serialize: jest.fn(), + title: visualizeAppStateStub.vis.title, + setState: jest.fn(), + }, + embeddableHandler: { + updateInput: jest.fn(), + reload: jest.fn(), + }, + savedVis: {}, + } as unknown) as SavedVisInstance; + visEditorController = { + render: jest.fn(), + destroy: jest.fn(), + }; + timeRange = { + from: 'now-15m', + to: 'now', + }; + mockFilters = ['mockFilters']; + // @ts-expect-error + mockServices.data.query.timefilter.timefilter.getTime.mockImplementation(() => timeRange); + // @ts-expect-error + mockServices.data.query.filterManager.getFilters.mockImplementation(() => mockFilters); + }); + + test('should set up current app state and render the editor', () => { + const { result } = renderHook(() => + useEditorUpdates( + mockServices, + eventEmitter, + setHasUnsavedChangesMock, + appState, + savedVisInstance, + visEditorController + ) + ); + + expect(result.current).toEqual({ + isEmbeddableRendered: false, + currentAppState: visualizeAppStateStub, + }); + expect(savedVisInstance.vis.uiState.setSilent).toHaveBeenCalledWith( + visualizeAppStateStub.uiState + ); + expect(visEditorController.render).toHaveBeenCalledWith({ + core: mockServices, + data: mockServices.data, + uiState: savedVisInstance.vis.uiState, + timeRange, + filters: mockFilters, + query: visualizeAppStateStub.query, + linked: false, + savedSearch: undefined, + }); + }); + + test('should update embeddable handler in embeded mode', () => { + renderHook(() => + useEditorUpdates( + mockServices, + eventEmitter, + setHasUnsavedChangesMock, + appState, + savedVisInstance, + undefined + ) + ); + + expect(savedVisInstance.embeddableHandler.updateInput).toHaveBeenCalledWith({ + timeRange, + filters: mockFilters, + query: visualizeAppStateStub.query, + }); + }); + + test('should update isEmbeddableRendered value when embedabble is rendered', () => { + const { result } = renderHook(() => + useEditorUpdates( + mockServices, + eventEmitter, + setHasUnsavedChangesMock, + appState, + savedVisInstance, + undefined + ) + ); + + act(() => { + eventEmitter.emit('embeddableRendered'); + }); + + expect(result.current.isEmbeddableRendered).toBe(true); + }); + + test('should destroy subscriptions on unmount', () => { + const { unmount } = renderHook(() => + useEditorUpdates( + mockServices, + eventEmitter, + setHasUnsavedChangesMock, + appState, + savedVisInstance, + undefined + ) + ); + + unmount(); + + expect(unsubscribeStateUpdatesMock).toHaveBeenCalledTimes(1); + expect(savedVisInstance.vis.uiState.off).toHaveBeenCalledTimes(1); + }); + + describe('subscribe on app state updates', () => { + test('should subscribe on appState updates', () => { + const { result } = renderHook(() => + useEditorUpdates( + mockServices, + eventEmitter, + setHasUnsavedChangesMock, + appState, + savedVisInstance, + undefined + ) + ); + // @ts-expect-error + const listener = appState.subscribe.mock.calls[0][0]; + + act(() => { + listener(visualizeAppStateStub); + }); + + expect(result.current.currentAppState).toEqual(visualizeAppStateStub); + expect(setHasUnsavedChangesMock).toHaveBeenCalledWith(true); + expect(savedVisInstance.embeddableHandler.updateInput).toHaveBeenCalledTimes(2); + }); + + test('should update vis state and reload the editor if changes come from url', () => { + const { result } = renderHook(() => + useEditorUpdates( + mockServices, + eventEmitter, + setHasUnsavedChangesMock, + appState, + savedVisInstance, + undefined + ) + ); + // @ts-expect-error + const listener = appState.subscribe.mock.calls[0][0]; + const newAppState = { + ...visualizeAppStateStub, + vis: { + ...visualizeAppStateStub.vis, + title: 'New title', + }, + }; + const { aggs, ...visState } = newAppState.vis; + const updateEditorSpy = jest.fn(); + + eventEmitter.on('updateEditor', updateEditorSpy); + + act(() => { + listener(newAppState); + }); + + expect(result.current.currentAppState).toEqual(newAppState); + expect(savedVisInstance.vis.setState).toHaveBeenCalledWith({ + ...visState, + data: { aggs }, + }); + expect(savedVisInstance.embeddableHandler.reload).toHaveBeenCalled(); + expect(updateEditorSpy).toHaveBeenCalled(); + }); + + describe('handle linked search changes', () => { + test('should update saved search id in saved instance', () => { + // @ts-expect-error + savedVisInstance.savedSearch = { + id: 'saved_search_id', + }; + + renderHook(() => + useEditorUpdates( + mockServices, + eventEmitter, + setHasUnsavedChangesMock, + appState, + savedVisInstance, + undefined + ) + ); + // @ts-expect-error + const listener = appState.subscribe.mock.calls[0][0]; + + act(() => { + listener({ + ...visualizeAppStateStub, + linked: true, + }); + }); + + expect(savedVisInstance.savedVis.savedSearchId).toEqual('saved_search_id'); + expect(savedVisInstance.vis.data.savedSearchId).toEqual('saved_search_id'); + }); + + test('should remove saved search id from vis instance', () => { + // @ts-expect-error + savedVisInstance.savedVis = { + savedSearchId: 'saved_search_id', + }; + // @ts-expect-error + savedVisInstance.savedSearch = { + id: 'saved_search_id', + }; + savedVisInstance.vis.data.savedSearchId = 'saved_search_id'; + + renderHook(() => + useEditorUpdates( + mockServices, + eventEmitter, + setHasUnsavedChangesMock, + appState, + savedVisInstance, + undefined + ) + ); + // @ts-expect-error + const listener = appState.subscribe.mock.calls[0][0]; + + act(() => { + listener(visualizeAppStateStub); + }); + + expect(savedVisInstance.savedVis.savedSearchId).toBeUndefined(); + expect(savedVisInstance.vis.data.savedSearchId).toBeUndefined(); + }); + }); + }); +}); diff --git a/src/plugins/visualize/public/application/utils/use/use_linked_search_updates.test.ts b/src/plugins/visualize/public/application/utils/use/use_linked_search_updates.test.ts new file mode 100644 index 0000000000000..4c9ebbc1d9abd --- /dev/null +++ b/src/plugins/visualize/public/application/utils/use/use_linked_search_updates.test.ts @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { EventEmitter } from 'events'; + +import { useLinkedSearchUpdates } from './use_linked_search_updates'; +import { VisualizeServices, SavedVisInstance, VisualizeAppStateContainer } from '../../types'; +import { createVisualizeServicesMock } from '../mocks'; + +describe('useLinkedSearchUpdates', () => { + let mockServices: jest.Mocked; + const eventEmitter = new EventEmitter(); + const savedVisInstance = ({ + vis: { + data: { + searchSource: { setField: jest.fn(), setParent: jest.fn() }, + }, + }, + savedVis: {}, + embeddableHandler: {}, + } as unknown) as SavedVisInstance; + + beforeEach(() => { + mockServices = createVisualizeServicesMock(); + }); + + it('should not subscribe on unlinkFromSavedSearch event if appState or savedSearch are not defined', () => { + renderHook(() => useLinkedSearchUpdates(mockServices, eventEmitter, null, savedVisInstance)); + + expect(mockServices.toastNotifications.addSuccess).not.toHaveBeenCalled(); + }); + + it('should subscribe on unlinkFromSavedSearch event if vis is based on saved search', () => { + const mockAppState = ({ + transitions: { + unlinkSavedSearch: jest.fn(), + }, + } as unknown) as VisualizeAppStateContainer; + savedVisInstance.savedSearch = ({ + searchSource: { + getParent: jest.fn(), + getField: jest.fn(), + getOwnField: jest.fn(), + }, + title: 'savedSearch', + } as unknown) as SavedVisInstance['savedSearch']; + + renderHook(() => + useLinkedSearchUpdates(mockServices, eventEmitter, mockAppState, savedVisInstance) + ); + + eventEmitter.emit('unlinkFromSavedSearch'); + + expect(savedVisInstance.savedSearch?.searchSource?.getParent).toHaveBeenCalled(); + expect(savedVisInstance.savedSearch?.searchSource?.getField).toHaveBeenCalledWith('index'); + expect(mockAppState.transitions.unlinkSavedSearch).toHaveBeenCalled(); + expect(mockServices.toastNotifications.addSuccess).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts new file mode 100644 index 0000000000000..a6b6d8ca0e837 --- /dev/null +++ b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts @@ -0,0 +1,224 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { EventEmitter } from 'events'; + +import { coreMock } from '../../../../../../core/public/mocks'; +import { useSavedVisInstance } from './use_saved_vis_instance'; +import { redirectWhenMissing } from '../../../../../kibana_utils/public'; +import { getEditBreadcrumbs, getCreateBreadcrumbs } from '../breadcrumbs'; +import { VisualizeServices } from '../../types'; +import { VisualizeConstants } from '../../visualize_constants'; + +const mockDefaultEditorControllerDestroy = jest.fn(); +const mockEmbeddableHandlerDestroy = jest.fn(); +const mockEmbeddableHandlerRender = jest.fn(); +const mockSavedVisDestroy = jest.fn(); +const savedVisId = '9ca7aa90-b892-11e8-a6d9-e546fe2bba5f'; +const mockSavedVisInstance = { + embeddableHandler: { + destroy: mockEmbeddableHandlerDestroy, + render: mockEmbeddableHandlerRender, + }, + savedVis: { + id: savedVisId, + title: 'Test Vis', + destroy: mockSavedVisDestroy, + }, + vis: { + type: {}, + }, +}; + +jest.mock('../get_visualization_instance', () => ({ + getVisualizationInstance: jest.fn(() => mockSavedVisInstance), +})); +jest.mock('../breadcrumbs', () => ({ + getEditBreadcrumbs: jest.fn((text) => text), + getCreateBreadcrumbs: jest.fn((text) => text), +})); +jest.mock('../../../../../vis_default_editor/public', () => ({ + DefaultEditorController: jest.fn(() => ({ destroy: mockDefaultEditorControllerDestroy })), +})); +jest.mock('../../../../../kibana_utils/public'); + +const mockGetVisualizationInstance = jest.requireMock('../get_visualization_instance') + .getVisualizationInstance; + +describe('useSavedVisInstance', () => { + const coreStartMock = coreMock.createStart(); + const toastNotifications = coreStartMock.notifications.toasts; + let mockServices: VisualizeServices; + const eventEmitter = new EventEmitter(); + + beforeEach(() => { + mockServices = ({ + ...coreStartMock, + toastNotifications, + history: { + location: { + pathname: VisualizeConstants.EDIT_PATH, + }, + replace: () => {}, + }, + visualizations: { + all: jest.fn(() => [ + { + name: 'area', + requiresSearch: true, + options: { + showIndexSelection: true, + }, + }, + { name: 'gauge' }, + ]), + }, + } as unknown) as VisualizeServices; + + mockDefaultEditorControllerDestroy.mockClear(); + mockEmbeddableHandlerDestroy.mockClear(); + mockEmbeddableHandlerRender.mockClear(); + mockSavedVisDestroy.mockClear(); + toastNotifications.addWarning.mockClear(); + mockGetVisualizationInstance.mockClear(); + }); + + test('should not load instance until chrome is defined', () => { + const { result } = renderHook(() => + useSavedVisInstance(mockServices, eventEmitter, undefined, undefined) + ); + expect(mockGetVisualizationInstance).not.toHaveBeenCalled(); + expect(result.current.visEditorController).toBeUndefined(); + expect(result.current.savedVisInstance).toBeUndefined(); + expect(result.current.visEditorRef).toBeDefined(); + }); + + describe('edit saved visualization route', () => { + test('should load instance and initiate an editor if chrome is set up', async () => { + const { result, waitForNextUpdate } = renderHook(() => + useSavedVisInstance(mockServices, eventEmitter, true, savedVisId) + ); + + expect(mockGetVisualizationInstance).toHaveBeenCalledWith(mockServices, savedVisId); + expect(mockGetVisualizationInstance.mock.calls.length).toBe(1); + + await waitForNextUpdate(); + expect(mockServices.chrome.setBreadcrumbs).toHaveBeenCalledWith('Test Vis'); + expect(getEditBreadcrumbs).toHaveBeenCalledWith('Test Vis'); + expect(getCreateBreadcrumbs).not.toHaveBeenCalled(); + expect(mockEmbeddableHandlerRender).not.toHaveBeenCalled(); + expect(result.current.visEditorController).toBeDefined(); + expect(result.current.savedVisInstance).toBeDefined(); + }); + + test('should destroy the editor and the savedVis on unmount if chrome exists', async () => { + const { unmount, waitForNextUpdate } = renderHook(() => + useSavedVisInstance(mockServices, eventEmitter, true, savedVisId) + ); + + await waitForNextUpdate(); + unmount(); + + expect(mockDefaultEditorControllerDestroy.mock.calls.length).toBe(1); + expect(mockEmbeddableHandlerDestroy).not.toHaveBeenCalled(); + expect(mockSavedVisDestroy.mock.calls.length).toBe(1); + }); + }); + + describe('create new visualization route', () => { + beforeEach(() => { + mockServices.history.location = { + ...mockServices.history.location, + pathname: VisualizeConstants.CREATE_PATH, + search: '?type=area&indexPattern=1a2b3c4d', + }; + delete mockSavedVisInstance.savedVis.id; + }); + + test('should create new visualization based on search params', async () => { + const { result, waitForNextUpdate } = renderHook(() => + useSavedVisInstance(mockServices, eventEmitter, true, undefined) + ); + + expect(mockGetVisualizationInstance).toHaveBeenCalledWith(mockServices, { + indexPattern: '1a2b3c4d', + type: 'area', + }); + + await waitForNextUpdate(); + + expect(getCreateBreadcrumbs).toHaveBeenCalled(); + expect(mockEmbeddableHandlerRender).not.toHaveBeenCalled(); + expect(result.current.visEditorController).toBeDefined(); + expect(result.current.savedVisInstance).toBeDefined(); + }); + + test('should throw error if vis type is invalid', async () => { + mockServices.history.location = { + ...mockServices.history.location, + search: '?type=myVisType&indexPattern=1a2b3c4d', + }; + + renderHook(() => useSavedVisInstance(mockServices, eventEmitter, true, undefined)); + + expect(mockGetVisualizationInstance).not.toHaveBeenCalled(); + expect(redirectWhenMissing).toHaveBeenCalled(); + expect(toastNotifications.addWarning).toHaveBeenCalled(); + }); + + test("should throw error if index pattern or saved search id doesn't exist in search params", async () => { + mockServices.history.location = { + ...mockServices.history.location, + search: '?type=area', + }; + + renderHook(() => useSavedVisInstance(mockServices, eventEmitter, true, undefined)); + + expect(mockGetVisualizationInstance).not.toHaveBeenCalled(); + expect(redirectWhenMissing).toHaveBeenCalled(); + expect(toastNotifications.addWarning).toHaveBeenCalled(); + }); + }); + + describe('embeded mode', () => { + test('should create new visualization based on search params', async () => { + const { result, unmount, waitForNextUpdate } = renderHook(() => + useSavedVisInstance(mockServices, eventEmitter, false, savedVisId) + ); + + // mock editor ref + // @ts-expect-error + result.current.visEditorRef.current = 'div'; + + expect(mockGetVisualizationInstance).toHaveBeenCalledWith(mockServices, savedVisId); + + await waitForNextUpdate(); + + expect(mockEmbeddableHandlerRender).toHaveBeenCalled(); + expect(result.current.visEditorController).toBeUndefined(); + expect(result.current.savedVisInstance).toBeDefined(); + + unmount(); + expect(mockDefaultEditorControllerDestroy).not.toHaveBeenCalled(); + expect(mockEmbeddableHandlerDestroy.mock.calls.length).toBe(1); + expect(mockSavedVisDestroy.mock.calls.length).toBe(1); + }); + }); +}); diff --git a/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts new file mode 100644 index 0000000000000..e885067c58184 --- /dev/null +++ b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts @@ -0,0 +1,210 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { EventEmitter } from 'events'; +import { Observable } from 'rxjs'; + +import { useVisualizeAppState } from './use_visualize_app_state'; +import { VisualizeServices, SavedVisInstance } from '../../types'; +import { visualizeAppStateStub } from '../stubs'; +import { VisualizeConstants } from '../../visualize_constants'; +import { createVisualizeServicesMock } from '../mocks'; + +jest.mock('../utils'); +jest.mock('../create_visualize_app_state'); +jest.mock('../../../../../data/public'); + +describe('useVisualizeAppState', () => { + const { visStateToEditorState } = jest.requireMock('../utils'); + const { createVisualizeAppState } = jest.requireMock('../create_visualize_app_state'); + const { connectToQueryState } = jest.requireMock('../../../../../data/public'); + const stopStateSyncMock = jest.fn(); + const stateContainerGetStateMock = jest.fn(() => visualizeAppStateStub); + const stopSyncingAppFiltersMock = jest.fn(); + const stateContainer = { + getState: stateContainerGetStateMock, + state$: new Observable(), + transitions: { + updateVisState: jest.fn(), + set: jest.fn(), + }, + }; + + visStateToEditorState.mockImplementation(() => visualizeAppStateStub); + createVisualizeAppState.mockImplementation(() => ({ + stateContainer, + stopStateSync: stopStateSyncMock, + })); + connectToQueryState.mockImplementation(() => stopSyncingAppFiltersMock); + + const eventEmitter = new EventEmitter(); + const savedVisInstance = ({ + vis: { + setState: jest.fn().mockResolvedValue({}), + }, + savedVis: {}, + embeddableHandler: {}, + } as unknown) as SavedVisInstance; + let mockServices: jest.Mocked; + + beforeEach(() => { + mockServices = createVisualizeServicesMock(); + + stopStateSyncMock.mockClear(); + stopSyncingAppFiltersMock.mockClear(); + visStateToEditorState.mockClear(); + }); + + it("should not create appState if vis instance isn't ready", () => { + const { result } = renderHook(() => useVisualizeAppState(mockServices, eventEmitter)); + + expect(result.current).toEqual({ + appState: null, + hasUnappliedChanges: false, + }); + }); + + it('should create appState and connect it to query search params', () => { + const { result } = renderHook(() => + useVisualizeAppState(mockServices, eventEmitter, savedVisInstance) + ); + + expect(visStateToEditorState).toHaveBeenCalledWith(savedVisInstance, mockServices); + expect(createVisualizeAppState).toHaveBeenCalledWith({ + stateDefaults: visualizeAppStateStub, + kbnUrlStateStorage: undefined, + }); + expect(mockServices.data.query.filterManager.setAppFilters).toHaveBeenCalledWith( + visualizeAppStateStub.filters + ); + expect(connectToQueryState).toHaveBeenCalledWith(mockServices.data.query, expect.any(Object), { + filters: 'appState', + }); + expect(result.current).toEqual({ + appState: stateContainer, + hasUnappliedChanges: false, + }); + }); + + it('should stop state and app filters syncing with query on destroy', () => { + const { unmount } = renderHook(() => + useVisualizeAppState(mockServices, eventEmitter, savedVisInstance) + ); + + unmount(); + + expect(stopStateSyncMock).toBeCalledTimes(1); + expect(stopSyncingAppFiltersMock).toBeCalledTimes(1); + }); + + it('should be subscribed on dirtyStateChange event from an editor', () => { + const { result } = renderHook(() => + useVisualizeAppState(mockServices, eventEmitter, savedVisInstance) + ); + + act(() => { + eventEmitter.emit('dirtyStateChange', { isDirty: true }); + }); + + expect(result.current.hasUnappliedChanges).toEqual(true); + expect(stateContainer.transitions.updateVisState).not.toHaveBeenCalled(); + expect(visStateToEditorState).toHaveBeenCalledTimes(1); + + act(() => { + eventEmitter.emit('dirtyStateChange', { isDirty: false }); + }); + + expect(result.current.hasUnappliedChanges).toEqual(false); + expect(stateContainer.transitions.updateVisState).toHaveBeenCalledWith( + visualizeAppStateStub.vis + ); + expect(visStateToEditorState).toHaveBeenCalledTimes(2); + }); + + describe('update vis state if the url params are not equal with the saved object vis state', () => { + const newAgg = { + id: '2', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'total_quantity', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: '', + }, + }; + const state = { + ...visualizeAppStateStub, + vis: { + ...visualizeAppStateStub.vis, + aggs: [...visualizeAppStateStub.vis.aggs, newAgg], + }, + }; + + it('should successfully update vis state and set up app state container', async () => { + // @ts-expect-error + stateContainerGetStateMock.mockImplementation(() => state); + const { result, waitForNextUpdate } = renderHook(() => + useVisualizeAppState(mockServices, eventEmitter, savedVisInstance) + ); + + await waitForNextUpdate(); + + const { aggs, ...visState } = stateContainer.getState().vis; + const expectedNewVisState = { + ...visState, + data: { aggs: state.vis.aggs }, + }; + + expect(savedVisInstance.vis.setState).toHaveBeenCalledWith(expectedNewVisState); + expect(result.current).toEqual({ + appState: stateContainer, + hasUnappliedChanges: false, + }); + }); + + it(`should add warning toast and redirect to the landing page + if setting new vis state was not successful, e.x. invalid query params`, async () => { + // @ts-expect-error + stateContainerGetStateMock.mockImplementation(() => state); + // @ts-expect-error + savedVisInstance.vis.setState.mockRejectedValue({ + message: 'error', + }); + + renderHook(() => useVisualizeAppState(mockServices, eventEmitter, savedVisInstance)); + + await new Promise((res) => { + setTimeout(() => res()); + }); + + expect(mockServices.toastNotifications.addWarning).toHaveBeenCalled(); + expect(mockServices.history.replace).toHaveBeenCalledWith( + `${VisualizeConstants.LANDING_PAGE_PATH}?notFound=visualization` + ); + }); + }); +}); From 79e671111875394b53418e03680edc8104d1e40d Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Wed, 8 Jul 2020 14:23:18 +0200 Subject: [PATCH 09/13] Improve description of refresh interval setting (#71062) Co-authored-by: Elastic Machine --- src/plugins/data/server/ui_settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/data/server/ui_settings.ts b/src/plugins/data/server/ui_settings.ts index de978c7968aee..e825ef7f6c945 100644 --- a/src/plugins/data/server/ui_settings.ts +++ b/src/plugins/data/server/ui_settings.ts @@ -518,7 +518,7 @@ export function getUiSettings(): Record> { }`, type: 'json', description: i18n.translate('data.advancedSettings.timepicker.refreshIntervalDefaultsText', { - defaultMessage: `The timefilter's default refresh interval`, + defaultMessage: `The timefilter's default refresh interval. The "value" needs to be specified in milliseconds.`, }), requiresPageReload: true, schema: schema.object({ From 67be99d545a3a0c3e12dcd887873499baad080d6 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Wed, 8 Jul 2020 07:43:17 -0500 Subject: [PATCH 10/13] [APM] Remove unused mock anomaly data. (#71040) This file was referenced in #71002 but is not being used. Delete it. --- .../__tests__/mockData/anomalyData.ts | 192 ------------------ 1 file changed, 192 deletions(-) delete mode 100644 x-pack/plugins/apm/public/selectors/__tests__/mockData/anomalyData.ts diff --git a/x-pack/plugins/apm/public/selectors/__tests__/mockData/anomalyData.ts b/x-pack/plugins/apm/public/selectors/__tests__/mockData/anomalyData.ts deleted file mode 100644 index 299e8a2104282..0000000000000 --- a/x-pack/plugins/apm/public/selectors/__tests__/mockData/anomalyData.ts +++ /dev/null @@ -1,192 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -export const anomalyData = { - dates: [ - 1530614880000, - 1530614940000, - 1530615000000, - 1530615060000, - 1530615120000, - 1530615180000, - 1530615240000, - 1530615300000, - 1530615360000, - 1530615420000, - 1530615480000, - 1530615540000, - 1530615600000, - 1530615660000, - 1530615720000, - 1530615780000, - 1530615840000, - 1530615900000, - 1530615960000, - 1530616020000, - 1530616080000, - 1530616140000, - 1530616200000, - 1530616260000, - 1530616320000, - 1530616380000, - 1530616440000, - 1530616500000, - 1530616560000, - 1530616620000, - ], - buckets: [ - { - anomalyScore: null, - lower: 15669, - upper: 54799, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: 0, - lower: 17808, - upper: 49874, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: 0, - lower: 18012, - upper: 49421, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: 0, - lower: 17889, - upper: 49654, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: 0, - lower: 17713, - upper: 50026, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: 0, - lower: 18044, - upper: 49371, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: 0, - lower: 17713, - upper: 50110, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: 0, - lower: 17582, - upper: 50419, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - ], -}; From 637a0d9f98faa8d72780c6f41cf498aea19a7b30 Mon Sep 17 00:00:00 2001 From: Sonja Krause-Harder Date: Wed, 8 Jul 2020 15:06:17 +0200 Subject: [PATCH 11/13] [Ingest Manager] Add usage collector for telemetry. (#69294) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add usage collector for telemetry. * Make minimal usage collector work. * Add all fields to Usage and schema * Type packages as array. * Temporarily remove schema. * Temporarily exclude our collector from schema checks. * Add fleet telemetry. * Remove events from agent stats. * Add package telemetry. * Use correct import. * Add telemetry about enabled packages. * Clean up comments. * Update x-pack/plugins/ingest_manager/server/collectors/package_collectors.ts Co-authored-by: Alejandro Fernández Haro * Update x-pack/plugins/ingest_manager/server/collectors/package_collectors.ts Co-authored-by: Nicolas Chaulet * Correctly check for element in array. * Use a real SavedObjectsClient. * Remove useless use of undefined. * Use less deep path to import SavedObjectsClient. Co-authored-by: Alejandro Fernández Haro Co-authored-by: Nicolas Chaulet --- x-pack/.telemetryrc.json | 1 + x-pack/plugins/ingest_manager/kibana.json | 2 +- .../server/collectors/agent_collectors.ts | 33 ++++++++++ .../server/collectors/config_collectors.ts | 11 ++++ .../server/collectors/helpers.ts | 15 +++++ .../server/collectors/package_collectors.ts | 49 +++++++++++++++ .../server/collectors/register.ts | 62 +++++++++++++++++++ .../plugins/ingest_manager/server/plugin.ts | 6 ++ 8 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/ingest_manager/server/collectors/agent_collectors.ts create mode 100644 x-pack/plugins/ingest_manager/server/collectors/config_collectors.ts create mode 100644 x-pack/plugins/ingest_manager/server/collectors/helpers.ts create mode 100644 x-pack/plugins/ingest_manager/server/collectors/package_collectors.ts create mode 100644 x-pack/plugins/ingest_manager/server/collectors/register.ts diff --git a/x-pack/.telemetryrc.json b/x-pack/.telemetryrc.json index 2c16491c1096b..4da44667e167f 100644 --- a/x-pack/.telemetryrc.json +++ b/x-pack/.telemetryrc.json @@ -7,6 +7,7 @@ "plugins/apm/server/lib/apm_telemetry/index.ts", "plugins/canvas/server/collectors/collector.ts", "plugins/infra/server/usage/usage_collector.ts", + "plugins/ingest_manager/server/collectors/register.ts", "plugins/lens/server/usage/collectors.ts", "plugins/reporting/server/usage/reporting_usage_collector.ts", "plugins/maps/server/maps_telemetry/collectors/register.ts" diff --git a/x-pack/plugins/ingest_manager/kibana.json b/x-pack/plugins/ingest_manager/kibana.json index 35447139607a6..181b93a9e2425 100644 --- a/x-pack/plugins/ingest_manager/kibana.json +++ b/x-pack/plugins/ingest_manager/kibana.json @@ -5,6 +5,6 @@ "ui": true, "configPath": ["xpack", "ingestManager"], "requiredPlugins": ["licensing", "data", "encryptedSavedObjects"], - "optionalPlugins": ["security", "features", "cloud"], + "optionalPlugins": ["security", "features", "cloud", "usageCollection"], "extraPublicDirs": ["common"] } diff --git a/x-pack/plugins/ingest_manager/server/collectors/agent_collectors.ts b/x-pack/plugins/ingest_manager/server/collectors/agent_collectors.ts new file mode 100644 index 0000000000000..920b336297171 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/collectors/agent_collectors.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClient } from 'kibana/server'; +import * as AgentService from '../services/agents'; +export interface AgentUsage { + total: number; + online: number; + error: number; + offline: number; +} + +export const getAgentUsage = async (soClient?: SavedObjectsClient): Promise => { + // TODO: unsure if this case is possible at all. + if (!soClient) { + return { + total: 0, + online: 0, + error: 0, + offline: 0, + }; + } + const { total, online, error, offline } = await AgentService.getAgentStatusForConfig(soClient); + return { + total, + online, + error, + offline, + }; +}; diff --git a/x-pack/plugins/ingest_manager/server/collectors/config_collectors.ts b/x-pack/plugins/ingest_manager/server/collectors/config_collectors.ts new file mode 100644 index 0000000000000..514984f7f859d --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/collectors/config_collectors.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IngestManagerConfigType } from '..'; + +export const getIsFleetEnabled = (config: IngestManagerConfigType) => { + return config.fleet.enabled; +}; diff --git a/x-pack/plugins/ingest_manager/server/collectors/helpers.ts b/x-pack/plugins/ingest_manager/server/collectors/helpers.ts new file mode 100644 index 0000000000000..c8ed54d5074fd --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/collectors/helpers.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup } from 'kibana/server'; +import { SavedObjectsClient } from '../../../../../src/core/server'; + +export async function getInternalSavedObjectsClient(core: CoreSetup) { + return core.getStartServices().then(async ([coreStart]) => { + const savedObjectsRepo = coreStart.savedObjects.createInternalRepository(); + return new SavedObjectsClient(savedObjectsRepo); + }); +} diff --git a/x-pack/plugins/ingest_manager/server/collectors/package_collectors.ts b/x-pack/plugins/ingest_manager/server/collectors/package_collectors.ts new file mode 100644 index 0000000000000..399e38f1919ba --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/collectors/package_collectors.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClient } from 'kibana/server'; +import _ from 'lodash'; +import { getPackageSavedObjects } from '../services/epm/packages/get'; +import { agentConfigService } from '../services'; +import { NewPackageConfig } from '../types'; + +export interface PackageUsage { + name: string; + version: string; + enabled: boolean; +} + +export const getPackageUsage = async (soClient?: SavedObjectsClient): Promise => { + if (!soClient) { + return []; + } + const packagesSavedObjects = await getPackageSavedObjects(soClient); + const agentConfigs = await agentConfigService.list(soClient, { + perPage: 1000, // avoiding pagination + withPackageConfigs: true, + }); + + // Once we provide detailed telemetry on agent configs, this logic should probably be moved + // to the (then to be created) agent config collector, so we only query and loop over these + // objects once. + + const packagesInConfigs = agentConfigs.items.map((agentConfig) => { + const packageConfigs: NewPackageConfig[] = agentConfig.package_configs as NewPackageConfig[]; + return packageConfigs + .map((packageConfig) => packageConfig.package?.name) + .filter((packageName): packageName is string => packageName !== undefined); + }); + + const enabledPackages = _.uniq(_.flatten(packagesInConfigs)); + + return packagesSavedObjects.saved_objects.map((p) => { + return { + name: p.attributes.name, + version: p.attributes.version, + enabled: enabledPackages.includes(p.attributes.name), + }; + }); +}; diff --git a/x-pack/plugins/ingest_manager/server/collectors/register.ts b/x-pack/plugins/ingest_manager/server/collectors/register.ts new file mode 100644 index 0000000000000..aad59ee74433c --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/collectors/register.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { CoreSetup } from 'kibana/server'; +import { getIsFleetEnabled } from './config_collectors'; +import { AgentUsage, getAgentUsage } from './agent_collectors'; +import { getInternalSavedObjectsClient } from './helpers'; +import { PackageUsage, getPackageUsage } from './package_collectors'; +import { IngestManagerConfigType } from '..'; + +interface Usage { + fleet_enabled: boolean; + agents: AgentUsage; + packages: PackageUsage[]; +} + +export function registerIngestManagerUsageCollector( + core: CoreSetup, + config: IngestManagerConfigType, + usageCollection: UsageCollectionSetup | undefined +): void { + // usageCollection is an optional dependency, so make sure to return if it is not registered. + // if for any reason the saved objects client is not available, also return + if (!usageCollection) { + return; + } + + // create usage collector + const ingestManagerCollector = usageCollection.makeUsageCollector({ + type: 'ingest_manager', + isReady: () => true, + fetch: async () => { + const soClient = await getInternalSavedObjectsClient(core); + return { + fleet_enabled: getIsFleetEnabled(config), + agents: await getAgentUsage(soClient), + packages: await getPackageUsage(soClient), + }; + }, + // schema: { // temporarily disabled because of type errors + // fleet_enabled: { type: 'boolean' }, + // agents: { + // total: { type: 'number' }, + // online: { type: 'number' }, + // error: { type: 'number' }, + // offline: { type: 'number' }, + // }, + // packages: { + // name: { type: 'keyword' }, + // version: { type: 'keyword' }, + // enabled: { type: boolean }, + // }, + // }, + }); + + // register usage collector + usageCollection.registerCollector(ingestManagerCollector); +} diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index 91201dbf9848b..d1adbd8b2f65d 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -14,6 +14,7 @@ import { SavedObjectsServiceStart, HttpServiceSetup, } from 'kibana/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { LicensingPluginSetup, ILicense } from '../../licensing/server'; import { EncryptedSavedObjectsPluginStart, @@ -62,6 +63,7 @@ import { } from './services/agents'; import { CloudSetup } from '../../cloud/server'; import { agentCheckinState } from './services/agents/checkin/state'; +import { registerIngestManagerUsageCollector } from './collectors/register'; export interface IngestManagerSetupDeps { licensing: LicensingPluginSetup; @@ -69,6 +71,7 @@ export interface IngestManagerSetupDeps { features?: FeaturesPluginSetup; encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; cloud?: CloudSetup; + usageCollection?: UsageCollectionSetup; } export type IngestManagerStartDeps = object; @@ -198,6 +201,9 @@ export class IngestManagerPlugin const router = core.http.createRouter(); const config = await this.config$.pipe(first()).toPromise(); + // Register usage collection + registerIngestManagerUsageCollector(core, config, deps.usageCollection); + // Always register app routes for permissions checking registerAppRoutes(router); From 9cf1dec6a9a08ce2c7e204eb869fd5a5070565e6 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Wed, 8 Jul 2020 09:24:34 -0400 Subject: [PATCH 12/13] Load configuration from EMS-metadata in region-maps (#70888) --- .../__tests__/map/ems_mocks/sample_files.json | 42 ++++++++++++ .../__tests__/map/ems_mocks/sample_tiles.json | 2 +- .../public/map/service_settings.js | 66 +++++++++++++------ .../public/map/service_settings.test.js | 19 +++++- .../public/region_map_visualization.js | 61 ++++++++++++----- 5 files changed, 151 insertions(+), 39 deletions(-) diff --git a/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_files.json b/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_files.json index cdbed7fa06367..470544cf35b30 100644 --- a/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_files.json +++ b/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_files.json @@ -406,6 +406,48 @@ "zh-tw": "國家" } }, + { + "layer_id": "world_countries_with_compromised_attribution", + "created_at": "2017-04-26T17:12:15.978370", + "attribution": [ + { + "label": { + "en": "
Made with NaturalEarth
" + }, + "url": { + "en": "http://www.naturalearthdata.com/about/terms-of-use" + } + }, + { + "label": { + "en": "Elastic Maps Service" + }, + "url": { + "en": "javascript:alert('foobar')" + } + } + ], + "formats": [ + { + "type": "geojson", + "url": "/files/world_countries_v1.geo.json", + "legacy_default": true + } + ], + "fields": [ + { + "type": "id", + "id": "iso2", + "label": { + "en": "ISO 3166-1 alpha-2 code" + } + } + ], + "legacy_ids": [], + "layer_name": { + "en": "World Countries (compromised)" + } + }, { "layer_id": "australia_states", "created_at": "2018-06-27T23:47:32.202380", diff --git a/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_tiles.json b/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_tiles.json index c038bb411daec..1bbd94879b70c 100644 --- a/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_tiles.json +++ b/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_tiles.json @@ -11,7 +11,7 @@ { "label": { "en": "OpenMapTiles" }, "url": { "en": "https://openmaptiles.org" } }, { "label": { "en": "MapTiler" }, "url": { "en": "https://www.maptiler.com" } }, { - "label": { "en": "Elastic Maps Service" }, + "label": { "en": "" }, "url": { "en": "https://www.elastic.co/elastic-maps-service" } } ], diff --git a/src/plugins/maps_legacy/public/map/service_settings.js b/src/plugins/maps_legacy/public/map/service_settings.js index f4f88bd5807d5..ae40b2c92d40e 100644 --- a/src/plugins/maps_legacy/public/map/service_settings.js +++ b/src/plugins/maps_legacy/public/map/service_settings.js @@ -89,28 +89,31 @@ export class ServiceSettings { }; } + _backfillSettings = (fileLayer) => { + // Older version of Kibana stored EMS state in the URL-params + // Creates object literal with required parameters as key-value pairs + const format = fileLayer.getDefaultFormatType(); + const meta = fileLayer.getDefaultFormatMeta(); + + return { + name: fileLayer.getDisplayName(), + origin: fileLayer.getOrigin(), + id: fileLayer.getId(), + created_at: fileLayer.getCreatedAt(), + attribution: getAttributionString(fileLayer), + fields: fileLayer.getFieldsInLanguage(), + format: format, //legacy: format and meta are split up + meta: meta, //legacy, format and meta are split up + }; + }; + async getFileLayers() { if (!this._mapConfig.includeElasticMapsService) { return []; } const fileLayers = await this._emsClient.getFileLayers(); - return fileLayers.map((fileLayer) => { - //backfill to older settings - const format = fileLayer.getDefaultFormatType(); - const meta = fileLayer.getDefaultFormatMeta(); - - return { - name: fileLayer.getDisplayName(), - origin: fileLayer.getOrigin(), - id: fileLayer.getId(), - created_at: fileLayer.getCreatedAt(), - attribution: fileLayer.getHTMLAttribution(), - fields: fileLayer.getFieldsInLanguage(), - format: format, //legacy: format and meta are split up - meta: meta, //legacy, format and meta are split up - }; - }); + return fileLayers.map(this._backfillSettings); } /** @@ -139,7 +142,7 @@ export class ServiceSettings { id: tmsService.getId(), minZoom: await tmsService.getMinZoom(), maxZoom: await tmsService.getMaxZoom(), - attribution: tmsService.getHTMLAttribution(), + attribution: getAttributionString(tmsService), }; }) ); @@ -159,16 +162,25 @@ export class ServiceSettings { this._emsClient.addQueryParams(additionalQueryParams); } - async getEMSHotLink(fileLayerConfig) { + async getFileLayerFromConfig(fileLayerConfig) { const fileLayers = await this._emsClient.getFileLayers(); - const layer = fileLayers.find((fileLayer) => { + return fileLayers.find((fileLayer) => { const hasIdByName = fileLayer.hasId(fileLayerConfig.name); //legacy const hasIdById = fileLayer.hasId(fileLayerConfig.id); return hasIdByName || hasIdById; }); + } + + async getEMSHotLink(fileLayerConfig) { + const layer = await this.getFileLayerFromConfig(fileLayerConfig); return layer ? layer.getEMSHotLink() : null; } + async loadFileLayerConfig(fileLayerConfig) { + const fileLayer = await this.getFileLayerFromConfig(fileLayerConfig); + return fileLayer ? this._backfillSettings(fileLayer) : null; + } + async _getAttributesForEMSTMSLayer(isDesaturated, isDarkMode) { const tmsServices = await this._emsClient.getTMSServices(); const emsTileLayerId = this._mapConfig.emsTileLayerId; @@ -189,7 +201,7 @@ export class ServiceSettings { url: await tmsService.getUrlTemplate(), minZoom: await tmsService.getMinZoom(), maxZoom: await tmsService.getMaxZoom(), - attribution: await tmsService.getHTMLAttribution(), + attribution: getAttributionString(tmsService), origin: ORIGIN.EMS, }; } @@ -255,3 +267,17 @@ export class ServiceSettings { return await response.json(); } } + +function getAttributionString(emsService) { + const attributions = emsService.getAttributions(); + const attributionSnippets = attributions.map((attribution) => { + const anchorTag = document.createElement('a'); + anchorTag.setAttribute('rel', 'noreferrer noopener'); + if (attribution.url.startsWith('http://') || attribution.url.startsWith('https://')) { + anchorTag.setAttribute('href', attribution.url); + } + anchorTag.textContent = attribution.label; + return anchorTag.outerHTML; + }); + return attributionSnippets.join(' | '); //!!!this is the current convention used in Kibana +} diff --git a/src/plugins/maps_legacy/public/map/service_settings.test.js b/src/plugins/maps_legacy/public/map/service_settings.test.js index 01facdc54137e..6e416f7fd5c84 100644 --- a/src/plugins/maps_legacy/public/map/service_settings.test.js +++ b/src/plugins/maps_legacy/public/map/service_settings.test.js @@ -98,6 +98,9 @@ describe('service_settings (FKA tile_map test)', function () { expect(attrs.url.includes('{x}')).toEqual(true); expect(attrs.url.includes('{y}')).toEqual(true); expect(attrs.url.includes('{z}')).toEqual(true); + expect(attrs.attribution).toEqual( + 'OpenStreetMap contributors | OpenMapTiles | MapTiler | <iframe id=\'iframe\' style=\'position:fixed;height: 40%;width: 100%;top: 60%;left: 5%;right:5%;border: 0px;background:white;\' src=\'http://256.256.256.256\'></iframe>' + ); const urlObject = url.parse(attrs.url, true); expect(urlObject.hostname).toEqual('tiles.foobar'); @@ -182,7 +185,7 @@ describe('service_settings (FKA tile_map test)', function () { minZoom: 0, maxZoom: 10, attribution: - 'OpenStreetMap contributors | OpenMapTiles | MapTiler | Elastic Maps Service', + 'OpenStreetMap contributors | OpenMapTiles | MapTiler | <iframe id=\'iframe\' style=\'position:fixed;height: 40%;width: 100%;top: 60%;left: 5%;right:5%;border: 0px;background:white;\' src=\'http://256.256.256.256\'></iframe>', subdomains: [], }, ]; @@ -276,7 +279,6 @@ describe('service_settings (FKA tile_map test)', function () { serviceSettings = makeServiceSettings({ includeElasticMapsService: false, }); - // mapConfig.includeElasticMapsService = false; const tilemapServices = await serviceSettings.getTMSServices(); const expected = []; expect(tilemapServices).toEqual(expected); @@ -289,7 +291,7 @@ describe('service_settings (FKA tile_map test)', function () { const serviceSettings = makeServiceSettings(); serviceSettings.setQueryParams({ foo: 'bar' }); const fileLayers = await serviceSettings.getFileLayers(); - expect(fileLayers.length).toEqual(18); + expect(fileLayers.length).toEqual(19); const assertions = fileLayers.map(async function (fileLayer) { expect(fileLayer.origin).toEqual(ORIGIN.EMS); const fileUrl = await serviceSettings.getUrlForRegionLayer(fileLayer); @@ -343,5 +345,16 @@ describe('service_settings (FKA tile_map test)', function () { const hotlink = await serviceSettings.getEMSHotLink(fileLayers[0]); expect(hotlink).toEqual('?locale=en#file/world_countries'); //url host undefined becuase emsLandingPageUrl is set at kibana-load }); + + it('should sanitize EMS attribution', async () => { + const serviceSettings = makeServiceSettings(); + const fileLayers = await serviceSettings.getFileLayers(); + const fileLayer = fileLayers.find((layer) => { + return layer.id === 'world_countries_with_compromised_attribution'; + }); + expect(fileLayer.attribution).toEqual( + '<div onclick=\'alert(1\')>Made with NaturalEarth</div> | Elastic Maps Service' + ); + }); }); }); diff --git a/src/plugins/region_map/public/region_map_visualization.js b/src/plugins/region_map/public/region_map_visualization.js index 002d020fcd568..43959c367558f 100644 --- a/src/plugins/region_map/public/region_map_visualization.js +++ b/src/plugins/region_map/public/region_map_visualization.js @@ -22,9 +22,11 @@ import ChoroplethLayer from './choropleth_layer'; import { getFormatService, getNotifications, getKibanaLegacy } from './kibana_services'; import { truncatedColorMaps } from '../../charts/public'; import { tooltipFormatter } from './tooltip_formatter'; -import { mapTooltipProvider } from '../../maps_legacy/public'; +import { mapTooltipProvider, ORIGIN } from '../../maps_legacy/public'; +import _ from 'lodash'; export function createRegionMapVisualization({ + regionmapsConfig, serviceSettings, uiSettings, BaseMapsVisualization, @@ -60,17 +62,18 @@ export function createRegionMapVisualization({ }); } - if (!this._params.selectedJoinField && this._params.selectedLayer) { - this._params.selectedJoinField = this._params.selectedLayer.fields[0]; + const selectedLayer = await this._loadConfig(this._params.selectedLayer); + if (!this._params.selectedJoinField && selectedLayer) { + this._params.selectedJoinField = selectedLayer.fields[0]; } - if (!this._params.selectedLayer) { + if (!selectedLayer) { return; } this._updateChoroplethLayerForNewMetrics( - this._params.selectedLayer.name, - this._params.selectedLayer.attribution, + selectedLayer.name, + selectedLayer.attribution, this._params.showAllShapes, results ); @@ -90,29 +93,57 @@ export function createRegionMapVisualization({ this._kibanaMap.useUiStateFromVisualization(this._vis); } + async _loadConfig(fileLayerConfig) { + // Load the selected layer from the metadata-service. + // Do not use the selectedLayer from the visState. + // These settings are stored in the URL and can be used to inject dirty display content. + + if ( + fileLayerConfig.isEMS || //Hosted by EMS. Metadata needs to be resolved through EMS + (fileLayerConfig.layerId && fileLayerConfig.layerId.startsWith(`${ORIGIN.EMS}.`)) //fallback for older saved objects + ) { + return await serviceSettings.loadFileLayerConfig(fileLayerConfig); + } + + //Configured in the kibana.yml. Needs to be resolved through the settings. + const configuredLayer = regionmapsConfig.layers.find( + (layer) => layer.name === fileLayerConfig.name + ); + + if (configuredLayer) { + return { + ...configuredLayer, + attribution: _.escape(configuredLayer.attribution ? configuredLayer.attribution : ''), + }; + } + + return null; + } + async _updateParams() { await super._updateParams(); - const visParams = this._params; - if (!visParams.selectedJoinField && visParams.selectedLayer) { - visParams.selectedJoinField = visParams.selectedLayer.fields[0]; + const selectedLayer = await this._loadConfig(this._params.selectedLayer); + + if (!this._params.selectedJoinField && selectedLayer) { + this._params.selectedJoinField = selectedLayer.fields[0]; } - if (!visParams.selectedJoinField || !visParams.selectedLayer) { + if (!this._params.selectedJoinField || !selectedLayer) { return; } this._updateChoroplethLayerForNewProperties( - visParams.selectedLayer.name, - visParams.selectedLayer.attribution, + selectedLayer.name, + selectedLayer.attribution, this._params.showAllShapes ); const metricFieldFormatter = getFormatService().deserialize(this._params.metric.format); - this._choroplethLayer.setJoinField(visParams.selectedJoinField.name); - this._choroplethLayer.setColorRamp(truncatedColorMaps[visParams.colorSchema].value); - this._choroplethLayer.setLineWeight(visParams.outlineWeight); + this._choroplethLayer.setJoinField(this._params.selectedJoinField.name); + this._choroplethLayer.setColorRamp(truncatedColorMaps[this._params.colorSchema].value); + this._choroplethLayer.setLineWeight(this._params.outlineWeight); this._choroplethLayer.setTooltipFormatter( this._tooltipFormatter, metricFieldFormatter, From 7f39cb5e3ad17b3191184d7b9b5808b3c77e269f Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Wed, 8 Jul 2020 06:59:37 -0700 Subject: [PATCH 13/13] Polish Data Streams copy, add doc links and tooltips (#70983) * Add learn more links to data streams, indices, and index templates tabs. * Add tooltips to detail panel. * Unify data streams description text. * Fix bug in which index tab showed an empty list, by clearing the filter state on unmount. * Add indices count to data stream detail panel. --- .../data_stream_detail_panel.tsx | 129 +++++++++++++----- .../data_stream_list/data_stream_list.tsx | 40 ++++-- .../index_list/index_table/index_table.js | 37 +++-- .../home/template_list/template_list.tsx | 32 +++-- .../application/services/documentation.ts | 4 + .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 7 files changed, 183 insertions(+), 63 deletions(-) diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx index 577f04a4a7efd..a0381557db21e 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx @@ -8,35 +8,35 @@ import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, - EuiFlyout, - EuiFlyoutHeader, - EuiTitle, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlexGroup, - EuiFlexItem, EuiButtonEmpty, EuiDescriptionList, - EuiDescriptionListTitle, EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiIconTip, + EuiLink, + EuiTitle, } from '@elastic/eui'; +import { reactRouterNavigate } from '../../../../../shared_imports'; import { SectionLoading, SectionError, Error } from '../../../../components'; import { useLoadDataStream } from '../../../../services/api'; import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal'; interface Props { dataStreamName: string; + backingIndicesLink: ReturnType; onClose: (shouldReload?: boolean) => void; } -/** - * NOTE: This currently isn't in use by data_stream_list.tsx because it doesn't contain any - * information that doesn't already exist in the table. We'll use it once we add additional - * info, e.g. storage size, docs count. - */ export const DataStreamDetailPanel: React.FunctionComponent = ({ dataStreamName, + backingIndicesLink, onClose, }) => { const { error, data: dataStream, isLoading } = useLoadDataStream(dataStreamName); @@ -68,28 +68,95 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ /> ); } else if (dataStream) { - const { timeStampField, generation } = dataStream; + const { indices, timeStampField, generation } = dataStream; content = ( - - - - + + + + + + + + + + + + } + position="top" + /> + + + - {timeStampField.name} + + {indices.length} + - - - - - {generation} - + + + + + + + + + } + position="top" + /> + + + + + {timeStampField.name} + + + + + + + + + + + + + + } + position="top" + /> + + + + + {generation} + + + ); } diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx index adfaa7820aff3..239b119051c06 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx @@ -8,14 +8,15 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EuiTitle, EuiText, EuiSpacer, EuiEmptyPrompt, EuiLink } from '@elastic/eui'; +import { EuiText, EuiSpacer, EuiEmptyPrompt, EuiLink } from '@elastic/eui'; import { ScopedHistory } from 'kibana/public'; import { reactRouterNavigate, extractQueryParams } from '../../../../shared_imports'; import { useAppContext } from '../../../app_context'; import { SectionError, SectionLoading, Error } from '../../../components'; import { useLoadDataStreams } from '../../../services/api'; -import { decodePathFromReactRouter } from '../../../services/routing'; +import { encodePathForReactRouter, decodePathFromReactRouter } from '../../../services/routing'; +import { documentationService } from '../../../services/documentation'; import { Section } from '../../home'; import { DataStreamTable } from './data_stream_table'; import { DataStreamDetailPanel } from './data_stream_detail_panel'; @@ -79,7 +80,7 @@ export const DataStreamList: React.FunctionComponent {' ' /* We need this space to separate these two sentences. */} {ingestManager ? ( @@ -134,14 +135,25 @@ export const DataStreamList: React.FunctionComponent {/* TODO: Add a switch for toggling on data streams created by Ingest Manager */} - - - - - + + + {i18n.translate('xpack.idxMgmt.dataStreamListDescription.learnMoreLinkText', { + defaultMessage: 'Learn more.', + })} + + ), + }} + /> + @@ -170,6 +182,12 @@ export const DataStreamList: React.FunctionComponent { history.push(`/${Section.DataStreams}`); diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js index 5f10eebc9d270..9122c6524d05d 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js @@ -33,7 +33,6 @@ import { EuiTableRowCell, EuiTableRowCellCheckbox, EuiText, - EuiTitle, } from '@elastic/eui'; import { UIM_SHOW_DETAILS_CLICK } from '../../../../../../common/constants'; @@ -41,6 +40,7 @@ import { reactRouterNavigate } from '../../../../../shared_imports'; import { REFRESH_RATE_INDEX_LIST } from '../../../../constants'; import { healthToColor } from '../../../../services'; import { encodePathForReactRouter } from '../../../../services/routing'; +import { documentationService } from '../../../../services/documentation'; import { AppContextConsumer } from '../../../../app_context'; import { renderBadges } from '../../../../lib/render_badges'; import { NoMatch, PageErrorForbidden } from '../../../../components'; @@ -121,6 +121,11 @@ export class IndexTable extends Component { } componentWillUnmount() { + // When you deep-link to an index from the data streams tab, the hidden indices are toggled on. + // However, this state is lost when you navigate away. We need to clear the filter too, or else + // navigating back to this tab would just show an empty list because the backing indices + // would be hidden. + this.props.filterChanged(''); clearInterval(this.interval); } @@ -494,14 +499,28 @@ export class IndexTable extends Component { - - - - - + + + {i18n.translate( + 'xpack.idxMgmt.indexTableDescription.learnMoreLinkText', + { + defaultMessage: 'Learn more.', + } + )} + + ), + }} + /> + diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx index afa8fa5b4ee04..18a65407ee20d 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx @@ -17,12 +17,14 @@ import { EuiFlexItem, EuiFlexGroup, EuiButton, + EuiLink, } from '@elastic/eui'; import { UIM_TEMPLATE_LIST_LOAD } from '../../../../../common/constants'; import { TemplateListItem } from '../../../../../common'; import { SectionError, SectionLoading, Error } from '../../../components'; import { useLoadIndexTemplates } from '../../../services/api'; +import { documentationService } from '../../../services/documentation'; import { useServices } from '../../../app_context'; import { getTemplateEditLink, @@ -109,14 +111,28 @@ export const TemplateList: React.FunctionComponent ( - - - - - + + + {i18n.translate( + 'xpack.idxMgmt.home.indexTemplatesDescription.learnMoreLinkText', + { + defaultMessage: 'Learn more.', + } + )} + + ), + }} + /> + filters={filters} onChange={setFilters} /> diff --git a/x-pack/plugins/index_management/public/application/services/documentation.ts b/x-pack/plugins/index_management/public/application/services/documentation.ts index ccccccce19766..972b4f4b25680 100644 --- a/x-pack/plugins/index_management/public/application/services/documentation.ts +++ b/x-pack/plugins/index_management/public/application/services/documentation.ts @@ -36,6 +36,10 @@ class DocumentationService { return `${this.esDocsBase}/mapping-routing-field.html`; } + public getDataStreamsDocumentationLink() { + return `${this.esDocsBase}/data-streams.html`; + } + public getTemplatesDocumentationLink() { return `${this.esDocsBase}/indices-templates.html`; } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3200240e9089a..72d21400540fd 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6455,9 +6455,7 @@ "xpack.idxMgmt.freezeIndicesAction.successfullyFrozeIndicesMessage": "[{indexNames}] が凍結されました", "xpack.idxMgmt.frozenBadgeLabel": "凍結", "xpack.idxMgmt.home.appTitle": "インデックス管理", - "xpack.idxMgmt.home.idxMgmtDescription": "Elasticsearch インデックスを個々に、または一斉に更新します", "xpack.idxMgmt.home.idxMgmtDocsLinkText": "インデックス管理ドキュメント", - "xpack.idxMgmt.home.indexTemplatesDescription": "インデックステンプレートを使用して設定、マッピング、エイリアスをインデックスに自動的に適用します。", "xpack.idxMgmt.home.indexTemplatesTabTitle": "インデックステンプレート", "xpack.idxMgmt.home.indicesTabTitle": "インデックス", "xpack.idxMgmt.indexActionsMenu.clearIndexCacheLabel": "{selectedIndexCount, plural, one {インデックス} other {インデックス} }のキャッシュを消去", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9758893732540..2fa281c8042ee 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6458,9 +6458,7 @@ "xpack.idxMgmt.freezeIndicesAction.successfullyFrozeIndicesMessage": "成功冻结:[{indexNames}]", "xpack.idxMgmt.frozenBadgeLabel": "已冻结", "xpack.idxMgmt.home.appTitle": "索引管理", - "xpack.idxMgmt.home.idxMgmtDescription": "单个或批量更新您的 Elasticsearch 索引。", "xpack.idxMgmt.home.idxMgmtDocsLinkText": "索引管理文档", - "xpack.idxMgmt.home.indexTemplatesDescription": "使用索引模板可将设置、映射和别名自动应用到索引。", "xpack.idxMgmt.home.indexTemplatesTabTitle": "索引模板", "xpack.idxMgmt.home.indicesTabTitle": "索引", "xpack.idxMgmt.indexActionsMenu.clearIndexCacheLabel": "清除 {selectedIndexCount, plural, one { 个索引} other { 个索引} } 缓存",